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