Software / code / prosody-modules
Comparison
mod_http_muc_log/mod_http_muc_log.lua @ 1549:f9f8bf82ece7
mod_http_muc_log: MUC log module using new archive API
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Sat, 08 Nov 2014 15:51:57 +0100 |
| child | 1550:1b2823b41f7f |
comparison
equal
deleted
inserted
replaced
| 1548:d3c847070618 | 1549:f9f8bf82ece7 |
|---|---|
| 1 local st = require "util.stanza"; | |
| 2 local datetime = require"util.datetime"; | |
| 3 local jid_split = require"util.jid".split; | |
| 4 local nodeprep = require"util.encodings".stringprep.nodeprep; | |
| 5 local uuid = require"util.uuid".generate; | |
| 6 local it = require"util.iterators"; | |
| 7 local gettime = require"socket".gettime; | |
| 8 | |
| 9 local archive = module:open_store("archive2", "archive"); | |
| 10 | |
| 11 -- Support both old and new MUC code | |
| 12 local mod_muc = module:depends"muc"; | |
| 13 local rooms = rawget(mod_muc, "rooms"); | |
| 14 local each_room = rawget(mod_muc, "each_room") or function() return it.values(rooms); end; | |
| 15 if not rooms then | |
| 16 rooms = module:shared"muc/rooms"; | |
| 17 end | |
| 18 local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or | |
| 19 function (jid) | |
| 20 return rooms[jid]; | |
| 21 end | |
| 22 | |
| 23 local function get_room(name) | |
| 24 local jid = name .. '@' .. module.host; | |
| 25 return get_room_from_jid(jid); | |
| 26 end | |
| 27 | |
| 28 module:depends"http"; | |
| 29 | |
| 30 local function template(data) | |
| 31 local _doc = [[ | |
| 32 Like util.template, but deals with plain text | |
| 33 Returns a closure that is called with a table of values | |
| 34 {name} is substituted for values["name"] and is XML escaped | |
| 35 {name!} is substituted without XML escaping | |
| 36 {name?} is optional and is replaced with an empty string if no value exists | |
| 37 ]] | |
| 38 return function(values) | |
| 39 return (data:gsub("{([^!}]-)(%p?)}", function (name, opt) | |
| 40 local value = values[name]; | |
| 41 if value then | |
| 42 if opt ~= "!" then | |
| 43 return st.xml_escape(value); | |
| 44 end | |
| 45 return value; | |
| 46 elseif opt == "?" then | |
| 47 return ""; | |
| 48 end | |
| 49 end)); | |
| 50 end | |
| 51 end | |
| 52 | |
| 53 local base = template[[ | |
| 54 <!DOCTYPE html> | |
| 55 <meta charset="utf-8"> | |
| 56 <title>{title}</title> | |
| 57 <style> | |
| 58 body { margin: 1ex 1em; } | |
| 59 ul { padding: 0; } | |
| 60 li.action dt, li.action dd { display: inline-block; margin-left: 0;} | |
| 61 li.action dd { margin-left: 1ex;} | |
| 62 li { list-style: none; } | |
| 63 li:hover { background: #eee; } | |
| 64 li time { float: right; font-size: small; opacity: 0.2; } | |
| 65 li:hover time { opacity: 1; } | |
| 66 li.join , li.leave { color: green; } | |
| 67 li.join dt, li.leave dt { color: green; } | |
| 68 nav { font-size: x-large; margin: 1ex 2em; } | |
| 69 nav a { text-decoration: none; } | |
| 70 </style> | |
| 71 <h1>{title}</h1> | |
| 72 {body!} | |
| 73 ]] | |
| 74 | |
| 75 local dates_template = template(base{ | |
| 76 title = "Logs for room {room}"; | |
| 77 body = [[ | |
| 78 <base href="{room}/"> | |
| 79 <nav> | |
| 80 <a href="..">↑</a> | |
| 81 </nav> | |
| 82 <ul> | |
| 83 {lines!}</ul> | |
| 84 ]]; | |
| 85 }) | |
| 86 | |
| 87 local date_line_template = template[[ | |
| 88 <li><a href="{date}">{date}</a></li> | |
| 89 ]]; | |
| 90 | |
| 91 local page_template = template(base{ | |
| 92 title = "Logs for room {room} on {date}"; | |
| 93 body = [[ | |
| 94 <nav> | |
| 95 <a class="prev" href="{prev}">←</a> | |
| 96 <a class="up" href="../{room}">↑</a> | |
| 97 <a class="next" href="{next}">→</a> | |
| 98 </nav> | |
| 99 <ul> | |
| 100 {logs!} | |
| 101 </ul> | |
| 102 ]]; | |
| 103 }); | |
| 104 | |
| 105 local line_templates = { | |
| 106 ["message<groupchat"] = template[[ | |
| 107 <li id="{key}" class="{st_name}"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>{body}</dd></dl></li> | |
| 108 ]]; | |
| 109 ["message<groupchat<subject"] = template[[ | |
| 110 <li id="{key}" class="{st_name} action subject"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>changed subject to {subject}</dd></dl></li> | |
| 111 ]]; | |
| 112 ["presence"] = template[[ | |
| 113 <li id="{key}" class="action join"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>joined</dd></dl></li> | |
| 114 ]]; | |
| 115 ["presence<unavailable"] = template[[ | |
| 116 <li id="{key}" class="action leave"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>left</dd></dl></li> | |
| 117 ]]; | |
| 118 }; | |
| 119 | |
| 120 local room_list_template = template(base{ | |
| 121 title = "Rooms on {host}"; | |
| 122 body = [[ | |
| 123 <dl> | |
| 124 {rooms!} | |
| 125 </dl> | |
| 126 ]]; | |
| 127 }); | |
| 128 | |
| 129 local room_item_template = template[[ | |
| 130 <dt><a href="{room}">{name}</a></dt> | |
| 131 <dd>{description?}</dd> | |
| 132 ]]; | |
| 133 | |
| 134 local function public_room(room) | |
| 135 if type(room) == "string" then | |
| 136 room = get_room(room); | |
| 137 end | |
| 138 return room and not room:get_hidden() and not room:get_members_only() and room._data.logging ~= false; | |
| 139 end | |
| 140 | |
| 141 -- FIXME Invent some more efficient API for this | |
| 142 local function dates_page(event, room) | |
| 143 local request, response = event.request, event.response; | |
| 144 | |
| 145 room = nodeprep(room); | |
| 146 if not room or not public_room(room) then return end | |
| 147 | |
| 148 local dates, i = {}, 1; | |
| 149 module:log("debug", "Find all dates with messages"); | |
| 150 local next_day; | |
| 151 repeat | |
| 152 local iter = archive:find(room, { | |
| 153 ["start"] = next_day; | |
| 154 limit = 1; | |
| 155 }) | |
| 156 if not iter then break end | |
| 157 next_day = nil; | |
| 158 for key, message, when in iter do | |
| 159 next_day = datetime.date(when); | |
| 160 dates[i], i = date_line_template{ | |
| 161 date = next_day; | |
| 162 }, i + 1; | |
| 163 next_day = datetime.parse(next_day .. "T23:59:59Z") + 1; | |
| 164 break; | |
| 165 end | |
| 166 until not next_day; | |
| 167 | |
| 168 return dates_template{ | |
| 169 room = room; | |
| 170 lines = table.concat(dates); | |
| 171 }; | |
| 172 end | |
| 173 | |
| 174 local function logs_page(event, path) | |
| 175 local request, response = event.request, event.response; | |
| 176 | |
| 177 local room, date = path:match("^(.-)/(%d%d%d%d%-%d%d%-%d%d)$"); | |
| 178 room = nodeprep(room); | |
| 179 if not room then | |
| 180 return dates_page(event, path); | |
| 181 end | |
| 182 if not public_room(room) then return end | |
| 183 | |
| 184 local logs, i = {}, 1; | |
| 185 local iter, err = archive:find(room, { | |
| 186 ["start"] = datetime.parse(date.."T00:00:00Z"); | |
| 187 ["end"] = datetime.parse(date.."T23:59:59Z"); | |
| 188 limit = math.huge; | |
| 189 -- with = "message<groupchat"; | |
| 190 }); | |
| 191 if not iter then return 500; end | |
| 192 | |
| 193 local templ, typ; | |
| 194 for key, message, when in iter do | |
| 195 templ = message.name; | |
| 196 local typ = message.attr.type; | |
| 197 if typ then templ = templ .. '<' .. typ; end | |
| 198 local subject = message:get_child_text("subject"); | |
| 199 if subject then templ = templ .. '<subject'; end | |
| 200 templ = line_templates[templ]; | |
| 201 if templ then | |
| 202 logs[i], i = templ { | |
| 203 key = key; | |
| 204 time = datetime.time(when); | |
| 205 nick = select(3, jid_split(message.attr.from)); | |
| 206 body = message:get_child_text("body"); | |
| 207 subject = subject; | |
| 208 st_name = message.name; | |
| 209 st_type = message.attr.type; | |
| 210 }, i + 1; | |
| 211 else | |
| 212 module:log("debug", "No template for %s", tostring(message)); | |
| 213 end | |
| 214 end | |
| 215 | |
| 216 local next_when = datetime.parse(date.."T12:00:00Z") + 86400; | |
| 217 local prev_when = datetime.parse(date.."T12:00:00Z") - 86400; | |
| 218 | |
| 219 module:log("debug", "Find next date with messages"); | |
| 220 for key, message, when in archive:find(room, { | |
| 221 ["start"] = datetime.parse(date.."T00:00:00Z") + 86401; | |
| 222 limit = math.huge; | |
| 223 }) do | |
| 224 next_when = when; | |
| 225 module:log("debug", "Next message: %s", datetime.datetime(when)); | |
| 226 break; | |
| 227 end | |
| 228 | |
| 229 module:log("debug", "Find prev date with messages"); | |
| 230 for key, message, when in archive:find(room, { | |
| 231 ["end"] = datetime.parse(date.."T00:00:00Z") - 1; | |
| 232 limit = math.huge; | |
| 233 reverse = true; | |
| 234 }) do | |
| 235 prev_when = when; | |
| 236 module:log("debug", "Previous message: %s", datetime.datetime(when)); | |
| 237 break; | |
| 238 end | |
| 239 | |
| 240 return page_template{ | |
| 241 room = room; | |
| 242 date = date; | |
| 243 logs = table.concat(logs); | |
| 244 next = datetime.date(next_when); | |
| 245 prev = datetime.date(prev_when); | |
| 246 }; | |
| 247 end | |
| 248 | |
| 249 local function list_rooms(event) | |
| 250 local room_list, i = {}, 1; | |
| 251 for room in each_room() do | |
| 252 if public_room(room) then | |
| 253 room_list[i], i = room_item_template { | |
| 254 room = jid_split(room.jid); | |
| 255 name = room:get_name(); | |
| 256 description = room:get_description(); | |
| 257 subject = room:get_subject(); | |
| 258 }, i + 1; | |
| 259 end | |
| 260 end | |
| 261 return room_list_template { | |
| 262 host = module.host; | |
| 263 rooms = table.concat(room_list); | |
| 264 }; | |
| 265 end | |
| 266 | |
| 267 local cache = setmetatable({}, {__mode = 'v'}); | |
| 268 | |
| 269 local function with_cache(f) | |
| 270 return function (event, path) | |
| 271 local request, response = event.request, event.response; | |
| 272 local ckey = path or ""; | |
| 273 local cached = cache[ckey]; | |
| 274 | |
| 275 if cached then | |
| 276 local etag = cached.etag; | |
| 277 local if_none_match = request.headers.if_none_match; | |
| 278 if etag == if_none_match then | |
| 279 module:log("debug", "Client cache hit"); | |
| 280 return 304; | |
| 281 end | |
| 282 module:log("debug", "Server cache hit"); | |
| 283 response.headers.etag = etag; | |
| 284 return cached[1]; | |
| 285 end | |
| 286 | |
| 287 local start = gettime(); | |
| 288 local render = f(event, path); | |
| 289 module:log("debug", "Rendering took %dms", math.floor( (gettime() - start) * 1000 + 0.5)); | |
| 290 | |
| 291 if type(render) == "string" then | |
| 292 local etag = uuid(); | |
| 293 cached = { render, etag = etag, date = datetime.date() }; | |
| 294 response.headers.etag = etag; | |
| 295 cache[ckey] = cached; | |
| 296 end | |
| 297 | |
| 298 return render; | |
| 299 end | |
| 300 end | |
| 301 | |
| 302 -- How is cache invalidation a hard problem? ;) | |
| 303 module:hook("muc-broadcast-message", function (event) | |
| 304 local room = event.room; | |
| 305 local room_name = jid_split(room.jid); | |
| 306 local today = datetime.date(); | |
| 307 cache[ room_name .. "/" .. today ] = nil; | |
| 308 if cache[room_name] and cache[room_name].date ~= today then | |
| 309 cache[room_name] = nil; | |
| 310 end | |
| 311 end); | |
| 312 | |
| 313 module:log("info", module:http_url()); | |
| 314 module:provides("http", { | |
| 315 route = { | |
| 316 ["GET /"] = list_rooms; | |
| 317 ["GET /*"] = with_cache(logs_page); | |
| 318 }; | |
| 319 }); | |
| 320 |