comparison mod_muc_log/mod_muc_log.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 d6521ebea967
children 0491aa849c91
comparison
equal deleted inserted replaced
93:611d16867410 94:941fd7d8b9b2
2 -- 2 --
3 -- This project is MIT/X11 licensed. Please see the 3 -- This project is MIT/X11 licensed. Please see the
4 -- COPYING file in the source package for more information. 4 -- COPYING file in the source package for more information.
5 -- 5 --
6 local prosody = prosody; 6 local prosody = prosody;
7 local tabSort = table.sort; 7 local tostring = _G.tostring;
8 local splitJid = require "util.jid".split; 8 local splitJid = require "util.jid".split;
9 local bareJid = require "util.jid".bare;
10 local config_get = require "core.configmanager".get; 9 local config_get = require "core.configmanager".get;
11 local httpserver = require "net.httpserver";
12 local serialize = require "util.serialization".serialize;
13 local datamanager = require "util.datamanager"; 10 local datamanager = require "util.datamanager";
14 local data_load, data_store, data_getpath = datamanager.load, datamanager.store, datamanager.getpath; 11 local data_load, data_store, data_getpath = datamanager.load, datamanager.store, datamanager.getpath;
15 local datastore = "muc_log"; 12 local datastore = "muc_log";
16 local muc_hosts = {}; 13 -- local mod_host = module:get_host();
17 local config = nil; 14 local config = nil;
18
19 15
20 --[[ LuaFileSystem 16 --[[ LuaFileSystem
21 * URL: http://www.keplerproject.org/luafilesystem/index.html 17 * URL: http://www.keplerproject.org/luafilesystem/index.html
22 * Install: luarocks install luafilesystem 18 * Install: luarocks install luafilesystem
23 * ]] 19 * ]]
24 local lfs = require "lfs"; 20 local lfs = require "lfs";
25
26 local lom = require "lxp.lom";
27
28
29 --[[ 21 --[[
30 * Default templates for the html output. 22 local function checkDatastorePathExists(node, host, today, create)
31 ]]-- 23 create = create or false;
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 //-->
59 </style>
60 <body>
61 ###BODY_STUFF###
62 </body>
63 </html>]];
64
65 html.components = {};
66 html.components.bit = [[<a href="###COMPONENT###/">###COMPONENT###</a><br />]]
67 html.components.body = [[<h2>MUC hosts available on this server:</h2><hr /><p>
68 ###COMPONENTS_STUFF###
69 </p><hr />]];
70
71 html.rooms = {};
72 html.rooms.bit = [[<a href="###ROOM###/">###ROOM###</a><br />]]
73 html.rooms.body = [[<h2>Rooms hosted on MUC host: ###COMPONENT###</h2><hr /><p>
74 ###ROOMS_STUFF###
75 </p><hr />]];
76
77 html.days = {};
78 html.days.bit = [[<a href="###BARE_DAY###/">20###YEAR###/###MONTH###/###DAY###</a><br />]];
79 html.days.body = [[<h2>available logged days of room: ###JID###</h2><hr /><p>
80 ###DAYS_STUFF###
81 </p><hr />]];
82
83 html.day = {};
84 html.day.title = [[Subject: <font class="muc_title">###TITLE###</font>]];
85 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
86 html.day.presence = {};
87 html.day.presence.join = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_join"> *** ###NICK### joins the room</font><br /></div>]];
88 html.day.presence.leave = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_leave"> *** ###NICK### leaves the room</font><br /></div>]];
89 html.day.presence.statusText = [[ and his status message is "###STATUS###"]];
90 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>]];
91 html.day.message = [[###TIME_STUFF###<font class="muc_msg_nick">&lt;###NICK###&gt;</font> ###MSG###<br />]];
92 html.day.titleChange = [[###TIME_STUFF###<font class="muc_titleChange"> *** ###NICK### changed the title to "###TITLE###"</font><br />]];
93 html.day.reason = [[, the reason was "###REASON###"]]
94 html.day.kick = [[###TIME_STUFF###<font class="muc_kick"> *** ###VICTIM### got kicked###REASON_STUFF###</font><br />]];
95 html.day.bann = [[###TIME_STUFF###<font class="muc_bann"> *** ###VICTIM### got banned###REASON_STUFF###</font><br />]];
96 html.day.body = [[<h2>room ###JID### logging of 20###YEAR###/###MONTH###/###DAY###</h2>
97 <p>###TITLE_STUFF###</p>
98 <input type="checkbox" onclick="showHide('joinLeave')" ###JOIN_CHECKED###/>show/hide joins and Leaves</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
99 <input type="checkbox" onclick="showHide('status')" ###STATUS_CHECKED###/>show/hide status changes</button>
100 <hr /><div id="main" style="overflow: scroll;">
101 ###DAY_STUFF###
102 </div><hr />
103 <script><!--
104 var ele = document.getElementById("main");
105 ele.style.height = window.innerHeight - ele.offsetTop - 25;
106 --></script>]];
107
108 html.help = [[
109 MUC logging is not configured correctly.<br />
110 Here is a example config:<br />
111 Component "rooms.example.com" "muc"<br />
112 &nbsp;&nbsp;modules_enabled = {<br />
113 &nbsp;&nbsp;&nbsp;&nbsp;"muc_log";<br />
114 &nbsp;&nbsp;}<br />
115 &nbsp;&nbsp;muc_log = {<br />
116 &nbsp;&nbsp;&nbsp;&nbsp;folder = "/opt/local/var/log/prosody/rooms";<br />
117 &nbsp;&nbsp;&nbsp;&nbsp;http_port = "/opt/local/var/log/prosody/rooms";<br />
118 &nbsp;&nbsp;}<br />
119 ]];
120
121 local function ensureDatastorePathExists(node, host, today)
122 local path = data_getpath(node, host, datastore, "dat", true); 24 local path = data_getpath(node, host, datastore, "dat", true);
123 path = path:gsub("/[^/]*$", ""); 25 path = path:gsub("/[^/]*$", "");
124 26
125 -- check existance 27 -- check existance
126 local attributes, err = lfs.attributes(path); 28 local attributes, err = lfs.attributes(path);
129 return false; 31 return false;
130 end 32 end
131 33
132 attributes, err = lfs.attributes(path .. "/" .. today); 34 attributes, err = lfs.attributes(path .. "/" .. today);
133 if attributes == nil then 35 if attributes == nil then
134 return lfs.mkdir(path .. "/" .. today); 36 if create then
37 return lfs.mkdir(path .. "/" .. today);
38 else
39 return false;
40 end
135 elseif attributes.mode == "directory" then 41 elseif attributes.mode == "directory" then
136 return true; 42 return true;
137 end 43 end
138 return false; 44 return false;
139 end 45 end
146 (stanza.name == "message" and tostring(stanza.attr.type) == "groupchat") 52 (stanza.name == "message" and tostring(stanza.attr.type) == "groupchat")
147 then 53 then
148 local node, host, resource = splitJid(stanza.attr.to); 54 local node, host, resource = splitJid(stanza.attr.to);
149 if node ~= nil and host ~= nil then 55 if node ~= nil and host ~= nil then
150 local bare = node .. "@" .. host; 56 local bare = node .. "@" .. host;
151 if muc_hosts[host] and prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bare] ~= nil then 57 if host == mod_host and prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bare] ~= nil then
152 local room = prosody.hosts[host].muc.rooms[bare] 58 local room = prosody.hosts[host].muc.rooms[bare]
153 local today = os.date("%y%m%d"); 59 local today = os.date("%y%m%d");
154 local now = os.date("%X") 60 local now = os.date("%X")
155 local mucTo = nil 61 local mucTo = nil
156 local mucFrom = nil; 62 local mucFrom = nil;
186 break; 92 break;
187 end 93 end
188 end 94 end
189 end 95 end
190 96
191 if (mucFrom ~= nil or mucTo ~= nil) and ensureDatastorePathExists(node, host, today) then 97 if (mucFrom ~= nil or mucTo ~= nil) and checkDatastorePathExists(node, host, today, true) then
192 local data = data_load(node, host, datastore .. "/" .. today); 98 local data = data_load(node, host, datastore .. "/" .. today);
193 local realFrom = stanza.attr.from; 99 local realFrom = stanza.attr.from;
194 local realTo = stanza.attr.to; 100 local realTo = stanza.attr.to;
195 101
196 if data == nil then 102 if data == nil then
217 data_store(node, host, datastore .. "/" .. today, data); 123 data_store(node, host, datastore .. "/" .. today, data);
218 end 124 end
219 end 125 end
220 end 126 end
221 end 127 end
222 return;
223 end 128 end
224 129
225 function createDoc(body) 130 module:hook("message/bare", logIfNeeded, 500);
226 return html.doc:gsub("###BODY_STUFF###", body or ""); 131 module:hook("iq/bare", logIfNeeded, 500);
227 end 132 module:hook("presence/full", logIfNeeded, 500);
228
229 local function htmlEscape(t)
230 t = t:gsub("<", "&lt;");
231 t = t:gsub(">", "&gt;");
232 t = t:gsub("(http://[%a%d@%.:/&%?=%-_#]+)", [[<a href="%1">%1</a>]]);
233 t = t:gsub("\n", "<br />");
234 -- TODO do any html escaping stuff ...
235 return t;
236 end
237
238 function splitUrl(url)
239 local tmp = url:sub(string.len("/muc_log/") + 1);
240 local day = nil;
241 local room = nil;
242 local component = nil;
243 local at = nil;
244 local slash = nil;
245 local slash2 = nil;
246
247 slash = tmp:find("/");
248 if slash then
249 component = tmp:sub(1, slash - 1);
250 if tmp:len() > slash then
251 room = tmp:sub(slash + 1);
252 slash = room:find("/");
253 if slash then
254 tmp = room;
255 room = tmp:sub(1, slash - 1);
256 if tmp:len() > slash then
257 day = tmp:sub(slash + 1);
258 slash = day:find("/");
259 if slash then
260 day = day:sub(1, slash - 1);
261 end
262 end
263 end
264 end
265 end
266
267 return room, component, day;
268 end
269
270 local function generateComponentListSiteContent()
271 local components = "";
272 for component,muc_host in pairs(muc_hosts) do
273 components = components .. html.components.bit:gsub("###COMPONENT###", component);
274 end
275
276 return html.components.body:gsub("###COMPONENTS_STUFF###", components);
277 end
278
279 local function generateRoomListSiteContent(component)
280 local rooms = "";
281 if prosody.hosts[component] and prosody.hosts[component].muc ~= nil then
282 for jid, room in pairs(prosody.hosts[component].muc.rooms) do
283 local node = splitJid(jid);
284 if not room._data.hidden and node then
285 rooms = rooms .. html.rooms.bit:gsub("###ROOM###", node):gsub("###COMPONENT###", component);
286 end
287 end
288 return html.rooms.body:gsub("###ROOMS_STUFF###", rooms):gsub("###COMPONENT###", component);
289 end
290 return generateComponentListSiteContent(); -- fallback
291 end
292
293 local function generateDayListSiteContentByRoom(bareRoomJid)
294 local days = "";
295 local arrDays = {};
296 local tmp;
297 local node, host, resource = splitJid(bareRoomJid);
298 local path = data_getpath(node, host, datastore);
299 local room = nil;
300 local attributes = nil;
301
302 path = path:gsub("/[^/]*$", "");
303 attributes = lfs.attributes(path);
304 if muc_hosts[host] and prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bareRoomJid] ~= nil then
305 room = prosody.hosts[host].muc.rooms[bareRoomJid];
306 if room._data.hidden then
307 room = nil
308 end
309 end
310 if attributes ~= nil and room ~= nil then
311 for file in lfs.dir(path) do
312 local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)");
313 if year ~= nil and month ~= nil and day ~= nil and
314 year ~= "" and month ~= "" and day ~= ""
315 then
316 arrDays[#arrDays + 1] = {bare=file, year=year, month=month, day=day};
317 end
318 end
319 tabSort(arrDays, function(a,b)
320 return a.bare < b.bare;
321 end);
322 for _, date in pairs(arrDays) do
323 tmp = html.days.bit;
324 tmp = tmp:gsub("###ROOM###", node):gsub("###COMPONENT###", host);
325 tmp = tmp:gsub("###BARE_DAY###", date.bare);
326 tmp = tmp:gsub("###YEAR###", date.year):gsub("###MONTH###", date.month):gsub("###DAY###", date.day);
327 days = tmp .. days;
328 end
329 end
330
331 if days ~= "" then
332 tmp = html.days.body:gsub("###DAYS_STUFF###", days);
333 return tmp:gsub("###JID###", bareRoomJid);
334 else
335 return generateRoomListSiteContent(host); -- fallback
336 end
337 end
338
339 local function parseIqStanza(stanza, timeStuff, nick)
340 local text = nil;
341 local victim = nil;
342 if(stanza.attr.type == "set") then
343 for _,tag in ipairs(stanza) do
344 if tag.tag == "query" then
345 for _,item in ipairs(tag) do
346 if item.tag == "item" and item.attr.nick ~= nil and tostring(item.attr.role) == 'none' then
347 victim = item.attr.nick;
348 for _,reason in ipairs(item) do
349 if reason.tag == "reason" then
350 text = reason[1];
351 break;
352 end
353 end
354 break;
355 end
356 end
357 break;
358 end
359 end
360 if victim ~= nil then
361 if text ~= nil then
362 text = html.day.reason:gsub("###REASON###", htmlEscape(text));
363 else
364 text = "";
365 end
366 return html.day.kick:gsub("###TIME_STUFF###", timeStuff):gsub("###VICTIM###", victim):gsub("###REASON_STUFF###", text);
367 end
368 end
369 return;
370 end
371
372 local function parsePresenceStanza(stanza, timeStuff, nick)
373 local ret = "";
374 local showJoin = "block"
375
376 if config and not config.showJoin then
377 showJoin = "none";
378 end
379
380 if stanza.attr.type == nil then
381 local showStatus = "block"
382 if config and not config.showStatus then
383 showStatus = "none";
384 end
385 local show, status = nil, "";
386 local alreadyJoined = false;
387 for _, tag in ipairs(stanza) do
388 if tag.tag == "alreadyJoined" then
389 alreadyJoined = true;
390 elseif tag.tag == "show" then
391 show = tag[1];
392 elseif tag.tag == "status" then
393 status = tag[1];
394 end
395 end
396 if alreadyJoined == true then
397 if show == nil then
398 show = "online";
399 end
400 ret = html.day.presence.statusChange:gsub("###TIME_STUFF###", timeStuff);
401 if status ~= "" then
402 status = html.day.presence.statusText:gsub("###STATUS###", htmlEscape(status));
403 end
404 ret = ret:gsub("###SHOW###", show):gsub("###NICK###", nick):gsub("###SHOWHIDE###", showStatus):gsub("###STATUS_STUFF###", status);
405 else
406 ret = html.day.presence.join:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick);
407 end
408 elseif stanza.attr.type ~= nil and stanza.attr.type == "unavailable" then
409
410 ret = html.day.presence.leave:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick);
411 end
412 return ret;
413 end
414
415 local function parseMessageStanza(stanza, timeStuff, nick)
416 local body, title, ret = nil, nil, "";
417
418 for _,tag in ipairs(stanza) do
419 if tag.tag == "body" then
420 body = tag[1];
421 if nick ~= nil then
422 break;
423 end
424 elseif tag.tag == "nick" and nick == nil then
425 nick = htmlEscape(tag[1]);
426 if body ~= nil or title ~= nil then
427 break;
428 end
429 elseif tag.tag == "subject" then
430 title = tag[1];
431 if nick ~= nil then
432 break;
433 end
434 end
435 end
436 if nick ~= nil and body ~= nil then
437 body = htmlEscape(body);
438 ret = html.day.message:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###MSG###", body);
439 elseif nick ~= nil and title ~= nil then
440 title = htmlEscape(title);
441 ret = html.day.titleChange:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###TITLE###", title);
442 end
443 return ret;
444 end
445
446 local function parseDay(bareRoomJid, roomSubject, bare_day)
447 local ret = "";
448 local year;
449 local month;
450 local day;
451 local tmp;
452 local node, host, resource = splitJid(bareRoomJid);
453 local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)");
454
455 if bare_day ~= nil then
456 local data = data_load(node, host, datastore .. "/" .. bare_day);
457 if data ~= nil then
458 for i=1, #data, 1 do
459 local stanza = lom.parse(data[i]);
460 if stanza ~= nil and stanza.attr ~= nil and stanza.attr.time ~= nil then
461 local timeStuff = html.day.time:gsub("###TIME###", stanza.attr.time);
462 if stanza[1] ~= nil then
463 local nick;
464 local tmp;
465
466 -- grep nick from "from" resource
467 if stanza[1].attr.from ~= nil then -- presence and messages
468 nick = htmlEscape(stanza[1].attr.from:match("/(.+)$"));
469 elseif stanza[1].attr.to ~= nil then -- iq
470 nick = htmlEscape(stanza[1].attr.to:match("/(.+)$"));
471 end
472
473 if stanza[1].tag == "presence" and nick ~= nil then
474 tmp = parsePresenceStanza(stanza[1], timeStuff, nick);
475 elseif stanza[1].tag == "message" then
476 tmp = parseMessageStanza(stanza[1], timeStuff, nick);
477 elseif stanza[1].tag == "iq" then
478 tmp = parseIqStanza(stanza[1], timeStuff, nick);
479 else
480 module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bareRoomJid, year .. "/" .. month .. "/" .. day);
481 end
482 if tmp ~= nil then
483 ret = ret .. tmp
484 tmp = nil;
485 end
486 end
487 end
488 end
489 else
490 return generateDayListSiteContentByRoom(bareRoomJid); -- fallback
491 end
492 tmp = html.day.body:gsub("###DAY_STUFF###", ret):gsub("###JID###", bareRoomJid);
493 tmp = tmp:gsub("###YEAR###", year):gsub("###MONTH###", month):gsub("###DAY###", day);
494 tmp = tmp:gsub("###TITLE_STUFF###", html.day.title:gsub("###TITLE###", roomSubject));
495 tmp = tmp:gsub("###STATUS_CHECKED###", config.showStatus and "checked='checked'" or "");
496 tmp = tmp:gsub("###JOIN_CHECKED###", config.showJoin and "checked='checked'" or "");
497 return tmp;
498 else
499 return generateDayListSiteContentByRoom(bareRoomJid); -- fallback
500 end
501 end
502
503 --[[
504 local function loggingMucComponents()
505 local n = 0;
506 for component,_ in pairs(muc_hosts) do
507 n = n + 1;
508 end
509 return n;
510 end
511 ]]-- 133 ]]--
512 134 module:log("debug", "module mod_muc_log loaded!");
513 function handle_request(method, body, request)
514 -- local query = splitQuery(request.url.query);
515 local node, host, day = splitUrl(request.url.path);
516 --[[if host == nil and loggingMucComponents() == 1 then
517 for component,_ in pairs(muc_hosts) do
518 host = component;
519 break;
520 end
521 module:log("debug", "host: %s", tostring(host));
522 end]]--
523
524 if node ~= nil and host ~= nil then
525 local bare = node .. "@" .. host;
526 if prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil then
527 if prosody.hosts[host].muc.rooms[bare] ~= nil then
528 local room = prosody.hosts[host].muc.rooms[bare];
529 if day == nil then
530 return createDoc(generateDayListSiteContentByRoom(bare));
531 else
532 local subject = ""
533 if room._data ~= nil and room._data.subject ~= nil then
534 subject = room._data.subject;
535 end
536 return createDoc(parseDay(bare, subject, day));
537 end
538 else
539 return createDoc(generateRoomListSiteContent(host));
540 end
541 else
542 return createDoc(generateComponentListSiteContent());
543 end
544 elseif host ~= nil then
545 return createDoc(generateRoomListSiteContent(host));
546 else
547 return createDoc(generateComponentListSiteContent());
548 end
549 return;
550 end
551
552 function module.load()
553 config = config_get("*", "core", "muc_log") or {};
554 if config.showStatus == nil then
555 config.showStatus = true;
556 end
557 if config.showJoin == nil then
558 config.showJoin = true;
559 end
560 httpserver.new_from_config({ config.http_port or true }, handle_request, { base = "muc_log" });
561
562 for jid, host in pairs(prosody.hosts) do
563 if host.muc then
564 local logging = config_get(jid, "core", "logging");
565 if logging then
566 module:log("debug", "component: %s", tostring(jid));
567 muc_hosts[jid] = true;
568 end
569 end
570 end
571 end
572
573 function module.unload()
574 muc_hosts = nil;
575 end
576
577 module:add_event_hook("component-activated", function(component, config)
578 if config.core.logging == true then
579 module:log("debug", "component: %s", tostring(component));
580 muc_hosts[component] = true;
581 end
582 end);
583
584 module:hook("message/bare", logIfNeeded, 500);
585 module:hook("pre-message/bare", logIfNeeded, 500);
586 module:hook("iq/bare", logIfNeeded, 500);
587 module:hook("pre-iq/bare", logIfNeeded, 500);
588 module:hook("presence/full", logIfNeeded, 500);
589 module:hook("pre-presence/full", logIfNeeded, 500);