Mercurial > prosody-modules
view 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 |
line wrap: on
line source
-- Copyright (C) 2009 Thilo Cestonaro -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local prosody = prosody; local tabSort = table.sort; local tonumber = _G.tonumber; local tostring = _G.tostring; local strformat = string.format; local splitJid = require "util.jid".split; local config_get = require "core.configmanager".get; local httpserver = require "net.httpserver"; local datamanager = require "util.datamanager"; local data_load, data_getpath = datamanager.load, datamanager.getpath; local datastore = "muc_log"; local muc_hosts = {}; local config = nil; local lom = require "lxp.lom"; --[[ LuaFileSystem * URL: http://www.keplerproject.org/luafilesystem/index.html * Install: luarocks install luafilesystem * ]] local lfs = require "lfs"; --[[ * Default templates for the html output. ]]-- local html = {}; html.doc = [[<html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" > <head> <title>muc_log</title> </head> <script type="text/javascript"><!-- function showHide(name) { var eles = document.getElementsByName(name); for (var i = 0; i < eles.length; i++) { eles[i].style.display = eles[i].style.display != "none" ? "none" : ""; } } --></script> <style type="text/css"> <!-- .timestuff {color: #AAAAAA; text-decoration: none;} .muc_join {color: #009900; font-style: italic;} .muc_leave {color: #009900; font-style: italic;} .muc_statusChange {color: #009900; font-style: italic;} .muc_title {color: #BBBBBB; font-size: 32px;} .muc_titleChange {color: #009900; font-style: italic;} .muc_kick {color: #009900; font-style: italic;} .muc_bann {color: #009900; font-style: italic;} .muc_msg_nick {color: #0000AA;} .muc_msg_me {color: #0000AA;} .join_link {font-height: 9px;} //--> </style> <body> ###BODY_STUFF### </body> </html>]]; html.components = {}; html.components.bit = [[<a href="###COMPONENT###/">###COMPONENT###</a><br />]] html.components.body = [[<h2>MUC hosts available on this server:</h2><hr /><p> ###COMPONENTS_STUFF### </p><hr />]]; html.rooms = {}; html.rooms.bit = [[<a href="###ROOM###/">###ROOM###</a><br />]] html.rooms.body = [[<h2>Rooms hosted on MUC host: ###COMPONENT###</h2><hr /><p> ###ROOMS_STUFF### </p><hr />]]; html.days = {}; html.days.bit = [[<a href="###BARE_DAY###/">20###YEAR###/###MONTH###/###DAY###</a><br />]]; html.days.body = [[<h2>available logged days of room: ###JID###</h2><hr /><p> ###DAYS_STUFF### </p><hr />]]; html.day = {}; html.day.title = [[Subject: <font class="muc_title">###TITLE###</font>]]; 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 html.day.presence = {}; html.day.presence.join = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_join"> *** ###NICK### joins the room</font><br /></div>]]; html.day.presence.leave = [[<div name="joinLeave" style="display: ###SHOWHIDE###;">###TIME_STUFF###<font class="muc_leave"> *** ###NICK### leaves the room</font><br /></div>]]; html.day.presence.statusText = [[ and his status message is "###STATUS###"]]; 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>]]; html.day.message = [[###TIME_STUFF###<font class="muc_msg_nick"><###NICK###></font> ###MSG###<br />]]; html.day.message_me = [[###TIME_STUFF###<font class="muc_msg_me">*###NICK### ###MSG###</font><br />]]; html.day.titleChange = [[###TIME_STUFF###<font class="muc_titleChange"> *** ###NICK### changed the title to "###TITLE###"</font><br />]]; html.day.reason = [[, the reason was "###REASON###"]] html.day.kick = [[###TIME_STUFF###<font class="muc_kick"> *** ###VICTIM### got kicked###REASON_STUFF###</font><br />]]; html.day.bann = [[###TIME_STUFF###<font class="muc_bann"> *** ###VICTIM### got banned###REASON_STUFF###</font><br />]]; html.day.day_link = [[<a href="../###DAY###/">###TEXT###</a>]] html.day.body = [[<h2>Logs of room ###JID### of 20###YEAR###/###MONTH###/###DAY###</h2> <p>###TITLE_STUFF###</p> <font class="join_link"><a href="http://speeqe.com/room/###JID###/" target="_blank">Join room now via speeqe.com!</a></font><br /> ###PREVIOUS_LINK### ###NEXT_LINK###<br /> <input type="checkbox" onclick="showHide('joinLeave')" ###JOIN_CHECKED###/>show/hide joins and Leaves</button> <input type="checkbox" onclick="showHide('status')" ###STATUS_CHECKED###/>show/hide status changes</button> <hr /><div id="main" style="overflow: auto;"> ###DAY_STUFF### </div><hr /> <script><!-- window.captureEvents(Event.RESIZE | Event.LOAD); window.onresize = resize; window.onload = resize; function resize(e) { var ele = document.getElementById("main"); ele.style.height = window.innerHeight - ele.offsetTop - 25; } --></script>]]; local function checkDatastorePathExists(node, host, today, create) create = create or false; local path = data_getpath(node, host, datastore, "dat", true); path = path:gsub("/[^/]*$", ""); -- check existance local attributes, err = lfs.attributes(path); if attributes == nil or attributes.mode ~= "directory" then module:log("warn", "muc_log folder isn't a folder: %s", path); return false; end attributes, err = lfs.attributes(path .. "/" .. today); if attributes == nil then if create then return lfs.mkdir(path .. "/" .. today); else return false; end elseif attributes.mode == "directory" then return true; end return false; end function createDoc(body) if body then return html.doc:gsub("###BODY_STUFF###", body); end end local function htmlEscape(t) t = t:gsub("<", "<"); t = t:gsub(">", ">"); t = t:gsub("(http://[%a%d@%.:/&%?=%-_#]+)", [[<a href="%1">%1</a>]]); t = t:gsub("\n", "<br />"); return t; end function splitUrl(url) local tmp = url:sub(string.len("/muc_log/") + 1); local day = nil; local room = nil; local component = nil; local at = nil; local slash = nil; local slash2 = nil; slash = tmp:find("/"); if slash then component = tmp:sub(1, slash - 1); if tmp:len() > slash then room = tmp:sub(slash + 1); slash = room:find("/"); if slash then tmp = room; room = tmp:sub(1, slash - 1); if tmp:len() > slash then day = tmp:sub(slash + 1); slash = day:find("/"); if slash then day = day:sub(1, slash - 1); end end end end end return room, component, day; end local function generateComponentListSiteContent() local components = ""; for component,muc_host in pairs(muc_hosts) do components = components .. html.components.bit:gsub("###COMPONENT###", component); end if components ~= "" then return html.components.body:gsub("###COMPONENTS_STUFF###", components); end end local function generateRoomListSiteContent(component) local rooms = ""; if prosody.hosts[component] and prosody.hosts[component].muc ~= nil then for jid, room in pairs(prosody.hosts[component].muc.rooms) do local node = splitJid(jid); if not room._data.hidden and node then rooms = rooms .. html.rooms.bit:gsub("###ROOM###", node):gsub("###COMPONENT###", component); end end if rooms ~= "" then return html.rooms.body:gsub("###ROOMS_STUFF###", rooms):gsub("###COMPONENT###", component); end end end local function generateDayListSiteContentByRoom(bareRoomJid) local days = ""; local arrDays = {}; local tmp; local node, host, resource = splitJid(bareRoomJid); local path = data_getpath(node, host, datastore); local room = nil; local attributes = nil; path = path:gsub("/[^/]*$", ""); attributes = lfs.attributes(path); if muc_hosts[host] and prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bareRoomJid] ~= nil then room = prosody.hosts[host].muc.rooms[bareRoomJid]; if room._data.hidden then room = nil end end if attributes ~= nil and room ~= nil then for file in lfs.dir(path) do local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)"); if year ~= nil and month ~= nil and day ~= nil and year ~= "" and month ~= "" and day ~= "" then arrDays[#arrDays + 1] = {bare=file, year=year, month=month, day=day}; end end tabSort(arrDays, function(a,b) return a.bare < b.bare; end); for _, date in pairs(arrDays) do tmp = html.days.bit; tmp = tmp:gsub("###ROOM###", node):gsub("###COMPONENT###", host); tmp = tmp:gsub("###BARE_DAY###", date.bare); tmp = tmp:gsub("###YEAR###", date.year):gsub("###MONTH###", date.month):gsub("###DAY###", date.day); days = tmp .. days; end end if days ~= "" then tmp = html.days.body:gsub("###DAYS_STUFF###", days); return tmp:gsub("###JID###", bareRoomJid); end end local function parseIqStanza(stanza, timeStuff, nick) local text = nil; local victim = nil; if(stanza.attr.type == "set") then for _,tag in ipairs(stanza) do if tag.tag == "query" then for _,item in ipairs(tag) do if item.tag == "item" and item.attr.nick ~= nil and tostring(item.attr.role) == 'none' then victim = item.attr.nick; for _,reason in ipairs(item) do if reason.tag == "reason" then text = reason[1]; break; end end break; end end break; end end if victim ~= nil then if text ~= nil then text = html.day.reason:gsub("###REASON###", htmlEscape(text)); else text = ""; end return html.day.kick:gsub("###TIME_STUFF###", timeStuff):gsub("###VICTIM###", victim):gsub("###REASON_STUFF###", text); end end return; end local function parsePresenceStanza(stanza, timeStuff, nick) local ret = ""; local showJoin = "block" if config and not config.showJoin then showJoin = "none"; end if stanza.attr.type == nil then local showStatus = "block" if config and not config.showStatus then showStatus = "none"; end local show, status = nil, ""; local alreadyJoined = false; for _, tag in ipairs(stanza) do if tag.tag == "alreadyJoined" then alreadyJoined = true; elseif tag.tag == "show" then show = tag[1]; elseif tag.tag == "status" then status = tag[1]; end end if alreadyJoined == true then if show == nil then show = "online"; end ret = html.day.presence.statusChange:gsub("###TIME_STUFF###", timeStuff); if status ~= "" then status = html.day.presence.statusText:gsub("###STATUS###", htmlEscape(status)); end ret = ret:gsub("###SHOW###", show):gsub("###NICK###", nick):gsub("###SHOWHIDE###", showStatus):gsub("###STATUS_STUFF###", status); else ret = html.day.presence.join:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick); end elseif stanza.attr.type ~= nil and stanza.attr.type == "unavailable" then ret = html.day.presence.leave:gsub("###TIME_STUFF###", timeStuff):gsub("###SHOWHIDE###", showJoin):gsub("###NICK###", nick); end return ret; end local function parseMessageStanza(stanza, timeStuff, nick) local body, title, ret = nil, nil, ""; for _,tag in ipairs(stanza) do if tag.tag == "body" then body = tag[1]; if nick ~= nil then break; end elseif tag.tag == "nick" and nick == nil then nick = htmlEscape(tag[1]); if body ~= nil or title ~= nil then break; end elseif tag.tag == "subject" then title = tag[1]; if nick ~= nil then break; end end end if nick ~= nil and body ~= nil then body = htmlEscape(body); local me = body:find("^/me"); local template = ""; if not me then template = html.day.message; else template = html.day.message_me; body = body:gsub("^/me ", ""); end ret = template:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###MSG###", body); elseif nick ~= nil and title ~= nil then title = htmlEscape(title); ret = html.day.titleChange:gsub("###TIME_STUFF###", timeStuff):gsub("###NICK###", nick):gsub("###TITLE###", title); end return ret; end local function incrementDay(bare_day) local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)"); local leapyear = false; module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year)) day = tonumber(day); month = tonumber(month); year = tonumber(year); if year%4 == 0 and year%100 == 0 then if year%400 == 0 then leapyear = true; else leapyear = false; -- turn of the century but not a leapyear end elseif year%4 == 0 then leapyear = true; end if (month == 2 and leapyear and day + 1 > 29) or (month == 2 and not leapyear and day + 1 > 28) or (month < 8 and month%2 == 1 and day + 1 > 31) or (month < 8 and month%2 == 0 and day + 1 > 30) or (month >= 8 and month%2 == 0 and day + 1 > 31) or (month >= 8 and month%2 == 1 and day + 1 > 30) then if month + 1 > 12 then year = year + 1; else month = month + 1; end else day = day + 1; end return strformat("%.02d%.02d%.02d", year, month, day); end local function findNextDay(bareRoomJid, bare_day) local node, host, resource = splitJid(bareRoomJid); local day = incrementDay(bare_day); local max_trys = 7; module:log("debug", day); while(not checkDatastorePathExists(node, host, day, false)) do max_trys = max_trys - 1; if max_trys == 0 then break; end day = incrementDay(day); end if max_trys == 0 then return nil; else return day; end end local function decrementDay(bare_day) local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)"); module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year)) day = tonumber(day); month = tonumber(month); year = tonumber(year); if day - 1 == 0 then if month - 1 == 0 then year = year - 1; else month = month - 1; end else day = day - 1; end return strformat("%.02d%.02d%.02d", year, month, day); end local function findPreviousDay(bareRoomJid, bare_day) local node, host, resource = splitJid(bareRoomJid); local day = decrementDay(bare_day); local max_trys = 7; module:log("debug", day); while(not checkDatastorePathExists(node, host, day, false)) do max_trys = max_trys - 1; if max_trys == 0 then break; end day = decrementDay(day); end if max_trys == 0 then return nil; else return day; end end local function parseDay(bareRoomJid, roomSubject, bare_day) local ret = ""; local year; local month; local day; local tmp; local node, host, resource = splitJid(bareRoomJid); local year, month, day = bare_day:match("^(%d%d)(%d%d)(%d%d)"); local previousDay = findPreviousDay(bareRoomJid, bare_day); local nextDay = findNextDay(bareRoomJid, bare_day); if bare_day ~= nil then local data = data_load(node, host, datastore .. "/" .. bare_day); if data ~= nil then for i=1, #data, 1 do local stanza = lom.parse(data[i]); if stanza ~= nil and stanza.attr ~= nil and stanza.attr.time ~= nil then local timeStuff = html.day.time:gsub("###TIME###", stanza.attr.time); if stanza[1] ~= nil then local nick; local tmp; -- grep nick from "from" resource if stanza[1].attr.from ~= nil then -- presence and messages nick = htmlEscape(stanza[1].attr.from:match("/(.+)$")); elseif stanza[1].attr.to ~= nil then -- iq nick = htmlEscape(stanza[1].attr.to:match("/(.+)$")); end if stanza[1].tag == "presence" and nick ~= nil then tmp = parsePresenceStanza(stanza[1], timeStuff, nick); elseif stanza[1].tag == "message" then tmp = parseMessageStanza(stanza[1], timeStuff, nick); elseif stanza[1].tag == "iq" then tmp = parseIqStanza(stanza[1], timeStuff, nick); else module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bareRoomJid, year .. "/" .. month .. "/" .. day); end if tmp ~= nil then ret = ret .. tmp tmp = nil; end end end end end if ret ~= "" then if nextDay then nextDay = html.day.day_link:gsub("###DAY###", nextDay):gsub("###TEXT###", "next day >>") end if previousDay then previousDay = html.day.day_link:gsub("###DAY###", previousDay):gsub("###TEXT###", "<< previous day"); end tmp = html.day.body:gsub("###DAY_STUFF###", ret):gsub("###JID###", bareRoomJid); tmp = tmp:gsub("###YEAR###", year):gsub("###MONTH###", month):gsub("###DAY###", day); tmp = tmp:gsub("###TITLE_STUFF###", html.day.title:gsub("###TITLE###", roomSubject)); tmp = tmp:gsub("###STATUS_CHECKED###", config.showStatus and "checked='checked'" or ""); tmp = tmp:gsub("###JOIN_CHECKED###", config.showJoin and "checked='checked'" or ""); tmp = tmp:gsub("###NEXT_LINK###", nextDay or ""); tmp = tmp:gsub("###PREVIOUS_LINK###", previousDay or ""); return tmp; end end end function handle_request(method, body, request) local node, host, day = splitUrl(request.url.path); if node ~= nil and host ~= nil then local bare = node .. "@" .. host; if prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil then if prosody.hosts[host].muc.rooms[bare] ~= nil then local room = prosody.hosts[host].muc.rooms[bare]; if day == nil then return createDoc(generateDayListSiteContentByRoom(bare)); else local subject = "" if room._data ~= nil and room._data.subject ~= nil then subject = room._data.subject; end return createDoc(parseDay(bare, subject, day)); end else return createDoc(generateRoomListSiteContent(host)); end else return createDoc(generateComponentListSiteContent()); end elseif host ~= nil then return createDoc(generateRoomListSiteContent(host)); else return createDoc(generateComponentListSiteContent()); end return; end function module.load() config = config_get("*", "core", "muc_log_http") or {}; if config.showStatus == nil then config.showStatus = true; end if config.showJoin == nil then config.showJoin = true; end httpserver.new_from_config({ config.http_port or true }, handle_request, { base = "muc_log" }); for jid, host in pairs(prosody.hosts) do if host.muc then local enabledModules = config_get(jid, "core", "modules_enabled"); if enabledModules then for _,mod in ipairs(enabledModules) do if(mod == "muc_log") then module:log("debug", "component: %s", tostring(jid)); muc_hosts[jid] = true; break; end end end end end end function module.unload() muc_hosts = nil; end module:add_event_hook("component-activated", function(component, config) if config.core and config.core.modules_enabled then for _,mod in ipairs(config.core.modules_enabled) do if(mod == "muc_log") then module:log("debug", "component: %s", tostring(component)); muc_hosts[component] = true; break; end end end end);