prosody.events.fire_event is a plain function (event_name, event_data), not a method. The colon form prosody.events:fire_event(name, data) passed the events table as event_name, so the lookup found no handlers and the vnc-fcm-invalidate-notify-cache / vcard-cache signals were silently lost. mod_vnc_muc_fcm hooks these via module:hook_global (which registers under the string event name), so its notify_cache was never invalidated cross-host. A stale empty cache entry for an MUC affiliate (left by an earlier test before the user had a token) then suppressed FCM pushes to that affiliate, breaking test_muc_fcm_push_to_offline_member in the full suite. Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/11>
693 lines
24 KiB
Lua
693 lines
24 KiB
Lua
-- vnc_fcm_common.lua
|
||
--
|
||
-- Shared FCM notification logic for mod_vnc_fcm and mod_vnc_muc_fcm.
|
||
--
|
||
-- Both modules require this file and call new(ctx) to obtain closures
|
||
-- bound to their respective stores, caches, and behavioural flags.
|
||
-- This eliminates the ~90 % code duplication between the two modules.
|
||
--
|
||
-- This file is MIT/X11 licensed.
|
||
|
||
local http = require "net.http"
|
||
local json = require "util.json"
|
||
local jid_split = require "util.jid".split
|
||
local st = require "util.stanza"
|
||
|
||
local M = {}
|
||
|
||
-- ─── pure utility functions ────────────────────────────────────────
|
||
|
||
local function deserializeDataElement(pdElement, expectedName)
|
||
if (not pdElement) then return nil end
|
||
if (type(pdElement) ~= "table") then return nil end
|
||
if (pdElement[1] and pdElement["attr"] and pdElement["attr"]["key"] and (pdElement["attr"]["key"] == expectedName)) then
|
||
return json.decode(pdElement[1]);
|
||
end
|
||
return nil;
|
||
end
|
||
|
||
local function contains(list, x)
|
||
if (list) then
|
||
for _, v in pairs(list) do
|
||
if (v == x) then return true end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function starts_with(str, start)
|
||
return str:sub(1, #start) == start
|
||
end
|
||
|
||
local function trim(s)
|
||
if s and (type(s) == "string") then
|
||
return (s:gsub("^%s*(.-)%s*$", "%1"))
|
||
else
|
||
return s
|
||
end
|
||
end
|
||
|
||
local function collapseSpaces(s)
|
||
if s and (type(s) == "string") then
|
||
return (s:gsub("%s+", " "))
|
||
else
|
||
return s
|
||
end
|
||
end
|
||
|
||
local function dumpTable(t, depth)
|
||
local result = "";
|
||
if (not depth) then depth = 0 end;
|
||
local prefix = string.rep(" ", depth);
|
||
if (type(t) ~= "table") then
|
||
result = result.." "..tostring(t);
|
||
else
|
||
for key,value in pairs(t) do
|
||
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
|
||
end
|
||
end
|
||
return result;
|
||
end
|
||
|
||
local function prettyUsername(userName)
|
||
if not userName then
|
||
userName = "";
|
||
end
|
||
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1)
|
||
if string.find(userName, "%.") then
|
||
local i = 1
|
||
while true do
|
||
local j = string.find(userName, "%.", i)
|
||
if j == nil or #userName == i then break end
|
||
userName = string.sub(userName, 1, j-1).." "..string.upper(string.sub(userName, j+1, j+1))..string.sub(userName, j+2, -1)
|
||
if #userName <= j then break end
|
||
i = j
|
||
end
|
||
end
|
||
return trim(collapseSpaces(userName));
|
||
end
|
||
|
||
M.utils = {
|
||
deserializeDataElement = deserializeDataElement,
|
||
contains = contains,
|
||
starts_with = starts_with,
|
||
trim = trim,
|
||
collapseSpaces = collapseSpaces,
|
||
dumpTable = dumpTable,
|
||
prettyUsername = prettyUsername,
|
||
}
|
||
|
||
-- ─── factory ───────────────────────────────────────────────────────
|
||
|
||
function M.new(ctx)
|
||
-- ctx fields:
|
||
-- mod prosody module object (for logging)
|
||
-- stores { private=…, vcard=…, fcmtoken=…, fcmtoken_map=… }
|
||
-- (fields updated by caller after lazy store re-open)
|
||
-- domain string – domain for nto field and session lookup
|
||
-- is_muc boolean – affects postFCMIOS title fallback
|
||
-- ios_always_push boolean – iOS devices always get push even when online
|
||
-- global_mute_early_return boolean – buildTokensForUser returns {} on globalMute
|
||
-- override_topic_on_hidden boolean – override gTopic to "new message" when nType==1
|
||
-- allow_etype_or_readtarget boolean – fcm_notify condition includes etype/readTarget
|
||
-- domain_check_enabled boolean – skip fcm_notify when user host ≠ domain
|
||
-- fcm_api_key string
|
||
-- fcm_api_url string
|
||
-- module_is_active boolean
|
||
-- inactive_devices table (shared with calling module's CSI hooks)
|
||
|
||
local mod = ctx.mod
|
||
local stores = ctx.stores
|
||
|
||
local notify_cache = {}
|
||
local notify_cache_ttl = 300
|
||
local vcard_cache = {}
|
||
local vcard_cache_ttl = 600
|
||
|
||
-- ─── cache invalidation ──────────────────────────────────────────
|
||
|
||
local function invalidateNotifyCache(userName)
|
||
notify_cache[userName] = nil;
|
||
end
|
||
|
||
local function invalidateVcardCache(userName)
|
||
vcard_cache[userName] = nil;
|
||
end
|
||
|
||
local function invalidateNotifyCacheGlobal(userName)
|
||
invalidateNotifyCache(userName);
|
||
prosody.events.fire_event("vnc-fcm-invalidate-notify-cache", { username = userName });
|
||
end
|
||
|
||
local function invalidateVcardCacheGlobal(userName)
|
||
invalidateVcardCache(userName);
|
||
prosody.events.fire_event("vnc-fcm-invalidate-vcard-cache", { username = userName });
|
||
end
|
||
|
||
-- ─── display name ────────────────────────────────────────────────
|
||
|
||
local function getDisplayName(userName)
|
||
local vCard, err = stores.vcard:get(userName);
|
||
if vCard then
|
||
vCard = st.deserialize(vCard);
|
||
end
|
||
local name = nil;
|
||
if not vCard or err then
|
||
mod:log("error", "Unable to get vCard for %s : %s ", userName, tostring(err));
|
||
else
|
||
name = vCard:find("{vcard-temp}FN#");
|
||
end
|
||
if not name then name = prettyUsername(userName) or userName end
|
||
mod:log("debug", "name for %s : %s", userName, name)
|
||
return name;
|
||
end
|
||
|
||
local function getDisplayName_cached(userName)
|
||
local entry = vcard_cache[userName];
|
||
if entry and (os.time() - entry.ts) < vcard_cache_ttl then
|
||
return entry.name;
|
||
end
|
||
local name = getDisplayName(userName);
|
||
vcard_cache[userName] = { name = name, ts = os.time() };
|
||
return name;
|
||
end
|
||
|
||
-- ─── stale-token cleanup ─────────────────────────────────────────
|
||
|
||
local function removeKeyFromUser(userName, fcmId)
|
||
local pd, pfFetchError = stores.private:get(userName);
|
||
if (pfFetchError) then
|
||
mod:log("error", "Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError));
|
||
return 0,{};
|
||
elseif (pd) then
|
||
local removed = false;
|
||
for key,value in pairs(pd) do
|
||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||
for k2,v2 in pairs(value) do
|
||
local fcm = deserializeDataElement(v2, "fcm");
|
||
if (fcm) then
|
||
mod:log("debug", "removing fcmID %s from private data record: %s", fcmId, json.encode(fcm));
|
||
fcm[fcmId] = nil;
|
||
v2[1]=json.encode(fcm);
|
||
removed = true;
|
||
break;
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if (removed) then
|
||
local ok, errmsg = stores.private:set(userName, pd);
|
||
if (not ok) then
|
||
mod:log("error", "failed to store private data for %s : '%s'", userName, tostring(errmsg));
|
||
end
|
||
invalidateNotifyCacheGlobal(userName);
|
||
else
|
||
mod:log("debug", "unable to remove fcmID %s from %s. Not found in data: %s", fcmId, userName, tostring(pd));
|
||
end
|
||
end
|
||
end
|
||
|
||
-- ─── FCM HTTP callback ───────────────────────────────────────────
|
||
|
||
local function getPostCallback(_fcmId, _userName)
|
||
return function(body, code, response)
|
||
local shortKey = string.sub(_fcmId, 1, 6)
|
||
if code ~= 200 then
|
||
mod:log("error", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||
else
|
||
mod:log("debug", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
|
||
local response2 = json.decode(body)
|
||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "NotRegistered"
|
||
then
|
||
removeKeyFromUser(_userName, _fcmId);
|
||
end
|
||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "MissingRegistration"
|
||
then
|
||
removeKeyFromUser(_userName, _fcmId);
|
||
end
|
||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidRegistration"
|
||
then
|
||
removeKeyFromUser(_userName, _fcmId);
|
||
end
|
||
if response2.results and (type(response2.results) == "table") and response2.results[1] and response2.results[1]["error"] == "InvalidPackageName"
|
||
then
|
||
removeKeyFromUser(_userName, _fcmId);
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
end
|
||
|
||
-- ─── FCM HTTP send (Android / Web) ───────────────────────────────
|
||
|
||
local function postFCM(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom, _isMessageCorrection, _enableSoundOpt, _readTarget)
|
||
|
||
if (not ctx.module_is_active) then return end;
|
||
local domainname = ctx.domain;
|
||
local v_attachment = "";
|
||
if (_attachment_type ~= "") and (_attachment_type ~= nil) then
|
||
v_attachment = _attachment_type;
|
||
end
|
||
|
||
local lsource = _source;
|
||
local ltype = _type;
|
||
local signal = nil;
|
||
local nfrom = _source;
|
||
local nJitsiURL = "";
|
||
local nJitsiRoom = "";
|
||
local replaceMsgId = "";
|
||
|
||
if (_isMessageCorrection ~= nil) then
|
||
replaceMsgId = _isMessageCorrection;
|
||
end
|
||
|
||
if (_vncTalkConferenceId ~= nil) then
|
||
if string.match(_vncTalkConferenceId, "@") then
|
||
lsource = _vncTalkConferenceId;
|
||
end
|
||
end
|
||
|
||
if (_vncTalkConferenceEtype ~= nil) then
|
||
signal = "1";
|
||
ltype = _vncTalkConferenceEtype;
|
||
end
|
||
|
||
if (_readTarget ~= nil) then
|
||
lsource = _readTarget;
|
||
ltype = "read";
|
||
end
|
||
|
||
if (_nFromJid ~= nil) then
|
||
nfrom = _nFromJid;
|
||
end
|
||
|
||
if (_jitsiURL ~= nil) then
|
||
nJitsiURL = _jitsiURL;
|
||
end
|
||
|
||
if (_jitsiRoom ~= nil) then
|
||
nJitsiRoom = _jitsiRoom;
|
||
end
|
||
|
||
local stamp = os.time();
|
||
|
||
local nto_addr;
|
||
if string.match(_userName, "@") then
|
||
nto_addr = _userName;
|
||
else
|
||
nto_addr = _userName.."@"..domainname;
|
||
end
|
||
|
||
local fcmRequest = {
|
||
to = _fcmId,
|
||
enableSoundOpt = _enableSoundOpt,
|
||
content_available = true,
|
||
priority = "high",
|
||
data = {
|
||
nType = "local_notification",
|
||
eType = ltype,
|
||
jid = lsource,
|
||
conferenceId = _vncTalkConferenceId,
|
||
nfrom = nfrom,
|
||
nto = nto_addr,
|
||
name = _senderName,
|
||
gt = _groupTopic,
|
||
aft = v_attachment,
|
||
callSignal = signal,
|
||
jitsiURL = nJitsiURL,
|
||
jitsiRoom = nJitsiRoom,
|
||
t = stamp,
|
||
lang = _lang,
|
||
msgid = _msgid,
|
||
replaceid = replaceMsgId,
|
||
body = _body
|
||
}
|
||
};
|
||
|
||
fcmRequest["android"] = { priority = "high", ttl = 345600 };
|
||
fcmRequest["webpush"] = { headers = { Urgency = "high", TTL = 345600 } };
|
||
|
||
local data = json.encode(fcmRequest);
|
||
|
||
local httpRequestOptions = {
|
||
method = "POST",
|
||
body = data,
|
||
headers = {
|
||
["Connection"] = 'close',
|
||
["Authorization"] = "key="..ctx.fcm_api_key,
|
||
["Content-Type"] = "application/json; charset=utf-8"
|
||
}
|
||
}
|
||
|
||
mod:log("info", "sending notification to %s for token %s - from: %s - msgid: %s lang: %s", nto_addr, _fcmId, _source, _msgid, _lang);
|
||
http.request(ctx.fcm_api_url, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||
end
|
||
|
||
-- ─── FCM HTTP send (iOS) ─────────────────────────────────────────
|
||
|
||
local function postFCMIOS(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom, _isMessageCorrection, _enableSoundOpt)
|
||
|
||
if (not ctx.module_is_active) then return end;
|
||
local domainname = ctx.domain;
|
||
local ntitle = "";
|
||
local v_attachment = "";
|
||
local nJitsiURL = "";
|
||
local nJitsiRoom = "";
|
||
local replaceMsgId = "";
|
||
|
||
if (_isMessageCorrection ~= nil) then
|
||
replaceMsgId = _isMessageCorrection;
|
||
end
|
||
|
||
if (_groupTopic ~= "") and (_groupTopic ~= nil) then
|
||
ntitle = _groupTopic;
|
||
else
|
||
if ctx.is_muc then
|
||
ntitle = jid_split(_source);
|
||
else
|
||
if (_senderName ~= "") and (_senderName ~= nil) then
|
||
ntitle = _senderName;
|
||
else
|
||
ntitle = _source;
|
||
end
|
||
end
|
||
end
|
||
|
||
if (_attachment_type ~= "") and (_attachment_type ~= nil) then
|
||
v_attachment = _attachment_type;
|
||
end
|
||
|
||
local _click_action = _type:upper();
|
||
|
||
if (_click_action == "NORMAL") then
|
||
_click_action = "CHAT";
|
||
end
|
||
|
||
local lsource = _source;
|
||
local ltype = _type;
|
||
local signal = nil;
|
||
local nfrom = _source;
|
||
|
||
if (_vncTalkConferenceId ~= nil) then
|
||
if string.match(_vncTalkConferenceId, "@") then
|
||
lsource = _vncTalkConferenceId;
|
||
end
|
||
end
|
||
|
||
if (_vncTalkConferenceEtype ~= nil) then
|
||
signal = "1";
|
||
ltype = _vncTalkConferenceEtype;
|
||
end
|
||
|
||
if (_nFromJid ~= nil) then
|
||
nfrom = _nFromJid;
|
||
end
|
||
|
||
if (_jitsiURL ~= nil) then
|
||
nJitsiURL = _jitsiURL;
|
||
end
|
||
|
||
if (_jitsiRoom ~= nil) then
|
||
nJitsiRoom = _jitsiRoom;
|
||
end
|
||
|
||
local uname;
|
||
if string.match(_userName, "@") then
|
||
uname = _userName;
|
||
else
|
||
uname = _userName.."@"..domainname;
|
||
end
|
||
|
||
local fcmRequest = {
|
||
to = _fcmId,
|
||
priority = "high",
|
||
enableSoundOpt = _enableSoundOpt,
|
||
notification = {
|
||
title = ntitle,
|
||
body = _body,
|
||
mutable_content = true,
|
||
sound = "incoming-call-loop.caff",
|
||
thread_id = lsource,
|
||
click_action = _click_action
|
||
},
|
||
data = {
|
||
nType = "local_notification",
|
||
eType = ltype,
|
||
jid = lsource,
|
||
conferenceId = _vncTalkConferenceId,
|
||
nfrom = nfrom,
|
||
nto = uname,
|
||
name = _senderName,
|
||
gt = ntitle,
|
||
aft = v_attachment,
|
||
callSignal = signal,
|
||
jitsiURL = nJitsiURL,
|
||
jitsiRoom = nJitsiRoom,
|
||
lang = _lang,
|
||
msgid = _msgid,
|
||
replaceid = replaceMsgId,
|
||
body = _body
|
||
}
|
||
};
|
||
|
||
local data = json.encode(fcmRequest);
|
||
|
||
local httpRequestOptions = {
|
||
method = "POST",
|
||
body = data,
|
||
headers = {
|
||
["Connection"] = 'close',
|
||
["Authorization"] = "key="..ctx.fcm_api_key,
|
||
["Content-Type"] = "application/json; charset=utf-8"
|
||
}
|
||
}
|
||
|
||
mod:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", uname, _fcmId, _source, _msgid, _body.."...");
|
||
http.request(ctx.fcm_api_url, httpRequestOptions, getPostCallback(_fcmId, _userName))
|
||
end
|
||
|
||
-- ─── notify options & token building ─────────────────────────────
|
||
|
||
local function getNotifyOptionsForUser(userName)
|
||
local pd, pfFetchError = stores.private:get(userName);
|
||
if (pfFetchError) then
|
||
mod:log("error", "Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError));
|
||
return { nType = 0, nLang = nil, enableSound = nil, globalMute = false, legacy_tokens = {}, devices = {} };
|
||
end
|
||
local legacy_tokens;
|
||
local nType;
|
||
local nLang;
|
||
local globalMute = false;
|
||
local enableSound;
|
||
if (pd) then
|
||
for key,value in pairs(pd) do
|
||
mod:log("debug", "PD( %s) : %s => %s ", userName, tostring(key), tostring(value));
|
||
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
|
||
for k2,v2 in pairs(value) do
|
||
if (legacy_tokens == nil) then
|
||
local fcm = deserializeDataElement(v2, "fcm");
|
||
if (type(fcm) == "table") then
|
||
legacy_tokens = {};
|
||
for token,x in pairs(fcm) do
|
||
if (x["os"] ~= nil) then
|
||
table.insert(legacy_tokens, token.."@"..x["os"])
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if (not nType) then
|
||
local options = deserializeDataElement(v2, "notifyOptions");
|
||
if (options and (type(options) == "table") and options["type"]) then
|
||
nType = tonumber(options["type"]);
|
||
mod:log("info", "got nType: %s", nType);
|
||
end
|
||
end
|
||
if (not nType) then
|
||
local options = deserializeDataElement(v2, "notification");
|
||
if (options) then
|
||
nType = tonumber(options);
|
||
end
|
||
end
|
||
if (enableSound == nil) then
|
||
local enableSoundOpt = deserializeDataElement(v2, "enabledSound");
|
||
enableSound = enableSoundOpt;
|
||
end;
|
||
|
||
local pGlobalMute = deserializeDataElement(v2, "globalMute");
|
||
if (pGlobalMute) then
|
||
nType = 0;
|
||
globalMute = true;
|
||
legacy_tokens = {};
|
||
end
|
||
if (not nLang) then
|
||
local options = deserializeDataElement(v2, "lang");
|
||
mod:log("info", "PDLang: %s => %s", userName, json.encode(options));
|
||
if (options) and (type(options) == "string") then
|
||
nLang = options;
|
||
end;
|
||
end
|
||
end
|
||
break;
|
||
end
|
||
end
|
||
end
|
||
if (not nLang) then nLang = "en"; end
|
||
if (not nType) then nType = 2; end
|
||
if (legacy_tokens == nil) then legacy_tokens = {}; end
|
||
|
||
local devices = {};
|
||
local map_res = stores.fcmtoken:get(userName);
|
||
if (map_res) then
|
||
for _device, _tkr in pairs(map_res) do
|
||
table.insert(devices, { device = _device, token = _tkr.token, os = _tkr.os });
|
||
end
|
||
end
|
||
|
||
return {
|
||
nType = nType,
|
||
nLang = nLang,
|
||
enableSound = enableSound,
|
||
globalMute = globalMute,
|
||
legacy_tokens = legacy_tokens,
|
||
devices = devices,
|
||
};
|
||
end
|
||
|
||
local function buildTokensForUser(userName, data)
|
||
if ctx.global_mute_early_return and data.globalMute then
|
||
return {};
|
||
end
|
||
local tokens = {};
|
||
for _, t in ipairs(data.legacy_tokens) do
|
||
table.insert(tokens, t);
|
||
end
|
||
local sessions = prosody.bare_sessions;
|
||
local ubjid = userName.."@"..ctx.domain;
|
||
local u_sessions = sessions[ubjid] or nil;
|
||
for _, dev in ipairs(data.devices) do
|
||
local deviceonline = false;
|
||
if (u_sessions) then
|
||
local u_sessions_device = u_sessions["sessions"] or nil;
|
||
if (u_sessions_device) then
|
||
local dsession = u_sessions_device[dev.device] or nil;
|
||
if (dsession) then
|
||
local hibernated = dsession["hibernated"] or nil;
|
||
local hibernating = dsession["hibernating"] or nil;
|
||
local awaiting_ack = dsession["awaiting_ack"] or nil;
|
||
mod:log("info", "found session for _device %s - hibernating: %s, hibernated %s", dev.device, hibernating, hibernated);
|
||
if not ((hibernated) or (hibernating)) then
|
||
local dev_in_background = ctx.inactive_devices[dev.device] or nil;
|
||
if dev_in_background then
|
||
if awaiting_ack then
|
||
mod:log("debug", "device %s is still connected, but inactive and awaiting_ack", dev.device);
|
||
else
|
||
mod:log("debug", "device %s is still connected, but inactive", dev.device);
|
||
if not ctx.ios_always_push or (dev.os ~= "ios") then
|
||
deviceonline = true;
|
||
end
|
||
end
|
||
else
|
||
if awaiting_ack then
|
||
mod:log("debug", "device %s is connected and active, but awaiting_ack", dev.device);
|
||
else
|
||
mod:log("debug", "device %s is connected and active", dev.device);
|
||
if not ctx.ios_always_push or (dev.os ~= "ios") then
|
||
deviceonline = true;
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if not(deviceonline) then
|
||
local ts = dev.token.."@"..dev.os;
|
||
mod:log("debug","already_added: %s", ts);
|
||
if not contains(tokens, ts) then
|
||
table.insert(tokens, ts);
|
||
end
|
||
end
|
||
end
|
||
return tokens;
|
||
end
|
||
|
||
local function getNotifyOptionsForUser_cached(userName)
|
||
local entry = notify_cache[userName];
|
||
local data;
|
||
if entry and (os.time() - entry.ts) < notify_cache_ttl then
|
||
data = entry;
|
||
else
|
||
data = getNotifyOptionsForUser(userName);
|
||
data.ts = os.time();
|
||
notify_cache[userName] = data;
|
||
end
|
||
local tokens = buildTokensForUser(userName, data);
|
||
return data.nType, tokens, data.nLang, data.enableSound;
|
||
end
|
||
|
||
-- ─── fcm_notify ──────────────────────────────────────────────────
|
||
|
||
local function fcm_notify(userName, title, body, type, _senderName, _groupTopic, vnc_attachment_type, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection, _readTarget)
|
||
if (not ctx.module_is_active) then return end;
|
||
local uname, uhost = jid_split(userName);
|
||
if ctx.domain_check_enabled and uhost ~= ctx.domain then return end
|
||
local nType, tokens, lang, soundOpt = getNotifyOptionsForUser_cached(uname);
|
||
|
||
local should_notify = (nType == 1) or (nType == 2);
|
||
if not should_notify and ctx.allow_etype_or_readtarget then
|
||
should_notify = (vncTalkConferenceEtype ~= nil) or (_readTarget ~= nil);
|
||
end
|
||
|
||
if (tokens and (#tokens) > 0) and should_notify
|
||
then
|
||
mod:log("debug", "in fcm_notify(%s, %s, %s) => type=%s, %s tokens", userName, title, body, tostring(nType), tostring((#tokens)));
|
||
local sName = _senderName;
|
||
local gTopic = _groupTopic;
|
||
if (nType == 1) then
|
||
sName = "new message";
|
||
if ctx.override_topic_on_hidden then
|
||
gTopic = "new message";
|
||
end
|
||
body = "";
|
||
end
|
||
if (_isMessageCorrection ~= nil) then
|
||
body = " ";
|
||
end;
|
||
for i,token in pairs(tokens) do
|
||
if (token) then
|
||
local rtoken, ros = jid_split(token);
|
||
if (ros == "ios") then
|
||
if (type ~= "read") then
|
||
postFCMIOS(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection, soundOpt);
|
||
end
|
||
else
|
||
postFCM(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom, _jitsiURL, _jitsiRoom, _isMessageCorrection, soundOpt, _readTarget);
|
||
end
|
||
end
|
||
end
|
||
else
|
||
mod:log("debug", "fcm_notify(%s, %s, %s) => type=%s, no tokens", userName, title, body, tostring(nType));
|
||
end
|
||
end
|
||
|
||
-- ─── public interface ────────────────────────────────────────────
|
||
|
||
return {
|
||
notify_cache = notify_cache,
|
||
vcard_cache = vcard_cache,
|
||
invalidateNotifyCache = invalidateNotifyCache,
|
||
invalidateVcardCache = invalidateVcardCache,
|
||
invalidateNotifyCacheGlobal = invalidateNotifyCacheGlobal,
|
||
invalidateVcardCacheGlobal = invalidateVcardCacheGlobal,
|
||
getDisplayName = getDisplayName,
|
||
getDisplayName_cached = getDisplayName_cached,
|
||
getNotifyOptionsForUser_cached = getNotifyOptionsForUser_cached,
|
||
fcm_notify = fcm_notify,
|
||
postFCM = postFCM,
|
||
postFCMIOS = postFCMIOS,
|
||
removeKeyFromUser = removeKeyFromUser,
|
||
}
|
||
end
|
||
|
||
return M
|