changeset 1994:f263fcf1b0ed

Merge with Goffi
author Kim Alvefur <zash@zash.se>
date Mon, 21 Dec 2015 12:24:21 +0100
parents 6d7699eda594 (diff) 66aaf7c3cb29 (current diff)
children 0e008f36a91c
files
diffstat 26 files changed, 433 insertions(+), 192 deletions(-) [+]
line wrap: on
line diff
--- a/README	Mon Dec 21 11:31:10 2015 +0100
+++ b/README	Mon Dec 21 12:24:21 2015 +0100
@@ -34,5 +34,5 @@
 prosody-modules for 0.8 are now maintained separately at 
 <http://0-8.prosody-modules.googlecode.com/hg/>.
 
-[Prosody]: http://prosody.im/
-[mailing list]: http://prosody.im/discuss
+[Prosody]: https://prosody.im/
+[mailing list]: https://prosody.im/discuss
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_alias/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -0,0 +1,57 @@
+---
+summary: Point alias accounts or domains to correct XMPP user
+...
+
+Introduction
+============
+
+This module allows you to set up aliases that alert people who try to
+contact them or add them to their roster what your actual JID is.  This
+is useful for changing JIDs, or just in the case where you own both
+example.com and example.net, and want people who contact you@example.com
+to be alerted to contact you at you@example.net instead.
+
+This type of aliasing is well supported in the email world, but very hard
+to handle with XMPP, this module sidesteps all the hard problems by just
+sending the user a helpful message, requiring humans to decide what they
+actually want to do.
+
+This doesn't require any special support on other clients or servers,
+just the ability to recieve messages.
+
+Configuration
+=============
+
+Add the module to the `modules_enabled` list.
+
+    modules_enabled = {
+        ...
+        "alias";
+    }
+
+Then set up your list of aliases, aliases can be full or bare JIDs,
+or hosts:
+
+    aliases = {
+        ["old@example.net"] = "new@example.net";
+        ["you@example.com"] = "you@example.net";
+        ["conference.example.com"] = "conference.example.net";
+    }
+
+You can also set up a custom response, by default it is:
+
+    alias_response = "User $alias can be contacted at $target";
+
+A script named mod_alias_postfixadmin.sh is included in this directory to
+generate the aliases array directly from a postfixadmin MySQL database.
+Instructions for use are included in the script.
+
+Compatibility
+=============
+
+  ------- --------------
+  trunk   Works
+  0.10    Works
+  0.9     Unknown
+  0.8     Unknown
+  ------- --------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_alias/mod_alias.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -0,0 +1,43 @@
+-- Copyright (C) 2015 Travis Burtrum
+-- This file is MIT/X11 licensed.
+
+-- set like so in prosody config, works on full or bare jids, or hosts:
+--aliases = {
+--		["old@example.net"] = "new@example.net";
+--		["you@example.com"] = "you@example.net";
+--		["conference.example.com"] = "conference.example.net";
+--}
+
+local aliases = module:get_option("aliases", {});
+local alias_response = module:get_option("alias_response", "User $alias can be contacted at $target");
+
+local st = require "util.stanza";
+
+function handle_alias(event)
+
+	if event.stanza.attr.type ~= "error" then
+		local alias = event.stanza.attr.to;
+		local target = aliases[alias];
+		if target then
+			local replacements = {
+				alias = alias,
+				target = target
+			};
+			local error_message = alias_response:gsub("%$([%w_]+)", function (v)
+					return replacements[v] or nil;
+				end);
+			local message = st.message{ type = "chat", from = alias, to = event.stanza.attr.from }:tag("body"):text(error_message);
+			module:send(message);
+			return event.origin.send(st.error_reply(event.stanza, "cancel", "gone", error_message));
+		end
+	end
+
+end
+
+module:hook("message/bare", handle_alias, 300);
+module:hook("message/full", handle_alias, 300);
+module:hook("message/host", handle_alias, 300);
+
+module:hook("presence/bare", handle_alias, 300);
+module:hook("presence/full", handle_alias, 300);
+module:hook("presence/host", handle_alias, 300);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_alias/mod_alias_postfixadmin.sh	Mon Dec 21 12:24:21 2015 +0100
@@ -0,0 +1,20 @@
+#!/bin/sh
+# Copyright (C) 2015 Travis Burtrum
+# This file is MIT/X11 licensed.
+
+# run like ./mod_alias_postfixadmin.sh "mysql -N -upostfixadmin -ppostfixadmin postfixadmin" > /etc/prosody/aliases.cfg.lua
+# then put:
+# Include "aliases.cfg.lua"
+# in prosody.cfg.lua
+
+mysql="$1"
+
+echo "-- alias plugin, generated by mod_alias_postfixadmin.sh"
+echo "aliases = {"
+
+echo "SELECT concat('["'"'"', address, '"'"'"] = "'"'"', goto, '"'"'";') FROM alias WHERE address != goto;
+SELECT concat('["'"'"', address, '"'"'"] = "'"'"', goto, '"'"'";') FROM (
+	select replace(address, concat('@', target_domain), concat('@', alias_domain)) as address, goto FROM alias JOIN alias_domain ON alias_domain.target_domain = SUBSTRING(alias.address, locate('@',alias.address) + 1, length(alias.address))
+) a WHERE a.address != a.goto;" | $mysql | sort | uniq
+
+echo "}"
--- a/mod_auth_ldap/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_auth_ldap/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -37,7 +37,7 @@
   ldap\_rootdn     The distinguished name to auth against                                                                                 `"" (anonymous)`
   ldap\_password   Password for rootdn                                                                                                    `""`
   ldap\_filter     Search filter, with `$user` and `$host` substituded for user- and hostname                                             `"(uid=$user)"`
-  ldap\_scope      Search scope. other values: "base" and "subtree"                                                                       `"onelevel"`
+  ldap\_scope      Search scope. other values: "base" and "onelevel"                                                                      `"subtree"`
   ldap\_tls        Enable TLS (StartTLS) to connect to LDAP (can be true or false). The non-standard 'LDAPS' protocol is not supported.   `false`
   ldap\_mode       How passwords are validated.                                                                                           `"bind"`
 
--- a/mod_auth_ldap/mod_auth_ldap.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_auth_ldap/mod_auth_ldap.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -9,7 +9,7 @@
 local ldap_rootdn = module:get_option_string("ldap_rootdn", "");
 local ldap_password = module:get_option_string("ldap_password", "");
 local ldap_tls = module:get_option_boolean("ldap_tls");
-local ldap_scope = module:get_option_string("ldap_scope", "onelevel");
+local ldap_scope = module:get_option_string("ldap_scope", "subtree");
 local ldap_filter = module:get_option_string("ldap_filter", "(uid=$user)"):gsub("%%s", "$user", 1);
 local ldap_base = assert(module:get_option_string("ldap_base"), "ldap_base is a required option for ldap");
 local ldap_mode = module:get_option_string("ldap_mode", "bind");
--- a/mod_http_muc_log/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_http_muc_log/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -8,41 +8,40 @@
 ============
 
 This module provides a built-in web interface to view chatroom logs
-stored by [mod\_mam\_muc](mod_mam_muc.html).
+stored by [mod\_mam\_muc].
 
 Installation
 ============
 
-Just copy the folder muc\_log\_http as it is, into the modules folder of
-your Prosody installation.
+Same as any other module, be sure to include the HTML template
+`http_muc_log.html` alongside `mod_http_muc_log.lua`.
 
-Configuration Details
-=====================
+Configuration
+=============
 
-You need to add muc\_log\_http to your global modules\_enabled, and the
-configuration options similarly must be put into your global
-(server-wide) options section:
+For example:
 
-        Component "conference.example.com" "muc"
-        modules_enabled = {
-            .....
-            "mam_muc";
-            "http_muc_log";
-            .....
-        }
-        storage = {
-            muc_log = "sql2"; -- for example
-        }
+``` lua
+Component "conference.example.com" "muc"
+modules_enabled = {
+    "mam_muc";
+    "http_muc_log";
+}
+storage = {
+    muc_log = "sql"; -- for example
+}
+```
 
 The web interface would then be reachable at the address:
 
     http://conference.example.com:5280/muc_log/
 
-See [the page about Prosodys HTTP server](http://prosody.im/doc/http)
-for info about the address.
+See [the page about Prosodys HTTP server][doc:http] for info about the
+address.
 
 Compatibility
 =============
 
 Requires Prosody 0.10 or above and a storage backend with support for
-stanza archives.
+stanza archives. See [mod\_storage\_muc\_log] for using legacy data from
+[mod\_muc\_log].
--- a/mod_http_upload/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_http_upload/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -58,6 +58,16 @@
 http_upload_file_size_limit = 10 * 1024 * 1024 -- this is 10MB in bytes
 ```
 
+Path
+----
+
+By default, uploaded files are put in a sub-directory of the default
+Prosody storage path (usually `/var/lib/prosody`). This can be changed:
+
+``` {.lua}
+http_upload_path = "/path/to/uploded/files"
+```
+
 Compatibility
 =============
 
--- a/mod_http_upload/mod_http_upload.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_http_upload/mod_http_upload.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -11,6 +11,7 @@
 local st = require"util.stanza";
 local lfs = require"lfs";
 local uuid = require"util.uuid".generate;
+local urlencode = require"util.http".urlencode;
 local t_concat = table.concat;
 local t_insert = table.insert;
 local s_upper = string.upper;
@@ -34,7 +35,7 @@
 -- state
 local pending_slots = module:shared("upload_slots");
 
-local storage_path = join_path(prosody.paths.data, module.name);
+local storage_path = module:get_option_string(module.name .. "_path", join_path(prosody.paths.data, module.name));
 lfs.mkdir(storage_path);
 
 -- hooks
@@ -66,7 +67,7 @@
 	reply:tag("slot", { xmlns = xmlns_http_upload });
 	local random = uuid();
 	pending_slots[random.."/"..filename] = origin.full_jid;
-	local url = module:http_url() .. "/" .. random .. "/" .. filename;
+	local url = module:http_url() .. "/" .. random .. "/" .. urlencode(filename);
 	reply:tag("get"):text(url):up();
 	reply:tag("put"):text(url):up();
 	origin.send(reply);
--- a/mod_mam/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_mam/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -7,8 +7,7 @@
 Introduction
 ============
 
-Implementation of [XEP-0313: Message Archive
-Management](http://xmpp.org/extensions/xep-0313.html).
+Implementation of [XEP-0313: Message Archive Management].
 
 Details
 =======
@@ -37,15 +36,14 @@
 Storage backend
 ---------------
 
-mod\_mam uses the store "archive2"[^1]. See [Prosodys data storage
-documentation](https://prosody.im/doc/storage) for information on how to
-configure storage.
+mod\_mam uses the store "archive2"[\^1]. See [Prosodys data storage
+documentation][doc:storage] for information on how to configure storage.
 
-For example, to use mod\_storage\_sql2[^2]:
+For example, to use mod\_storage\_sql:
 
 ``` {.lua}
 storage = {
-  archive2 = "sql2";
+  archive2 = "sql";
 }
 ```
 
@@ -85,14 +83,12 @@
 
   ------- ---------------
   trunk   Works
-  0.10    Works [^3]
+  0.10    Works [^2]
   0.9     Unsupported
   0.8     Does not work
   ------- ---------------
 
 [^1]: Might be changed to "mam" at some point
 
-[^2]: mod\_storage\_sql2 will replace mod\_storage\_sql at some point
-
-[^3]: requires a storage driver with archive support, eg
-    mod\_storage\_sql2 in 0.10
+[^2]: requires a storage driver with archive support, eg
+    mod\_storage\_sql in 0.10
--- a/mod_mam/mod_mam.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_mam/mod_mam.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -224,7 +224,7 @@
 	-- Stanza without 'to' are treated as if it was to their own bare jid
 
 	-- We store chat messages or normal messages that have a body
-	if not(orig_type == "chat" or orig_type == "normal" and stanza:get_child("body") ) then
+	if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then
 		module:log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
 		return;
 	end
--- a/mod_mam_muc/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_mam_muc/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -9,9 +9,8 @@
 
 This module logs the conversation of chatrooms running on the server to
 Prosody's archive storage. To access them you will need a client with
-support for [XEP-0313: Message Archive
-Management](http://xmpp.org/extensions/xep-0313.html) or a module such
-as [mod\_http\_muc\_log](mod_http_muc_log.html).
+support for [XEP-0313: Message Archive Management] or a module such
+as [mod\_http\_muc\_log].
 
 Usage
 =====
@@ -25,16 +24,18 @@
 modules_enabled = {
   "mam_muc",
 }
+```
+
+And configure it to use an archive-capable storage module:
+
+``` {.lua}
 storage = {
-  -- This makes mod_mam_muc use the sql2 storage backend (others will use internal)
-    -- which at the time of this writing is the only one supporting stanza archives
-    muc_log = "sql2";
+    muc_log = "sql"; -- Requires 0.10 or later
 }
 ```
 
-See [Prosodys data storage
-documentation](https://prosody.im/doc/storage) for more info on how to
-configure storage for different plugins.
+See [Prosodys data storage documentation][doc:storage] for more info on
+how to configure storage for different plugins.
 
 Configuration
 =============
@@ -57,9 +58,13 @@
 Compatibility
 =============
 
-  ------- ---------------
-  trunk   Works
-  0.10    Works
+  ------- -----------------
+  trunk   Works best
+  0.10    Works partially
   0.9     Does not work
   0.8     Does not work
-  ------- ---------------
+  ------- -----------------
+
+Prosody trunk (after April 2014) has a major rewrite of the MUC module,
+allowing easier integration. Without this (0.10), some features do not
+work, such as correct advertising and join/part logging.
--- a/mod_mam_muc/mod_mam_muc.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_mam_muc/mod_mam_muc.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -3,6 +3,11 @@
 --
 -- This file is MIT/X11 licensed.
 
+if module:get_host_type() ~= "component" then
+	module:log("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name);
+	return;
+end
+
 local xmlns_mam     = "urn:xmpp:mam:0";
 local xmlns_delay   = "urn:xmpp:delay";
 local xmlns_forward = "urn:xmpp:forward:0";
--- a/mod_migrate/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_migrate/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -37,7 +37,7 @@
 ========
 
     for store in accounts roster private blocklist vcard archive2-archive; do
-      prosodyctl migrate example.com $store sql2
+      prosodyctl migrate example.com $store sql
     done
 
 Compatibility
--- a/mod_migrate/mod_migrate.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_migrate/mod_migrate.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -5,55 +5,57 @@
 local mm = require"core.modulemanager";
 
 function module.command(arg)
-	local host, source_store, migrate_to, user = unpack(arg);
+	local host, source_stores, migrate_to, user = unpack(arg);
 	if not migrate_to then
 		return print("Usage: prosodyctl mod_migrate example.com <source-store>[-<store-type>] <target-driver> [users]*");
 	end
 	sm.initialize_host(host);
 	um.initialize_host(host);
 	local module = module:context(host);
-	local store_type = source_store:match("%-(%a+)$");
-	if store_type then
-		source_store = source_store:sub(1, -2-#store_type);
-	end
-	local storage = module:open_store(source_store, store_type);
-	local target = assert(sm.load_driver(host, migrate_to));
-	target = assert(target:open(source_store, store_type));
+	for source_store in source_stores:gmatch("[^,]+") do
+		local store_type = source_store:match("%-(%a+)$");
+		if store_type then
+			source_store = source_store:sub(1, -2-#store_type);
+		end
+		local storage = module:open_store(source_store, store_type);
+		local target = assert(sm.load_driver(host, migrate_to));
+		target = assert(target:open(source_store, store_type));
 
-	local function migrate_user(username)
-		module:log("info", "Migrating data for %s", username);
-		local data, err = storage:get(username);
-		assert(data or err==nil, err);
-		assert(target:set(username, data));
-	end
+		local function migrate_user(username)
+			module:log("info", "Migrating data for %s", username);
+			local data, err = storage:get(username);
+			assert(data or err==nil, err);
+			assert(target:set(username, data));
+		end
 
-	if store_type == "archive" then
-		function migrate_user(username)
-			module:log("info", "Migrating archive items for %s", username);
-			local count, errs = 0, 0;
-			for id, item, when, with in storage:find(username) do
-				local ok, err = target:append(username, id, item, when, with);
-				if ok then
-					count = count + 1;
-				else
-					module:log("warn", "Error: %s", err);
-					errs = errs + 1;
+		if store_type == "archive" then
+			function migrate_user(username)
+				module:log("info", "Migrating archive items for %s", username);
+				local count, errs = 0, 0;
+				for id, item, when, with in storage:find(username) do
+					local ok, err = target:append(username, id, item, when, with);
+					if ok then
+						count = count + 1;
+					else
+						module:log("warn", "Error: %s", err);
+						errs = errs + 1;
+					end
+					if ( count + errs ) % 100 == 0 then
+						module:log("info", "%d items migrated, %d errors", count, errs);
+					end
 				end
-				if ( count + errs ) % 100 == 0 then
-					module:log("info", "%d items migrated, %d errors", count, errs);
-				end
+				module:log("info", "%d items migrated, %d errors", count, errs);
 			end
-			module:log("info", "%d items migrated, %d errors", count, errs);
 		end
-	end
 
-	if arg[4] then
-		for i = 4, #arg do
-			migrate_user(arg[i]);
-		end
-	else
-		for user in um.users(host) do
-			migrate_user(user);
+		if arg[4] then
+			for i = 4, #arg do
+				migrate_user(arg[i]);
+			end
+		else
+			for user in um.users(host) do
+				migrate_user(user);
+			end
 		end
 	end
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_access_control/mod_muc_access_control.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -0,0 +1,57 @@
+local st = require "util.stanza";
+local jid = require "util.jid";
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+
+local unprepped_access_lists = module:get_option("muc_access_lists", {});
+local access_lists = {};
+
+-- Make sure all input is prepped
+for unprepped_room_name, unprepped_list in pairs(unprepped_access_lists) do
+	local prepped_room_name = nodeprep(unprepped_room_name);
+	if not prepped_room_name then
+		module:log("error", "Invalid room name: %s", unprepped_room_name);
+	else
+		local prepped_list = {};
+		for _, unprepped_jid in ipairs(unprepped_list) do
+			local prepped_jid = jid.prep(jid);
+			if not prepped_jid then
+				module:log("error", "Invalid JID: %s", unprepped_jid);
+			else
+				table.insert(prepped_list, jid.pep(jid));
+			end
+		end
+	end
+end
+
+local function is_restricted(room, who)
+	local allowed = access_lists[room];
+
+	if allowed == nil or allowed[who] or allowed[select(2, jid.split(who))] then
+		return nil;
+	end
+
+	return "forbidden";
+end
+
+module:hook("presence/full", function(event)
+        local stanza = event.stanza;
+
+        if stanza.name == "presence" and stanza.attr.type == "unavailable" then   -- Leaving events get discarded
+                return;
+        end
+
+	-- Get the room
+	local room = jid.split(stanza.attr.to);
+        if not room then return; end
+
+	-- Get who has tried to join it
+	local who = jid.bare(stanza.attr.from)
+
+	-- Checking whether room is restricted
+	local check_restricted = is_restricted(room, who)
+        if check_restricted ~= nil then
+                event.allowed = false;
+                event.stanza.attr.type = 'error';
+	        return event.origin.send(st.error_reply(event.stanza, "cancel", "forbidden", "You're not allowed to enter this room: " .. check_restricted));
+        end
+end, 10);
--- a/mod_muc_log_http/muc_log_http/themes/prosody/day_body.html	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_muc_log_http/muc_log_http/themes/prosody/day_body.html	Mon Dec 21 12:24:21 2015 +0100
@@ -1,7 +1,7 @@
 <div id="title">
 	<div id="date">###DATE###</div>
 	<div id="roomjid">###JID###</div>
-	<div id="links">(join via <a class="component" href="xmpp:###JID###?join">client</a> / <a class="component" href="http://speeqe.com/room/###JID###/">speeqe</a>)</div>
+	<div id="links">(join via <a class="component" href="xmpp:###JID###?join">client</a>)</div>
 </div>
 <div id="calendar">
 	<div id="navigation">
--- a/mod_muc_log_http/muc_log_http/themes/prosody/days_body.html	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_muc_log_http/muc_log_http/themes/prosody/days_body.html	Mon Dec 21 12:24:21 2015 +0100
@@ -1,7 +1,7 @@
 <div id="title">
 	<div id="date">###SINCE### - ###TO###</div>
 	<div id="roomjid">###JID###</div>
-	<div id="links">(join via <a class="component" href="xmpp:###JID###?join">client</a> / <a class="component" href="http://speeqe.com/room/###JID###/">speeqe</a>)</div>
+	<div id="links">(join via <a class="component" href="xmpp:###JID###?join">client</a>)</div>
 </div>
 <div id="calendar">
 	<div id="navigation">
--- a/mod_presence_cache/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_presence_cache/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -14,7 +14,8 @@
 replies to presence probes. Also see [mod\_throttle\_presence].
 
 By default, only binary (online or offline) state is stored. It can
-optionally store the full presence but this requires much more memory.
+optionally store the full presence but this requires much more memory
+and bandwidth.
 
 Configuration
 =============
@@ -29,10 +30,13 @@
 Advanced configuration
 ======================
 
-To enable full stanza caching:
+To enable *experimental* full stanza caching:
 
     presence_cache_full = false
 
+This will usually double the presence data sent to clients, pending
+support for deduplication.
+
 TODO
 ====
 
--- a/mod_pubsub_hub/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_pubsub_hub/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -1,18 +1,19 @@
 ---
-summary: PubSubHubbub service
+summary: PubSubHubbub hub
 ...
 
 Introduction
-------------
+============
 
 This module implements a
-[PubSubHubbub](http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html)(PuSH)
-hub, allowing PuSH clients to subscribe to local XMPP
+[PubSubHubbub](http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html)
+(PuSH) hub, allowing PuSH clients to subscribe to local XMPP
 [Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html) nodes
-stored by [mod\_pubsub](http://prosody.im/doc/modules/mod_pubsub).
+stored by [mod\_pubsub](http://prosody.im/doc/modules/mod_pubsub) and
+receive real time updates to feeds.
 
 Configuration
--------------
+=============
 
     Component "pubsub.example.com" "pubsub"
 
@@ -20,13 +21,16 @@
       "pubsub_hub";
     }
 
-The hub is then available on {http://pubsub.example.com:5280/hub}.
+The hub is then available on `http://pubsub.example.com:5280/hub`.
 
 Compatibility
--------------
+=============
 
   ------- --------------
   trunk   Works
+  0.10    Should work
   0.9     Works
   0.8     Doesn't work
   ------- --------------
+
+
--- a/mod_s2s_auth_dane/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_s2s_auth_dane/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -26,8 +26,7 @@
 Configuration
 =============
 
-After [installing the
-module](https://prosody.im/doc/installing_modules), just add it to
+After [installing the module][doc:installing\_modules], just add it to
 `modules_enabled`;
 
     modules_enabled = {
@@ -78,8 +77,7 @@
 Further reading
 ===============
 
--   [DANE TLSA implementation and operational
-    guidance](http://tools.ietf.org/html/draft-ietf-dane-ops)
+-   [DANE Operational Guidance][rfc7671]
 
 Compatibility
 =============
--- a/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -67,11 +67,12 @@
 -- Takes a s2sin/out and a callback
 local function dane_lookup(host_session, cb)
 	cb = cb or noop;
+	local log = host_session.log or module._log;
 	if host_session.dane ~= nil then return end -- Has already done a lookup
 
 	if host_session.direction == "incoming" then
 		if not host_session.from_host then
-			module:log("debug", "Session doesn't have a 'from' host set");
+			log("debug", "Session doesn't have a 'from' host set");
 			return;
 		end
 		-- We don't know what hostname or port to use for Incoming connections
@@ -80,19 +81,24 @@
 		-- and incoming connections, so this should work well
 		local name = host_session.from_host and idna_to_ascii(host_session.from_host);
 		if not name then
-			module:log("warn", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host));
+			log("warn", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host));
 			return;
 		end
+		log("debug", "Querying SRV records from _xmpp-server._tcp.%s.", name);
 		host_session.dane = dns_lookup(function (answer, err)
 			host_session.dane = false; -- Mark that we already did the lookup
 
 			if not answer then
-				module:log("debug", "Resolver error: %s", tostring(err));
+				log("debug", "Resolver error: %s", tostring(err));
 				return cb(host_session);
 			end
 
-			if not answer.secure then
-				module:log("debug", "Results are not secure");
+			if answer.bogus then
+				log("warn", "Results are bogus!");
+				-- Bad sign, probably not a good idea to do any fallback here
+				host_session.dane = answer;
+			elseif not answer.secure then
+				log("debug", "Results are not secure");
 				return cb(host_session);
 			end
 
@@ -111,7 +117,9 @@
 			local dane;
 			for _, record in ipairs(answer) do
 				t_insert(srv_hosts, record.srv);
+				log("debug", "Querying TLSA record for %s:%d", record.srv.target, record.srv.port);
 				dns_lookup(function(dane_answer)
+					log("debug", "Got answer for %s:%d", record.srv.target, record.srv.port);
 					n = n - 1;
 					-- There are three kinds of answers
 					-- Insecure, Secure and Bogus
@@ -129,10 +137,13 @@
 					if (dane_answer.bogus or dane_answer.secure) and not dane then
 						-- The first answer we care about
 						-- For services with only one SRV record, this will be the only one
+						log("debug", "First secure (or bogus) TLSA")
 						dane = dane_answer;
 					elseif dane_answer.bogus then
+						log("debug", "Got additional bogus TLSA")
 						dane.bogus = dane_answer.bogus;
 					elseif dane_answer.secure then
+						log("debug", "Got additional secure TLSA")
 						for _, dane_record in ipairs(dane_answer) do
 							t_insert(dane, dane_record);
 						end
@@ -143,7 +154,7 @@
 							if #dane > 0 and dane.bogus then
 								-- Got at least one non-bogus reply,
 								-- This should trigger a failure if one of them did not match
-								host_session.log("warn", "Ignoring bogus replies");
+								log("warn", "Ignoring bogus replies");
 								dane.bogus = nil;
 							end
 							if #dane == 0 and dane.bogus == nil then
@@ -166,6 +177,7 @@
 		end
 		-- Do TLSA lookup for currently selected SRV record
 		local srv_choice = srv_hosts[host_session.srv_choice or 0] or { target = idna_to_ascii(host_session.to_host), port = 5269 };
+		log("debug", "Querying TLSA record for %s:%d", srv_choice.target, srv_choice.port);
 		host_session.dane = dns_lookup(function(answer)
 			if answer and ((answer.secure and #answer > 0) or answer.bogus) then
 				srv_choice.dane = answer;
@@ -221,13 +233,12 @@
 			return false;
 		end
 		-- Cleanup
-		session.dane = nil;
 		session.srv_hosts = nil;
 	end);
 end
 
 -- Compare one TLSA record against a certificate
-local function one_dane_check(tlsa, cert)
+local function one_dane_check(tlsa, cert, log)
 	local select, match, certdata = tlsa.select, tlsa.match;
 
 	if select == 0 then
@@ -235,7 +246,7 @@
 	elseif select == 1 and cert.pubkey then
 		certdata = pem2der(cert:pubkey());
 	else
-		module:log("warn", "DANE selector %s is unsupported", tlsa:getSelector() or select);
+		log("warn", "DANE selector %s is unsupported", tlsa:getSelector() or select);
 		return;
 	end
 
@@ -244,12 +255,12 @@
 	elseif match == 2 then
 		certdata = hashes.sha512(certdata);
 	elseif match ~= 0 then
-		module:log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match);
+		log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match);
 		return;
 	end
 
 	if #certdata ~= #tlsa.data then
-		module:log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data);
+		log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data);
 	end
 	return certdata == tlsa.data;
 end
@@ -263,14 +274,14 @@
 		local match_found, supported_found;
 		for i = 1, #dane do
 			local tlsa = dane[i].tlsa;
-			module:log("debug", "TLSA #%d: %s", i, tostring(tlsa))
+			log("debug", "TLSA #%d: %s", i, tostring(tlsa))
 			local use = tlsa.use;
 
 			if enabled_uses:contains(use) then
 				-- DANE-EE or PKIX-EE
 				if use == 3 or use == 1 then
 					-- Should we check if the cert subject matches?
-					local is_match = one_dane_check(tlsa, cert);
+					local is_match = one_dane_check(tlsa, cert, log);
 					if is_match ~= nil then
 						supported_found = true;
 					end
@@ -286,6 +297,7 @@
 							session.cert_chain_status = "valid";
 						end
 						match_found = true;
+						dane.matching = tlsa;
 						break;
 					end
 				-- DANE-TA or PKIX-CA
@@ -294,7 +306,7 @@
 					local chain = session.conn:socket():getpeerchain();
 					for c = 1, #chain do
 						local cacert = chain[c];
-						local is_match = one_dane_check(tlsa, cacert);
+						local is_match = one_dane_check(tlsa, cacert, log);
 						if is_match ~= nil then
 							supported_found = true;
 						end
@@ -315,6 +327,7 @@
 								end
 							end
 							match_found = true;
+							dane.matching = tlsa;
 							break;
 						end
 					end
@@ -350,3 +363,32 @@
 	end
 end);
 
+-- Telnet command
+if module:get_option_set("modules_enabled", {}):contains("admin_telnet") then
+	module:depends("admin_telnet"); -- Make sure the env is there
+	local def_env = module:shared("admin_telnet/env");
+
+	local sessions = module:shared("s2s/sessions");
+
+	local function annotate(session, line)
+		line = line or {};
+		table.insert(line, "--");
+		if session.dane == nil then
+			table.insert(line, "No DANE attempted, probably insecure SRV response");
+		elseif session.dane == false then
+			table.insert(line, "DANE failed or response was insecure");
+		elseif type(session.dane) ~= "table" then
+			table.insert(line, "Waiting for DANE records...");
+		elseif session.dane.matching then
+			table.insert(line, "Matching DANE record:\n|       " .. tostring(session.dane.matching));
+		else
+			table.insert(line, "DANE records:\n|       " .. tostring(session.dane));
+		end
+		return table.concat(line, " ");
+	end
+
+	function def_env.s2s:show_dane(...)
+		return self:show(..., annotate);
+	end
+end
+
--- a/mod_storage_gdbm/mod_storage_gdbm.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_storage_gdbm/mod_storage_gdbm.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -29,10 +29,6 @@
 	return getmetatable(s) == st.stanza_mt;
 end
 
-local function t(c, a, b)
-	if c then return a; end return b;
-end
-
 local base_path = path.resolve_relative_path(prosody.paths.data, module.host);
 lfs.mkdir(base_path);
 
@@ -106,26 +102,26 @@
 	query = query or empty;
 	local meta = self:get(username) or empty;
 	local prefix = (username or "@") .. "#";
-	local r = query.reverse;
-	local d = t(r, -1, 1);
-	local s = meta[t(r, query.before, query.after)];
+	local reverse = query.reverse;
+	local step = reverse and -1 or 1;
+	local first = meta[query.after];
+	local last = meta[query.before];
+	-- we don't want to include the actual 'after' or 'before' item
+	if first then first = first + 1; end
+	if last then last = last - 1; end
+	first, last = first or 1, last or #meta;
+	if reverse then first, last = last, first; end
 	local limit = query.limit;
-	if s then
-		s = s + d;
-	else
-		s = t(r, #meta, 1)
-	end
-	local e = t(r, 1, #meta);
-	local c = 0;
+	local count = 0;
 	return function ()
-		if limit and c >= limit then return end
+		if limit and count >= limit then return end
 		local item, value;
-		for i = s, e, d do
+		for i = first, last, step do
 			item = meta[i];
 			if (not query.with or item.with == query.with)
 			and (not query.start or item.when >= query.start)
 			and (not query["end"] or item.when <= query["end"]) then
-				s = i + d; c = c + 1;
+				first = i + step; count = count + 1;
 				value = self:get(prefix..item.key);
 				return item.key, (deserialize_map[item.type] or id)(value), item.when, item.with;
 			end
--- a/mod_storage_lmdb/mod_storage_lmdb.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_storage_lmdb/mod_storage_lmdb.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -8,6 +8,11 @@
 --
 -- luacheck: globals prosody open
 
+local assert = assert;
+local select = select;
+local xpcall = xpcall;
+local traceback = debug.traceback;
+
 local lmdb = require"lightningmdb";
 local lfs = require"lfs";
 local path = require"util.paths";
@@ -15,6 +20,36 @@
 local serialize = serialization.serialize;
 local deserialize = serialization.deserialize;
 
+local function transaction(env, flag, func, ...)
+	local args, n_args = {...}, select("#", ...);
+	local t = env:txn_begin(nil, flag);
+	local function f() return func(t, unpack(args, 1, n_args)); end
+	local success, a, b, c = xpcall(f, traceback);
+	if not success then
+		t:abort();
+		return success, a;
+	end
+	local ok, err = t:commit();
+	if not ok then
+		return ok, err;
+	end
+	return success, a, b, c;
+end
+
+local function keyvalue_set(t, db, key, value)
+	if value ~= nil then
+		return assert(t:put(db, key, value, 0));
+	else
+		return t:del(db, key, value);
+	end
+end
+
+local function keyvalue_get(t, db, key)
+	local data, err = t:get(db, key, 0);
+	assert(data or not err, err);
+	return data;
+end
+
 local drivers = {};
 local provider = {};
 
@@ -22,35 +57,19 @@
 local keyval_mt = { __index = keyval, flags = lmdb.MDB_CREATE };
 drivers.keyval = keyval_mt;
 
-function keyval:set(user, value)
-	local t = self.env:txn_begin(nil, 0);
+function keyval:set(key, value)
 	if type(value) == "table" and next(value) == nil then
 		value = nil;
 	end
 	if value ~= nil then
 		value = serialize(value);
 	end
-	local ok, err;
-	if value ~= nil then
-		ok, err = t:put(self.db, user, value, 0);
-	else
-		ok, err = t:del(self.db, user, value);
-	end
-	if not ok then
-		t:abort();
-		return nil, err;
-	end
-	return t:commit();
+	return transaction(self.env, 0, keyvalue_set, self.db, key, value);
 end
 
-function keyval:get(user)
-	local t = self.env:txn_begin(nil, 0);
-	local data, err = t:get(self.db, user, 0);
-	if not data then
-		t:abort();
-		return nil, err;
-	end
-	t:commit();
+function keyval:get(key)
+	local ok, data = transaction(self.env, lmdb.MDB_RDONLY, keyvalue_get, self.db, key);
+	if not ok then return ok, data; end
 	return deserialize(data);
 end
 
@@ -95,7 +114,7 @@
 		maxdbs = module:get_option_number("lmdb_maxdbs", 20);
 	});
 
-	function module.unload()
+	function module.unload() --luacheck: ignore
 		provider.env:sync(1);
 		provider.env:close();
 	end
--- a/mod_storage_xmlarchive/README.markdown	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_storage_xmlarchive/README.markdown	Mon Dec 21 12:24:21 2015 +0100
@@ -23,7 +23,7 @@
 }
 ```
 
-To use it with [mod\_mam\_muc] or [mod_http_muc_log]:
+To use it with [mod\_mam\_muc] or [mod\_http\_muc\_log]:
 
 ``` lua
 storage = {
@@ -31,8 +31,8 @@
 }
 ```
 
-Refer to [Prosodys data storage
-documentation](https://prosody.im/doc/storage) for more information.
+Refer to [Prosodys data storage documentation][doc:storage] for more
+information.
 
 Note that this module does not implement the "keyval" storage method and
 can't be used by anything other than archives.
--- a/mod_storage_xmlarchive/mod_storage_xmlarchive.lua	Mon Dec 21 11:31:10 2015 +0100
+++ b/mod_storage_xmlarchive/mod_storage_xmlarchive.lua	Mon Dec 21 12:24:21 2015 +0100
@@ -12,21 +12,6 @@
 local new_stream = require "util.xmppstream".new;
 local empty = {};
 
-local function fallocate(f, offset, len)
-	-- This assumes that current position == offset
-	local fake_data = (" "):rep(len);
-	local ok, msg = f:write(fake_data);
-	if not ok then
-		return ok, msg;
-	end
-	return f:seek("set", offset);
-end;
-
-pcall(function()
-	local pposix = require "util.pposix";
-	fallocate = pposix.fallocate or fallocate;
-end);
-
 local archive = {};
 local archive_mt = { __index = archive };
 
@@ -43,25 +28,23 @@
 	data = tostring(data) .. "\n";
 
 	local day = dt.date(when);
-	local filename = dm.getpath(username.."@"..day, module.host, self.store, "xml", true);
-
-	local ok, err;
-	local f = io.open(filename, "r+");
-	if not f then
-		f, err = io.open(filename, "w");     if not f then return nil, err; end
-		ok, err = dm.list_append(username, module.host, self.store, day);
-		if not ok then return nil, err; end
+	local ok, err = dm.append_raw(username.."@"..day, module.host, self.store, "xml", data);
+	if not ok then
+		return nil, err;
 	end
 
-	local offset = f:seek("end"); -- Seek to the end and collect current file length
-	-- then make sure there is enough free space for what we're about to add
-	ok, err = fallocate(f, offset, #data); if not ok then return nil, err; end
-	ok, err = f:write(data);               if not ok then return nil, err; end
-	ok, err = f:close();                   if not ok then return nil, err; end
+	local offset = ok and err;
 
 	local id = day .. "-" .. hmac_sha256(username.."@"..day.."+"..offset, data, true):sub(-16);
 	ok, err = dm.list_append(username.."@"..day, module.host, self.store, { id = id, when = dt.datetime(when), with = with, offset = offset, length = #data });
-	if not ok then return nil, err; end
+	if offset == 0 then
+		-- means the message is at the beginnig of the file, so it's a new day
+		-- so we add this new day to the "index"
+		dm.list_append(username, module.host, self.store, day);
+	end
+	if not ok then
+		return nil, err;
+	end
 	return id;
 end