# HG changeset patch # User tmolitor # Date 1489105973 -3600 # Node ID 538c54d2dab3cac8c0f527aa3e03c548844259b4 # Parent 8908d001faf3de644df2dd03314b05aad3e2222c mod_csi_battery_saver: CSI module to save battery on mobile devices, based on mod_csi_pump diff -r 8908d001faf3 -r 538c54d2dab3 mod_csi_battery_saver/README.markdown --- /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. diff -r 8908d001faf3 -r 538c54d2dab3 mod_csi_battery_saver/mod_csi_battery_saver.lua --- /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);