comparison mod_muc_log_http/mod_muc_log_http.lua @ 94:941fd7d8b9b2

mod_muc_log: split into mod_muc_log and mod_muc_log_http mod_muc_log: should be enabled per muc component which should log! mod_muc_log_http: handle /me messages, add previous, next day links to day view, add link to speeqe.com to directly join the room, make the window recalculate the content div size, scrollbars are only shown when needed
author Thilo Cestonaro <thilo@cestona.ro>
date Tue, 17 Nov 2009 21:19:17 +0100
parents
children e3c09996cf7b
comparison
equal deleted inserted replaced
93:611d16867410 94:941fd7d8b9b2
1 -- Copyright (C) 2009 Thilo Cestonaro
2 --
3 -- This project is MIT/X11 licensed. Please see the
4 -- COPYING file in the source package for more information.
5 --
6 local prosody = prosody;
7 local tabSort = table.sort;
8 local tonumber = _G.tonumber;
9 local tostring = _G.tostring;
10 local strformat = string.format;
11 local splitJid = require "util.jid".split;
12 local config_get = require "core.configmanager".get;
13 local httpserver = require "net.httpserver";
14 local datamanager = require "util.datamanager";
15 local data_load, data_getpath = datamanager.load, datamanager.getpath;
16 local datastore = "muc_log";
17 local muc_hosts = {};
18 local config = nil;
19
20 local lom = require "lxp.lom";
21
22 --[[ LuaFileSystem
23 * URL: http://www.keplerproject.org/luafilesystem/index.html
24 * Install: luarocks install luafilesystem
25 * ]]
26 local lfs = require "lfs";
27
28
29 --[[
30 * Default templates for the html output.
31 ]]--
32 local html = {};
33 html.doc = [[<html>
34 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
35 <head>
36 <title>muc_log</title>
37 </head>
38 <script type="text/javascript"><!--
39 function showHide(name) {
40 var eles = document.getElementsByName(name);
41 for (var i = 0; i < eles.length; i++) {
42 eles[i].style.display = eles[i].style.display != "none" ? "none" : "";
43 }
44
45 }
46 --></script>
47 <style type="text/css">
48 <!--
49 .timestuff {color: #AAAAAA; text-decoration: none;}
50 .muc_join {color: #009900; font-style: italic;}
51 .muc_leave {color: #009900; font-style: italic;}
52 .muc_statusChange {color: #009900; font-style: italic;}
53 .muc_title {color: #BBBBBB; font-size: 32px;}
54 .muc_titleChange {color: #009900; font-style: italic;}
55 .muc_kick {color: #009900; font-style: italic;}
56 .muc_bann {color: #009900; font-style: italic;}
57 .muc_msg_nick {color: #0000AA;}
58 .muc_msg_me {color: #0000AA;}
59 .join_link {font-height: 9px;}
60 //-->
61 </style>
62 <body>
63 ###BODY_STUFF###
64 </body>
65 </html>]];
66
67 html.components = {};
68 html.components.bit = [[<a href="###COMPONENT###/">###COMPONENT###</a><br />]]
69 html.components.body = [[<h2>MUC hosts available on this server:</h2><hr /><p>
70 ###COMPONENTS_STUFF###
71 </p><hr />]];
72
73 html.rooms = {};
74 html.rooms.bit = [[<a href="###ROOM###/">###ROOM###</a><br />]]
75 html.rooms.body = [[<h2>Rooms hosted on MUC host: ###COMPONENT###</h2><hr /><p>
76 ###ROOMS_STUFF###
77 </p><hr />]];
78
79 html.days = {};
80 html.days.bit = [[<a href="###BARE_DAY###/">20###YEAR###/###MONTH###/###DAY###</a><br />]];
81 html.days.body = [[<h2>available logged days of room: ###JID###</h2><hr /><p>
82 ###DAYS_STUFF###
83 </p><hr />]];
84
85 html.day = {};
86 html.day.title = [[Subject: <font class="muc_title">###TITLE###</font>]];
87 html.day.time = [[<a name="###TIME###" href="####TIME###" class="timestuff">[###TIME###]</a> ]]; -- the one ####TIME### need to stay! it will evaluate to e.g. #09:10:56 which is an anker then
88 html.day.presence = {};
89 html.day.presence.join = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_join"> *** ###NICK### joins the room</font><br /></div>]];
90 html.day.presence.leave = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_leave"> *** ###NICK### leaves the room</font><br /></div>]];
91 html.day.presence.statusText = [[ and his status message is "###STATUS###"]];
92 html.day.presence.statusChange = [[<div name="status" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_statusChange"> *** ###NICK### shows now as "###SHOW###"###STATUS_STUFF###</font><br /></div>]];
93 html.day.message = [[###TIME_STUFF###<font class="muc_msg_nick">&lt;###NICK###&gt;</font> ###MSG###<br />]];
94 html.day.message_me = [[###TIME_STUFF###<font class="muc_msg_me">*###NICK### ###MSG###</font><br />]];
95 html.day.titleChange = [[###TIME_STUFF###<font class="muc_titleChange"> *** ###NICK### changed the title to "###TITLE###"</font><br />]];
96 html.day.reason = [[, the reason was "###REASON###"]]
97 html.day.kick = [[###TIME_STUFF###<font class="muc_kick"> *** ###VICTIM### got kicked###REASON_STUFF###</font><br />]];
98 html.day.bann = [[###TIME_STUFF###<font class="muc_bann"> *** ###VICTIM### got banned###REASON_STUFF###</font><br />]];
99 html.day.day_link = [[<a href="../###DAY###/">###TEXT###</a>]]
100 html.day.body = [[<h2>Logs of room ###JID### of 20###YEAR###/###MONTH###/###DAY###</h2>
101 <p>###TITLE_STUFF###</p>
102 <font class="join_link"><a href="http://speeqe.com/room/###JID###/" target="_blank">Join room now via speeqe.com!</a></font><br />
103 ###PREVIOUS_LINK### ###NEXT_LINK###<br />
104 <input type="checkbox" onclick="showHide('joinLeave')" ###JOIN_CHECKED###/>show/hide joins and Leaves</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
105 <input type="checkbox" onclick="showHide('status')" ###STATUS_CHECKED###/>show/hide status changes</button>
106 <hr /><div id="main" style="overflow: auto;">
107 ###DAY_STUFF###
108 </div><hr />
109 <script><!--
110 window.captureEvents(Event.RESIZE | Event.LOAD);
111 window.onresize = resize;
112 window.onload = resize;
113 function resize(e) {
114 var ele = document.getElementById("main");
115 ele.style.height = window.innerHeight - ele.offsetTop - 25;
116 }
117
118 --></script>]];
119
120 local function checkDatastorePathExists(node, host, today, create)
121 create = create or false;
122 local path = data_getpath(node, host, datastore, "dat", true);
123 path = path:gsub("/[^/]*$", "");
124
125 -- check existance
126 local attributes, err = lfs.attributes(path);
127 if attributes == nil or attributes.mode ~= "directory" then
128 module:log("warn", "muc_log folder isn't a folder: %s", path);
129 return false;
130 end
131
132 attributes, err = lfs.attributes(path .. "/" .. today);
133 if attributes == nil then
134 if create then
135 return lfs.mkdir(path .. "/" .. today);
136 else
137 return false;
138 end
139 elseif attributes.mode == "directory" then
140 return true;
141 end
142 return false;
143 end
144
145 function createDoc(body)
146 if body then
147 return html.doc:gsub("###BODY_STUFF###", body);
148 end
149 end
150
151 local function htmlEscape(t)
152 t = t:gsub("<", "&lt;");
153 t = t:gsub(">", "&gt;");
154 t = t:gsub("(http://[%a%d@%.:/&%?=%-_#]+)", [[<a href="%1">%1</a>]]);
155 t = t:gsub("\n", "<br />");
156 return t;
157 end
158
159 function splitUrl(url)
160 local tmp = url:sub(string.len("/muc_log/") + 1);
161 local day = nil;
162 local room = nil;
163 local component = nil;
164 local at = nil;
165 local slash = nil;
166 local slash2 = nil;
167
168 slash = tmp:find("/");
169 if slash then
170 component = tmp:sub(1, slash - 1);
171 if tmp:len() > slash then
172 room = tmp:sub(slash + 1);
173 slash = room:find("/");
174 if slash then
175 tmp = room;
176 room = tmp:sub(1, slash - 1);
177 if tmp:len() > slash then
178 day = tmp:sub(slash + 1);
179 slash = day:find("/");
180 if slash then
181 day = day:sub(1, slash - 1);
182 end
183 end
184 end
185 end
186 end
187
188 return room, component, day;
189 end
190
191 local function generateComponentListSiteContent()
192 local components = "";
193 for component,muc_host in pairs(muc_hosts) do
194 components = components .. html.components.bit:gsub("###COMPONENT###", component);
195 end
196 if components ~= "" then
197 return html.components.body:gsub("###COMPONENTS_STUFF###", components);
198 end
199 end
200
201 local function generateRoomListSiteContent(component)
202 local rooms = "";
203 if prosody.hosts[component] and prosody.hosts[component].muc ~= nil then
204 for jid, room in pairs(prosody.hosts[component].muc.rooms) do
205 local node = splitJid(jid);
206 if not room._data.hidden and node then
207 rooms = rooms .. html.rooms.bit:gsub("###ROOM###", node):gsub("###COMPONENT###", component);
208 end
209 end
210 if rooms ~= "" then
211 return html.rooms.body:gsub("###ROOMS_STUFF###", rooms):gsub("###COMPONENT###", component);
212 end
213 end
214 end
215
216 local function generateDayListSiteContentByRoom(bareRoomJid)
217 local days = "";
218 local arrDays = {};
219 local tmp;
220 local node, host, resource = splitJid(bareRoomJid);
221 local path = data_getpath(node, host, datastore);
222 local room = nil;
223 local attributes = nil;
224
225 path = path:gsub("/[^/]*$", "");
226 attributes = lfs.attributes(path);
227 if muc_hosts[host] and prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bareRoomJid] ~= nil then
228 room = prosody.hosts[host].muc.rooms[bareRoomJid];
229 if room._data.hidden then
230 room = nil
231 end
232 end
233 if attributes ~= nil and room ~= nil then
234 for file in lfs.dir(path) do
235 local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)");
236 if year ~= nil and month ~= nil and day ~= nil and
237 year ~= "" and month ~= "" and day ~= ""
238 then
239 arrDays[#arrDays + 1] = {bare=file, year=year, month=month, day=day};
240 end
241 end
242 tabSort(arrDays, function(a,b)
243 return a.bare < b.bare;
244 end);
245 for _, date in pairs(arrDays) do
246 tmp = html.days.bit;
247 tmp = tmp:gsub("###ROOM###", node):gsub("###COMPONENT###", host);
248 tmp = tmp:gsub("###BARE_DAY###", date.bare);
249 tmp = tmp:gsub("###YEAR###", date.year):gsub("###MONTH###", date.month):gsub("###DAY###", date.day);
250 days = tmp .. days;
251 end
252 end
253
254 if days ~= "" then
255 tmp = html.days.body:gsub("###DAYS_STUFF###", days);
256 return tmp:gsub("###JID###", bareRoomJid);
257 end
258 end
259
260 local function parseIqStanza(stanza, timeStuff, nick)
261 local text = nil;
262 local victim = nil;
263 if(stanza.attr.type == "set") then
264 for _,tag in ipairs(stanza) do
265 if tag.tag == "query" then
266 for _,item in ipairs(tag) do
267 if item.tag == "item" and item.attr.nick ~= nil and tostring(item.attr.role) == 'none' then
268 victim = item.attr.nick;
269 for _,reason in ipairs(item) do
270 if reason.tag == "reason" then
271 text = reason[1];
272 break;
273 end
274 end
275 break;
276 end
277 end
278 break;
279 end
280 end
281 if victim ~= nil then
282 if text ~= nil then
283 text = html.day.reason:gsub("###REASON###", htmlEscape(text));
284 else
285 text = "";
286 end
287 return html.day.kick:gsub("###TIME_STUFF###", timeStuff):gsub("###VICTIM###", victim):gsub("###REASON_STUFF###", text);
288 end
289 end
290 return;
291 end
292
293 local function parsePresenceStanza(stanza, timeStuff, nick)
294 local ret = "";
295 local showJoin = "block"
296
297 if config and not config.showJoin then
298 showJoin = "none";
299 end
300
301 if stanza.attr.type == nil then
302 local showStatus = "block"
303 if config and not config.showStatus then
304 showStatus = "none";
305 end
306 local show, status = nil, "";
307 local alreadyJoined = false;
308 for _, tag in ipairs(stanza) do
309 if tag.tag == "alreadyJoined" then
310 alreadyJoined = true;
311 elseif tag.tag == "show" then
312 show = tag[1];
313 elseif tag.tag == "status" then
314 status = tag[1];
315 end
316 end
317 if alreadyJoined == true then
318 if show == nil then
319 show = "online";
320 end
321 ret = html.day.presence.statusChange:gsub("###TIME_STUFF###", timeStuff);
322 if status ~= "" then
323 status = html.day.presence.statusText:gsub("###STATUS###", htmlEscape(status));
324 end
325 ret = ret:gsub("###SHOW###", show):gsub("###NICK###", nick):gsub("###SHOWHIDE###", showStatus):gsub("###STATUS_STUFF###", status);
326 else
327 ret = html.day.presence.join:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick);
328 end
329 elseif stanza.attr.type ~= nil and stanza.attr.type == "unavailable" then
330
331 ret = html.day.presence.leave:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick);
332 end
333 return ret;
334 end
335
336 local function parseMessageStanza(stanza, timeStuff, nick)
337 local body, title, ret = nil, nil, "";
338
339 for _,tag in ipairs(stanza) do
340 if tag.tag == "body" then
341 body = tag[1];
342 if nick ~= nil then
343 break;
344 end
345 elseif tag.tag == "nick" and nick == nil then
346 nick = htmlEscape(tag[1]);
347 if body ~= nil or title ~= nil then
348 break;
349 end
350 elseif tag.tag == "subject" then
351 title = tag[1];
352 if nick ~= nil then
353 break;
354 end
355 end
356 end
357 if nick ~= nil and body ~= nil then
358 body = htmlEscape(body);
359 local me = body:find("^/me");
360 local template = "";
361 if not me then
362 template = html.day.message;
363 else
364 template = html.day.message_me;
365 body = body:gsub("^/me ", "");
366 end
367 ret = template:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###MSG###", body);
368 elseif nick ~= nil and title ~= nil then
369 title = htmlEscape(title);
370 ret = html.day.titleChange:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###TITLE###", title);
371 end
372 return ret;
373 end
374
375 local function incrementDay(bare_day)
376 local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)");
377 local leapyear = false;
378 module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year))
379
380 day = tonumber(day);
381 month = tonumber(month);
382 year = tonumber(year);
383
384 if year%4 == 0 and year%100 == 0 then
385 if year%400 == 0 then
386 leapyear = true;
387 else
388 leapyear = false; -- turn of the century but not a leapyear
389 end
390 elseif year%4 == 0 then
391 leapyear = true;
392 end
393
394 if (month == 2 and leapyear and day + 1 > 29) or
395 (month == 2 and not leapyear and day + 1 > 28) or
396 (month < 8 and month%2 == 1 and day + 1 > 31) or
397 (month < 8 and month%2 == 0 and day + 1 > 30) or
398 (month >= 8 and month%2 == 0 and day + 1 > 31) or
399 (month >= 8 and month%2 == 1 and day + 1 > 30)
400 then
401 if month + 1 > 12 then
402 year = year + 1;
403 else
404 month = month + 1;
405 end
406 else
407 day = day + 1;
408 end
409 return strformat("%.02d%.02d%.02d", year, month, day);
410 end
411
412 local function findNextDay(bareRoomJid, bare_day)
413 local node, host, resource = splitJid(bareRoomJid);
414 local day = incrementDay(bare_day);
415 local max_trys = 7;
416
417 module:log("debug", day);
418 while(not checkDatastorePathExists(node, host, day, false)) do
419 max_trys = max_trys - 1;
420 if max_trys == 0 then
421 break;
422 end
423 day = incrementDay(day);
424 end
425 if max_trys == 0 then
426 return nil;
427 else
428 return day;
429 end
430 end
431
432 local function decrementDay(bare_day)
433 local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)");
434 module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year))
435 day = tonumber(day);
436 month = tonumber(month);
437 year = tonumber(year);
438
439 if day - 1 == 0 then
440 if month - 1 == 0 then
441 year = year - 1;
442 else
443 month = month - 1;
444 end
445 else
446 day = day - 1;
447 end
448 return strformat("%.02d%.02d%.02d", year, month, day);
449 end
450
451 local function findPreviousDay(bareRoomJid, bare_day)
452 local node, host, resource = splitJid(bareRoomJid);
453 local day = decrementDay(bare_day);
454 local max_trys = 7;
455 module:log("debug", day);
456 while(not checkDatastorePathExists(node, host, day, false)) do
457 max_trys = max_trys - 1;
458 if max_trys == 0 then
459 break;
460 end
461 day = decrementDay(day);
462 end
463 if max_trys == 0 then
464 return nil;
465 else
466 return day;
467 end
468 end
469
470 local function parseDay(bareRoomJid, roomSubject, bare_day)
471 local ret = "";
472 local year;
473 local month;
474 local day;
475 local tmp;
476 local node, host, resource = splitJid(bareRoomJid);
477 local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)");
478 local previousDay = findPreviousDay(bareRoomJid, bare_day);
479 local nextDay = findNextDay(bareRoomJid, bare_day);
480
481 if bare_day ~= nil then
482 local data = data_load(node, host, datastore .. "/" .. bare_day);
483 if data ~= nil then
484 for i=1, #data, 1 do
485 local stanza = lom.parse(data[i]);
486 if stanza ~= nil and stanza.attr ~= nil and stanza.attr.time ~= nil then
487 local timeStuff = html.day.time:gsub("###TIME###", stanza.attr.time);
488 if stanza[1] ~= nil then
489 local nick;
490 local tmp;
491
492 -- grep nick from "from" resource
493 if stanza[1].attr.from ~= nil then -- presence and messages
494 nick = htmlEscape(stanza[1].attr.from:match("/(.+)$"));
495 elseif stanza[1].attr.to ~= nil then -- iq
496 nick = htmlEscape(stanza[1].attr.to:match("/(.+)$"));
497 end
498
499 if stanza[1].tag == "presence" and nick ~= nil then
500 tmp = parsePresenceStanza(stanza[1], timeStuff, nick);
501 elseif stanza[1].tag == "message" then
502 tmp = parseMessageStanza(stanza[1], timeStuff, nick);
503 elseif stanza[1].tag == "iq" then
504 tmp = parseIqStanza(stanza[1], timeStuff, nick);
505 else
506 module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bareRoomJid, year .. "/" .. month .. "/" .. day);
507 end
508 if tmp ~= nil then
509 ret = ret .. tmp
510 tmp = nil;
511 end
512 end
513 end
514 end
515 end
516 if ret ~= "" then
517 if nextDay then
518 nextDay = html.day.day_link:gsub("###DAY###", nextDay):gsub("###TEXT###", "next day &gt;&gt;")
519 end
520 if previousDay then
521 previousDay = html.day.day_link:gsub("###DAY###", previousDay):gsub("###TEXT###", "&lt;&lt; previous day");
522 end
523 tmp = html.day.body:gsub("###DAY_STUFF###", ret):gsub("###JID###", bareRoomJid);
524 tmp = tmp:gsub("###YEAR###", year):gsub("###MONTH###", month):gsub("###DAY###", day);
525 tmp = tmp:gsub("###TITLE_STUFF###", html.day.title:gsub("###TITLE###", roomSubject));
526 tmp = tmp:gsub("###STATUS_CHECKED###", config.showStatus and "checked='checked'" or "");
527 tmp = tmp:gsub("###JOIN_CHECKED###", config.showJoin and "checked='checked'" or "");
528 tmp = tmp:gsub("###NEXT_LINK###", nextDay or "");
529 tmp = tmp:gsub("###PREVIOUS_LINK###", previousDay or "");
530
531 return tmp;
532 end
533 end
534 end
535
536 function handle_request(method, body, request)
537 local node, host, day = splitUrl(request.url.path);
538
539 if node ~= nil and host ~= nil then
540 local bare = node .. "@" .. host;
541 if prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil then
542 if prosody.hosts[host].muc.rooms[bare] ~= nil then
543 local room = prosody.hosts[host].muc.rooms[bare];
544 if day == nil then
545 return createDoc(generateDayListSiteContentByRoom(bare));
546 else
547 local subject = ""
548 if room._data ~= nil and room._data.subject ~= nil then
549 subject = room._data.subject;
550 end
551 return createDoc(parseDay(bare, subject, day));
552 end
553 else
554 return createDoc(generateRoomListSiteContent(host));
555 end
556 else
557 return createDoc(generateComponentListSiteContent());
558 end
559 elseif host ~= nil then
560 return createDoc(generateRoomListSiteContent(host));
561 else
562 return createDoc(generateComponentListSiteContent());
563 end
564 return;
565 end
566
567 function module.load()
568 config = config_get("*", "core", "muc_log_http") or {};
569 if config.showStatus == nil then
570 config.showStatus = true;
571 end
572 if config.showJoin == nil then
573 config.showJoin = true;
574 end
575 httpserver.new_from_config({ config.http_port or true }, handle_request, { base = "muc_log" });
576
577 for jid, host in pairs(prosody.hosts) do
578 if host.muc then
579 local enabledModules = config_get(jid, "core", "modules_enabled");
580 if enabledModules then
581 for _,mod in ipairs(enabledModules) do
582 if(mod == "muc_log") then
583 module:log("debug", "component: %s", tostring(jid));
584 muc_hosts[jid] = true;
585 break;
586 end
587 end
588 end
589 end
590 end
591 end
592
593 function module.unload()
594 muc_hosts = nil;
595 end
596
597 module:add_event_hook("component-activated", function(component, config)
598 if config.core and config.core.modules_enabled then
599 for _,mod in ipairs(config.core.modules_enabled) do
600 if(mod == "muc_log") then
601 module:log("debug", "component: %s", tostring(component));
602 muc_hosts[component] = true;
603 break;
604 end
605 end
606 end
607 end);