Files
vnctalk-prosody/vnctalk/mod_vnc_muc_fcm_hin.lua
T

655 lines
24 KiB
Lua

-- mod_muc_notifications
--
--
-- This file is MIT/X11 licensed.
--
-- A module to notify non-present members of messages in a group chat
--
local http = require "net.http";
local json = require "util.json";
-- local ltn12 = require "ltn12"
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local timer = require "util.timer";
local id = require"util.id"
local st = require"util.stanza"
local storage_host = module:get_option_string("storage_host");
module:log("info","founs storage_host: %s", storage_host);
local fcmAPIKey = module:get_option_string("fcm_api_key");
local fcmAPIURL = module:get_option_string("fcm_api_url");
local moduleIsActive = fcmAPIURL and fcmAPIKey;
local inactive_devices = {};
local ThisDomain = module.host;
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local fcm_token_store = module:open_store("fcmtoken");
local fcm_token_map_store = module:open_store("fcmtoken", "map");
local vcard_storage = module:open_store("vcard");
local private_storage = module:open_store("private");
timer.add_task(3, function ()
private_storage = module:open_host_store(storage_host, "private");
vcard_storage = module:open_host_store(storage_host, "vcard");
fcm_token_store = module:open_host_store(storage_host, "fcmtoken");
fcm_token_map_store = module:open_host_store(storage_host, "fcmtoken", "map");
if not private_storage or not vcard_storage then
module:log("info", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
return 3;
else
module:log("info", "private/vcard_store are now %s/%s", private_storage, vcard_storage);
end
end
);
-- some helper functions
local function deserializeDataElement(pdElement, expectedName)
--module:log("debug", "deserializeDataElement("..expectedName..", "..dumpTable(pdElement)..")");
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
local result = json.decode(pdElement[1]);
--module:log("debug", "deserializeDataElement() = '"..dumpTable(result).."'");
return result;
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)
userName = string.upper(string.sub(userName, 1, 1))..string.sub(userName, 2, -1) -- upcase first char in any case
if string.find(userName, "%.") -- contains dot
then
local i = 1
while true do
local j = string.find(userName, "%.", i)
if j == nil or #userName == i then break end
-- replace dot with space and upcase next char
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
local function getDisplayName(userName)
-- module:log("info", "getDisplayName user: %s", userName);
local vCard, err = vcard_storage:get(userName);
if vCard then
vCard = st.deserialize(vCard);
end
local name = nil;
if not vCard or err then
module:log("debug", "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
-- module:log("info", "name for %s : %s", userName, name)
return name;
end
local function getPostCallback(_fcmId, _userName)
return function(body, code, response)
local shortKey = string.sub(_fcmId, 1, 6)
if code ~= 200 then
module:log("info", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
-- if body then module:log("error", body); end
else
module:log("debug", "FCM result %s@%s : HTTP %s %s", _userName, shortKey, tostring(code), tostring(body))
end
return false -- close request
--return true -- keep request open
end
end
local function postFCM(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
if (not moduleIsActive) then return end;
local domainname = "";
local v_attachment = "";
domainname = module:get_option_string("storage_host");
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 = "";
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 stamp = os.time();
module:log("info", "postFCM username %s", _userName);
local uname = "";
if string.match(_userName, "@") then
uname = _userName;
else
uname = _userName.."@"..domainname;
end
local fcmRequest = {
to = _fcmId,
content_available = true,
priority = "high",
data = {
nType = "local_notification", -- notification type
eType = ltype, -- event type
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
nfrom = nfrom,
nto = uname,
name = _senderName, -- group chat nick name or single chat peer vCard name",
gt = _groupTopic, -- topic of room iff groupchat
aft = v_attachment,
callSignal = signal,
jitsiURL = nJitsiURL,
jitsiRoom = nJitsiRoom,
t = stamp,
lang = _lang,
msgid = _msgid,
body = _body -- notification body/text content
}
};
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="..fcmAPIKey,
["Content-Type"] = "application/json; charset=utf-8"
}
}
-- module:log("info", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body);
-- ssl does not seem to work with net.http, so we proxy it
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
end
local function postFCMIOS(_userName, _fcmId, _source, _body, _type, _senderName, _groupTopic, _attachment_type, _lang, _msgid, _vncTalkConferenceEtype, _vncTalkConferenceId, _nFromJid, _jitsiURL, _jitsiRoom)
if (not moduleIsActive) then return end;
local domainname = "";
local ntitle = "";
local nbody = "";
local v_attachment = "";
local nJitsiURL = "";
local nJitsiRoom = "";
domainname = module:get_option_string("storage_host");
if (_groupTopic ~= "") and (_groupTopic ~= nil) then
ntitle = _groupTopic;
else
ntitle = jid_split(_source);
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 fcmRequest = {
to = _fcmId,
priority = "high",
notification = {
title = ntitle,
body = _body,
mutable_content = true,
sound = "incoming-call-loop.caff",
thread_id = lsource,
click_action = _click_action
},
data = {
nType = "local_notification", -- notification type
eType = ltype, -- event type
jid = lsource, -- source jid (room jid or 1:1 chat peer bare jid)
nfrom = nfrom,
nto = _userName.."@"..domainname,
name = _senderName, -- group chat nick name or single chat peer vCard name",
gt = ntitle, -- topic of room iff groupchat
aft = v_attachment,
callSignal = signal,
jitsiURL = nJitsiURL,
jitsiRoom = nJitsiRoom,
lang = _lang,
msgid = _msgid,
body = _body -- notification body/text content
}
};
local data = json.encode(fcmRequest);
local httpRequestOptions = {
method = "POST",
body = data,
headers = {
["Connection"] = 'close',
["Authorization"] = "key="..fcmAPIKey,
["Content-Type"] = "application/json; charset=utf-8"
}
}
-- module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, string.sub(_body, 20).."...");
module:log("debug", "sending notification to %s for token %s - from: %s - msgid: %s - body: %s", _userName.."@"..domainname, _fcmId, _source, _msgid, _body.."...");
-- ssl does not seem to work with net.http, so we proxy it
--http.request("https://fcm.googleapis.com/fcm/send", httpRequestOptions, postCallback)
module:log("debug", "calling http.request -- fcmAPIURL: %s", fcmAPIURL);
http.request(fcmAPIURL, httpRequestOptions, getPostCallback(_fcmId, _userName))
end
-- Given a stanza, compute if it qualifies as important (notifiable)
-- return true for message stanzas with non-empty body
-- Should probably use something similar to muc-message-is-historic event
local function is_important(stanza)
local body = stanza:find("body#")
return body and #body
end
-- get notify options for local users
local function getNotifyOptionsForUser(userName)
--module:log("debug", "getFCMtokenForUser("..userName..")");
local pd, pfFetchError = private_storage:get(userName);
if (pfFetchError) then
module:log(
"error",
"Fetching private data for '%s' failed : %s", tostring(userName), tostring(pfFetchError)
);
return 0,{};
elseif (pd) then
local tokens;
local nType;
local nLang;
for key,value in pairs(pd)
do
-- module:log("info", "PD("..userName..") : "..tostring(key).." => "..tostring(value));
module:log("debug", "PD( %s) : %s => %s ", userName,tostring(key),tostring(value));
if ((key == "documents:stanza:io:json") and (type(value) == "table")) then
--module:log("debug", "PD("..userName..") : "..json.encode(value));
-- module:log("debug", "PD( %s ) : %s ", userName ,json.encode(value));
for k2,v2 in pairs(value) do
if (not tokens) then
local fcm = deserializeDataElement(v2, "fcm");
if (type(fcm) == "table") then
tokens = {};
for token,x in pairs(fcm) do
-- module:log("debug", "token: %s - x[os]: %s", token, x["os"]);
if (x["os"] ~= nil) then
table.insert(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"]);
end
end
if (not nLang) then
local options = deserializeDataElement(v2, "lang");
-- module:log("debug", "PDLang: %s => %s", userName, json.encode(options));
if (options) and (type(options) == "string") then
nLang = options;
end;
-- module:log("debug", "type of options: %s", type(options));
end
end
break; -- we can stop after the documents:stanza:io:json element
end
end
--module:log("debug", "getNotifyOptionsForUser("..userName..") = "..tostring(tokens).." => "..tostring(nType));
local map_res = fcm_token_store:get(userName);
-- module:log("debug", "result from map store for %s is: %s", userName, dumpTable(map_res));
if (map_res) then
for _device, _tkr in pairs(map_res) do
-- module:log("debug", "got for %s device %s => %s", userName, _device, dumpTable(_tkr));
-- module:log("debug", "got for %s device %s => token: %s - os: %s", userName, _device, _tkr.token, _tkr.os);
local sessions = prosody.bare_sessions;
local ubjid = userName.."@"..storage_host;
local u_sessions = sessions[ubjid] or nil;
local deviceonline = false;
if (u_sessions) then
local u_sessions_device = u_sessions["sessions"] or nil;
if (u_sessions_device) then
-- module:log("info", "devise is %s", _device);
-- module:log("debug", "u_sessions_device %s", dumpTable(u_sessions_device));
local dsession = u_sessions_device[_device] or nil;
-- module:log("debug", "dsession %s", dumpTable(dsession));
if (dsession) then
local hibernated = dsession["hibernated"] or nil;
local hibernating = dsession["hibernating"] or nil;
local awaiting_ack = dsession["awaiting_ack"] or nil;
module:log("info", "found session for _device %s - hibernating: %s, hibernated %s", _device, hibernating, hibernated);
if not ((hibernated) or (hibernating)) then
local dev_in_background = inactive_devices[_device] or nil;
if dev_in_background then
if awaiting_ack then
module:log("debug", "device %s is still connected, but inactive and awaiting_ack", _device);
-- module:log("info", "handling1: %s", _tkr.token.."@".._tkr.os);
else
module:log("debug", "device %s is still connected, but inactive", _device);
-- module:log("info", "handling2: %s", _tkr.token.."@".._tkr.os);
if (_tkr.os ~= "ios") then
deviceonline = true;
end
end
else
if awaiting_ack then
module:log("debug", "device %s is connected and active, but awaiting_ack", _device);
-- module:log("info", "handling3: %s", _tkr.token.."@".._tkr.os);
else
module:log("debug", "device %s is connected and active", _device);
-- module:log("info", "handling4: %s", _tkr.token.."@".._tkr.os);
if (_tkr.os ~= "ios") then
deviceonline = true;
end
end
end
end
end
end
end
if not(deviceonline) then
local already_added = contains(tokens, _tkr.token.."@".._tkr.os);
module:log("debug","already_added: %s", _tkr.token.."@".._tkr.os);
if not(tokens) then
tokens = {};
end
if not(already_added) then
table.insert(tokens, _tkr.token.."@".._tkr.os);
end
end
-- module:log("debug","tokens table is now: %s", dumpTable(tokens));
end
end
if (not tokens) then tokens = {}; end;
if (not nType) then nType = 0; end;
local sessions = prosody.bare_sessions;
local ubjid = userName.."@"..ThisDomain;
for _user, _resdetails in pairs(sessions) do
if (_user == ubjid) then
for _res, _detail1 in pairs(_resdetails["sessions"]) do
local hibernated = _detail1["hibernated"] or _detail1["hibernating"] or nil;
if (hibernated) then
module:log("info", "for _user: %s and _res: %s got hibernated since: %s", _user, _res, hibernated);
else
module:log("info", "for _user: %s and _res: %s got active session?", _user, _res);
--module:log("debug", "for _user: %s and _res: %s got detail: %s", _user, _res, dumpTable(_detail1));
end
end
-- module:log("debug", "session for _res %s is: %s", _res, dumpTable(_resdetails));
end
end
-- module:log("debug", "sessions: %s", dumpTable(sessions[ubjid]));
return nType, tokens, nLang;
end
end
local function fcm_notify(userName, title, body, type, _senderName, _groupTopic, vnc_attachment_type, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom)
if (not moduleIsActive) then module:log("info", "module not active - bailung out"); return end;
module:log("info", "getting notify opts for %s", userName);
local uname, uhost = jid_split(userName);
if (uhost ~= storage_host) then return end;
local nType, tokens, lang = getNotifyOptionsForUser(uname);
if (tokens and (#tokens) > 0) and ((nType == 1) or (nType == 2))
then
-- module:log(
-- "info", "in fcm_notify(%s, %s, %s) => type=%s, %s tokens",
-- userName, title, body, tostring(nType), tostring((#tokens))
--);
local sName = _senderName;
local gTopic = _groupTopic;
sName = "Neue Nachricht";
gTopic = "Neue Nachricht";
if (lang == "en") then
sName = "new message";
gTopic = "new message";
end;
if (lang == "fr") then
sName = "Nouveau message";
gTopic = "Nouveau message";
end;
body = "";
for i,token in pairs(tokens) do
if (token)
then
local rtoken, ros = jid_split(token);
-- module:log("debug","got rtoken: %s - os: %s ", rtoken, ros);
if (ros == "ios") then
-- module:log("debug", "postFCMIOS with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang);
postFCMIOS(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom);
else
-- module:log("info", "postFCM with (%s, %s, %s, %s, %s, %s, %s, %s, %s)", userName, rtoken, title, body, type, _senderName, _groupTopic, vnc_attachment_type, lang, _nFrom);
postFCM(userName, rtoken, title, body, type, sName, gTopic, vnc_attachment_type, lang, msgid, vncTalkConferenceEtype, vncTalkConferenceId, _nFrom);
end
end
end
else
module:log("debug", "fcm_notify(%s, %s, %s) => type=%s, no tokens", userName, title, body, tostring(nType));
end
end
local function handle_muc_message(event)
-- event.room and event.stanza are available
local room = event.room
local stanza = event.stanza
-- module:log("info", "handling stanza %s", stanza);
-- module:log("info", "room: %s", dumpTable(room));
local message_id = event.stanza.attr.id;
-- module:log("info","message id: %s", 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;
-- module:log("info", "handling message from=%s, to=%s", stanza.attr.from, to);
local content = trim(stanza:find("body#"));
local delayedDelivery = stanza:get_child("delay", "urn:xmpp:delay");
-- if we have a vncTalk incoming or whiteboard element, it overrides the type
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
-- module:log("message is to broadcast - bailing out");
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");
-- module:log("debug", "broadcast body: %s", content);
-- module:log("debug", "jid_split(to): %s", jid_split(to));
-- fcm_notify(jid_split(to), vnctalk_broadcast_target, content, type, getDisplayName(jid_split(from)), "Broadcast: "..vnctalk_broadcast_title, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId);
return nil;
end
local auxType = vncTalkIncomingType or (vncTalkWhiteboard and "whiteboard");
if (auxType)
then
content = auxType;
type = auxType;
end;
local doNotify = content and (content ~= " ") and (content ~= "") and (delayedDelivery == nil) and (isSentCarbonMessage == nil) and (
(type == "chat") or (type == "groupchat") or (type == "audio") or (type == "video") or (type == "whiteboard") or (type == "screen")
);
if (doNotify) then
local sender = "";
for _, occupant in pairs(room._occupants) do
if (stanza.attr.from == occupant.nick) then sender=occupant.bare_jid; end;
end
-- module:log("info","sender is %s", sender);
local roomTopic = room._data.subject or nil;
for jid, aff in pairs(room._affiliations) do
if aff ~= "outcast" then
-- module:log("info", "found jid: %s as aff: %s", jid, aff);
local roomJid = to;
if (sender ~= jid) then
module:log("info", "from is: %s", from);
fcm_notify(jid, roomJid, content, type, getDisplayName(jid_split(sender)), roomTopic, vnc_attachment_type, message_id, vncTalkConferenceEtype, vncTalkConferenceId, from, jitsiURL, jitsiRoom);
end
end
end
end
end
module:hook("muc-broadcast-message", handle_muc_message)
module:hook("host-activated", function(host)
module:log("info", "got activated event for host %s", host);
end);
module:log("debug", "Module loaded")