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>
376 lines
16 KiB
Lua
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
|