changeset 2976:df86ce6bb0b4

Implement dummy body message to indicate high priority push This adds a dummy body sent alongside the push when the original message also contained a body to indicate a high priority push. The body can be configured and its contents are generally meaningless with most app servers because it will be stripped before pushed to the client.
author tmolitor <thilo@eightysoft.de>
date Sun, 01 Apr 2018 23:24:33 +0200
parents 7eb6fa9b03fd
children 7036e82f83f5
files mod_cloud_notify/README.markdown mod_cloud_notify/mod_cloud_notify.lua
diffstat 2 files changed, 90 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/mod_cloud_notify/README.markdown	Sun Apr 01 22:43:15 2018 +0200
+++ b/mod_cloud_notify/README.markdown	Sun Apr 01 23:24:33 2018 +0200
@@ -21,29 +21,37 @@
 The business rules outlined [here] are all honored[^2].
 
 To cooperate with [mod_smacks] this module consumes some events:
-"smacks-ack-delayed", "smacks-hibernation-start" and "smacks-hibernation-end".
+`smacks-ack-delayed`, `smacks-hibernation-start` and `smacks-hibernation-end`.
 These events allow this module to send out notifications for messages received
 while the session is hibernated by [mod_smacks] or even when smacks
 acknowledgements for messages are delayed by a certain amount of seconds
-configurable with the [mod_smacks] setting "smacks_max_ack_delay".
+configurable with the [mod_smacks] setting `smacks_max_ack_delay`.
 
-The "smacks_max_ack_delay" setting allows to send out notifications to clients
+The `smacks_max_ack_delay` setting allows to send out notifications to clients
 which aren't already in smacks hibernation state (because the read timeout or
-connection close didn't happen already) but also aren't responding to acknowledgement
-request in a timely manner, thus allowing conversations to be smoother under such
-circumstances.
+connection close didn't already happen) but also aren't responding to acknowledgement
+request in a timely manner. This setting thus allows conversations to be smoother
+under such circumstances.
 
-The new event "cloud-notify-ping" can be used by any module to send out a cloud
+The new event `cloud-notify-ping` can be used by any module to send out a cloud
 notification to either all registered endpoints for the given user or only the endpoints
 given in the event data.
 
+The config setting `push_notification_important_body` can be used to specify an alternative
+body text to send to the remote pubsub node if the stanza is encrypted or has a body.
+This way the real contents of the message aren't revealed to the push appserver but it
+can still see that the push is important.
+This is used by Chatsecure on iOS to send out high priority pushes in those cases for example.
+
 Configuration
 =============
 
-  Option                            Default   Description
-  --------------------------------- --------- ------------------------------------------------------------------
-  `push_notification_with_body`     `false`   Whether or not to send the message body to remote pubsub node.
-  `push_notification_with_sender`   `false`   Whether or not to send the message sender to remote pubsub node.
+  Option                               Default   Description
+  ------------------------------------ --------- ---------------------------------------------------------------------------------------------------------------
+  `push_notification_with_body`        `false`   Whether or not to send the message body to remote pubsub node.
+  `push_notification_with_sender`      `false`   Whether or not to send the message sender to remote pubsub node.
+  `push_max_errors`                    `50`      How much persistent push errors are tolerated before notifications for the identifier in question are disabled
+  `push_notification_important_body`   ``        The body text to use when the stanza is important (see above), no message body is sent if this is empty
 
 There are privacy implications for enabling these options because
 plaintext content and metadata will be shared with centralized servers
--- a/mod_cloud_notify/mod_cloud_notify.lua	Sun Apr 01 22:43:15 2018 +0200
+++ b/mod_cloud_notify/mod_cloud_notify.lua	Sun Apr 01 23:24:33 2018 +0200
@@ -5,6 +5,8 @@
 -- This file is MIT/X11 licensed.
 
 local t_insert = table.insert;
+local s_match = string.match;
+local s_sub = string.sub;
 local st = require"util.stanza";
 local jid = require"util.jid";
 local dataform = require"util.dataforms".new;
@@ -17,6 +19,7 @@
 local include_body = module:get_option_boolean("push_notification_with_body", false);
 local include_sender = module:get_option_boolean("push_notification_with_sender", false);
 local max_push_errors = module:get_option_number("push_max_errors", 50);
+local dummy_body = module:get_option_string("push_notification_important_body", "");
 
 local host_sessions = prosody.hosts[module.host].sessions;
 local push_errors = {};
@@ -208,21 +211,75 @@
 end
 module:hook("iq-set/self/"..xmlns_push..":disable", push_disable);
 
--- clone a stanza and strip it
-local function strip_stanza(stanza)
-	local tags = {};
-	local new = { name = stanza.name, attr = { xmlns = stanza.attr.xmlns, type = stanza.attr.type }, tags = tags };
-	for i=1,#stanza do
-		local child = stanza[i];
-		if type(child) == "table" then		-- don't add raw text nodes
-			if child.name then
-				child = strip_stanza(child);
-				t_insert(tags, child);
+-- Patched version of util.stanza:find() that supports giving stanza names
+-- without their namespace, allowing for every namespace.
+local function find(self, path)
+	local pos = 1;
+	local len = #path + 1;
+
+	repeat
+		local xmlns, name, text;
+		local char = s_sub(path, pos, pos);
+		if char == "@" then
+			return self.attr[s_sub(path, pos + 1)];
+		elseif char == "{" then
+			xmlns, pos = s_match(path, "^([^}]+)}()", pos + 1);
+		end
+		name, text, pos = s_match(path, "^([^@/#]*)([/#]?)()", pos);
+		name = name ~= "" and name or nil;
+		if pos == len then
+			if text == "#" then
+				local child = xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
+				return child and child:get_text() or nil;
 			end
-			t_insert(new, child);
+			return xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
 		end
+		self = xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
+	until not self
+	return nil;
+end
+
+-- is this push a high priority one (this is needed for ios apps not using voip pushes)
+local function is_important(stanza)
+	local st_name = stanza and stanza.name or nil;
+	if not st_name then return false; end	-- nonzas are never important here
+	if st_name == "presence" then
+		return false;						-- same for presences
+	elseif st_name == "message" then
+		-- unpack carbon copies
+		local stanza_direction = "in";
+		local carbon;
+		local st_type;
+		-- support carbon copied message stanzas having an arbitrary message-namespace or no message-namespace at all
+		if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:2}/forwarded/message"); end
+		if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:1}/forwarded/message"); end
+		stanza_direction = carbon and stanza:child_with_name("sent") and "out" or "in";
+		if carbon then stanza = carbon; end
+		st_type = stanza.attr.type;
+		
+		-- headline message are always not important
+		if st_type == "headline" then return false; end
+		
+		-- carbon copied outgoing messages are not important
+		if carbon and stanza_direction == "out" then return false; end
+		
+		-- We can't check for body contents in encrypted messages, so let's treat them as important
+		-- Some clients don't even set a body or an empty body for encrypted messages
+		
+		-- check omemo https://xmpp.org/extensions/inbox/omemo.html
+		if stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") or stanza:get_child("encrypted", "urn:xmpp:omemo:0") then return true; end
+		
+		-- check xep27 pgp https://xmpp.org/extensions/xep-0027.html
+		if stanza:get_child("x", "jabber:x:encrypted") then return true; end
+		
+		-- check xep373 pgp (OX) https://xmpp.org/extensions/xep-0373.html
+		if stanza:get_child("openpgp", "urn:xmpp:openpgp:0") then return true; end
+		
+		local body = stanza:get_child_text("body");
+		if st_type == "groupchat" and stanza:get_child_text("subject") then return false; end		-- groupchat subjects are not important here
+		return body ~= nil and body ~= "";			-- empty bodies are not important
 	end
-	return setmetatable(new, st.stanza_mt);
+	return false;		-- this stanza wasn't one of the above cases --> it is not important, too
 end
 
 local push_form = dataform {
@@ -231,7 +288,6 @@
 	{ name = "pending-subscription-count"; type = "text-single"; };
 	{ name = "last-message-sender"; type = "jid-single"; };
 	{ name = "last-message-body"; type = "text-single"; };
-	{ name = "last-message-priority"; type = "text-single"; };
 };
 
 -- http://xmpp.org/extensions/xep-0357.html#publishing
@@ -269,18 +325,10 @@
 			end
 			if stanza and include_body then
 				form_data["last-message-body"] = stanza:get_child_text("body");
+			elseif stanza and dummy_body ~= "" and is_important(stanza) then
+				form_data["last-message-body"] = dummy_body;
 			end
 			push_publish:add_child(push_form:form(form_data));
-			if stanza and push_info.include_payload == "stripped" then
-				push_publish:tag("payload", { type = "stripped" })
-					:add_child(strip_stanza(stanza));
-				push_publish:up(); -- / payload
-			end
-			if stanza and push_info.include_payload == "full" then
-				push_publish:tag("payload", { type = "full" })
-					:add_child(st.clone(stanza));
-				push_publish:up(); -- / payload
-			end
 			push_publish:up(); -- / notification
 			push_publish:up(); -- / publish
 			push_publish:up(); -- / pubsub