Changeset

701:cc5805f83583

mod_mam: Implement support for Result Set Management in queries.
author Kim Alvefur <zash@zash.se>
date Fri, 08 Jun 2012 03:13:31 +0200
parents 700:0c130c45b7c1
children 702:d94ee0848b27
files mod_mam/mod_mam.lua mod_mam/rsm.lib.lua
diffstat 2 files changed, 126 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/mod_mam/mod_mam.lua	Thu Jun 07 23:41:25 2012 +0200
+++ b/mod_mam/mod_mam.lua	Fri Jun 08 03:13:31 2012 +0200
@@ -8,6 +8,7 @@
 local xmlns_forward = "urn:xmpp:forward:0";
 
 local st = require "util.stanza";
+local rsm = module:require "rsm";
 local jid_bare = require "util.jid".bare;
 local jid_split = require "util.jid".split;
 local host = module.host;
@@ -62,7 +63,6 @@
 		local default = prefs[false];
 		default = default ~= nil and default_attrs[default] or global_default_policy;
 		local reply = st.reply(stanza):tag("prefs", { xmlns = xmlns_mam, default = default })
-		--module:log("debug", "get_prefs(%q) => %s", user, require"util.serialization".serialize(prefs));
 		local always = st.stanza("always");
 		local never = st.stanza("never");
 		for k,v in pairs(prefs) do
@@ -97,7 +97,6 @@
 			end
 		end
 
-		--module:log("debug", "set_prefs(%q, %s)", user, require"util.serialization".serialize(prefs));
 		local ok, err = set_prefs(user, prefs);
 		if not ok then
 			origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err)));
@@ -119,6 +118,7 @@
 		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");
 
@@ -136,19 +136,35 @@
 			return true
 		end
 
+		-- RSM stuff
+		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=1,#data do
+		for i=start,#data do
 			local item = data[i];
 			local when, with, with_bare = item.when, item.with, item.with_bare;
 			local ts = item.timestamp;
 			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 qwith or ((qwith == with) or (qwith == with_bare)))
 					and (not qstart or when >= qstart)
-					and (not qend or when <= qend) then
-				-- Optimizable? Do this when archiving?
-				--module:log("debug", "sending");
+					and (not qend or when <= qend)
+					and (not qset or qset_matches) then
 				local fwd_st = st.message{ to = origin.full_jid }
 					:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }):up()
 					:tag("forwarded", { xmlns = xmlns_forward })
@@ -157,13 +173,32 @@
 				orig_stanza.attr.xmlns = "jabber:client";
 				fwd_st:add_child(orig_stanza);
 				origin.send(fwd_st);
-			elseif qend and when > qend then
+				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
+				if qset.max and n >= qset.max then
+					module:log("debug", "Max number of items matched");
+					break
+				end
+			end
 		end
 		-- That's all folks!
 		module:log("debug", "Archive query %s completed", tostring(qid));
-		origin.send(st.reply(stanza));
+		origin.send(st.reply(stanza):add_child(rsm.generate{first = { index = index; first }, last = last}));
 		return true
 	end
 end);
@@ -221,8 +256,6 @@
 	local target_jid = c2s and orig_to or orig_from;
 	local target_bare = jid_bare(target_jid);
 
-	assert(store_host == host, "This should not happen.");
-
 	if shall_store(store_user, target_bare) then
 		module:log("debug", "Archiving stanza: %s", stanza:top_tag());
 
@@ -232,8 +265,7 @@
 		local ok, err = dm_list_append(store_user, store_host, archive_store, {
 			-- WARNING This format may change.
 			id = id,
-			when = when, -- This might be an UNIX timestamp. Probably.
-			timestamp = timestamp(when), -- Textual timestamp. But I'll assume that comparing numbers is faster and less annoying in case of timezones.
+			when = when,
 			with = target_jid,
 			with_bare = target_bare, -- Optimization, to avoid loads of jid_bare() calls when filtering.
 			stanza = st.preserialize(stanza)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_mam/rsm.lib.lua	Fri Jun 08 03:13:31 2012 +0200
@@ -0,0 +1,82 @@
+local stanza = require"util.stanza".stanza;
+local tostring, tonumber = tostring, tonumber;
+local type = type;
+local pairs = pairs;
+
+local xmlns_rsm = 'http://jabber.org/protocol/rsm';
+
+local element_parsers;
+
+do
+	local function xs_int(st)
+		return tonumber(st:get_text());
+	end
+	local function xs_string(st)
+		return st:get_text();
+	end
+
+	element_parsers = {
+		after = xs_string;
+		before = function(st)
+			return st:get_text() or true;
+		end;
+		max = xs_int;
+		index = xs_int;
+
+		first = function(st)
+			return { index = tonumber(st.attr.index); st:get_text() };
+		end;
+		last = xs_string;
+		count = xs_int;
+	}
+end
+
+local element_generators = setmetatable({
+	first = function(st, data)
+		if type(data) == "table" then
+			st:tag("first", { index = data.index }):text(data[1]):up();
+		else
+			st:tag("first"):text(tostring(data)):up();
+		end
+	end;
+}, {
+	__index = function(_, name)
+		return function(st, data)
+			st:tag(name):text(tostring(data)):up();
+		end
+	end;
+});
+
+
+local function parse(stanza)
+	local rs = {};
+	for tag in stanza:childtags() do
+		local name = tag.name;
+		local parser = name and element_parsers[name];
+		if parser then
+			rs[name] = parser(tag);
+		end
+	end
+	return rs;
+end
+
+local function generate(t)
+	local st = stanza("rsm", { xmlns = xmlns_rsm });
+	for k,v in pairs(t) do
+		if element_parsers[k] then
+			element_generators[k](st, v);
+		end
+	end
+	return st;
+end
+
+local function get(st)
+	local set = st:get_child("set", xmlns_rsm);
+	if set and #set.tags > 0 then
+		return parse(set);
+	else
+		module:log("debug", "RSM parse failed, %s", tostring(st));
+	end
+end
+
+return { parse = parse, generate = generate, get = get };