mod_muc_log/mod_muc_log.lua
changeset 56 e9de45beaf5e
parent 55 d9749ed44f6e
child 57 cddcea7c091a
equal deleted inserted replaced
55:d9749ed44f6e 56:e9de45beaf5e
     6 local prosody = prosody;
     6 local prosody = prosody;
     7 local splitJid = require "util.jid".split;
     7 local splitJid = require "util.jid".split;
     8 local bareJid = require "util.jid".bare;
     8 local bareJid = require "util.jid".bare;
     9 local config_get = require "core.configmanager".get;
     9 local config_get = require "core.configmanager".get;
    10 local httpserver = require "net.httpserver";
    10 local httpserver = require "net.httpserver";
    11 -- local dump = require "util.logger".dump;
    11 local serialize = require "util.serialization".serialize;
    12 local config = {};
    12 local config = {};
       
    13 
    13 
    14 
    14 --[[ LuaFileSystem 
    15 --[[ LuaFileSystem 
    15 * URL: http://www.keplerproject.org/luafilesystem/index.html
    16 * URL: http://www.keplerproject.org/luafilesystem/index.html
    16 * Install: luarocks install luafilesystem
    17 * Install: luarocks install luafilesystem
    17 * ]]
    18 * ]]
    18 local lfs = require "lfs";
    19 local lfs = require "lfs";
    19 
    20 
    20 local lom = require "lxp.lom";
    21 local lom = require "lxp.lom";
       
    22 
       
    23 
       
    24 --[[
       
    25 * Default templates for the html output.
       
    26 ]]--
       
    27 local html = {};
       
    28 html.doc = [[<html>
       
    29 <head>
       
    30 	<title>muc_log</title>
       
    31 </head>
       
    32 <style type="text/css">
       
    33 <!--
       
    34 .timestuff {color: #AAAAAA; text-decoration: none;}
       
    35 .muc_join {color: #009900; font-style: italic;}
       
    36 .muc_leave {color: #009900; font-style: italic;}
       
    37 .muc_statusChange {color: #009900; font-style: italic;}
       
    38 .muc_title {color: #009900;}
       
    39 .muc_titlenick {color: #009900; font-style: italic;}
       
    40 .muc_kick {color: #009900; font-style: italic;}
       
    41 .muc_bann {color: #009900; font-style: italic;}
       
    42 .muc_name {color: #0000AA;}
       
    43 //-->
       
    44 </style>
       
    45 <body>
       
    46 ###BODY_STUFF###
       
    47 </body>
       
    48 </html>]];
       
    49 
       
    50 html.hosts = {};
       
    51 html.hosts.bit = [[<a href="/muc_log/###JID###">###JID###</a><br />]]
       
    52 html.hosts.body = [[<h2>Rooms hosted on this server:</h2><hr /><p>
       
    53 ###HOSTS_STUFF###
       
    54 </p><hr />]];
       
    55 
       
    56 html.days = {};
       
    57 html.days.bit = [[<a href="/muc_log/###JID###/?year=###YEAR###&month=###MONTH###&day=###DAY###">20###YEAR###/###MONTH###/###DAY###</a><br />]];
       
    58 html.days.body = [[<h2>available logged days of room: ###JID###</h2><hr /><p>
       
    59 ###DAYS_STUFF###
       
    60 </p><hr />]];
       
    61 
       
    62 html.day = {};
       
    63 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
       
    64 html.day.presence = {};
       
    65 html.day.presence.join = [[###TIME_STUFF###<font class="muc_join"> *** ###NICK### joins the room</font><br />]];
       
    66 html.day.presence.leave = [[###TIME_STUFF###<font class="muc_leave"> *** ###NICK### leaves the room</font><br />]];
       
    67 html.day.presence.statusChange = [[###TIME_STUFF###<font class="muc_statusChange"> *** ###NICK### changed his/her status to: ###STATUS###</font><br />]];
       
    68 html.day.message = [[###TIME_STUFF###<font class="muc_name">&lt;###NICK###&gt;</font> ###MSG###<br />]];
       
    69 html.day.titleChange = [[###TIME_STUFF###<font class="muc_titlenick"> *** ###NICK### change title to:</font> <font class="muc_title">###MSG###</font><br />]];
       
    70 html.day.kick = [[###TIME_STUFF###<font class="muc_titlenick"> *** ###NICK### kicked ###VICTIM###</font><br />]];
       
    71 html.day.bann = [[###TIME_STUFF###<font class="muc_titlenick"> *** ###NICK### banned ###VICTIM###</font><br />]];
       
    72 html.day.body = [[<h2>room ###JID### logging of 20###YEAR###/###MONTH###/###DAY###</h2><hr /><p>
       
    73 ###DAY_STUFF###
       
    74 </p><hr />]];
       
    75 
       
    76 html.help = [[
       
    77 MUC logging is not configured correctly.<br />
       
    78 Here is a example config:<br />
       
    79 Component "rooms.example.com" "muc"<br />
       
    80 &nbsp;&nbsp;modules_enabled = {<br />
       
    81 &nbsp;&nbsp;&nbsp;&nbsp;"muc_log";<br />
       
    82 &nbsp;&nbsp;}<br />
       
    83 &nbsp;&nbsp;muc_log = {<br />
       
    84 &nbsp;&nbsp;&nbsp;&nbsp;folder = "/opt/local/var/log/prosody/rooms";<br />
       
    85 &nbsp;&nbsp;&nbsp;&nbsp;http_port = "/opt/local/var/log/prosody/rooms";<br />
       
    86 &nbsp;&nbsp;}<br />
       
    87 ]];
    21 
    88 
    22 function validateLogFolder()
    89 function validateLogFolder()
    23 	if config.folder == nil then
    90 	if config.folder == nil then
    24 		module:log("warn", "muc_log folder isn't configured. configure it please!");
    91 		module:log("warn", "muc_log folder isn't configured. configure it please!");
    25 		return false;
    92 		return false;
    84 	end
   151 	end
    85 	return;
   152 	return;
    86 end
   153 end
    87 
   154 
    88 function createDoc(body)
   155 function createDoc(body)
    89 	return [[<html>
   156 	return html.doc:gsub("###BODY_STUFF###", body);
    90 	<head>
   157 end
    91 		<title>muc_log</title>
   158 
    92 	</head>
   159 local function htmlEscape(t)
    93 	<style type="text/css">
   160 	t = t:gsub("\n", "<br />");
    94 	<!--
   161 	-- TODO link text into klickable link and such stuff
    95 	.timestuff {color: #AAAAAA; text-decoration: none;}
   162 	return t;
    96 	.muc_join {color: #009900; font-style: italic;}
       
    97 	.muc_leave {color: #009900; font-style: italic;}
       
    98 	.muc_kick {color: #009900; font-style: italic;}
       
    99 	.muc_bann {color: #009900; font-style: italic;}
       
   100 	.muc_name {color: #0000AA;}
       
   101 	//-->
       
   102 	</style>
       
   103 	<body>
       
   104 	]] .. tostring(body) .. [[
       
   105 	</body>
       
   106 	</html>]];
       
   107 end
   163 end
   108 
   164 
   109 function splitQuery(query)
   165 function splitQuery(query)
   110 	local ret = {};
   166 	local ret = {};
   111 	if query == nil then return ret; end
   167 	if query == nil then return ret; end
   139 	end
   195 	end
   140 	return node, host;
   196 	return node, host;
   141 end
   197 end
   142 
   198 
   143 local function generateRoomListSiteContent()
   199 local function generateRoomListSiteContent()
   144 	local ret = "<h2>Rooms hosted on this server:</h2><hr /><p>";
   200 	local rooms = "";
   145 	for host, config in pairs(prosody.hosts) do
   201 	for host, config in pairs(prosody.hosts) do
   146 		if prosody.hosts[host].muc ~= nil then
   202 		if prosody.hosts[host].muc ~= nil then
   147 			for jid, room in pairs(prosody.hosts[host].muc.rooms) do
   203 			for jid, room in pairs(prosody.hosts[host].muc.rooms) do
   148 				ret = ret .. "<a href=\"/muc_log/" .. jid .. "/\">" .. jid .."</a><br />\n";
   204 				rooms = rooms .. html.hosts.bit:gsub("###JID###", jid);
   149 			end
   205 			end
   150 		end
   206 		end
   151 	end
   207 	end
   152 	return ret .. "</p><hr />";
   208 	
       
   209 	return html.hosts.body:gsub("###HOSTS_STUFF###", rooms);
   153 end
   210 end
   154 
   211 
   155 local function generateDayListSiteContentByRoom(bareRoomJid)
   212 local function generateDayListSiteContentByRoom(bareRoomJid)
   156 	local ret = "";
   213 	local days = "";
       
   214 	local tmp;
   157 
   215 
   158 	for file in lfs.dir(config.folder) do
   216 	for file in lfs.dir(config.folder) do
   159 		local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)_" .. bareRoomJid .. ".log");
   217 		local year, month, day = file:match("^(%d%d)(%d%d)(%d%d)_" .. bareRoomJid .. ".log");
   160 		if	year ~= nil and month ~= nil and day ~= nil and
   218 		if	year ~= nil and month ~= nil and day ~= nil and
   161 			year ~= ""  and month ~= ""  and day ~= ""
   219 			year ~= ""  and month ~= ""  and day ~= ""
   162 		then
   220 		then
   163 			ret = "<a href=\"/muc_log/" .. bareRoomJid .. "/?year=" .. year .. "&month=" .. month .. "&day=" .. day .. "\">20" .. year .. "/" .. month .. "/" .. day .. "</a><br />\n" .. ret;
   221 			tmp = html.days.bit;
   164 		end
   222 			tmp = tmp:gsub("###JID###", bareRoomJid);
   165 	end
   223 			tmp = tmp:gsub("###YEAR###", year);
   166 	if ret ~= "" then
   224 			tmp = tmp:gsub("###MONTH###", month);
   167 		return "<h2>available logged days of room: " .. bareRoomJid .. "</h2><hr /><p>" .. ret .. "</p><hr />";
   225 			tmp = tmp:gsub("###DAY###", day);
       
   226 			days = tmp .. days;
       
   227 		end
       
   228 	end
       
   229 	if days ~= "" then
       
   230 		tmp = html.days.body:gsub("###DAYS_STUFF###", days);
       
   231 		return tmp:gsub("###JID###", bareRoomJid);
   168 	else
   232 	else
   169 		return generateRoomListSiteContent(); -- fallback
   233 		return generateRoomListSiteContent(); -- fallback
   170 	end
   234 	end
   171 end
   235 end
   172 
   236 
   173 local function parseDay(bareRoomJid, query)
   237 local function parseDay(bareRoomJid, query)
   174 	local ret = "";
   238 	local ret = "";
   175 	local year;
   239 	local year;
   176 	local month;
   240 	local month;
   177 	local day;
   241 	local day;
       
   242 	local tmp;
   178 	
   243 	
   179 	for _,str in ipairs(query) do 
   244 	for _,str in ipairs(query) do 
   180 		local name, value;
   245 		local name, value;
   181 		name, value = str:match("^(%a+)=(%d+)$");
   246 		name, value = str:match("^(%a+)=(%d+)$");
   182 		if name == "year" then
   247 		if name == "year" then
   197 			local content = f:read("*a");
   262 			local content = f:read("*a");
   198 			local parsed = lom.parse("<xml>" .. content .. "</xml>");
   263 			local parsed = lom.parse("<xml>" .. content .. "</xml>");
   199 			if parsed ~= nil then
   264 			if parsed ~= nil then
   200 				for _,stanza in ipairs(parsed) do
   265 				for _,stanza in ipairs(parsed) do
   201 					if stanza.attr ~= nil and stanza.attr.time ~= nil then
   266 					if stanza.attr ~= nil and stanza.attr.time ~= nil then
   202 						local tmp = "<a name=\"" .. stanza.attr.time .. "\" href=\"#" .. stanza.attr.time .. "\" class=\"timestuff\">[" .. stanza.attr.time .. "]</a> ";
   267 						local timeStuff = html.day.time:gsub("###TIME###", stanza.attr.time);
   203 						if stanza[1] ~= nil then
   268 						if stanza[1] ~= nil then
   204 							local nick;
   269 							local nick;
       
   270 							
       
   271 							-- grep nick from "from" resource
   205 							if stanza[1].attr.from ~= nil then
   272 							if stanza[1].attr.from ~= nil then
   206 								nick = stanza[1].attr.from:match("/(.+)$");
   273 								nick = stanza[1].attr.from:match("/(.+)$");
   207 							end						
   274 							end
       
   275 							
   208 							if stanza[1].tag == "presence" and nick ~= nil then
   276 							if stanza[1].tag == "presence" and nick ~= nil then
       
   277 								
   209 								if stanza[1].attr.type == nil then
   278 								if stanza[1].attr.type == nil then
   210 									ret = ret .. tmp .. "<font class=\"muc_join\"> *** " .. nick .. " joins the room</font><br />\n";
   279 									tmp = html.day.presence.join:gsub("###TIME_STUFF###", timeStuff);
       
   280 									ret = ret .. tmp:gsub("###NICK###", nick);
   211 								elseif stanza[1].attr.type ~= nil and stanza[1].attr.type == "unavailable" then
   281 								elseif stanza[1].attr.type ~= nil and stanza[1].attr.type == "unavailable" then
   212 									ret = ret .. tmp .. "<font class=\"muc_leave\"> *** " .. nick .. " leaves the room</font><br />\n";
   282 									tmp = html.day.presence.leave:gsub("###TIME_STUFF###", timeStuff);
       
   283 									ret = ret .. tmp:gsub("###NICK###", nick);
   213 								else
   284 								else
   214 									ret = ret .. tmp .. "<font class=\"muc_leave\"> *** " .. nick .. " changed his/her status to: " .. stanza[1].attr.type .. "</font><br />\n";
   285 									tmp = html.day.presence.leave:gsub("###TIME_STUFF###", timeStuff);
       
   286 									tmp = tmp:gsub("###STATUS###", stanza[1].attr.type);
       
   287 									ret = ret .. tmp:gsub("###NICK###", nick);
   215 								end
   288 								end
   216 							elseif stanza[1].tag == "message" then
   289 							elseif stanza[1].tag == "message" then
   217 								local body;
   290 								local body;
   218 								for _,tag in ipairs(stanza[1]) do
   291 								for _,tag in ipairs(stanza[1]) do
   219 									if tag.tag == "body" then
   292 									if tag.tag == "body" then
   220 										body = tag[1]:gsub("\n", "<br />\n");
   293 										body = htmlEscape(tag[1]);
   221 										if nick ~= nil then
   294 										if nick ~= nil then
   222 											break;
   295 											break;
   223 										end
   296 										end
   224 									elseif tag.tag == "nick" and nick == nil then
   297 									elseif tag.tag == "nick" and nick == nil then
   225 										nick = tag[1];
   298 										nick = tag[1];
   227 											break;
   300 											break;
   228 										end
   301 										end
   229 									end
   302 									end
   230 								end
   303 								end
   231 								if nick ~= nil and body ~= nil then
   304 								if nick ~= nil and body ~= nil then
   232 									ret = ret .. tmp .. "<font class=\"muc_name\">&lt;" .. nick .. "&gt;</font> " .. body .. "<br />\n";
   305 									tmp = html.day.message:gsub("###TIME_STUFF###", timeStuff);
       
   306 									tmp = tmp:gsub("###NICK###", nick);
       
   307 									ret = ret .. tmp:gsub("###MSG###", body);
   233 								end
   308 								end
   234 							else
   309 							else
   235 								module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bareRoomJid, year .. "/" .. month .. "/" .. day);
   310 								module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bareRoomJid, year .. "/" .. month .. "/" .. day);
   236 							end
   311 							end
   237 						end
   312 						end
   242 			end
   317 			end
   243 			f:close();
   318 			f:close();
   244 		else
   319 		else
   245 			ret = err;
   320 			ret = err;
   246 		end
   321 		end
   247 		return "<h2>room " .. bareRoomJid .. " logging of 20" .. year .. "/" .. month .. "/" .. day .. "</h2><hr /><p>" .. ret .. "</p><hr />";
   322 		tmp = html.day.body:gsub("###DAY_STUFF###", ret);
       
   323 		tmp = tmp:gsub("###JID###", bareRoomJid);
       
   324 		tmp = tmp:gsub("###YEAR###", year);
       
   325 		tmp = tmp:gsub("###MONTH###", month);
       
   326 		tmp = tmp:gsub("###DAY###", day);
       
   327 		return tmp;
   248 	else
   328 	else
   249 		return generateDayListSiteContentByRoom(bareRoomJid); -- fallback
   329 		return generateDayListSiteContentByRoom(bareRoomJid); -- fallback
   250 	end
   330 	end
   251 end
   331 end
   252 
   332 
   253 function handle_request(method, body, request)
   333 function handle_request(method, body, request)
       
   334 	module:log("debug", "got a request ...")
   254 	local query = splitQuery(request.url.query);
   335 	local query = splitQuery(request.url.query);
   255 	local node, host = grepRoomJid(request.url.path);
   336 	local node, host = grepRoomJid(request.url.path);
   256 	
   337 	
   257 	if validateLogFolder() == false then
   338 	if validateLogFolder() == false then
   258 		return createDoc([[
   339 		return createDoc(html.help);
   259 		MUC logging is not configured correctly. Add a section to Host * "muc_log" and configure the value for the logging "folder".<br />
       
   260 		Like:<br />
       
   261 		Host "*"<br />
       
   262 		....<br />
       
   263 		&nbsp;&nbsp;&nbsp;&nbsp;muc_log = {<br />
       
   264 		&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;folder = "/opt/local/var/log/prosody/rooms";<br />
       
   265 		&nbsp;&nbsp;&nbsp;&nbsp;}<br />
       
   266 		]]);
       
   267 	end
   340 	end
   268 	if node ~= nil  and host ~= nil then
   341 	if node ~= nil  and host ~= nil then
   269 		local bare = node .. "@" .. host;
   342 		local bare = node .. "@" .. host;
   270 		if prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bare] ~= nil then
   343 		if prosody.hosts[host] ~= nil and prosody.hosts[host].muc ~= nil and prosody.hosts[host].muc.rooms[bare] ~= nil then
   271 			local room = prosody.hosts[host].muc.rooms[bare];
   344 			local room = prosody.hosts[host].muc.rooms[bare];
   282 	end
   355 	end
   283 	return;
   356 	return;
   284 end
   357 end
   285 
   358 
   286 config = config_get(module:get_host(), "core", "muc_log");
   359 config = config_get(module:get_host(), "core", "muc_log");
       
   360 module:log("debug", serialize(config));
   287 
   361 
   288 httpserver.new_from_config({ config.http_port or true }, handle_request, { base = "muc_log" });
   362 httpserver.new_from_config({ config.http_port or true }, handle_request, { base = "muc_log" });
   289 
   363 
   290 module:hook("message/bare", logIfNeeded, 500);
   364 module:hook("message/bare", logIfNeeded, 500);
   291 module:hook("pre-message/bare", logIfNeeded, 500);
   365 module:hook("pre-message/bare", logIfNeeded, 500);