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 |