view mod_mam_muc/mod_mam_muc.lua @ 933:5a975ba6a845

mod_candy: Example of how easy it is to serve files from a prosody module
author Kim Alvefur <zash@zash.se>
date Sun, 24 Mar 2013 23:58:40 +0100
parents 005037032d65
children 5c97ee75cadb
line wrap: on
line source

-- XEP-0313: Message Archive Management for Prosody
-- Copyright (C) 2011-2012 Kim Alvefur
--
-- This file is MIT/X11 licensed.

local xmlns_mam     = "urn:xmpp:mam:tmp";
local xmlns_delay   = "urn:xmpp:delay";
local xmlns_forward = "urn:xmpp:forward:0";

local st = require "util.stanza";
local rsm = module:require "mod_mam/rsm";
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local jid_prep = require "util.jid".prep;
local host = module.host;

local dm_list_load = require "util.datamanager".list_load;
local dm_list_append = require "util.datamanager".list_append;

local tostring = tostring;
local time_now = os.time;
local m_min = math.min;
local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
local uuid = require "util.uuid".generate;
local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
--local rooms_to_archive = module:get_option_set("rooms_to_archive",{});
-- TODO Should be possible to enforce it too

local archive_store = "archive2";

-- Handle archive queries
module:hook("iq/bare/"..xmlns_mam..":query", function(event)
	local origin, stanza = event.origin, event.stanza;
	local room = jid_split(stanza.attr.to);
	local query = stanza.tags[1];

	local room_obj = hosts[module.host].modules.muc.rooms[jid_bare(stanza.attr.to)];
	if not room_obj then
		return -- FIXME not found
	end
	local from = jid_bare(stanza.attr.from);

	if room_obj._affiliations[from] == "outcast"
		or room_obj._data.members_only and not room_obj._affiliations[from] then
		return -- FIXME unauth
	end

	if stanza.attr.type == "get" then
		local qid = query.attr.queryid;

		-- Search query parameters
		local qwith = query:get_child_text("with");
		local qstart = query:get_child_text("start");
		local qend = query:get_child_text("end");
		local qset = rsm.get(query);
		module:log("debug", "Archive query, id %s with %s from %s until %s)",
			tostring(qid), qwith or "anyone", qstart or "the dawn of time", qend or "now");

		if qstart or qend then -- Validate timestamps
			local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend))
			if (qstart and not vstart) or (qend and not vend) then
				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
				return true
			end
			qstart, qend = vstart, vend;
		end

		local qres;
		if qwith then -- Validate the 'with' jid
			local pwith = qwith and jid_prep(qwith);
			if pwith and not qwith then -- it failed prepping
				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid JID"))
				return true
			end
			local _, _, resource = jid_split(qwith);
			qwith = jid_bare(pwith);
			qres = resource;
		end

		-- Load all the data!
		local data, err = dm_list_load(room, module.host, archive_store);
		if not data then
			if (not err) then
				module:log("debug", "The archive was empty.");
				origin.send(st.reply(stanza));
			else
				origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error loading archive: "..tostring(err)));
			end
			return true
		end

		-- RSM stuff
		local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
		local qset_matches = not (qset and qset.after);
		local first, last, index;
		local n = 0;
		local start = qset and qset.index or 1;

		module:log("debug", "Loaded %d items, about to filter", #data);
		for i=start,#data do
			local item = data[i];
			local when, nick = item.when, item.resource;
			local id = item.id;
			--module:log("debug", "id is %s", id);

			-- RSM pre-send-checking
			if qset then
				if qset.before == id then
					module:log("debug", "End of matching range found");
					qset_matches = false;
					break;
				end
			end

			--module:log("debug", "message with %s at %s", with, when or "???");
			-- Apply query filter
			if (not qres or (qres == nick))
					and (not qstart or when >= qstart)
					and (not qend or when <= qend)
					and (not qset or qset_matches) then
				local fwd_st = st.message{ to = stanza.attr.from }
					:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }):up()
					:tag("forwarded", { xmlns = xmlns_forward })
						:tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
				local orig_stanza = st.deserialize(item.stanza);
				orig_stanza.attr.xmlns = "jabber:client";
				fwd_st:add_child(orig_stanza);
				origin.send(fwd_st);
				if not first then
					index = i;
					first = id;
				end
				last = id;
				n = n + 1;
			elseif (qend and when > qend) then
				module:log("debug", "We have passed into messages more recent than requested");
				break -- We have passed into messages more recent than requested
			end

			-- RSM post-send-checking
			if qset then
				if qset.after == id then
					module:log("debug", "Start of matching range found");
					qset_matches = true;
				end
			end
			if n >= qmax then
				module:log("debug", "Max number of items matched");
				break
			end
		end
		-- That's all folks!
		module:log("debug", "Archive query %s completed", tostring(qid));

		local reply = st.reply(stanza);
		if last then
			-- This is a bit redundant, isn't it?
			reply:query(xmlns_mam):add_child(rsm.generate{first = first, last = last, count = n});
		end
		origin.send(reply);
		return true
	end
end);

-- Handle messages
local function message_handler(event)
	local origin, stanza = event.origin, event.stanza;
	local orig_type = stanza.attr.type or "normal";
	local orig_to = stanza.attr.to;
	local orig_from = stanza.attr.from;

	-- Still needed?
	if not orig_from then
		orig_from = origin.full_jid;
	end

	-- Only store groupchat messages
	if not (orig_type == "groupchat" and (stanza:get_child("body") or stanza:get_child("subject"))) then
		return;
	end

	local room = jid_split(orig_to);
	local room_obj = hosts[host].modules.muc.rooms[orig_to]
	if not room_obj then return end

	local id = uuid();
	local when = time_now();
	local stanza = st.clone(stanza); -- Private copy
	--stanza.attr.to = nil;
	local nick = room_obj._jid_nick[orig_from];
	if not nick then return end
	stanza.attr.from = nick;
	local _, _, nick = jid_split(nick);
	-- And stash it
	local ok, err = dm_list_append(room, host, archive_store, {
		-- WARNING This format may change.
		id = id,
		when = when,
		resource = nick,
		stanza = st.preserialize(stanza)
	});
	--[[ This was dropped from the spec
	if ok then 
		stanza:tag("archived", { xmlns = xmlns_mam, by = host, id = id }):up();
	end
	--]]
end

module:hook("message/bare", message_handler, 2);

module:add_feature(xmlns_mam);