Files
Stefan-Sanger 2252b08139 fix(fcm): pass full JID to fcm_notify and use st.clone in MUC handler
fcm_notify splits userName internally via jid_split, so callers were
double-splitting: passing jid_split(to) fed the bare node as userName
and shifted every subsequent argument by one (host→title, resource→body,
…), corrupting all notification payloads. Pass the full JID instead.

In mod_vnc_muc_fcm, replace event.stanza:clone() with st.clone() and
add the util.stanza import for robustness.

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/11>
2026-07-22 16:33:17 +02:00

376 lines
16 KiB
Lua

-- mod_vnc_fcm.lua
--
-- FCM push notifications for 1:1 chat (and inter-host MUC relay).
--
-- Shared logic (caching, FCM HTTP, token building, stale-token cleanup)
-- lives in vnc_fcm_common.lua. This module retains the message-routing
-- logic, CSI tracking, IQ handler, and event hooks that are specific to
-- the main VirtualHost.
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local timer = require "util.timer";
local st = require "util.stanza";
local common = require "vnc_fcm_common";
local trim = common.utils.trim;
local starts_with = common.utils.starts_with;
local dumpTable = common.utils.dumpTable;
local private_storage = module:open_store("private");
local vcard_storage = module:open_store("vcard");
local fcm_token_store = module:open_store("fcmtoken");
local fcm_token_map_store = module:open_store("fcmtoken", "map");
local fcmAPIKey = module:get_option_string("fcm_api_key");
local fcmAPIURL = module:get_option_string("fcm_api_url");
local moduleIsActive = fcmAPIURL and fcmAPIKey;
local isMUC = module:get_host_type() == "component";
local ThisDomain = module.host;
local storage_host;
local ThisBroadcast = "broadcast@"..ThisDomain;
module:add_feature("urn:xmpp:vnctalk:fcm");
local inactive_devices = {};
local stores = {
private = private_storage,
vcard = vcard_storage,
fcmtoken = fcm_token_store,
fcmtoken_map = fcm_token_map_store,
};
local domain = ThisDomain;
if isMUC then
storage_host = module:get_option_string("storage_host");
domain = storage_host;
module:log("debug", "opening stores for host %s", storage_host);
timer.add_task(3, function ()
stores.private = module:open_store(storage_host, "private");
stores.vcard = module:open_store(storage_host, "vcard");
stores.fcmtoken = module:open_store(storage_host, "fcmtoken");
stores.fcmtoken_map = module:open_store(storage_host, "fcmtoken", "map");
if not stores.private or not stores.vcard then
module:log("debug", "private/vcard_store %s/%s not found? - will try again", stores.private, stores.vcard);
return 3;
else
module:log("debug", "private/vcard_store are now %s/%s", stores.private, stores.vcard);
end
end);
end
local F = common.new({
mod = module,
stores = stores,
domain = domain,
is_muc = isMUC,
ios_always_push = false,
global_mute_early_return = false,
override_topic_on_hidden = true,
allow_etype_or_readtarget = true,
domain_check_enabled = false,
fcm_api_key = fcmAPIKey,
fcm_api_url = fcmAPIURL,
module_is_active = moduleIsActive,
inactive_devices = inactive_devices,
});
local fcm_notify = F.fcm_notify;
local getDisplayName_cached = F.getDisplayName_cached;
local invalidateNotifyCacheGlobal = F.invalidateNotifyCacheGlobal;
local invalidateVcardCacheGlobal = F.invalidateVcardCacheGlobal;
local invalidateNotifyCache = F.invalidateNotifyCache;
local invalidateVcardCache = F.invalidateVcardCache;
-- ─── message handler ───────────────────────────────────────────────
local function message_handler(event, fromLocal)
local origin, stanza = event.origin, event.stanza;
local message_id = event.stanza.attr.id;
local type = stanza.attr.type or "normal";
local from = jid_bare(stanza.attr.from);
local to = jid_bare(stanza.attr.to) or from;
local vncTalkConferenceEtype = stanza:find("{xmpp:vnctalk}vncTalkConference/eventType#") or nil;
local vncTalkSignal = stanza:find("{xmpp:vnctalk:signal}signal") or nil;
local vnctalk_broadcast = stanza:find("{xmpp:vnctalk}vncTalkBroadcast", "xmpp:vnctalk" ) or false;
local vnctalk_avatarup = false;
if (vnctalk_broadcast) then
vnctalk_avatarup = vnctalk_broadcast.attr.avatarup or false;
end
if (to == ThisBroadcast) then
vnctalk_avatarup = true;
end
if ((to == from) and vncTalkSignal and fromLocal) then
local signalType = vncTalkSignal:find("{xmpp:vnctalk}type#") or nil;
if (signalType == "read") then
local signalTarget = vncTalkSignal:find("{xmpp:vnctalk}target#") or nil;
if (signalTarget ~= nil) then
fcm_notify(to, nil, nil, "read", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, signalTarget);
end
elseif (signalType == "notification-settings") then
local u = jid_split(to);
if (u) then
module:log("debug", "notification-settings signal for %s - invalidating notify cache", u);
invalidateNotifyCacheGlobal(u);
end
end
end
if ((to ~= from) or ((to == from) and vncTalkConferenceEtype))
then
local content = trim(stanza:find("body#"));
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
local vncTalkIncomingType = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceType#");
local vncTalkConferenceId = stanza:find("{xmpp:vnctalk}vncTalkConference/conferenceId#") or nil;
local jitsiURL = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiURL#") or nil;
local jitsiRoom = stanza:find("{xmpp:vnctalk}vncTalkConference/jitsiRoom#") or nil;
local vncTalkWhiteboard = stanza:find("{xmpp:vnctalk}whiteboard");
local isSentCarbonMessage = stanza:get_child("sent", "urn:xmpp:carbons:2");
local vnc_attachment_type = stanza:find("{xmpp:vnctalk}attachment/fileType#");
local vnctalk_broadcast = stanza:get_child("vncTalkBroadcast", "xmpp:vnctalk");
local vnctalk_broadcast_childid = nil;
if vnctalk_broadcast then
if starts_with(to, "broadcast") then
return nil;
end
vnctalk_broadcast_childid = vnctalk_broadcast.attr.id;
local vnctalk_broadcast_title = vnctalk_broadcast.attr.title;
local vnctalk_broadcast_target = vnctalk_broadcast.attr.origtarget;
local body = stanza:get_child("body");
if (not(vnctalk_avatarup)) then
fcm_notify(to, vnctalk_broadcast_target, content, type, getDisplayName_cached(jid_split(from)), "Broadcast: "..vnctalk_broadcast_title, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom);
end
return nil;
end
local isMessageCorrection = stanza:find("{urn:xmpp:message-correct:0}replace@id") or nil;
if (isMessageCorrection) then
content = " ";
type = "CORRECTION";
end
module:log("debug","continue to process message with body: %s", content);
local auxType = vncTalkIncomingType or (vncTalkWhiteboard and "whiteboard");
if (auxType)
then
content = auxType;
type = auxType;
end;
local doNotify = content and (content ~= " ") and (content ~= "") and (not(vnctalk_avatarup)) and (delayedDelivery == nil) and (isSentCarbonMessage == nil) and (
(type == "chat") or (type == "groupchat") or (type == "audio") or (type == "video") or (type == "whiteboard") or (type == "screen") or (type == "CORRECTION")
);
if (doNotify)
then
module:log("debug", "536-doNotify: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
if (not isMUC)
then
module:log("debug", "537-doNotify not isMUC: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
local fromuser2, fromdomain2 = jid_split(stanza.attr.from);
local touser2, todomain2 = jid_split(stanza.attr.to);
if (fromLocal and (fromdomain2 == ThisDomain) and ((todomain2 == ThisDomain) or (todomain2 == nil))) and (type ~= "groupchat") then
fcm_notify(to, from, content, type, getDisplayName_cached(jid_split(from)), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom, isMessageCorrection);
else
if (type ~= "groupchat") and (fromdomain2 ~= ThisDomain) then
fcm_notify(to, from, content, type, getDisplayName_cached(jid_split(from)), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom, isMessageCorrection);
else
if not (prosody.hosts[fromdomain2]) then
module:log("info", "I do not know about %s", fromdomain2);
end
if not (prosody.hosts[fromdomain2]) and type == "groupchat" then
local rjnode, rjhost, rjres = jid_split(stanza.attr.from);
local rdn, rdh = jid_split(rjres);
local tdn, tdh, tdr = jid_split(stanza.attr.to);
if ((rdn == tdn) and (rdh == tdh)) then
module:log("debug", "766 ... rdn=%s, tdn=%s, rdh=%s, tdh=%s", rdn, tdn, rdh, tdh);
else
module:log("debug", "541-doNotify IOM: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s, rdn=%s",
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify), rdn);
fcm_notify(to, from, content, type, getDisplayName_cached(rdn), nil, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, nil, jitsiURL, jitsiRoom, isMessageCorrection);
end
else
module:log("debug", " WTF ignoring message");
end
end
end
else -- MUC
module:log("debug", "538-doNotify isMUC: %s -> %s fl=%s, type=%s, content=%s, carbon=%s, isMUC=%s => doNotify=%s",
stanza.attr.from, stanza.attr.to, tostring(fromLocal), type, content, isSentCarbonMessage, tostring(isMUC), tostring(doNotify));
local muc, host = jid_split(to);
if prosody.hosts[host] then
local muc_mod = prosody.hosts[host].modules.muc;
local get_room_from_jid = muc_mod and (rawget(muc_mod, "get_room_from_jid") or
function (jid)
local rooms = rawget(muc_mod, "rooms");
return rooms and rooms[jid];
end);
local room = get_room_from_jid and get_room_from_jid(to);
module.log("info", "processing to and found local muc component: %s", to);
local affs = room and room._affiliations;
if affs then
local roomTopic = room._data.subject or nil;
local senderDisplayName = getDisplayName_cached(jid_split(from));
local roomJid = to;
for _aff, role in pairs(affs) do
if from ~= _aff then
module:log("debug", " notify MUC affilaite Jid=%s", _aff);
fcm_notify(_aff, roomJid, content, type, senderDisplayName, roomTopic, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, from, jitsiURL, jitsiRoom, isMessageCorrection);
end
end
end
end
end
else
module:log("debug", "ignoring from=%s, to=%s, type=%s, content=%s", from, to, type, content);
end
end
return;
end
local function iom_muc_handler(event)
local origin, stanza = event.origin, event.stanza;
local type = stanza.attr.type or "normal";
local from = jid_bare(stanza.attr.from);
local to = jid_bare(stanza.attr.to) or from;
local fromloc,fromhost = jid_split(from);
local toloc,tohost = jid_split(to);
if (prosody.hosts[tohost] and (prosody.hosts[fromhost] == nil)) then
module:log("debug", "iom message type %s from host %s - to host %s", type, fromhost, tohost);
return message_handler(event, false);
else
return nil;
end
end
local function message_handler_to_local(event)
return message_handler(event, false);
end
local function message_handler_from_local(event)
return message_handler(event, true);
end
-- ─── CSI tracking ──────────────────────────────────────────────────
local function logCsiEvent(event, active)
local csires = event.origin.resource or nil;
if active
then
inactive_devices[csires] = nil;
module:log("debug", "ACTIVE: %s", tostring(event.origin));
module:log("debug", "ACTIVE resource %s", csires);
else
module:log("debug", "INACTIVE: %s", tostring(event.origin));
module:log("debug", "INACTIVE resource %s", csires);
local t = os.time();
inactive_devices[csires] = t;
end
end
-- ─── IQ handler (FCM token registration) ───────────────────────────
local function handle_iq(event)
local origin, stanza = event.origin, event.stanza;
local from_node, from_host, from_res = jid_split(stanza.attr.from);
local reply = st.reply(stanza);
local res = false;
if stanza.attr.type == "set" then
local fcm_child = stanza:get_child("add", "xmpp:vnctalk:fcm") or nil;
if (fcm_child) then
local fcm_token = fcm_child:get_child("fcm", "xmpp:vnctalk:fcm") or nil;
if (fcm_token) then
local _device = fcm_token.attr.device or nil;
local _token = fcm_token.attr.token or nil;
local _os = fcm_token.attr.os or nil;
module:log("debug", "going to add token for %s - device %s, token: %s ", from_node, _device, _token);
local do_add_token = (_device) and (_token) and (_os);
if (do_add_token) then
local tk_os = { token = _token , os = _os };
local _store_fcm = stores.fcmtoken_map:set(from_node, _device, tk_os);
invalidateNotifyCacheGlobal(from_node);
res = true;
origin.send(st.reply(stanza):tag('add', {xmlns='xmpp:vnctalk:fcm'}):text("ok"));
return true;
end
end
end
end
if res then
return true;
end
end
-- ─── cross-module cache invalidation listener ─────────────────────
module:hook_global("vnc-fcm-invalidate-notify-cache", function(event)
if event and event.username then
invalidateNotifyCache(event.username);
end
end);
module:hook_global("vnc-fcm-invalidate-vcard-cache", function(event)
if event and event.username then
invalidateVcardCache(event.username);
end
end);
-- ─── event hooks ───────────────────────────────────────────────────
if (moduleIsActive)
then
module:hook("message/bare", message_handler_to_local, 2);
module:hook("message/full", iom_muc_handler, 2);
module:hook("pre-message/bare", message_handler_from_local, 2);
module:hook("pre-message/full", message_handler_from_local, 2);
module:hook("vnc-rest-message", message_handler_from_local, 2);
module:hook("csi-client-active", function(event) logCsiEvent(event, true); end);
module:hook("csi-client-inactive", function(event) logCsiEvent(event, false); end);
module:hook("resource-bind", function(event)
local resbind = event.session.resource;
inactive_devices[resbind] = nil;
end);
module:hook("iq/bare/vcard-temp:vCard", function(event)
if event.stanza.attr.type == "set" and event.origin and event.origin.username then
invalidateVcardCacheGlobal(event.origin.username);
end
end, 1);
module:hook("iq/bare/jabber:iq:private:query", function(event)
if event.stanza.attr.type == "set" and event.origin and event.origin.username then
local u = event.origin.username;
timer.add_task(0, function() invalidateNotifyCacheGlobal(u); end);
end
end, 1);
module:hook("pre-iq/bare", handle_iq, 100);
module:log("info", "FCM notifications ACTIVATED");
end