Mercurial > prosody-modules
view mod_muc_log_http/muc_log_http/mod_muc_log_http.lua @ 234:abcb59ab355c
Add new motd_sequential module. This module lets you define numbered messages shown to each user in order, but only once per user, and persistent across server restarts. Useful for notifying users of added features and changes in an
incremental fashion.
author | Jeff Mitchell <jeffrey.mitchell@gmail.com> |
---|---|
date | Wed, 04 Aug 2010 22:29:51 +0000 |
parents | 58c37562c67d |
children | 07c17b60229a |
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 strchar = string.char; 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; 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 body = body:gsub("%%", "%%%%"); return html.doc:gsub("###BODY_STUFF###", body); end end function urlunescape (escapedUrl) escapedUrl = escapedUrl:gsub("+", " ") escapedUrl = escapedUrl:gsub("%%(%x%x)", function(h) return strchar(tonumber(h,16)) end) escapedUrl = escapedUrl:gsub("\r\n", "\n") return escapedUrl end local function htmlEscape(t) if t then t = t:gsub("<", "<"); t = t:gsub(">", ">"); t = t:gsub("(http://[%a%d@%.:/&%?=%-_#%%~]+)", function(h) h = urlunescape(h) return "<a href='" .. h .. "'>" .. h .. "</a>"; end); t = t:gsub("\n", "<br />"); t = t:gsub("%%", "%%%%"); else t = ""; end 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("<span style='color:#DDDDDD'>"..tostring(i).."</span>"); if dayCallback ~= nil and dayCallback.callback ~= nil then tmp = dayCallback.callback(dayCallback.path, i, month, year, dayCallback.room, dayCallback.webPath); end if tmp == nil then tmp = tostring("<span style='color:#DDDDDD'>"..tostring(i).."</span>"); 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 = ""; 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 if htmlStr ~= "" then return "<div name='yearDiv' style='padding: 40px; text-align: center;'>" .. html.year.title:gsub("###YEAR###", tostring(year)) .. htmlStr .. "</div><br style='clear:both;'/> \n"; end return ""; end local function perDayCallback(path, day, month, year, room, webPath) local webPath = webPath or "" 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.."/"..room..".dat") if attributes ~= nil and attributes.mode == "file" then local s = html.days.bit; s = s:gsub("###BARE_DAY###", webPath .. 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 nextRoom = ""; local previousRoom = ""; local rooms = ""; local attributes = nil; local since = ""; local to = ""; local topic = ""; 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 local found = 0; for jid, room in pairs(prosody.hosts[host].muc.rooms) do local node = splitJid(jid) if not room._data.hidden and node then if found == 0 then previousRoom = node elseif found == 1 then nextRoom = node found = -1 end if jid == bareRoomJid then found = 1 end rooms = rooms .. html.days.rooms.bit:gsub("###ROOM###", node); end end room = prosody.hosts[host].muc.rooms[bareRoomJid]; if room._data.hidden then room = nil end end if attributes ~= nil and room ~= nil then local first = 1; local alreadyDoneYears = {}; local temptime = {day=0, month=0, year=0}; topic = room._data.subject or "(no subject)" if topic:len() > 135 then topic = topic:sub(1, topic:find(" ", 120)) .. " ..." end for folder in lfs.dir(path) do local year, month, day = folder:match("^(%d%d)(%d%d)(%d%d)"); if year ~= nil and alreadyDoneYears[year] == nil then temptime.day = tonumber(day) temptime.month = tonumber(month) temptime.year = 2000 + tonumber(year) if first == 1 then to = tostring(os_date("%B %Y", os_time(temptime))) first = 0 end since = tostring(os_date("%B %Y", os_time(temptime))) module:log("debug", "creating overview for: " .. tostring(since)) days = createYear(year, {callback=perDayCallback, path=path, room=node}) .. days; alreadyDoneYears[year] = true; end end end if days ~= "" then tmp = html.days.body:gsub("###DAYS_STUFF###", days); tmp = tmp:gsub("###PREVIOUS_ROOM###", previousRoom == "" and node or previousRoom); tmp = tmp:gsub("###NEXT_ROOM###", nextRoom == "" and node or nextRoom); tmp = tmp:gsub("###ROOMS###", rooms); tmp = tmp:gsub("###ROOMTOPIC###", topic); tmp = tmp:gsub("###SINCE###", since); tmp = tmp:gsub("###TO###", to); 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" and tag[1] ~= nil 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; month = 1; day = 1; else month = month + 1; day = 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)"); 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 day - 1 == 0 then if month - 1 == 0 then year = year - 1; month = 12; day = 31; else month = month - 1; if (month == 2 and leapyear) then day = 29 elseif (month == 2 and not leapyear) then day = 28 elseif (month < 8 and month%2 == 1) or (month >= 8 and month%2 == 0) then day = 31 else day = 30 end 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); local temptime = {day=0, month=0, year=0}; local path = data_getpath(node, host, datastore); path = path:gsub("/[^/]*$", ""); local calendar = "" if tonumber(year) <= 99 then year = year + 2000; end temptime.day = tonumber(day) temptime.month = tonumber(month) temptime.year = tonumber(year) calendar = createMonth(temptime.month, temptime.year, {callback=perDayCallback, path=path, room=node, webPath="../"}) or "" 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):gsub("###UTC###", stanza.attr.utc or 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###", ">") end if previousDay then previousDay = html.day.dayLink:gsub("###DAY###", previousDay):gsub("###TEXT###", "<"); end ret = ret:gsub("%%", "%%%%"); tmp = html.day.body:gsub("###DAY_STUFF###", ret):gsub("###JID###", bareRoomJid); tmp = tmp:gsub("###CALENDAR###", calendar); tmp = tmp:gsub("###DATE###", tostring(os_date("%A, %B %d, %Y", os_time(temptime)))); 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 and html.doc ~= 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 "prosody"; 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);