TT#66583 remove modules merged at prosody 0.11 version

- mod_carbons
- mod_csi
- mod_man
- mod_websocket

* debian/control:
  we want prosody 0.11 version
  remove ngcp-system-tools from dependences, not called
   since removal of postinst
  remove modules merged upstream from description

Change-Id: I83f5fd48ba17fe4c91763177fed7dee69ca02db1
changes/52/34952/8
Victor Seva 6 years ago
parent 34e3345f53
commit 4fb063dd25

8
debian/control vendored

@ -17,9 +17,7 @@ Depends:
lua-redis,
lua-rex-pcre,
lua-sec,
ngcp-system-tools-ce | ngcp-system-tools,
prosody (>= 0.9.8~),
prosody (<< 0.10~),
prosody (>= 0.11~),
${misc:Depends},
Description: ngcp modules for the prosody Jabber/XMPP server
This package provides modules for the Prosody Jabber/XMPP server
@ -46,17 +44,13 @@ Description: ngcp modules for the prosody Jabber/XMPP server
* mod_auth_sql.lua: simple SQL Authentication module
* mod_blocking: allows the client to manage a simple
list of blocked JIDs
* mod_carbons: XEP-0280: Message Carbons
* mod_carbons_adhoc
* mod_carbons_copies
* mod_csi: XEP-0352: Client State Indication
* mod_filter_chatstates: will filter Chat State Notifications out
while the session is considered inactive
* mod_limit_auth: lets you put a per-IP limit on the number of
failed authentication attempts
* mod_log_auth: logs IP address in a failed authentication attempt
* mod_man: XEP-0313: Message Archive Management
* mod_smacks: XEP-0198: Stream Management
* mod_throttle_presence: cuts down on presence traffic when clients
indicate they are inactive
* mod_websocket.lua: XMPP over websocket

@ -1,54 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Fri, 7 Apr 2017 11:12:23 +0200
Subject: TT#14278 mam: add all supported versions to disco
* sipwise app needs 'urn:xmpp:mam:1' at disco result
* upstream did implement supporting urn:xmpp_mam:[0-2] slightly different
looking through the code y noticed that if the disco has no 'to' it shows
the versions of mam supported
<!-- Out Fri 07 Apr 2017 10:53:38 AM CEST -->
<iq type='get'
id='info1'>
<query xmlns='http://jabber.org/protocol/disco#info'/>
</iq>
<!-- In Fri 07 Apr 2017 10:53:38 AM CEST -->
<iq id='info1' type='result' to='vseva@sipwise.com/Gajim' from='vseva@sipwise.com'>
<query xmlns='http://jabber.org/protocol/disco#info';>
<feature var='urn:xmpp:mam:0'/>
<feature var='urn:xmpp:mam:1'/>
<feature var='urn:xmpp:mam:2'/>
<feature var='urn:xmpp:sid:0'/>
<identity type='pep' category='pubsub'/>
<feature var='http://jabber.org/protocol/pubsub#publish'/>
</query>
</iq>
That is not what the XEP 0313 says:
<quote>
7. Determining support
If a server or other entity hosts archives and supports MAM queries, it MUST
advertise the 'urn:xmpp:mam:2' feature in response to Service Discovery
(XEP-0030) [15] requests made to archiving JIDs
(i.e. JIDs hosting an archive, such as users' bare JIDs):
</quote>
Change-Id: Ib79b6f2f60f71a80030bbd12b2eb893b60ac025d
---
plugins/mod_mam/mod_mam.lua | 2 ++
1 file changed, 2 insertions(+)
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index 61b4388..f8a2321 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -423,6 +423,8 @@ module:hook("message/bare", message_handler, priority);
module:hook("message/full", message_handler, priority);
module:add_feature(xmlns_mam0); -- COMPAT with XEP-0313 v 0.1
+module:add_feature(xmlns_mam1); -- COMPAT with XEP-0313 v 0.5
+module:add_feature(xmlns_mam2); -- COMPAT with XEP-0313 v 0.6
module:hook("account-disco-info", function(event)
(event.reply or event.stanza):tag("feature", {var=xmlns_mam0}):up();

@ -1,211 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Tue, 3 Jan 2017 17:14:49 +0100
Subject: [PATCH] TT#8297 mam: implement archive to DB
Change-Id: If9644ac66fdd257a430cfb7fa185230c77cd783e
Change-Id: I587debda7ed78cc51088bd4a5ba96c878db45e6f
---
plugins/mod_mam/mod_mam.lua | 16 +---
plugins/mod_mam/sipwise_archive.lib.lua | 165 ++++++++++++++++++++++++++++++++
2 files changed, 166 insertions(+), 15 deletions(-)
create mode 100644 plugins/mod_mam/sipwise_archive.lib.lua
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index f8a2321..faff0dd 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -40,21 +40,7 @@ if global_default_policy ~= "roster" then
global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
end
-local archive_store = module:get_option_string("archive_store", "archive2");
-local archive = module:open_store(archive_store, "archive");
-
-if archive.name == "null" or not archive.find then
- -- luacheck: ignore 631
- if not archive.find then
- module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API");
- module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
- else
- module:log("debug", "Attempt to open archive storage returned null driver");
- end
- module:log("debug", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
- module:log("info", "Using in-memory fallback archive driver");
- archive = module:require "fallback_archive";
-end
+local archive = module:require "sipwise_archive";
local use_total = true;
diff --git a/plugins/mod_mam/sipwise_archive.lib.lua b/plugins/mod_mam/sipwise_archive.lib.lua
new file mode 100644
index 0000000..f772ea6
--- /dev/null
+++ b/plugins/mod_mam/sipwise_archive.lib.lua
@@ -0,0 +1,165 @@
+-- luacheck: ignore 212/self
+local uuid = require "util.uuid".generate;
+local archive_store = { _provided_by = "mam"; name = "fallback"; };
+
+local serialize = require "util.serialization".serialize;
+local deserialize = require "util.serialization".deserialize;
+local st = require "util.stanza";
+
+local mod_sql = module:require("sql");
+local params = module:get_option("sql", {});
+local engine = mod_sql:create_engine(params);
+engine:execute("SET NAMES 'utf8' COLLATE 'utf8_bin';");
+local log = require "util.logger".init("sipwise_archive");
+local ut_tostring = require "util.table".table.tostring;
+
+local store_query=[[
+INSERT INTO `sipwise_mam` (`username`, `key`, `stanza`, `epoch`, `with`)
+VALUES (?,UuidToBin(?),?,?,?);
+]]
+
+local delete_query=[[
+DELETE FROM `sipwise_mam`
+WHERE `username` = ?;
+]]
+
+local delete_query_extra=[[
+DELETE FROM `sipwise_mam`
+WHERE `username` = ? AND `epoch` <= ?;
+]]
+
+local select_key_query=[[
+SELECT id FROM `sipwise_mam`
+WHERE `key` = UuidToBin(?)
+]]
+
+local select_query_base=[[
+SELECT UuidFromBin(`key`),`stanza`,`epoch`,`with` FROM `sipwise_mam`
+WHERE `username` = ?
+]]
+
+-- Reconnect to DB if necessary
+local function reconect_check()
+ if not engine.conn:ping() then
+ engine.conn = nil;
+ log("debug", "DDBB reconecting");
+ engine:connect();
+ end
+end
+
+local function load_db(query, _params)
+ local res;
+ reconect_check();
+ log("debug", "query[%s]", query);
+ log("debug", "_params[%s]", ut_tostring(_params));
+ res = engine:select(query, unpack(_params));
+ local out = {};
+ for row in res do
+ table.insert(out, {row[1], row[2], row[3], row[4]});
+ end
+ return out;
+end
+
+local function key_get_id(key)
+ local res;
+ reconect_check();
+ res = engine:select(select_key_query, key);
+ local out = {};
+ for row in res do
+ table.insert(out, row[1]);
+ end
+ return out[1];
+end
+
+local function key_in_db(key)
+ local res = key_get_id(key);
+ if res then
+ return true;
+ else
+ return false;
+ end
+end
+
+function archive_store:append(username, key, value, when, with)
+ reconect_check();
+ if not key or key_in_db(key) then
+ key = uuid();
+ end
+ engine:insert(store_query, username, key, serialize(st.preserialize(value)),
+ when, with);
+ engine.conn:commit();
+end
+
+function archive_store:find(username, query)
+ local qstart, qend, qwith = -math.huge, math.huge;
+ local qlimit, qid;
+ local db_query = select_query_base;
+ local _params = { username, };
+ local i, values = 0;
+
+ if query then
+ if query.reverse then
+ if query.before then
+ qid = key_get_id(query.before);
+ end
+ elseif query.after then
+ qid = key_get_id(query.after);
+ end
+ qwith = query.with;
+ qlimit = query.limit;
+ qstart = query.start or qstart;
+ qend = query["end"] or qend;
+ end
+
+ if qwith then
+ db_query = db_query.." AND `with` = ?";
+ table.insert(_params, qwith);
+ end
+ if qid then
+ if query.reverse then
+ db_query = db_query.." AND `id` < ?";
+ else
+ db_query = db_query.." AND `id` > ?";
+ end
+ table.insert(_params, qid);
+ end
+ db_query = db_query.." AND (`epoch` >= ? AND `epoch` <= ?)";
+ table.insert(_params, qstart);
+ table.insert(_params, qend);
+ db_query = db_query.." ORDER BY `epoch`";
+ if query.reverse then
+ db_query = db_query.." DESC";
+ end
+ if qlimit then
+ db_query = db_query.." LIMIT ?";
+ table.insert(_params, qlimit);
+ end
+ db_query = db_query..";"
+ values = load_db(db_query, _params);
+
+ return function ()
+ i = i + 1;
+ if values[i] then
+ return values[i][1], deserialize(values[i][2]), values[i][3], values[i][4];
+ end
+ end
+end
+
+function archive_store:delete(username, query)
+ if not query or next(query) == nil then
+ -- no specifics, delete everything
+ reconect_check();
+ engine:delete(delete_query, username);
+ engine.conn:commit();
+ return true;
+ end
+
+ local qend = query["end"] or math.huge;
+
+ reconect_check();
+ engine:delete(delete_query_extra, username, qend);
+ engine.conn:commit();
+ return true;
+end
+
+return archive_store;

@ -1,39 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Thu, 19 Jan 2017 16:25:24 +0100
Subject: [PATCH] TT#9303 mod_mam: sipwise_archive.lib fix crash at find()
> mod_c2s: Traceback[c2s]: /usr/lib/prosody/util/sql.lua:189:
> Error executing statement parameters: Data too long for column '_uuid' at row 1
Change-Id: I9e7d6a310ee5e5e8f83cbc97cb2f7ebcf4f534b5
---
plugins/mod_mam/sipwise_archive.lib.lua | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/plugins/mod_mam/sipwise_archive.lib.lua b/plugins/mod_mam/sipwise_archive.lib.lua
index f772ea6..087c306 100644
--- a/plugins/mod_mam/sipwise_archive.lib.lua
+++ b/plugins/mod_mam/sipwise_archive.lib.lua
@@ -29,8 +29,8 @@ WHERE `username` = ? AND `epoch` <= ?;
]]
local select_key_query=[[
-SELECT id FROM `sipwise_mam`
-WHERE `key` = UuidToBin(?)
+SELECT `id` FROM `sipwise_mam`
+WHERE `key` = UuidToBin(?);
]]
local select_query_base=[[
@@ -63,6 +63,11 @@ end
local function key_get_id(key)
local res;
reconect_check();
+ -- key is an uuid
+ if string.len(key) ~= 36 then
+ log("warn", "key[%s] is not a proper uuid");
+ return nil;
+ end
res = engine:select(select_key_query, key);
local out = {};
for row in res do

@ -1,63 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Thu, 19 Jan 2017 16:38:26 +0100
Subject: [PATCH] TT#9311 mod_mam/sipwise_archive.lib: full username
* domain was not included
Change-Id: Id2bbc496545ba7c167cd4dacb24153708e826ca2
---
plugins/mod_mam/sipwise_archive.lib.lua | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/plugins/mod_mam/sipwise_archive.lib.lua b/plugins/mod_mam/sipwise_archive.lib.lua
index 087c306..46166f8 100644
--- a/plugins/mod_mam/sipwise_archive.lib.lua
+++ b/plugins/mod_mam/sipwise_archive.lib.lua
@@ -12,6 +12,7 @@ local engine = mod_sql:create_engine(params);
engine:execute("SET NAMES 'utf8' COLLATE 'utf8_bin';");
local log = require "util.logger".init("sipwise_archive");
local ut_tostring = require "util.table".table.tostring;
+local host = module.host;
local store_query=[[
INSERT INTO `sipwise_mam` (`username`, `key`, `stanza`, `epoch`, `with`)
@@ -90,7 +91,7 @@ function archive_store:append(username, key, value, when, with)
if not key or key_in_db(key) then
key = uuid();
end
- engine:insert(store_query, username, key, serialize(st.preserialize(value)),
+ engine:insert(store_query, username..'@'..host, key, serialize(st.preserialize(value)),
when, with);
engine.conn:commit();
end
@@ -99,7 +100,7 @@ function archive_store:find(username, query)
local qstart, qend, qwith = -math.huge, math.huge;
local qlimit, qid;
local db_query = select_query_base;
- local _params = { username, };
+ local _params = { username..'@'..host, };
local i, values = 0;
if query then
@@ -151,10 +152,11 @@ function archive_store:find(username, query)
end
function archive_store:delete(username, query)
+ local jid = username..'@'..host
if not query or next(query) == nil then
-- no specifics, delete everything
reconect_check();
- engine:delete(delete_query, username);
+ engine:delete(delete_query, jid);
engine.conn:commit();
return true;
end
@@ -162,7 +164,7 @@ function archive_store:delete(username, query)
local qend = query["end"] or math.huge;
reconect_check();
- engine:delete(delete_query_extra, username, qend);
+ engine:delete(delete_query_extra, jid, qend);
engine.conn:commit();
return true;
end

@ -1,57 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Thu, 19 Jan 2017 17:13:12 +0100
Subject: [PATCH] TT#9317 mod_mam: don't store bodyless chat messages
* implement store hints
https://prosody.im/issues/issue/750
mod_mam doesn't honor forced storage hint.
When a message without a body is received mod_mam always ignores the forced
storage hint, even if <store xmlns='urn:xmpp:hints'/> is present.
This can cause issue with OMEMO, which sends message elements without a body
Change-Id: I590b1fb9bd95afdce6a117778052d11d8102f718
---
plugins/mod_mam/mod_mam.lua | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index faff0dd..759a5e2 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -285,19 +285,30 @@ local function message_handler(event, c2s)
event.stanza = stanza;
end
- -- 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
- log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
- return;
- end
-
-- or if hints suggest we shouldn't
if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
+ -- We store chat messages or normal messages that have a body
+ local body = stanza:get_child("body");
+ if not(orig_type == "chat" or (orig_type == "normal" and body)) then
+ log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
+ return;
+ elseif (orig_type == 'chat' and not body) then
+ log("debug", "Not archiving stanza: %s (type), has no body",
+ stanza:top_tag());
+ return;
+ end
+
if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
return;
end
+ else
+ log("debug", "store hint detected");
+ if orig_type == 'error' then
+ log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
+ return;
+ end
end
-- Check with the users preferences

@ -1,34 +0,0 @@
From: Victor Seva <vseva@sipwise.com>
Date: Tue, 24 Jan 2017 11:14:40 +0100
Subject: TT#9314 mod_mam/sipwise_archive.lib: support emojis
* we do need utf8mb4 charset at sipwise_mam table
Change-Id: I44b17058f1bb0839de835151e6885a64e203fac1
---
plugins/mod_mam/sipwise_archive.lib.lua | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/mod_mam/sipwise_archive.lib.lua b/plugins/mod_mam/sipwise_archive.lib.lua
index 46166f8..e58f593 100644
--- a/plugins/mod_mam/sipwise_archive.lib.lua
+++ b/plugins/mod_mam/sipwise_archive.lib.lua
@@ -9,7 +9,7 @@ local st = require "util.stanza";
local mod_sql = module:require("sql");
local params = module:get_option("sql", {});
local engine = mod_sql:create_engine(params);
-engine:execute("SET NAMES 'utf8' COLLATE 'utf8_bin';");
+engine:execute("SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';");
local log = require "util.logger".init("sipwise_archive");
local ut_tostring = require "util.table".table.tostring;
local host = module.host;
@@ -43,8 +43,8 @@ WHERE `username` = ?
local function reconect_check()
if not engine.conn:ping() then
engine.conn = nil;
- log("debug", "DDBB reconecting");
engine:connect();
+ engine:execute("SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';");
end
end

@ -1,21 +0,0 @@
From: Sipwise Development Team <support@sipwise.com>
Date: Wed, 11 Dec 2019 11:15:14 +0100
Subject: TT-27653-mod_mam-sipwise_archive.lib-fix-uuid-trailing-garbage
---
plugins/mod_mam/sipwise_archive.lib.lua | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/mod_mam/sipwise_archive.lib.lua b/plugins/mod_mam/sipwise_archive.lib.lua
index e58f593..15acb33 100644
--- a/plugins/mod_mam/sipwise_archive.lib.lua
+++ b/plugins/mod_mam/sipwise_archive.lib.lua
@@ -56,7 +56,7 @@ local function load_db(query, _params)
res = engine:select(query, unpack(_params));
local out = {};
for row in res do
- table.insert(out, {row[1], row[2], row[3], row[4]});
+ table.insert(out, {string.sub(row[1], 0, 36), row[2], row[3], row[4]});
end
return out;
end

@ -1,9 +1,2 @@
mod_auth_sql.path
0001-mod_mam_disco.path
0002-TT-8297-mam-implement-archive-to-DB.patch
0003-TT-9303-mod_mam-sipwise_archive.lib-fix-crash-at-fin.patch
0004-TT-9311-mod_mam-sipwise_archive.lib-full-username.patch
0005-TT-9317-mod_mam-don-t-store-bodyless-chat-messages.patch
0006-TT-9314-mod_mam-sipwise_archive.lib-support-emojis.patch
0007-TT-27653-mod_mam-sipwise_archive.lib-fix-uuid-trailing-garbage.patch
0009-TT-35604-mod_sipwise_pushd.lib-add-silent-push

@ -1,49 +0,0 @@
---
labels:
- 'Stage-Merged'
summary: Message Carbons
...
Introduction
============
This module implements [XEP-0280: Message
Carbons](http://xmpp.org/extensions/xep-0280.html), allowing users to
maintain a shared and synchronized view of all conversations across all
their online clients and devices.
Configuration
=============
As with all modules, you enable it by adding it to the modules\_enabled
list.
modules_enabled = {
...
"carbons";
...
}
The module has no further configuration.
Clients
=======
Clients that support XEP-0280:
- [Gajim](http://gajim.org/) (Desktop)
- [Adium (1.6)](http://adium.im/) (Desktop - OS X)
- [Yaxim](http://yaxim.org/) (Mobile - Android)
- [Conversations](https://play.google.com/store/apps/details?id=eu.siacs.conversations)
(Mobile - Android)
- [poezio](http://poezio.eu/en/) (Console)
Compatibility
=============
------- -----------------------
0.8 Works
0.9 Works
0.10 Included with prosody
trunk Included with prosody
------- -----------------------

@ -1,136 +0,0 @@
-- XEP-0280: Message Carbons implementation for Prosody
-- Copyright (C) 2011-2016 Kim Alvefur
--
-- This file is MIT/X11 licensed.
local st = require "util.stanza";
local jid_bare = require "util.jid".bare;
local xmlns_carbons = "urn:xmpp:carbons:2";
local xmlns_carbons_old = "urn:xmpp:carbons:1";
local xmlns_carbons_really_old = "urn:xmpp:carbons:0";
local xmlns_forward = "urn:xmpp:forward:0";
local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions;
local function toggle_carbons(event)
local origin, stanza = event.origin, event.stanza;
local state = stanza.tags[1].attr.mode or stanza.tags[1].name;
module:log("debug", "%s %sd carbons", origin.full_jid, state);
origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns;
origin.send(st.reply(stanza));
return true;
end
module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons);
-- COMPAT
module:hook("iq-set/self/"..xmlns_carbons_old..":disable", toggle_carbons);
module:hook("iq-set/self/"..xmlns_carbons_old..":enable", toggle_carbons);
module:hook("iq-set/self/"..xmlns_carbons_really_old..":carbons", toggle_carbons);
local function message_handler(event, c2s)
local origin, stanza = event.origin, event.stanza;
local orig_type = stanza.attr.type or "normal";
local orig_from = stanza.attr.from;
local orig_to = stanza.attr.to;
if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then
return -- Only chat type messages
end
-- Stanza sent by a local client
local bare_jid = jid_bare(orig_from);
local target_session = origin;
local top_priority = false;
local user_sessions = bare_sessions[bare_jid];
-- Stanza about to be delivered to a local client
if not c2s then
bare_jid = jid_bare(orig_to);
target_session = full_sessions[orig_to];
user_sessions = bare_sessions[bare_jid];
if not target_session and user_sessions then
-- The top resources will already receive this message per normal routing rules,
-- so we are going to skip them in order to avoid sending duplicated messages.
local top_resources = user_sessions.top_resources;
top_priority = top_resources and top_resources[1].priority
end
end
if not user_sessions then
module:log("debug", "Skip carbons for offline user");
return -- No use in sending carbons to an offline user
end
if stanza:get_child("private", xmlns_carbons) then
if not c2s then
stanza:maptags(function(tag)
if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then
return tag;
end
end);
end
module:log("debug", "Message tagged private, ignoring");
return
elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
module:log("debug", "Message has no-copy hint, ignoring");
return
elseif not c2s and bare_jid == orig_from and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
module:log("debug", "MUC PM, ignoring");
return
end
-- Create the carbon copy and wrap it as per the Stanza Forwarding XEP
local copy = st.clone(stanza);
copy.attr.xmlns = "jabber:client";
local carbon = st.message{ from = bare_jid, type = orig_type, }
:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons })
:tag("forwarded", { xmlns = xmlns_forward })
:add_child(copy):reset();
-- COMPAT
local carbon_old = st.message{ from = bare_jid, type = orig_type, }
:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons_old }):up()
:tag("forwarded", { xmlns = xmlns_forward })
:add_child(copy):reset();
-- COMPAT
local carbon_really_old = st.clone(stanza)
:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons_really_old }):up()
user_sessions = user_sessions and user_sessions.sessions;
for _, session in pairs(user_sessions) do
-- Carbons are sent to resources that have enabled it
if session.want_carbons
-- but not the resource that sent the message, or the one that it's directed to
and session ~= target_session
-- and isn't among the top resources that would receive the message per standard routing rules
and (c2s or session.priority ~= top_priority)
-- don't send v0 carbons (or copies) for c2s
and (not c2s or session.want_carbons ~= xmlns_carbons_really_old) then
carbon.attr.to = session.full_jid;
module:log("debug", "Sending carbon to %s", session.full_jid);
local carbon = session.want_carbons == xmlns_carbons_old and carbon_old -- COMPAT
or session.want_carbons == xmlns_carbons_really_old and carbon_really_old -- COMPAT
or carbon;
session.send(carbon);
end
end
end
local function c2s_message_handler(event)
return message_handler(event, true)
end
-- Stanzas sent by local clients
module:hook("pre-message/host", c2s_message_handler, 0.05); -- priority between mod_message (0 in 0.9) and mod_firewall (0.1)
module:hook("pre-message/bare", c2s_message_handler, 0.05);
module:hook("pre-message/full", c2s_message_handler, 0.05);
-- Stanzas to local clients
module:hook("message/bare", message_handler, 0.05);
module:hook("message/full", message_handler, 0.05);
module:add_feature(xmlns_carbons);
module:add_feature(xmlns_carbons_old);
if module:get_option_boolean("carbons_v0") then
module:add_feature(xmlns_carbons_really_old);
end

@ -1,34 +0,0 @@
---
summary: Client State Indication support
labels:
- 'Stage-Merged'
...
Introduction
============
This module implements [XEP-0352: Client State
Indication](http://xmpp.org/extensions/xep-0352.html), a way for mobile
clients to tell the server that they are sitting in someones pocket and
would rather not get some less urgent things pushed to it.
This module has been merged into Prosody 0.11. Please see the
[mod_csi documentation](https://prosody.im/doc/modules/mod_csi) for more
information about how it is used.
Configuration
=============
There is no configuration for this module, just add it to
modules\_enabled as normal.
Compatibility
=============
----- -------
0.9 Works
----- -------
0.10 Works
----- -------
0.11 Works (included)
----- -------

@ -1,23 +0,0 @@
local st = require "util.stanza";
local xmlns_csi = "urn:xmpp:csi:0";
local csi_feature = st.stanza("csi", { xmlns = xmlns_csi });
module:hook("stream-features", function (event)
if event.origin.username then
event.features:add_child(csi_feature);
end
end);
function refire_event(name)
return function (event)
if event.origin.username then
event.origin.state = event.stanza.name;
module:fire_event(name, event);
return true;
end
end;
end
module:hook("stanza/"..xmlns_csi..":active", refire_event("csi-client-active"));
module:hook("stanza/"..xmlns_csi..":inactive", refire_event("csi-client-inactive"));

@ -1,129 +0,0 @@
---
labels:
- 'Stage-Merged'
summary: 'XEP-0313: Message Archive Management'
...
Introduction
============
Implementation of [XEP-0313: Message Archive Management].
Details
=======
This module will archive all messages that match the simple rules setup
by the user, and allow the user to access this archive.
Usage
=====
First copy the module to the prosody plugins directory.
Then add "mam" to your modules\_enabled list:
``` {.lua}
modules_enabled = {
-- ...
"mam",
-- ...
}
```
Configuration
=============
Option summary
--------------
option type default
------------------------------ ----------------------- -----------
max\_archive\_query\_results number `50`
default\_archive\_policy boolean or `"roster"` `true`
archive\_expires\_after string `"1w"`
archive\_cleanup\_interval number `4*60*60`
Storage backend
---------------
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\_sql (requires Prosody 0.10 or later):
``` {.lua}
storage = {
archive2 = "sql";
}
```
If no archive-capable storage backend can be opened then an in-memory
one will be used as fallback.
Query size limits
-----------------
max_archive_query_results = 20;
This is the largest number of messages that are allowed to be retrieved
in one request *page*. A query that does not fit in one page will
include a reference to the next page, letting clients page through the
result set. Setting large number is not recommended, as Prosody will be
blocked while processing the request and will not be able to do anything
else.
Archive expiry
--------------
Messages in the archive will expire after some time, by default one
week. This can be changed by setting `archive_expires_after`:
``` {.lua}
archive_expires_after = "1d" -- one day
archive_expires_after = "1w" -- one week, the default
archive_expires_after = "2m" -- two months
archive_expires_after = "1y" -- one year
archive_expires_after = 60 * 60 -- one hour
archive_expires_after = "never" -- forever
```
The format is an integer number of seconds or a multiple of a period
given by a suffix that can be one of `d` (day), `w` (week), `m` (month)
or `y` (year). No multiplier means seconds.
Message matching policy
-----------------------
The MAM protocol includes a way for clients to control what messages
should be stored. This allows users to enable or disable archiving by
default or for specific contacts.
``` {.lua}
default_archive_policy = true
```
`default_archive_policy =` Meaning
---------------------------- ------------------------------------------------------
`false` Store no messages.
`"roster"` Store messages to/from contacts in the users roster.
`true` Store all messages. This is the default.
Compatibility
=============
------- -----------------------
trunk Included with Prosody
0.10 Included with Prosody
0.9 Works
0.8 Does not work
------- -----------------------
[^1]: Might be changed to "mam" at some point

@ -1,83 +0,0 @@
-- luacheck: ignore 212/self
local uuid = require "util.uuid".generate;
local store = module:shared("archive");
local archive_store = { _provided_by = "mam"; name = "fallback"; };
function archive_store:append(username, key, value, when, with)
local archive = store[username];
if not archive then
archive = { [0] = 0 };
store[username] = archive;
end
local index = (archive[0] or #archive)+1;
local item = { key = key, when = when, with = with, value = value };
if not key or archive[key] then
key = uuid();
item.key = key;
end
archive[index] = item;
archive[key] = index;
archive[0] = index;
return key;
end
function archive_store:find(username, query)
local archive = store[username] or {};
local start, stop, step = 1, archive[0] or #archive, 1;
local qstart, qend, qwith = -math.huge, math.huge;
local limit;
if query then
if query.reverse then
start, stop, step = stop, start, -1;
if query.before and archive[query.before] then
start = archive[query.before] - 1;
end
elseif query.after and archive[query.after] then
start = archive[query.after] + 1;
end
qwith = query.with;
limit = query.limit;
qstart = query.start or qstart;
qend = query["end"] or qend;
end
return function ()
if limit and limit <= 0 then return end
for i = start, stop, step do
local item = archive[i];
if (not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend then
if limit then limit = limit - 1; end
start = i + step; -- Start on next item
return item.key, item.value, item.when, item.with;
end
end
end
end
function archive_store:delete(username, query)
if not query or next(query) == nil then
-- no specifics, delete everything
store[username] = nil;
return true;
end
local archive = store[username];
if not archive then return true; end -- no messages, nothing to delete
local qstart = query.start or -math.huge;
local qend = query["end"] or math.huge;
local qwith = query.with;
store[username] = nil;
for i = 1, #archive do
local item = archive[i];
local when, with = item.when, item.when;
-- Add things that don't match the query
if not ((not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend) then
self:append(username, item.key, item.value, when, with);
end
end
return true;
end
return archive_store;

@ -1,46 +0,0 @@
-- XEP-0313: Message Archive Management for Prosody
-- Copyright (C) 2011-2013 Kim Alvefur
--
-- This file is MIT/X11 licensed.
-- luacheck: ignore 122/prosody
local global_default_policy = module:get_option("default_archive_policy", true);
do
-- luacheck: ignore 211/prefs_format
local prefs_format = {
[false] = "roster",
-- default ::= true | false | "roster"
-- true = always, false = never, nil = global default
["romeo@montague.net"] = true, -- always
["montague@montague.net"] = false, -- newer
};
end
local sessions = prosody.hosts[module.host].sessions;
local archive_store = module:get_option_string("archive_store", "archive");
local prefs = module:open_store(archive_store .. "_prefs");
local function get_prefs(user)
local user_sessions = sessions[user];
local user_prefs = user_sessions and user_sessions.archive_prefs
if not user_prefs then
user_prefs = prefs:get(user);
if user_sessions then
user_sessions.archive_prefs = user_prefs;
end
end
return user_prefs or { [false] = global_default_policy };
end
local function set_prefs(user, user_prefs)
local user_sessions = sessions[user];
if user_sessions then
user_sessions.archive_prefs = user_prefs;
end
return prefs:set(user, user_prefs);
end
return {
get = get_prefs,
set = set_prefs,
}

@ -1,57 +0,0 @@
-- XEP-0313: Message Archive Management for Prosody
-- Copyright (C) 2011-2013 Kim Alvefur
--
-- This file is MIT/X11 licensed.
local st = require"util.stanza";
local default_attrs = {
always = true, [true] = "always",
never = false, [false] = "never",
roster = "roster",
}
local function tostanza(prefs, xmlns_mam)
local default = prefs[false];
default = default_attrs[default];
local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default });
local always = st.stanza("always");
local never = st.stanza("never");
for jid, choice in pairs(prefs) do
if jid then
(choice and always or never):tag("jid"):text(jid):up();
end
end
prefstanza:add_child(always):add_child(never);
return prefstanza;
end
local function fromstanza(prefstanza)
local prefs = {};
local default = prefstanza.attr.default;
if default then
prefs[false] = default_attrs[default];
end
local always = prefstanza:get_child("always");
if always then
for rule in always:childtags("jid") do
local jid = rule:get_text();
prefs[jid] = true;
end
end
local never = prefstanza:get_child("never");
if never then
for rule in never:childtags("jid") do
local jid = rule:get_text();
prefs[jid] = false;
end
end
return prefs;
end
return {
tostanza = tostanza;
fromstanza = fromstanza;
}

@ -1,433 +0,0 @@
-- XEP-0313: Message Archive Management for Prosody
-- Copyright (C) 2011-2016 Kim Alvefur
--
-- This file is MIT/X11 licensed.
local xmlns_mam0 = "urn:xmpp:mam:0";
local xmlns_mam1 = "urn:xmpp:mam:1";
local xmlns_mam2 = "urn:xmpp:mam:2";
local xmlns_delay = "urn:xmpp:delay";
local xmlns_forward = "urn:xmpp:forward:0";
local xmlns_st_id = "urn:xmpp:sid:0";
local um = require "core.usermanager";
local st = require "util.stanza";
local rsm = module:require "rsm";
local get_prefs = module:require"mamprefs".get;
local set_prefs = module:require"mamprefs".set;
local prefs_to_stanza = module:require"mamprefsxml".tostanza;
local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local jid_prepped_split = require "util.jid".prepped_split;
local dataform = require "util.dataforms".new;
local host = module.host;
local rm_load_roster = require "core.rostermanager".load_roster;
local getmetatable = getmetatable;
local function is_stanza(x)
return getmetatable(x) == st.stanza_mt;
end
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 default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
local global_default_policy = module:get_option("default_archive_policy", true);
if global_default_policy ~= "roster" then
global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
end
local archive_store = module:get_option_string("archive_store", "archive2");
local archive = module:open_store(archive_store, "archive");
if archive.name == "null" or not archive.find then
-- luacheck: ignore 631
if not archive.find then
module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API");
module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
else
module:log("debug", "Attempt to open archive storage returned null driver");
end
module:log("debug", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
module:log("info", "Using in-memory fallback archive driver");
archive = module:require "fallback_archive";
end
local use_total = true;
local cleanup;
local function schedule_cleanup(username)
if cleanup and not cleanup[username] then
table.insert(cleanup, username);
cleanup[username] = true;
end
end
-- Handle prefs.
local function handle_prefs(event)
local origin, stanza = event.origin, event.stanza;
local xmlns_mam = stanza.tags[1].attr.xmlns;
local user = origin.username;
if stanza.attr.type == "get" then
local prefs = prefs_to_stanza(get_prefs(user), xmlns_mam);
local reply = st.reply(stanza):add_child(prefs);
origin.send(reply);
else -- type == "set"
local new_prefs = stanza:get_child("prefs", xmlns_mam);
local prefs = prefs_from_stanza(new_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)));
else
origin.send(st.reply(stanza));
end
end
return true;
end
module:hook("iq/self/"..xmlns_mam0..":prefs", handle_prefs);
module:hook("iq/self/"..xmlns_mam1..":prefs", handle_prefs);
module:hook("iq/self/"..xmlns_mam2..":prefs", handle_prefs);
local query_form = dataform {
{ name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam0; };
{ name = "with"; type = "jid-single"; };
{ name = "start"; type = "text-single" };
{ name = "end"; type = "text-single"; };
};
-- Serve form
local function handle_get_form(event)
local origin, stanza = event.origin, event.stanza;
local xmlns_mam = stanza.tags[1].attr.xmlns;
query_form[1].value = xmlns_mam;
origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form()));
return true;
end
module:hook("iq-get/self/"..xmlns_mam0..":query", handle_get_form);
module:hook("iq-get/self/"..xmlns_mam1..":query", handle_get_form);
module:hook("iq-get/self/"..xmlns_mam2..":query", handle_get_form);
-- Handle archive queries
local function handle_mam_query(event)
local origin, stanza = event.origin, event.stanza;
local xmlns_mam = stanza.tags[1].attr.xmlns;
local query = stanza.tags[1];
local qid = query.attr.queryid;
origin.mam_requested = true;
schedule_cleanup(origin.username);
-- Search query parameters
local qwith, qstart, qend;
local form = query:get_child("x", "jabber:x:data");
if form then
local err;
query_form[1].value = xmlns_mam;
form, err = query_form:data(form);
if err then
origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
return true;
end
qwith, qstart, qend = form["with"], form["start"], form["end"];
qwith = qwith and jid_bare(qwith); -- dataforms does jidprep
end
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
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");
-- RSM stuff
local qset = rsm.get(query);
local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
local reverse = qset and qset.before or false;
local before, after = qset and qset.before, qset and qset.after;
if type(before) ~= "string" then before = nil; end
-- Load all the data!
local data, err = archive:find(origin.username, {
start = qstart; ["end"] = qend; -- Time range
with = qwith;
limit = qmax + 1;
before = before; after = after;
reverse = reverse;
total = use_total;
});
if not data then
origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
return true;
end
local total = tonumber(err);
if xmlns_mam == xmlns_mam0 then
origin.send(st.reply(stanza));
end
local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
local results = {};
-- Wrap it in stuff and deliver
local first, last;
local count = 0;
local complete = "true";
for id, item, when in data do
count = count + 1;
if count > qmax then
complete = nil;
break;
end
local fwd_st = st.message(msg_reply_attr)
:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
:tag("forwarded", { xmlns = xmlns_forward })
:tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
if not is_stanza(item) then
item = st.deserialize(item);
end
item.attr.xmlns = "jabber:client";
fwd_st:add_child(item);
if not first then first = id; end
last = id;
if reverse then
results[count] = fwd_st;
else
origin.send(fwd_st);
end
end
if reverse then
for i = #results, 1, -1 do
origin.send(results[i]);
end
first, last = last, first;
end
-- That's all folks!
module:log("debug", "Archive query %s completed", tostring(qid));
local fin;
if xmlns_mam == xmlns_mam0 then
fin = st.message(msg_reply_attr);
else
fin = st.reply(stanza);
end
do
fin:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
:add_child(rsm.generate {
first = first, last = last, count = total })
end
origin.send(fin);
return true;
end
module:hook("iq-set/self/"..xmlns_mam0..":query", handle_mam_query);
module:hook("iq-set/self/"..xmlns_mam1..":query", handle_mam_query);
module:hook("iq-set/self/"..xmlns_mam2..":query", handle_mam_query);
local function has_in_roster(user, who)
local roster = rm_load_roster(user, host);
module:log("debug", "%s has %s in roster? %s", user, who, roster[who] and "yes" or "no");
return roster[who];
end
local function shall_store(user, who)
-- TODO Cache this?
if not um.user_exists(user, host) then
return false;
end
local prefs = get_prefs(user);
local rule = prefs[who];
module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule));
if rule ~= nil then
return rule;
end
-- Below could be done by a metatable
local default = prefs[false];
module:log("debug", "%s's default rule is %s", user, tostring(default));
if default == nil then
default = global_default_policy;
module:log("debug", "Using global default rule, %s", tostring(default));
end
if default == "roster" then
return has_in_roster(user, who);
end
return default;
end
-- Handle messages
local function message_handler(event, c2s)
local origin, stanza = event.origin, event.stanza;
local log = c2s and origin.log or module._log;
local orig_type = stanza.attr.type or "normal";
local orig_from = stanza.attr.from;
local orig_to = stanza.attr.to or orig_from;
-- Stanza without 'to' are treated as if it was to their own bare jid
-- Whos storage do we put it in?
local store_user = c2s and origin.username or jid_split(orig_to);
-- And who are they chatting with?
local with = jid_bare(c2s and orig_to or orig_from);
-- Filter out <stanza-id> that claim to be from us
if stanza:get_child("stanza-id", xmlns_st_id) then
stanza = st.clone(stanza);
stanza:maptags(function (tag)
if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then
local by_user, by_host, res = jid_prepped_split(tag.attr.by);
if not res and by_host == module.host and by_user == store_user then
return nil;
end
end
return tag;
end);
event.stanza = stanza;
end
-- 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
log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
return;
end
-- or if hints suggest we shouldn't
if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
return;
end
end
-- Check with the users preferences
if shall_store(store_user, with) then
log("debug", "Archiving stanza: %s", stanza:top_tag());
-- And stash it
local ok = archive:append(store_user, nil, stanza, time_now(), with);
if ok then
local clone_for_other_handlers = st.clone(stanza);
local id = ok;
clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up();
event.stanza = clone_for_other_handlers;
schedule_cleanup(store_user);
module:fire_event("archive-message-added", { origin = origin, stanza = stanza, for_user = store_user, id = id });
end
else
log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag());
end
end
local function c2s_message_handler(event)
return message_handler(event, true);
end
local function strip_stanza_id(event)
local strip_by = jid_bare(event.origin.full_jid);
event.stanza = st.clone(event.stanza);
event.stanza:maptags(function(tag)
if not ( tag.attr.xmlns == xmlns_st_id and tag.attr.by == strip_by ) then
return tag;
end
end);
end
module:hook("pre-message/bare", strip_stanza_id, 0.01);
module:hook("pre-message/full", strip_stanza_id, 0.01);
local cleanup_after = module:get_option_string("archive_expires_after", "1w");
local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
if cleanup_after ~= "never" then
local day = 86400;
local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
if not n then
module:log("error", "Could not parse archive_expires_after string %q", cleanup_after);
return false;
end
cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after);
if not archive.delete then
module:log("error", "archive_expires_after set but mod_%s does not support deleting", archive._provided_by);
return false;
end
-- Set of known users to do message expiry for
-- Populated either below or when new messages are added
cleanup = {};
-- Iterating over users is not supported by all authentication modules
-- Catch and ignore error if not supported
pcall(function ()
-- If this works, then we schedule cleanup for all known users on startup
for user in um.users(module.host) do
schedule_cleanup(user);
end
end);
-- At odd intervals, delete old messages for one user
module:add_timer(math.random(10, 60), function()
local user = table.remove(cleanup, 1);
if user then
module:log("debug", "Removing old messages for user %q", user);
local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; })
if not ok then
module:log("warn", "Could not expire archives for user %s: %s", user, err);
else
-- :affected() is a recent addition for eg SQLite3 in LuaDBI
pcall(function(stmt)
module:log("debug", "Removed %d messages", stmt:affected());
end, err);
end
cleanup[user] = nil;
end
return math.random(cleanup_interval, cleanup_interval * 2);
end);
else
-- Don't ask the backend to count the potentially unbounded number of items,
-- it'll get slow.
use_total = false;
end
-- Stanzas sent by local clients
local priority = 0.075
assert(priority < 0.1, "priority must be after mod_firewall");
assert(priority > 0.05, "priority must be before mod_carbons");
assert(priority > 0.01, "priority must be before strip_stanza_id");
module:hook("pre-message/bare", c2s_message_handler, priority);
module:hook("pre-message/full", c2s_message_handler, priority);
-- Stanszas to local clients
priority = 0.075
assert(priority > 0, "priority must be before mod_message");
assert(priority < 0.1, "priority must be after mod_firewall");
assert(priority > 0.05, "priority must be before mod_carbons");
module:hook("message/bare", message_handler, priority);
module:hook("message/full", message_handler, priority);
module:add_feature(xmlns_mam0); -- COMPAT with XEP-0313 v 0.1
module:hook("account-disco-info", function(event)
(event.reply or event.stanza):tag("feature", {var=xmlns_mam0}):up();
(event.reply or event.stanza):tag("feature", {var=xmlns_mam1}):up();
(event.reply or event.stanza):tag("feature", {var=xmlns_mam2}):up();
(event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up();
end);

@ -1,87 +0,0 @@
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 parsers = element_parsers;
local function xs_int(st)
return tonumber((st:get_text()));
end
local function xs_string(st)
return st:get_text();
end
parsers.after = xs_string;
parsers.before = function(st)
local text = st:get_text();
return text == "" or text;
end;
parsers.max = xs_int;
parsers.index = xs_int;
parsers.first = function(st)
return { index = tonumber(st.attr.index); st:get_text() };
end;
parsers.last = xs_string;
parsers.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;
before = function(st, data)
if data == true then
st:tag("before"):up();
else
st:tag("before"):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(set)
local rs = {};
for tag in set: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("set", { 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);
end
end
return { parse = parse, generate = generate, get = get };

@ -1,256 +0,0 @@
-- Prosody IM
-- Copyright (C) 2012 Florian Zeitz
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
module:set_global();
local add_filter = require "util.filters".add_filter;
local sha1 = require "util.hashes".sha1;
local base64 = require "util.encodings".base64.encode;
local softreq = require "util.dependencies".softreq;
local portmanager = require "core.portmanager";
local bit;
pcall(function() bit = require"bit"; end);
bit = bit or softreq"bit32"
if not bit then module:log("error", "No bit module found. Either LuaJIT 2, lua-bitop or Lua 5.2 is required"); end
local band = bit.band;
local bxor = bit.bxor;
local rshift = bit.rshift;
local cross_domain = module:get_option("cross_domain_websocket");
if cross_domain then
if cross_domain == true then
cross_domain = "*";
elseif type(cross_domain) == "table" then
cross_domain = table.concat(cross_domain, ", ");
end
if type(cross_domain) ~= "string" then
cross_domain = nil;
end
end
module:depends("c2s")
local sessions = module:shared("c2s/sessions");
local c2s_listener = portmanager.get_service("c2s").listener;
-- Websocket helpers
local function parse_frame(frame)
local result = {};
local pos = 1;
local length_bytes = 0;
local counter = 0;
local tmp_byte;
if #frame < 2 then return; end
tmp_byte = string.byte(frame, pos);
result.FIN = band(tmp_byte, 0x80) > 0;
result.RSV1 = band(tmp_byte, 0x40) > 0;
result.RSV2 = band(tmp_byte, 0x20) > 0;
result.RSV3 = band(tmp_byte, 0x10) > 0;
result.opcode = band(tmp_byte, 0x0F);
pos = pos + 1;
tmp_byte = string.byte(frame, pos);
result.MASK = band(tmp_byte, 0x80) > 0;
result.length = band(tmp_byte, 0x7F);
if result.length == 126 then
length_bytes = 2;
result.length = 0;
elseif result.length == 127 then
length_bytes = 8;
result.length = 0;
end
if #frame < (2 + length_bytes) then return; end
for i = 1, length_bytes do
pos = pos + 1;
result.length = result.length * 256 + string.byte(frame, pos);
end
if #frame < (2 + length_bytes + (result.MASK and 4 or 0) + result.length) then return; end
if result.MASK then
result.key = {string.byte(frame, pos+1), string.byte(frame, pos+2),
string.byte(frame, pos+3), string.byte(frame, pos+4)}
pos = pos + 5;
result.data = "";
for i = pos, pos + result.length - 1 do
result.data = result.data .. string.char(bxor(result.key[counter+1], string.byte(frame, i)));
counter = (counter + 1) % 4;
end
else
result.data = frame:sub(pos + 1, pos + result.length);
end
return result, 2 + length_bytes + (result.MASK and 4 or 0) + result.length;
end
local function build_frame(desc)
local length;
local result = "";
local data = desc.data or "";
result = result .. string.char(0x80 * (desc.FIN and 1 or 0) + desc.opcode);
length = #data;
if length <= 125 then -- 7-bit length
result = result .. string.char(length);
elseif length <= 0xFFFF then -- 2-byte length
result = result .. string.char(126);
result = result .. string.char(rshift(length, 8)) .. string.char(length%0x100);
else -- 8-byte length
result = result .. string.char(127);
for i = 7, 0, -1 do
result = result .. string.char(rshift(length, 8*i) % 0x100);
end
end
result = result .. data;
return result;
end
--- Filter stuff
function handle_request(event, path)
local request, response = event.request, event.response;
local conn = response.conn;
if not request.headers.sec_websocket_key then
response.headers.content_type = "text/html";
return [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
<p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p>
</body></html>]];
end
local wants_xmpp = false;
(request.headers.sec_websocket_protocol or ""):gsub("([^,]*),?", function (proto)
if proto == "xmpp" then wants_xmpp = true; end
end);
if not wants_xmpp then
return 501;
end
local function websocket_close(code, message)
local data = string.char(rshift(code, 8)) .. string.char(code%0x100) .. message;
conn:write(build_frame({opcode = 0x8, FIN = true, data = data}));
conn:close();
end
local dataBuffer;
local function handle_frame(frame)
module:log("debug", "Websocket received: %s (%i bytes)", frame.data, #frame.data);
-- Error cases
if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
websocket_close(1002, "Reserved bits not zero");
return false;
end
if frame.opcode >= 0x8 and frame.length > 125 then -- Control frame with too much payload
websocket_close(1002, "Payload too large");
return false;
end
if frame.opcode >= 0x8 and not frame.FIN then -- Fragmented control frame
websocket_close(1002, "Fragmented control frame");
return false;
end
if (frame.opcode > 0x2 and frame.opcode < 0x8) or (frame.opcode > 0xA) then
websocket_close(1002, "Reserved opcode");
return false;
end
if frame.opcode == 0x0 and not dataBuffer then
websocket_close(1002, "Unexpected continuation frame");
return false;
end
if (frame.opcode == 0x1 or frame.opcode == 0x2) and dataBuffer then
websocket_close(1002, "Continuation frame expected");
return false;
end
-- Valid cases
if frame.opcode == 0x0 then -- Continuation frame
dataBuffer = dataBuffer .. frame.data;
elseif frame.opcode == 0x1 then -- Text frame
dataBuffer = frame.data;
elseif frame.opcode == 0x2 then -- Binary frame
websocket_close(1003, "Only text frames are supported");
return;
elseif frame.opcode == 0x8 then -- Close request
websocket_close(1000, "Goodbye");
return;
elseif frame.opcode == 0x9 then -- Ping frame
frame.opcode = 0xA;
conn:write(build_frame(frame));
return "";
else
log("warn", "Received frame with unsupported opcode %i", frame.opcode);
return "";
end
if frame.FIN then
data = dataBuffer;
dataBuffer = nil;
return data;
end
return "";
end
conn:setlistener(c2s_listener);
c2s_listener.onconnect(conn);
local frameBuffer = "";
add_filter(sessions[conn], "bytes/in", function(data)
local cache = "";
frameBuffer = frameBuffer .. data;
local frame, length = parse_frame(frameBuffer);
while frame do
frameBuffer = frameBuffer:sub(length + 1);
local result = handle_frame(frame);
if not result then return; end
cache = cache .. result;
frame, length = parse_frame(frameBuffer);
end
return cache;
end);
add_filter(sessions[conn], "bytes/out", function(data)
return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
end);
response.status_code = 101;
response.headers.upgrade = "websocket";
response.headers.connection = "Upgrade";
response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
response.headers.sec_webSocket_protocol = "xmpp";
response.headers.access_control_allow_origin = cross_domain;
return "";
end
function module.add_host(module)
module:depends("http");
module:provides("http", {
name = "websocket";
default_path = "xmpp-websocket";
route = {
["GET"] = handle_request;
["GET /"] = handle_request;
};
});
end

@ -1,13 +1,9 @@
mod_auth_sql
mod_blocking
mod_carbons
mod_carbons_adhoc
mod_carbons_copies
mod_csi
mod_filter_chatstates
mod_limit_auth
mod_log_auth
mod_mam
mod_smacks
mod_throttle_presence
mod_websocket

Loading…
Cancel
Save