File

mod_http_muc_log/mod_http_muc_log.lua @ 1554:8059b7cdaf17

mod_http_muc_log: Make MUC local code identical to mod_mam_muc
author Kim Alvefur <zash@zash.se>
date Sun, 09 Nov 2014 08:37:43 +0100
parent 1553:1398d2bbcd42
child 1555:2e51f70cd7ea
line wrap: on
line source

local st = require "util.stanza";
local datetime = require"util.datetime";
local jid_split = require"util.jid".split;
local nodeprep = require"util.encodings".stringprep.nodeprep;
local uuid = require"util.uuid".generate;
local it = require"util.iterators";
local gettime = require"socket".gettime;

local archive = module:open_store("archive2", "archive");

-- Support both old and new MUC code
local mod_muc = module:depends"muc";
local rooms = rawget(mod_muc, "rooms");
local each_room = rawget(mod_muc, "each_room") or function() return it.values(rooms); end;
local new_muc = not rooms;
if new_muc then
	rooms = module:shared"muc/rooms";
end
local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or
	function (jid)
		return rooms[jid];
	end

local function get_room(name)
	local jid = name .. '@' .. module.host;
	return get_room_from_jid(jid);
end

module:depends"http";

local function template(data)
	local _doc = [[
	Like util.template, but deals with plain text
	Returns a closure that is called with a table of values
	{name} is substituted for values["name"] and is XML escaped
	{name!} is substituted without XML escaping
	{name?} is optional and is replaced with an empty string if no value exists
	]]
	return function(values)
		return (data:gsub("{([^!}]-)(%p?)}", function (name, opt)
			local value = values[name];
			if value then
				if opt ~= "!" then
					return st.xml_escape(value);
				end
				return value;
			elseif opt == "?" then
				return "";
			end
		end));
	end
end

local base = template[[
<!DOCTYPE html>
<meta charset="utf-8">
<title>{title}</title>
<style>
body { margin: 1ex 1em; }
ul { padding: 0; }
li.action dt, li.action dd { display: inline-block; margin-left: 0;}
li.action dd { margin-left: 1ex;}
li { list-style: none; }
li:hover { background: #eee; }
li time { float: right; font-size: small; opacity: 0.2; }
li:hover time { opacity: 1; }
li.join , li.leave { color: green; }
li.join dt, li.leave dt { color: green; }
nav { font-size: x-large; margin: 1ex 2em; }
nav a { text-decoration: none; }
</style>
<h1>{title}</h1>
{body!}
]]

local dates_template = template(base{
	title = "Logs for room {room}";
	body = [[
<base href="{room}/">
<nav>
<a href="..">↑</a>
</nav>
<ul>
{lines!}</ul>
]];
})

local date_line_template = template[[
<li><a href="{date}">{date}</a></li>
]];

local page_template = template(base{
	title = "Logs for room {room} on {date}";
	body = [[
<nav>
<a class="prev" href="{prev}">←</a>
<a class="up" href=".">↑</a>
<a class="next" href="{next}">→</a>
</nav>
<ul>
{logs!}
</ul>
]];
});

local line_templates = {
	["message<groupchat"] = template[[
<li id="{key}" class="{st_name}"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>{body}</dd></dl></li>
]];
	["message<groupchat<subject"] = template[[
<li id="{key}" class="{st_name} action subject"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>changed subject to {subject}</dd></dl></li>
]];
	["presence"] = template[[
<li id="{key}" class="action join"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>joined</dd></dl></li>
]];
	["presence<unavailable"] = template[[
<li id="{key}" class="action leave"><a href="#{key}"><time>{time}</time></a><dl><dt>{nick}</dt><dd>left</dd></dl></li>
]];
};

local room_list_template = template(base{
	title = "Rooms on {host}";
	body = [[
<dl>
{rooms!}
</dl>
]];
});

local room_item_template = template[[
<dt><a href="{room}/">{name}</a></dt>
<dd>{description?}</dd>
]];

local function public_room(room)
	if type(room) == "string" then
		room = get_room(room);
	end
	return room and not room:get_hidden() and not room:get_members_only() and room._data.logging ~= false;
end

-- FIXME Invent some more efficient API for this
local function dates_page(event, path)
	local request, response = event.request, event.response;

	local room = nodeprep(path:match("^(.*)/$"));
	if not room or not public_room(room) then return end

	local dates, i = {}, 1;
	module:log("debug", "Find all dates with messages");
	local next_day;
	repeat
		local iter = archive:find(room, {
			["start"] = next_day;
			limit = 1;
			with = "message<groupchat";
		})
		if not iter then break end
		next_day = nil;
		for key, message, when in iter do
			next_day = datetime.date(when);
			dates[i], i = date_line_template{
				date = next_day;
			}, i + 1;
			next_day = datetime.parse(next_day .. "T23:59:59Z") + 1;
			break;
		end
	until not next_day;

	return dates_template{
		room = room;
		lines = table.concat(dates);
	};
end

local function logs_page(event, path)
	local request, response = event.request, event.response;

	local room, date = path:match("^(.-)/(%d%d%d%d%-%d%d%-%d%d)$");
	room = nodeprep(room);
	if not room then
		return dates_page(event, path);
	end
	if not public_room(room) then return end

	local logs, i = {}, 1;
	local iter, err = archive:find(room, {
		["start"] = datetime.parse(date.."T00:00:00Z");
		["end"]   = datetime.parse(date.."T23:59:59Z");
		limit = math.huge;
		-- with = "message<groupchat";
	});
	if not iter then return 500; end

	local templ, typ;
	for key, message, when in iter do
		templ = message.name;
		local typ = message.attr.type;
		if typ then templ = templ .. '<' .. typ; end
		local subject = message:get_child_text("subject");
		if subject then templ = templ .. '<subject'; end
		templ = line_templates[templ];
		if templ then
			logs[i], i = templ { 
				key = key;
				time = datetime.time(when);
				nick = select(3, jid_split(message.attr.from));
				body = message:get_child_text("body");
				subject = subject;
				st_name = message.name;
				st_type = message.attr.type;
			}, i + 1;
		else
			module:log("debug", "No template for %s", tostring(message));
		end
	end

	local next_when = datetime.parse(date.."T12:00:00Z") + 86400;
	local prev_when = datetime.parse(date.."T12:00:00Z") - 86400;

	module:log("debug", "Find next date with messages");
	for key, message, when in archive:find(room, {
		["start"] = datetime.parse(date.."T00:00:00Z") + 86401;
		limit = math.huge;
	}) do
		next_when = when;
		module:log("debug", "Next message: %s", datetime.datetime(when));
		break;
	end

	module:log("debug", "Find prev date with messages");
	for key, message, when in archive:find(room, {
		["end"] = datetime.parse(date.."T00:00:00Z") - 1;
		limit = math.huge;
		reverse = true;
	}) do
		prev_when = when;
		module:log("debug", "Previous message: %s", datetime.datetime(when));
		break;
	end

	return page_template{
		room = room;
		date = date;
		logs = table.concat(logs);
		next = datetime.date(next_when);
		prev = datetime.date(prev_when);
	};
end

local function list_rooms(event)
	local room_list, i = {}, 1;
	for room in each_room() do
		if public_room(room) then
			room_list[i], i = room_item_template {
				room = jid_split(room.jid);
				name = room:get_name();
				description = room:get_description();
				subject = room:get_subject();
			}, i + 1;
		end
	end
	return room_list_template {
		host = module.host;
		rooms = table.concat(room_list);
	};
end

local cache = setmetatable({}, {__mode = 'v'});

local function with_cache(f)
	return function (event, path)
		local request, response = event.request, event.response;
		local ckey = path or "";
		local cached = cache[ckey];

		if cached then
			local etag = cached.etag;
			local if_none_match = request.headers.if_none_match;
			if etag == if_none_match then
				module:log("debug", "Client cache hit");
				return 304;
			end
			module:log("debug", "Server cache hit");
			response.headers.etag = etag;
			return cached[1];
		end

		local start = gettime();
		local render = f(event, path);
		module:log("debug", "Rendering took %dms", math.floor( (gettime() - start) * 1000 + 0.5));

		if type(render) == "string" then
			local etag = uuid();
			cached = { render, etag = etag, date = datetime.date() };
			response.headers.etag = etag;
			cache[ckey] = cached;
		end

		return render;
	end
end

-- How is cache invalidation a hard problem? ;)
module:hook("muc-broadcast-message", function (event)
	local room = event.room;
	local room_name = jid_split(room.jid);
	local today = datetime.date();
	cache[ room_name .. "/" .. today ] = nil;
	if cache[room_name] and cache[room_name].date ~= today then
		cache[room_name] = nil;
	end
end);

module:log("info", module:http_url());
module:provides("http", {
	route = {
		["GET /"] = list_rooms;
		["GET /*"] = with_cache(logs_page);
	};
});