Changeset

2606:538c54d2dab3

mod_csi_battery_saver: CSI module to save battery on mobile devices, based on mod_csi_pump
author tmolitor <thilo@eightysoft.de>
date Fri, 10 Mar 2017 01:32:53 +0100
parents 2605:8908d001faf3
children 2607:a7ef9b765891
files mod_csi_battery_saver/README.markdown mod_csi_battery_saver/mod_csi_battery_saver.lua
diffstat 2 files changed, 209 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_csi_battery_saver/README.markdown	Fri Mar 10 01:32:53 2017 +0100
@@ -0,0 +1,26 @@
+---
+description: CSI module to save battery on mobile devices
+labels:
+- 'Stage-Alpha'
+---
+
+Stanzas are queued in a buffer until either an "important" stanza is
+encountered or the buffer becomes full. Then all queued stanzas are sent
+at the same time. This way, nothing is lost or reordered while still
+allowing for power usage savings by not requiring mobile clients to
+bring up their radio for unimportant stanzas.
+
+`IQ` stanzas, smacks "stanzas" and `message` stanzas containing a body are
+considered important. Groupchat messages must set a subject or have
+the user's username or nickname in their messages to count as "important".
+`Presence` stanzas are not "important".
+
+All buffered stanzas that allow timestamping are properly stamped to
+reflect their original send time, see [XEP-0203].
+
+Use with other CSI plugins such as [mod_throttle_presence],
+[mod_filter_chatstates] or [mod_csi_pump] is *not* supported.
+Please use this module instead of [mod_csi_pump] if you want timestamping
+and properly handled carbon copies.
+
+The internal stanza buffer of this module is hardcoded to 100 stanzas.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_csi_battery_saver/mod_csi_battery_saver.lua	Fri Mar 10 01:32:53 2017 +0100
@@ -0,0 +1,183 @@
+-- Copyright (C) 2016 Kim Alvefur
+-- Copyright (C) 2017 Thilo Molitor
+--
+
+module:depends"csi"
+module:depends"track_muc_joins"
+local s_match = string.match;
+local s_sub = string.sub;
+local jid = require "util.jid";
+local new_queue = require "util.queue".new;
+local datetime = require "util.datetime";
+
+local xmlns_delay = "urn:xmpp:delay";
+
+-- a log id for this module instance
+local id = s_sub(require "util.hashes".sha256(datetime.datetime(), true), 1, 4);
+
+-- 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
+			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
+
+local function new_pump(output, ...)
+	-- luacheck: ignore 212/self
+	local q = new_queue(...);
+	local flush = true;
+	function q:pause()
+		flush = false;
+	end
+	function q:resume()
+		flush = true;
+		return q:flush();
+	end
+	local push = q.push;
+	function q:push(item)
+		local ok = push(self, item);
+		if not ok then
+			q:flush();
+			output(item, self);
+		elseif flush then
+			return q:flush();
+		end
+		return true;
+	end
+	function q:flush()
+		local item = self:pop();
+		while item do
+			output(item, self);
+			item = self:pop();
+		end
+		return true;
+	end
+	return q;
+end
+
+local function is_stamp_needed(stanza, session)
+	local st_name = stanza and stanza.name or nil;
+	if st_name == "presence" then
+		return true;
+	elseif st_name == "message" then
+		if stanza:get_child("delay", xmlns_delay) then return false; end
+		if stanza.attr.type == "chat" or stanza.attr.type == "groupchat" then return true; end
+	end
+	return false;
+end
+
+local function add_stamp(stanza, session)
+	stanza = stanza:tag("delay", { xmlns = xmlns_delay, from = session.host, stamp = datetime.datetime()});
+	return stanza;
+end
+
+local function is_important(stanza, session)
+	local st_name = stanza and stanza.name or nil;
+	if not st_name then return false; end
+	if st_name == "presence" then
+		-- TODO check for MUC status codes?
+		return false;
+	elseif st_name == "message" then
+		-- unpack carbon copies
+		local stanza_direction = "in";
+		local carbon;
+		-- 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";
+		--session.log("debug", "mod_csi_battery_saver(%s): stanza_direction = %s, carbon = %s, stanza = %s", id, stanza_direction, carbon and "true" or "false", tostring(stanza));
+		if carbon then stanza = carbon; end
+		-- carbon copied outgoing messages aren't important (but incoming carbon copies are!)
+		if carbon and stanza_direction == "out" then return false; end
+		
+		local st_type = stanza.attr.type;
+		if st_type == "headline" then
+			return false;
+		end
+		local body = stanza:get_child_text("body");
+		if st_type == "groupchat" then
+			if stanza:get_child_text("subject") then return true; end
+			if not body then return false; end
+			if body:find(session.username, 1, true) then return true; end
+			local rooms = session.rooms_joined;
+			if not rooms then return false; end
+			local room_nick = rooms[jid.bare(stanza_direction == "in" and stanza.attr.from or stanza.attr.to)];
+			if room_nick and body:find(room_nick, 1, true) then return true; end
+			return false;
+		end
+		return body ~= nil and body ~= "";
+	end
+	return true;
+end
+
+module:hook("csi-client-inactive", function (event)
+	local session = event.origin;
+	if session.pump then
+		session.pump:pause();
+	else
+		session.log("debug", "mod_csi_battery_saver(%s): Client is inactive the first time, initializing module for this session", id);
+		local pump = new_pump(session.send, 100);
+		pump:pause();
+		session.pump = pump;
+		session._pump_orig_send = session.send;
+		function session.send(stanza)
+			session.log("debug", "mod_csi_battery_saver(%s): Got stanza: <%s>", id, tostring(stanza.name));
+			local important = is_important(stanza, session);
+			-- add delay stamp to unimported (buffered) stanzas that can/need be stamped
+			if not important and is_stamp_needed(stanza, session) then stanza = add_stamp(stanza, session); end
+			pump:push(stanza);
+			if important then
+				session.log("debug", "mod_csi_battery_saver(%s): Encountered important stanza, flushing buffer: <%s>", id, tostring(stanza.name));
+				pump:flush();
+			end
+			return true;
+		end
+	end
+	session.log("debug", "mod_csi_battery_saver(%s): Client is inactive, buffering unimportant stanzas", id);
+end);
+
+module:hook("csi-client-active", function (event)
+	local session = event.origin;
+	if session.pump then
+		session.log("debug", "mod_csi_battery_saver(%s): Client is active, resuming direct delivery", id);
+		session.pump:resume();
+	end
+end);
+
+function module.unload()
+	module:log("info", "%s: Unloading module, flushing all buffers", id);
+	local host_sessions = prosody.hosts[module.host].sessions;
+	for _, user in pairs(host_sessions) do
+		for _, session in pairs(user.sessions) do
+			if session.pump then
+				session.pump:flush();
+				session.send = session._pump_orig_send;
+				session.pump = nil;
+				session._pump_orig_send = nil;
+			end
+		end
+	end
+end
+
+module:log("info", "%s: Successfully loaded module", id);