mod_csi_battery_saver/mod_csi_battery_saver.lua
changeset 2610 538c54d2dab3
child 2739 b5fae17e4403
equal deleted inserted replaced
2609:8908d001faf3 2610:538c54d2dab3
       
     1 -- Copyright (C) 2016 Kim Alvefur
       
     2 -- Copyright (C) 2017 Thilo Molitor
       
     3 --
       
     4 
       
     5 module:depends"csi"
       
     6 module:depends"track_muc_joins"
       
     7 local s_match = string.match;
       
     8 local s_sub = string.sub;
       
     9 local jid = require "util.jid";
       
    10 local new_queue = require "util.queue".new;
       
    11 local datetime = require "util.datetime";
       
    12 
       
    13 local xmlns_delay = "urn:xmpp:delay";
       
    14 
       
    15 -- a log id for this module instance
       
    16 local id = s_sub(require "util.hashes".sha256(datetime.datetime(), true), 1, 4);
       
    17 
       
    18 -- Patched version of util.stanza:find() that supports giving stanza names
       
    19 -- without their namespace, allowing for every namespace.
       
    20 local function find(self, path)
       
    21 	local pos = 1;
       
    22 	local len = #path + 1;
       
    23 	
       
    24 	repeat
       
    25 		local xmlns, name, text;
       
    26 		local char = s_sub(path, pos, pos);
       
    27 		if char == "@" then
       
    28 			return self.attr[s_sub(path, pos + 1)];
       
    29 		elseif char == "{" then
       
    30 			xmlns, pos = s_match(path, "^([^}]+)}()", pos + 1);
       
    31 		end
       
    32 		name, text, pos = s_match(path, "^([^@/#]*)([/#]?)()", pos);
       
    33 		name = name ~= "" and name or nil;
       
    34 		if pos == len then
       
    35 			if text == "#" then
       
    36 				local child = xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
       
    37 				return child and child:get_text() or nil;
       
    38 			end
       
    39 			return xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
       
    40 		end
       
    41 		self = xmlns ~= nil and self:get_child(name, xmlns) or self:child_with_name(name);
       
    42 	until not self
       
    43 	return nil;
       
    44 end
       
    45 
       
    46 local function new_pump(output, ...)
       
    47 	-- luacheck: ignore 212/self
       
    48 	local q = new_queue(...);
       
    49 	local flush = true;
       
    50 	function q:pause()
       
    51 		flush = false;
       
    52 	end
       
    53 	function q:resume()
       
    54 		flush = true;
       
    55 		return q:flush();
       
    56 	end
       
    57 	local push = q.push;
       
    58 	function q:push(item)
       
    59 		local ok = push(self, item);
       
    60 		if not ok then
       
    61 			q:flush();
       
    62 			output(item, self);
       
    63 		elseif flush then
       
    64 			return q:flush();
       
    65 		end
       
    66 		return true;
       
    67 	end
       
    68 	function q:flush()
       
    69 		local item = self:pop();
       
    70 		while item do
       
    71 			output(item, self);
       
    72 			item = self:pop();
       
    73 		end
       
    74 		return true;
       
    75 	end
       
    76 	return q;
       
    77 end
       
    78 
       
    79 local function is_stamp_needed(stanza, session)
       
    80 	local st_name = stanza and stanza.name or nil;
       
    81 	if st_name == "presence" then
       
    82 		return true;
       
    83 	elseif st_name == "message" then
       
    84 		if stanza:get_child("delay", xmlns_delay) then return false; end
       
    85 		if stanza.attr.type == "chat" or stanza.attr.type == "groupchat" then return true; end
       
    86 	end
       
    87 	return false;
       
    88 end
       
    89 
       
    90 local function add_stamp(stanza, session)
       
    91 	stanza = stanza:tag("delay", { xmlns = xmlns_delay, from = session.host, stamp = datetime.datetime()});
       
    92 	return stanza;
       
    93 end
       
    94 
       
    95 local function is_important(stanza, session)
       
    96 	local st_name = stanza and stanza.name or nil;
       
    97 	if not st_name then return false; end
       
    98 	if st_name == "presence" then
       
    99 		-- TODO check for MUC status codes?
       
   100 		return false;
       
   101 	elseif st_name == "message" then
       
   102 		-- unpack carbon copies
       
   103 		local stanza_direction = "in";
       
   104 		local carbon;
       
   105 		-- support carbon copied message stanzas having an arbitrary message-namespace or no message-namespace at all
       
   106 		if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:2}/forwarded/message"); end
       
   107 		if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:1}/forwarded/message"); end
       
   108 		stanza_direction = carbon and stanza:child_with_name("sent") and "out" or "in";
       
   109 		--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));
       
   110 		if carbon then stanza = carbon; end
       
   111 		-- carbon copied outgoing messages aren't important (but incoming carbon copies are!)
       
   112 		if carbon and stanza_direction == "out" then return false; end
       
   113 		
       
   114 		local st_type = stanza.attr.type;
       
   115 		if st_type == "headline" then
       
   116 			return false;
       
   117 		end
       
   118 		local body = stanza:get_child_text("body");
       
   119 		if st_type == "groupchat" then
       
   120 			if stanza:get_child_text("subject") then return true; end
       
   121 			if not body then return false; end
       
   122 			if body:find(session.username, 1, true) then return true; end
       
   123 			local rooms = session.rooms_joined;
       
   124 			if not rooms then return false; end
       
   125 			local room_nick = rooms[jid.bare(stanza_direction == "in" and stanza.attr.from or stanza.attr.to)];
       
   126 			if room_nick and body:find(room_nick, 1, true) then return true; end
       
   127 			return false;
       
   128 		end
       
   129 		return body ~= nil and body ~= "";
       
   130 	end
       
   131 	return true;
       
   132 end
       
   133 
       
   134 module:hook("csi-client-inactive", function (event)
       
   135 	local session = event.origin;
       
   136 	if session.pump then
       
   137 		session.pump:pause();
       
   138 	else
       
   139 		session.log("debug", "mod_csi_battery_saver(%s): Client is inactive the first time, initializing module for this session", id);
       
   140 		local pump = new_pump(session.send, 100);
       
   141 		pump:pause();
       
   142 		session.pump = pump;
       
   143 		session._pump_orig_send = session.send;
       
   144 		function session.send(stanza)
       
   145 			session.log("debug", "mod_csi_battery_saver(%s): Got stanza: <%s>", id, tostring(stanza.name));
       
   146 			local important = is_important(stanza, session);
       
   147 			-- add delay stamp to unimported (buffered) stanzas that can/need be stamped
       
   148 			if not important and is_stamp_needed(stanza, session) then stanza = add_stamp(stanza, session); end
       
   149 			pump:push(stanza);
       
   150 			if important then
       
   151 				session.log("debug", "mod_csi_battery_saver(%s): Encountered important stanza, flushing buffer: <%s>", id, tostring(stanza.name));
       
   152 				pump:flush();
       
   153 			end
       
   154 			return true;
       
   155 		end
       
   156 	end
       
   157 	session.log("debug", "mod_csi_battery_saver(%s): Client is inactive, buffering unimportant stanzas", id);
       
   158 end);
       
   159 
       
   160 module:hook("csi-client-active", function (event)
       
   161 	local session = event.origin;
       
   162 	if session.pump then
       
   163 		session.log("debug", "mod_csi_battery_saver(%s): Client is active, resuming direct delivery", id);
       
   164 		session.pump:resume();
       
   165 	end
       
   166 end);
       
   167 
       
   168 function module.unload()
       
   169 	module:log("info", "%s: Unloading module, flushing all buffers", id);
       
   170 	local host_sessions = prosody.hosts[module.host].sessions;
       
   171 	for _, user in pairs(host_sessions) do
       
   172 		for _, session in pairs(user.sessions) do
       
   173 			if session.pump then
       
   174 				session.pump:flush();
       
   175 				session.send = session._pump_orig_send;
       
   176 				session.pump = nil;
       
   177 				session._pump_orig_send = nil;
       
   178 			end
       
   179 		end
       
   180 	end
       
   181 end
       
   182 
       
   183 module:log("info", "%s: Successfully loaded module", id);