--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_log_http/muc_log_http/mod_muc_log_http.lua Tue Dec 08 21:12:40 2009 +0100
@@ -0,0 +1,734 @@
+-- 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 urlBase = "muc_log";
+local muc_hosts = {};
+local config = nil;
+local tostring = _G.tostring;
+local tonumber = _G.tonumber;
+local os_date, os_time = os.date, os.time;
+local str_format = string.format;
+local io_open = io.open;
+local themesParent = (CFG_PLUGINDIR or "./plugins/") .. "muc_log_http/themes";
+
+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 = {};
+local theme = "default";
+
+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 or {}) 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
+
+-- Calendar stuff
+local function getDaysForMonth(month, year)
+ local daysCount = 30;
+ local leapyear = false;
+
+ 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 then
+ daysCount = 29;
+ elseif month == 2 and not leapyear then
+ daysCount = 28;
+ elseif month < 8 and month%2 == 1 or
+ month >= 8 and month%2 == 0
+ then
+ daysCount = 31;
+ end
+ return daysCount;
+end
+
+local function createMonth(month, year, dayCallback)
+ local htmlStr = html.month.header;
+ local days = getDaysForMonth(month, year);
+ local time = os_time{year=year, month=month, day=1};
+ local dow = tostring(os_date("%a", time))
+ local title = tostring(os_date("%B", time));
+ local weekDays = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
+ local weekDay = 0;
+ local weeks = 1;
+ local logAvailableForMinimumOneDay = false;
+
+ local weekDaysHtml = "";
+ for _, tmp in ipairs(weekDays) do
+ weekDaysHtml = weekDaysHtml .. html.month.weekDay:gsub("###DAY###", tmp) .. "\n";
+ end
+
+ htmlStr = htmlStr:gsub("###TITLE###", title):gsub("###WEEKDAYS###", weekDaysHtml);
+
+ for i = 1, 31 do
+ weekDay = weekDay + 1;
+ if weekDay == 1 then htmlStr = htmlStr .. "<tr>\n"; end
+ if i == 1 then
+ for _, tmp in ipairs(weekDays) do
+ if dow ~= tmp then
+ htmlStr = htmlStr .. html.month.emptyDay .. "\n";
+ weekDay = weekDay + 1;
+ else
+ break;
+ end
+ end
+ end
+ if i < days + 1 then
+ local tmp = tostring("<font color='#DDDDDD'>"..tostring(i).."</font>");
+ if dayCallback ~= nil and dayCallback.callback ~= nil then
+ tmp = dayCallback.callback(dayCallback.path, i, month, year);
+ end
+ if tmp == nil then
+ tmp = tostring("<font color='#DDDDDD'>"..tostring(i).."</font>");
+ else
+ logAvailableForMinimumOneDay = true;
+ end
+ htmlStr = htmlStr .. html.month.day:gsub("###DAY###", tmp) .. "\n";
+ end
+
+ if i >= days then
+ break;
+ end
+
+ if weekDay == 7 then
+ weekDay = 0;
+ weeks = weeks + 1;
+ htmlStr = htmlStr .. "</tr>\n";
+ end
+ end
+
+ if weekDay + 1 < 8 or weeks < 6 then
+ weekDay = weekDay + 1;
+ if weekDay > 7 then
+ weekDay = 1;
+ end
+ if weekDay == 1 then
+ weeks = weeks + 1;
+ end
+ for y = weeks, 6 do
+ if weekDay == 1 then
+ htmlStr = htmlStr .. "<tr>\n";
+ end
+ for i = weekDay, 7 do
+ htmlStr = htmlStr .. html.month.emptyDay .. "\n";
+ end
+ weekDay = 1
+ htmlStr = htmlStr .. "</tr>\n";
+ end
+ end
+ htmlStr = htmlStr .. html.month.footer;
+ if logAvailableForMinimumOneDay then
+ return htmlStr;
+ end
+end
+
+local function createYear(year, dayCallback)
+ local year = year;
+ local tmp;
+ if tonumber(year) <= 99 then
+ year = year + 2000;
+ end
+ local htmlStr = "<div name='yearDiv' style='padding: 40px; text-align: center;'>" .. html.year.title:gsub("###YEAR###", tostring(year));
+ for i=1, 12 do
+ tmp = createMonth(i, year, dayCallback);
+ if tmp then
+ htmlStr = htmlStr .. "<div style='float: left; padding: 5px;'>\n" .. tmp .. "</div>\n";
+ end
+ end
+ return htmlStr .. "</div><br style='clear:both;'/> \n";
+end
+
+local function perDayCallback(path, day, month, year)
+ local year = year;
+ if year > 2000 then
+ year = year - 2000;
+ end
+ local bareDay = str_format("%.02d%.02d%.02d", year, month, day);
+ local attributes, err = lfs.attributes(path.."/"..bareDay)
+ if attributes ~= nil and attributes.mode == "directory" then
+ local s = html.days.bit;
+ s = s:gsub("###BARE_DAY###", bareDay);
+ s = s:gsub("###DAY###", day);
+ return s;
+ end
+ return;
+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 ~= nil and 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
+ local alreadyDoneYears = {};
+ for file in lfs.dir(path) do
+ local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)");
+ if year ~= nil and alreadyDoneYears[year] == nil then
+ days = days .. createYear(year, {callback=perDayCallback, path=path});
+ alreadyDoneYears[year] = true;
+ end
+ 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.messageMe;
+ 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.dayLink:gsub("###DAY###", nextDay):gsub("###TEXT###", "next day >>")
+ end
+ if previousDay then
+ previousDay = html.day.dayLink: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 muc_hosts ~= nil then
+ 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
+ end
+ return;
+end
+
+-- Compatibility: Lua-5.1
+function split(str, pat)
+ local t = {} -- NOTE: use {n = 0} in Lua-5.0
+ local fpat = "(.-)" .. pat
+ local last_end = 1
+ local s, e, cap = str:find(fpat, 1)
+ while s do
+ if s ~= 1 or cap ~= "" then
+ table.insert(t,cap)
+ end
+ last_end = e+1
+ s, e, cap = str:find(fpat, last_end)
+ end
+ if last_end <= #str then
+ cap = str:sub(last_end)
+ table.insert(t, cap)
+ end
+ return t
+end
+
+local function assign(arr, content)
+ local tmp = html;
+ local idx = nil;
+ for _,i in ipairs(arr) do
+ if idx ~= nil then
+ if tmp[idx] == nil then
+ tmp[idx] = {};
+ end
+ tmp = tmp[idx];
+ end
+ idx = i;
+ end
+ tmp[idx] = content;
+end
+
+local function readFile(filepath)
+ local f = assert(io_open(filepath, "r"));
+ local t = f:read("*all");
+ f:close()
+ return t;
+end
+
+local function loadTheme(path)
+ local iter = lfs.dir(path);
+ for file in iter do
+ if file ~= "." and file ~= ".." then
+ module:log("debug", "opening theme file: " .. file);
+ local tmp = split(file:gsub("\.html$", ""), "_");
+ local content = readFile(path .. "/" .. file);
+ assign(tmp, content);
+ end
+ end
+ return true;
+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
+
+ theme = config.theme or "default";
+ local themePath = themesParent .. "/" .. tostring(theme);
+ local attributes, err = lfs.attributes(themePath);
+ if attributes == nil or attributes.mode ~= "directory" then
+ module:log("error", "Theme folder of theme \"".. tostring(theme) .. "\" isn't existing. expected Path: " .. themePath);
+ return false;
+ end
+
+ -- module:log("debug", (require "util.serialization").serialize(html));
+ if(not loadTheme(themePath)) then
+ module:log("error", "Theme \"".. tostring(theme) .. "\" is missing something.");
+ return false;
+ end
+ -- module:log("debug", (require "util.serialization").serialize(html));
+
+ httpserver.new_from_config({ config.http_port or true }, handle_request, { base = urlBase, ssl = false, port = 5290 });
+
+ 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
+ module:log("debug", "loaded mod_muc_log_http");
+end
+
+function module.unload()
+ muc_hosts = nil;
+ module:log("debug", "unloaded mod_muc_log_http");
+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);