Comparison

mod_push2/mod_push2.lua @ 6232:d72010642b31 draft

Merge update
author Trần H. Trung <xmpp:trần.h.trung@trung.fun>
date Fri, 11 Apr 2025 23:19:21 +0700
parent 6219:06621ab30be0
child 6233:1c16bb49f6f6
comparison
equal deleted inserted replaced
6211:750d64c47ec6 6232:d72010642b31
7 local uuid = require "util.uuid"; 7 local uuid = require "util.uuid";
8 local base64 = require "util.encodings".base64; 8 local base64 = require "util.encodings".base64;
9 local crypto = require "util.crypto"; 9 local crypto = require "util.crypto";
10 local jwt = require "util.jwt"; 10 local jwt = require "util.jwt";
11 11
12 pcall(function() module:depends("track_muc_joins") end)
13
12 local xmlns_push = "urn:xmpp:push2:0"; 14 local xmlns_push = "urn:xmpp:push2:0";
13 15
14 -- configuration 16 -- configuration
15 local contact_uri = module:get_option_string("contact_uri", "xmpp:" .. module.host) 17 local contact_uri = module:get_option_string("contact_uri", "xmpp:" .. module.host)
16 local extended_hibernation_timeout = module:get_option_number("push_max_hibernation_timeout", 72*3600) -- use same timeout like ejabberd 18 local extended_hibernation_timeout = module:get_option_number("push_max_hibernation_timeout", 72*3600) -- use same timeout like ejabberd
19 local hibernate_past_first_push = module:get_option_boolean("hibernate_past_first_push", true)
17 20
18 local host_sessions = prosody.hosts[module.host].sessions 21 local host_sessions = prosody.hosts[module.host].sessions
19 local push2_registrations = module:open_store("push2_registrations", "keyval") 22 local push2_registrations = module:open_store("push2_registrations", "keyval")
20 23
21 if _VERSION:match("5%.1") or _VERSION:match("5%.2") then 24 if _VERSION:match("5%.1") or _VERSION:match("5%.2") then
26 (event.reply or event.stanza):tag("feature", {var=xmlns_push}):up() 29 (event.reply or event.stanza):tag("feature", {var=xmlns_push}):up()
27 end 30 end
28 module:hook("account-disco-info", account_dico_info); 31 module:hook("account-disco-info", account_dico_info);
29 32
30 local function parse_match(matchel) 33 local function parse_match(matchel)
31 local match = { match = matchel.attr.profile } 34 local match = { match = matchel.attr.profile, chats = {} }
35
36 for chatel in matchel:childtags("chat") do
37 local chat = {}
38 if chatel:get_child("mention") then
39 chat.mention = true
40 end
41 if chatel:get_child("reply") then
42 chat.reply = true
43 end
44 match.chats[chatel.attr.jid] = chat
45 end
46
47 match.grace = matchel:get_child_text("grace")
48 if match.grace then match.grace = tonumber(match.grace) end
49
32 local send = matchel:get_child("send", "urn:xmpp:push2:send:notify-only:0") 50 local send = matchel:get_child("send", "urn:xmpp:push2:send:notify-only:0")
33 if send then 51 if send then
34 match.send = send.attr.xmlns 52 match.send = send.attr.xmlns
35 return match 53 return match
36 end 54 end
154 172
155 return body ~= nil and body ~= "" 173 return body ~= nil and body ~= ""
156 end 174 end
157 175
158 -- is this push a high priority one 176 -- is this push a high priority one
159 local function is_important(stanza) 177 local function is_important(stanza, session)
160 local is_voip_stanza, urgent_reason = is_voip(stanza) 178 local is_voip_stanza, urgent_reason = is_voip(stanza)
161 if is_voip_stanza then return true; end 179 if is_voip_stanza then return true; end
162 180
163 local st_name = stanza and stanza.name or nil 181 local st_name = stanza and stanza.name or nil
164 if not st_name then return false; end -- nonzas are never important here 182 if not st_name then return false; end -- nonzas are never important here
175 if st_type == "headline" then return false; end 193 if st_type == "headline" then return false; end
176 194
177 -- carbon copied outgoing messages are not important 195 -- carbon copied outgoing messages are not important
178 if carbon and stanza_direction == "out" then return false; end 196 if carbon and stanza_direction == "out" then return false; end
179 197
180 -- groupchat subjects are not important here 198 -- groupchat reflections are not important here
181 if st_type == "groupchat" and stanza:get_child_text("subject") then 199 if st_type == "groupchat" and session and session.rooms_joined then
200 local muc = jid.bare(stanza.attr.from)
201 local from_nick = jid.resource(stanza.attr.from)
202 if from_nick == session.rooms_joined[muc] then
203 return false
204 end
205 end
206
207 -- edits are not imporatnt
208 if stanza:get_child("replace", "urn:xmpp:message-correct:0") then
182 return false 209 return false
183 end 210 end
184 211
185 -- empty bodies are not important 212 -- empty bodies are not important
186 return has_body(stanza) 213 return has_body(stanza)
220 end) 247 end)
221 envelope_bytes = tostring(envelope) 248 envelope_bytes = tostring(envelope)
222 end 249 end
223 if string.len(envelope_bytes) > max_data_size then 250 if string.len(envelope_bytes) > max_data_size then
224 local body = stanza:get_child_text("body") 251 local body = stanza:get_child_text("body")
225 if string.len(body) > 50 then 252 if body and string.len(body) > 50 then
226 stanza_clone:maptags(function(el) 253 stanza_clone:maptags(function(el)
227 if el.name == "body" then 254 if el.name == "body" then
228 return nil 255 return nil
229 else 256 else
230 return el 257 return el
249 return nil 276 return nil
250 end 277 end
251 end) 278 end)
252 envelope_bytes = tostring(envelope) 279 envelope_bytes = tostring(envelope)
253 end 280 end
254 if string.len(envelope_bytes) < max_data_size/2 then 281 local padding_size = math.min(150, max_data_size/3 - string.len(envelope_bytes))
255 envelope:text_tag("rpad", base64.encode(random.bytes(math.min(150, max_data_size/3 - string.len(envelope_bytes))))) 282 if padding_size > 0 then
283 envelope:text_tag("rpad", base64.encode(random.bytes(padding_size)))
256 envelope_bytes = tostring(envelope) 284 envelope_bytes = tostring(envelope)
257 end 285 end
258 286
259 local p256dh_raw = base64.decode(match.ua_public .. "==") 287 local p256dh_raw = base64.decode(match.ua_public .. "==")
260 local p256dh = crypto.import_public_ec_raw(p256dh_raw, "prime256v1") 288 local p256dh = crypto.import_public_ec_raw(p256dh_raw, "prime256v1")
292 end 320 end
293 payload.sub = contact_uri 321 payload.sub = contact_uri
294 push_notification_payload:text_tag("jwt", signer(payload), { key = base64.encode(public_key) }) 322 push_notification_payload:text_tag("jwt", signer(payload), { key = base64.encode(public_key) })
295 end 323 end
296 324
297 local function handle_notify_request(stanza, node, user_push_services, log_push_decline) 325 local function handle_notify_request(stanza, node, user_push_services, session, log_push_decline)
298 local pushes = 0; 326 local pushes = 0;
299 if not #user_push_services then return pushes end 327 if not #user_push_services then return pushes end
300 328
301 local notify_push_services = {}; 329 local notify_push_services = {};
302 if is_important(stanza) then 330 if is_important(stanza, session) then
303 notify_push_services = user_push_services 331 notify_push_services = user_push_services
304 else 332 else
305 for identifier, push_info in pairs(user_push_services) do 333 for identifier, push_info in pairs(user_push_services) do
306 for _, match in ipairs(push_info.matches) do 334 for _, match in ipairs(push_info.matches) do
307 if match.match == "urn:xmpp:push2:match:important" then 335 if match.match == "urn:xmpp:push2:match:important" then
327 end 355 end
328 356
329 if send_push then 357 if send_push then
330 local push_notification_payload = st.stanza("notification", { xmlns = xmlns_push }) 358 local push_notification_payload = st.stanza("notification", { xmlns = xmlns_push })
331 push_notification_payload:text_tag("client", push_info.client) 359 push_notification_payload:text_tag("client", push_info.client)
332 push_notification_payload:text_tag("priority", is_voip(stanza) and "high" or (is_important(stanza) and "normal" or "low")) 360 push_notification_payload:text_tag("priority", is_voip(stanza) and "high" or (is_important(stanza, session) and "normal" or "low"))
333 if is_voip(stanza) then 361 if is_voip(stanza) then
334 push_notification_payload:tag("voip"):up() 362 push_notification_payload:tag("voip"):up()
335 end 363 end
336 364
337 local sends_added = {}; 365 local sends_added = {};
338 for _, match in ipairs(push_info.matches) do 366 for _, match in ipairs(push_info.matches) do
339 local does_match = false; 367 local does_match = false;
340 if match.match == "urn:xmpp:push2:match:all" then 368 if match.match == "urn:xmpp:push2:match:all" then
341 does_match = true 369 does_match = true
342 elseif match.match == "urn:xmpp:push2:match:important" then 370 elseif match.match == "urn:xmpp:push2:match:important" then
343 does_match = is_important(stanza) 371 does_match = is_important(stanza, session)
344 elseif match.match == "urn:xmpp:push2:match:archived" then 372 elseif match.match == "urn:xmpp:push2:match:archived" then
345 does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0") 373 does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0")
346 elseif match.match == "urn:xmpp:push2:match:archived-with-body" then 374 elseif match.match == "urn:xmpp:push2:match:archived-with-body" then
347 does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0") and has_body(stanza) 375 does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0") and has_body(stanza)
376 end
377
378 local to_user, to_host = jid.split(stanza.attr.to)
379 to_user = to_user or session.username
380 to_host = to_host or module.host
381
382 -- If another session has recent activity within configured grace period, don't send push
383 if does_match and match.grace and to_host == module.host and host_sessions[to_user] then
384 local now = os_time()
385 for _, session in pairs(host_sessions[to_user].sessions) do
386 if session.last_activity and session.push_registration_id ~= push_registration_id and (now - session.last_activity) < match.grace then
387 does_match = false
388 end
389 end
390 end
391
392 local chat = match.chats and (match.chats[stanza.attr.from] or match.chats[jid.bare(stanza.attr.from)] or match.chats[jid.host(stanza.attr.from)])
393 if does_match and chat then
394 does_match = false
395
396 local nick = (session.rooms_joined and session.rooms_joined[jid.bare(stanza.attr.from)]) or to_user
397
398 if not does_match and chat.mention then
399 local body = stanza:get_child_text("body")
400 if body and body:find(nick, 1, true) then
401 does_match = true
402 end
403 end
404 if not does_match and chat.reply then
405 local reply = stanza:get_child("reply", "urn:xmpp:reply:0")
406 if reply and (reply.attr.to == to_user.."@"..to_host or (jid.bare(reply.attr.to) == jid.bare(stanza.attr.from) and jid.resource(reply.attr.to) == nick)) then
407 does_match = true
408 end
409 end
348 end 410 end
349 411
350 if does_match and not sends_added[match.send] then 412 if does_match and not sends_added[match.send] then
351 sends_added[match.send] = true 413 sends_added[match.send] = true
352 if match.send == "urn:xmpp:push2:send:notify-only" then 414 if match.send == "urn:xmpp:push2:send:notify-only" then
382 444
383 -- publish on offline message 445 -- publish on offline message
384 module:hook("message/offline/handle", function(event) 446 module:hook("message/offline/handle", function(event)
385 local node, user_push_services = get_push_settings(event.stanza, event.origin); 447 local node, user_push_services = get_push_settings(event.stanza, event.origin);
386 module:log("debug", "Invoking handle_notify_request() for offline stanza"); 448 module:log("debug", "Invoking handle_notify_request() for offline stanza");
387 handle_notify_request(event.stanza, node, user_push_services, true); 449 handle_notify_request(event.stanza, node, user_push_services, event.origin, true);
388 end, 1); 450 end, 1);
389 451
390 -- publish on bare groupchat 452 -- publish on bare groupchat
391 -- this picks up MUC messages when there are no devices connected 453 -- this picks up MUC messages when there are no devices connected
392 module:hook("message/bare/groupchat", function(event) 454 module:hook("message/bare/groupchat", function(event)
400 notify_push_services[identifier] = push_info; 462 notify_push_services[identifier] = push_info;
401 end 463 end
402 end 464 end
403 end 465 end
404 466
405 handle_notify_request(event.stanza, node, notify_push_services, true); 467 handle_notify_request(event.stanza, node, notify_push_services, event.origin, true);
406 end, 1); 468 end, 1);
407 469
408 local function process_stanza_queue(queue, session, queue_type) 470 local function process_stanza_queue(queue, session, queue_type)
409 if not session.push_registration_id then return; end 471 if not session.push_registration_id then return; end
410 local notified = { unimportant = false; important = false } 472 local notified = { unimportant = false; important = false }
413 -- fast ignore of already pushed stanzas 475 -- fast ignore of already pushed stanzas
414 if stanza and not (stanza._push_notify2 and stanza._push_notify2[session.push_registration_id]) then 476 if stanza and not (stanza._push_notify2 and stanza._push_notify2[session.push_registration_id]) then
415 local node, all_push_services = get_push_settings(stanza, session) 477 local node, all_push_services = get_push_settings(stanza, session)
416 local user_push_services = {[session.push_registration_id] = all_push_services[session.push_registration_id]} 478 local user_push_services = {[session.push_registration_id] = all_push_services[session.push_registration_id]}
417 local stanza_type = "unimportant"; 479 local stanza_type = "unimportant";
418 if is_important(stanza) then stanza_type = "important"; end 480 if is_important(stanza, session) then stanza_type = "important"; end
419 if not notified[stanza_type] then -- only notify if we didn't try to push for this stanza type already 481 if not notified[stanza_type] then -- only notify if we didn't try to push for this stanza type already
420 if handle_notify_request(stanza, node, user_push_services, false) ~= 0 then 482 if handle_notify_request(stanza, node, user_push_services, session, false) ~= 0 then
421 if session.hibernating and not session.first_hibernated_push then 483 if session.hibernating and not session.first_hibernated_push then
422 -- if the message was important 484 -- if the message was important
423 -- then record the time of first push in the session for the smack module which will extend its hibernation 485 -- then record the time of first push in the session for the smack module which will extend its hibernation
424 -- timeout based on the value of session.first_hibernated_push 486 -- timeout based on the value of session.first_hibernated_push
425 if is_important(stanza) then 487 if is_important(stanza, session) and not hibernate_past_first_push then
426 session.first_hibernated_push = os_time(); 488 session.first_hibernated_push = os_time();
427 -- check for prosody 0.12 mod_smacks 489 -- check for prosody 0.12 mod_smacks
428 if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then 490 if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then
429 -- restore old smacks watchdog (--> the start of our original timeout will be delayed until first push) 491 -- restore old smacks watchdog (--> the start of our original timeout will be delayed until first push)
430 session.hibernating_watchdog:cancel(); 492 session.hibernating_watchdog:cancel();
565 notify_push_services[identifier] = push_info 627 notify_push_services[identifier] = push_info
566 end 628 end
567 end 629 end
568 end 630 end
569 631
570 handle_notify_request(stanza, to_user, notify_push_services, true); 632 handle_notify_request(stanza, to_user, notify_push_services, event.origin, true);
571 end 633 end
572 end 634 end
573 635
574 module:hook("smacks-hibernation-start", hibernate_session); 636 module:hook("smacks-hibernation-start", hibernate_session);
575 module:hook("smacks-hibernation-end", restore_session); 637 module:hook("smacks-hibernation-end", restore_session);
576 module:hook("smacks-ack-delayed", ack_delayed); 638 module:hook("smacks-ack-delayed", ack_delayed);
577 module:hook("smacks-hibernation-stanza-queued", process_smacks_stanza); 639 module:hook("smacks-hibernation-stanza-queued", process_smacks_stanza);
578 module:hook("archive-message-added", archive_message_added); 640 module:hook("archive-message-added", archive_message_added);
641
642 local function track_activity(event)
643 if has_body(event.stanza) or event.stanza:child_with_ns("http://jabber.org/protocol/chatstates") then
644 event.origin.last_activity = os_time()
645 end
646 end
647
648 module:hook("pre-message/bare", track_activity)
649 module:hook("pre-message/full", track_activity)
579 650
580 module:log("info", "Module loaded"); 651 module:log("info", "Module loaded");
581 function module.unload() 652 function module.unload()
582 module:log("info", "Unloading module"); 653 module:log("info", "Unloading module");
583 -- cleanup some settings, reloading this module can cause process_smacks_stanza() to stop working otherwise 654 -- cleanup some settings, reloading this module can cause process_smacks_stanza() to stop working otherwise