Software / code / prosody-modules
Comparison
mod_push2/mod_push2.lua @ 6263:10a1016d1c3a
Merge update
| author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
|---|---|
| date | Sun, 01 Jun 2025 11:43:16 +0700 |
| parent | 6233:1c16bb49f6f6 |
| child | 6266:6164849469f1 |
comparison
equal
deleted
inserted
replaced
| 6262:a72388da5cd4 | 6263:10a1016d1c3a |
|---|---|
| 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("filter") 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 |