Software /
code /
prosody-modules
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 |