# HG changeset patch
# User Kim Alvefur <zash@zash.se>
# Date 1490979702 -7200
# Node ID f237d0155e598d661afd0a169361446f028c1e6e
# Parent  ecb110f45c92d02ea02f5c292ad65410b6d169aa# Parent  8a7c4497569a53e4a4518d1c0a4ea2de60378a55
Merge 0.10->trunk

diff -r ecb110f45c92 -r f237d0155e59 plugins/mod_offline.lua
--- a/plugins/mod_offline.lua	Tue Mar 28 20:14:35 2017 +0200
+++ b/plugins/mod_offline.lua	Fri Mar 31 19:01:42 2017 +0200
@@ -7,29 +7,24 @@
 --
 
 
-local datamanager = require "util.datamanager";
-local st = require "util.stanza";
 local datetime = require "util.datetime";
-local ipairs = ipairs;
 local jid_split = require "util.jid".split;
 
+local offline_messages = module:open_store("offline", "archive");
+
 module:add_feature("msgoffline");
 
 module:hook("message/offline/handle", function(event)
 	local origin, stanza = event.origin, event.stanza;
 	local to = stanza.attr.to;
-	local node, host;
+	local node;
 	if to then
-		node, host = jid_split(to)
+		node = jid_split(to)
 	else
-		node, host = origin.username, origin.host;
+		node = origin.username;
 	end
 
-	stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
-	local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
-	stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
-
-	return result;
+	return offline_messages:append(node, nil, stanza);
 end, -1);
 
 module:hook("message/offline/broadcast", function(event)
@@ -37,15 +32,12 @@
 
 	local node, host = origin.username, origin.host;
 
-	local data = datamanager.list_load(node, host, "offline");
+	local data = offline_messages:find(node);
 	if not data then return true; end
-	for _, stanza in ipairs(data) do
-		stanza = st.deserialize(stanza);
-		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
-		stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
-		stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+	for _, stanza, when in data do
+		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime(when)}):up(); -- XEP-0203
 		origin.send(stanza);
 	end
-	datamanager.list_store(node, host, "offline", nil);
+	offline_messages:delete(node);
 	return true;
 end, -1);
diff -r ecb110f45c92 -r f237d0155e59 plugins/mod_storage_internal.lua
--- a/plugins/mod_storage_internal.lua	Tue Mar 28 20:14:35 2017 +0200
+++ b/plugins/mod_storage_internal.lua	Fri Mar 31 19:01:42 2017 +0200
@@ -1,34 +1,148 @@
 local datamanager = require "core.storagemanager".olddm;
+local array = require "util.array";
+local datetime = require "util.datetime";
+local st = require "util.stanza";
+local now = require "util.time".now;
+local id = require "util.id".medium;
 
 local host = module.host;
 
 local driver = {};
-local driver_mt = { __index = driver };
 
 function driver:open(store, typ)
-	if typ and typ ~= "keyval" then
+	local mt = self[typ or "keyval"]
+	if not mt then
 		return nil, "unsupported-store";
 	end
-	return setmetatable({ store = store, type = typ }, driver_mt);
+	return setmetatable({ store = store, type = typ }, mt);
+end
+
+function driver:stores(username) -- luacheck: ignore 212/self
+	return datamanager.stores(username, host);
 end
-function driver:get(user)
+
+function driver:purge(user) -- luacheck: ignore 212/self
+	return datamanager.purge(user, host);
+end
+
+local keyval = { };
+driver.keyval = { __index = keyval };
+
+function keyval:get(user)
 	return datamanager.load(user, host, self.store);
 end
 
-function driver:set(user, data)
+function keyval:set(user, data)
 	return datamanager.store(user, host, self.store, data);
 end
 
-function driver:stores(username)
-	return datamanager.stores(username, host);
-end
-
-function driver:users()
+function keyval:users()
 	return datamanager.users(host, self.store, self.type);
 end
 
-function driver:purge(user)
-	return datamanager.purge(user, host);
+local archive = {};
+driver.archive = { __index = archive };
+
+function archive:append(username, key, value, when, with)
+	key = key or id();
+	when = when or now();
+	if not st.is_stanza(value) then
+		return nil, "unsupported-datatype";
+	end
+	value = st.preserialize(st.clone(value));
+	value.key = key;
+	value.when = when;
+	value.with = with;
+	value.attr.stamp = datetime.datetime(when);
+	value.attr.stamp_legacy = datetime.legacy(when);
+	local ok, err = datamanager.list_append(username, host, self.store, value);
+	if not ok then return ok, err; end
+	return key;
+end
+
+function archive:find(username, query)
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then return items, err; end
+	local count = #items;
+	local i = 0;
+	if query then
+		items = array(items);
+		if query.with then
+			items:filter(function (item)
+				return item.with == query.with;
+			end);
+		end
+		if query.start then
+			items:filter(function (item)
+				return item.when >= query.start;
+			end);
+		end
+		if query["end"] then
+			items:filter(function (item)
+				return item.when <= query["end"];
+			end);
+		end
+		count = #items;
+		if query.reverse then
+			items:reverse();
+			if query.before then
+				for j = 1, count do
+					if (items[j].key or tostring(j)) == query.before then
+						i = j;
+						break;
+					end
+				end
+			end
+		elseif query.after then
+			for j = 1, count do
+				if (items[j].key or tostring(j)) == query.after then
+					i = j;
+					break;
+				end
+			end
+		end
+		if query.limit and #items - i > query.limit then
+			items[i+query.limit+1] = nil;
+		end
+	end
+	return function ()
+		i = i + 1;
+		local item = items[i];
+		if not item then return; end
+		local key = item.key or tostring(i);
+		local when = item.when or datetime.parse(item.attr.stamp);
+		local with = item.with;
+		item.key, item.when, item.with = nil, nil, nil;
+		item.attr.stamp = nil;
+		item.attr.stamp_legacy = nil;
+		item = st.deserialize(item);
+		return key, item, when, with;
+	end, count;
+end
+
+function archive:dates(username)
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then return items, err; end
+	return array(items):pluck("when"):map(datetime.date):unique();
+end
+
+function archive:delete(username, query)
+	if not query or next(query) == nil then
+		return datamanager.list_store(username, host, self.store, nil);
+	end
+	for k in pairs(query) do
+		if k ~= "end" then return nil, "unsupported-query-field"; end
+	end
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then return items, err; end
+	items = array(items);
+	items:filter(function (item)
+		return item.when > query["end"];
+	end);
+	local count = #items;
+	local ok, err = datamanager.list_store(username, host, self.store, items);
+	if not ok then return ok, err; end
+	return count;
 end
 
 module:provides("storage", driver);
diff -r ecb110f45c92 -r f237d0155e59 plugins/muc/mod_muc.lua
diff -r ecb110f45c92 -r f237d0155e59 util/array.lua
--- a/util/array.lua	Tue Mar 28 20:14:35 2017 +0200
+++ b/util/array.lua	Fri Mar 31 19:01:42 2017 +0200
@@ -92,6 +92,18 @@
 	return outa;
 end
 
+function array_base.unique(outa, ina)
+	local seen = {};
+	return array_base.filter(outa, ina, function (item)
+		if seen[item] then
+			return false;
+		else
+			seen[item] = true;
+			return true;
+		end
+	end);
+end
+
 function array_base.pluck(outa, ina, key)
 	for i = 1, #ina do
 		outa[i] = ina[i][key];
diff -r ecb110f45c92 -r f237d0155e59 util/id.lua
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/id.lua	Fri Mar 31 19:01:42 2017 +0200
@@ -0,0 +1,26 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2008-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local s_gsub = string.gsub;
+local random_bytes = require "util.random".bytes;
+local base64_encode = require "util.encodings".base64.encode;
+
+local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" };
+local function b64url_random(len)
+	return (s_gsub(base64_encode(random_bytes(len)), "[+/=]", b64url));
+end
+
+return {
+	short =  function () return b64url_random(6); end;
+	medium = function () return b64url_random(12); end;
+	long =   function () return b64url_random(24); end;
+	custom = function (size)
+		return function () return b64url_random(size); end;
+	end;
+}