mirror of https://github.com/sipwise/prosody.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
16 KiB
565 lines
16 KiB
--
|
|
-- Copyright (C) 2014-2015 Sipwise GmbH <development@sipwise.com>
|
|
--
|
|
-- This project is MIT/X11 licensed. Please see the
|
|
-- COPYING file in the source package for more information.
|
|
--
|
|
|
|
module:depends("sipwise_vcard_cusax");
|
|
module:depends("sipwise_pushd_blocking");
|
|
|
|
local datamanager = require "util.datamanager";
|
|
local sql = require "util.sql";
|
|
local format = string.format;
|
|
local jid_split = require "util.jid".split;
|
|
local jid_bare = require "util.jid".bare;
|
|
local jid_host = require "util.jid".host;
|
|
local jid_resource = require "util.jid".resource;
|
|
local hosts = prosody.hosts;
|
|
local http = require "net.http";
|
|
local uuid = require "util.uuid";
|
|
local ut = require "ngcp.utils";
|
|
local set = require "util.set";
|
|
local st = require "util.stanza";
|
|
|
|
local sipwise_offline = module:shared("sipwise_offline/sipwise_offline");
|
|
local pushd_blocking = module:shared("sipwise_pushd_blocking/pushd_blocking");
|
|
|
|
local muc_config = {
|
|
force_persistent = true,
|
|
owner_on_join = true,
|
|
exclude = {}
|
|
};
|
|
local pushd_config = {
|
|
url = "https://127.0.0.1:8080/push",
|
|
fcm = true,
|
|
apns = true,
|
|
call_sound = 'incoming_call.caf',
|
|
msg_sound = 'incoming_message.caf',
|
|
muc_config = muc_config
|
|
};
|
|
|
|
local push_usr_query = [[
|
|
SELECT vp.attribute, vup.value FROM provisioning.voip_preferences vp
|
|
LEFT JOIN provisioning.voip_usr_preferences vup ON vup.attribute_id = vp.id
|
|
LEFT JOIN provisioning.voip_subscribers vs ON vs.id = vup.subscriber_id
|
|
LEFT JOIN provisioning.voip_domains vd ON vd.id = vs.domain_id
|
|
WHERE vp.attribute = 'mobile_push_enable'
|
|
AND vs.username = ?
|
|
AND vd.domain = ?;
|
|
]];
|
|
|
|
local push_dom_query = [[
|
|
SELECT vp.attribute, vup.value FROM provisioning.voip_preferences vp
|
|
LEFT JOIN provisioning.voip_dom_preferences vup ON vup.attribute_id = vp.id
|
|
LEFT JOIN provisioning.voip_domains vd ON vd.id = vup.domain_id
|
|
WHERE vp.attribute = 'mobile_push_enable'
|
|
AND vd.domain = ?;
|
|
]];
|
|
|
|
local push_silent_query = [[
|
|
SELECT vp.id FROM provisioning.voip_preferences vp
|
|
LEFT JOIN provisioning.voip_usr_preferences vup ON vup.attribute_id = vp.id
|
|
LEFT JOIN provisioning.voip_subscribers vs ON vs.id = vup.subscriber_id
|
|
LEFT JOIN provisioning.voip_domains vd ON vd.id = vs.domain_id
|
|
WHERE vp.attribute = 'mobile_push_silent_list'
|
|
AND vs.username = ?
|
|
AND vd.domain = ?
|
|
AND vup.value = ?;
|
|
]];
|
|
|
|
local default_params = module:get_option("sql");
|
|
local engine;
|
|
|
|
-- Reconnect to DB if necessary
|
|
local function reconect_check()
|
|
if not engine.conn:ping() then
|
|
engine.conn = nil;
|
|
engine:connect();
|
|
end
|
|
end
|
|
|
|
-- luacheck: ignore request
|
|
local function process_response(response, code, request)
|
|
if code >= 200 and code < 299 then
|
|
module:log("debug", "pushd response OK[%s] %s",
|
|
tostring(code), tostring(response));
|
|
else
|
|
module:log("error", "pushd response KO[%s] %s",
|
|
tostring(code), tostring(response));
|
|
end
|
|
end
|
|
|
|
local function push_silent(username, domain, other)
|
|
reconect_check();
|
|
for row in engine:select(push_silent_query, username, domain, other) do
|
|
module:log("debug", "silent push preference mobile_push_silent_list matches");
|
|
return true;
|
|
end
|
|
return false;
|
|
end
|
|
|
|
local function push_enable(username, domain)
|
|
reconect_check();
|
|
local flag
|
|
|
|
for row in engine:select(push_usr_query, username, domain) do
|
|
if row[2] == "never" then
|
|
module:log("debug", "usr mobile_push_enable pref set to 'never'");
|
|
return false;
|
|
end
|
|
module:log("debug", "usr pref %s", ut.table.tostring(row))
|
|
flag = true
|
|
end
|
|
if flag then return true end
|
|
for row in engine:select(push_dom_query, domain) do
|
|
if row[2] == "never" then
|
|
module:log("debug", "domain mobile_push_enable pref set to 'never'");
|
|
return false;
|
|
end
|
|
module:log("debug", "dom pref %s", ut.table.tostring(row))
|
|
end
|
|
return true;
|
|
end
|
|
|
|
local function is_local_domain(dom)
|
|
return ut.table.contains(ut.table.keys(hosts), dom);
|
|
end
|
|
|
|
local function get_caller_info(jid, caller_defaults)
|
|
if not jid then
|
|
return nil;
|
|
end
|
|
local node, host = jid_split(jid);
|
|
if not is_local_domain(host) then
|
|
module:log("debug", "caller[%s] not local", jid);
|
|
return nil;
|
|
end
|
|
local vcard = module:shared(format("/%s/sipwise_vcard_cusax/vcard", host));
|
|
if vcard and vcard.get_subscriber_info then
|
|
local info = vcard.get_subscriber_info(node, host);
|
|
module:log("debug", "caller_info of %s", jid);
|
|
if not info.display_name then
|
|
module:log("debug", "set display_name to %s", node);
|
|
info.display_name = node;
|
|
end
|
|
if not info.aliases then
|
|
info.aliases = caller_defaults.aliases;
|
|
end
|
|
return info;
|
|
end
|
|
end
|
|
|
|
local function get_members(muc_room)
|
|
local res = set.new();
|
|
if muc_room then
|
|
for o_jid, _, _ in muc_room:each_affiliation() do
|
|
res:add(o_jid);
|
|
end
|
|
end
|
|
return res;
|
|
end
|
|
|
|
local function get_occupants(muc_room)
|
|
local res = set.new();
|
|
for _, occupant in muc_room:each_occupant() do
|
|
res:add(jid_bare(occupant.jid));
|
|
end
|
|
return res;
|
|
end
|
|
|
|
local function get_muc_room(room_jid)
|
|
local host = jid_host(room_jid);
|
|
|
|
if not hosts[host] then
|
|
module:log("warn", "host [%s] not defined", tostring(host));
|
|
return nil
|
|
end
|
|
if not hosts[host].modules.muc then
|
|
module:log("warn", "muc not enabled here [%s]", tostring(host));
|
|
return nil
|
|
end
|
|
return hosts[host].modules.muc.get_room_from_jid(jid_bare(room_jid));
|
|
end
|
|
|
|
local function get_muc_caller(room_jid)
|
|
local nick = jid_resource(room_jid);
|
|
|
|
local room = get_muc_room(room_jid);
|
|
if not room then
|
|
module:log("warn", "room %s not here", jid_bare(room_jid));
|
|
return nil;
|
|
end
|
|
return room:get_occupant_by_nick(nick);
|
|
end
|
|
|
|
local function get_callee_badge(jid)
|
|
local node, host = jid_split(jid);
|
|
local result = 0;
|
|
if sipwise_offline.get_num then
|
|
result = sipwise_offline.get_num(node, host);
|
|
else
|
|
local offline = datamanager.list_load(node, host, "offline");
|
|
if offline then
|
|
result = #offline;
|
|
end
|
|
end
|
|
-- plus the one in process
|
|
result = result + 1;
|
|
module:log("debug", "%d offline messages for %s", result, jid);
|
|
return result;
|
|
end
|
|
|
|
local function is_invite(stanza)
|
|
if stanza:get_child('x', 'http://jabber.org/protocol/muc#user') then
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function is_attachment(stanza)
|
|
if stanza:get_child('x', 'jabber:x:oob') then
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function get_message(stanza)
|
|
local body = stanza:get_child('body');
|
|
if body then
|
|
return body:get_text();
|
|
end
|
|
end
|
|
|
|
local function get_muc_info(stanza, caller_info)
|
|
local from = stanza.attr.from;
|
|
local muc_stanza = stanza:get_child('x', 'jabber:x:conference');
|
|
local muc = {};
|
|
local room;
|
|
|
|
if muc_stanza then
|
|
muc.jid = muc_stanza.attr.jid;
|
|
else
|
|
muc.jid = jid_bare(from);
|
|
end
|
|
muc.name, muc.domain = jid_split(muc.jid);
|
|
if not is_local_domain(muc.domain) then
|
|
module:log("debug", "not from local host[%s]", tostring(muc.domain));
|
|
return nil;
|
|
end
|
|
local room_host = hosts[muc.domain].modules.muc;
|
|
if not room_host then
|
|
module:log("debug", "not from MUC host[%s]", muc.domain);
|
|
return nil;
|
|
end
|
|
room = room_host.get_room_from_jid(muc.jid);
|
|
if room then
|
|
muc.room = room:get_description() or muc.name;
|
|
if muc_stanza then
|
|
muc.invite = format("Group chat invitation to '%s' from %s",
|
|
muc.room, caller_info.display_name);
|
|
end
|
|
return muc;
|
|
end
|
|
end
|
|
|
|
local function is_pushd_blocked(from, to)
|
|
local node_to, host_to = jid_split(to);
|
|
local blocked_list = pushd_blocking.get_blocked_jids(node_to, host_to);
|
|
|
|
module:log("debug", "pushd_blockedlist: %s for [%s]", ut.table.tostring(blocked_list), to);
|
|
|
|
local _, host_from, resource_from = jid_split(from);
|
|
if ut.table.contains(blocked_list, from)
|
|
or ut.table.contains(blocked_list, jid_bare(from))
|
|
or ut.table.contains(blocked_list, host_from) then
|
|
return true;
|
|
end
|
|
if resource_from and ut.table.contains(blocked_list, host_from.."/"..resource_from) then
|
|
return true;
|
|
end
|
|
return false;
|
|
end
|
|
|
|
local function handle_offline(event)
|
|
module:log("debug", "handle_offline");
|
|
local origin, stanza = event.origin, event.stanza;
|
|
local to = stanza.attr.to;
|
|
local from = stanza.attr.from;
|
|
local node, host;
|
|
local caller = { username = 'unknow', host = 'unknown.local' };
|
|
local caller_defaults = {display_name = '', aliases = {''}};
|
|
local caller_info;
|
|
-- defaults to "application/x-www-form-urlencoded"
|
|
local http_options = {
|
|
method = "POST",
|
|
body = "",
|
|
}
|
|
local message;
|
|
|
|
local function build_push_common_query(msg, muc)
|
|
if muc then
|
|
msg.data_room_jid = muc['jid'];
|
|
msg.data_room_description = muc['room'];
|
|
if msg.data_type == 'invite' then
|
|
msg.data_message = muc['invite'];
|
|
end
|
|
end
|
|
msg.data_sender_number = tostring(caller_info.aliases[1]);
|
|
msg.data_sender_name = tostring(caller_info.display_name);
|
|
return msg;
|
|
end
|
|
local function build_push_apns_query(msg, muc, silent)
|
|
if silent then
|
|
msg['apns_content-available'] = '1';
|
|
else
|
|
if msg.data_type ~= 'invite' then
|
|
msg.apns_alert = string.format("Message from %s:\n",
|
|
caller_info.display_name) .. msg.data_message;
|
|
else
|
|
msg.apns_alert = muc['invite'];
|
|
end
|
|
msg.apns_sound = pushd_config.msg_sound or '';
|
|
msg.apns_badge = tostring(get_callee_badge(to));
|
|
end
|
|
return msg;
|
|
end
|
|
local function build_push_query(msg)
|
|
local muc;
|
|
local caller_jid;
|
|
local silent;
|
|
if stanza.attr.type == 'groupchat' then
|
|
msg.data_type = 'groupchat'
|
|
caller_jid = get_muc_caller(stanza.attr.from);
|
|
module:log("debug", "from:%s -> caller_jid:%s",
|
|
tostring(stanza.attr.from), tostring(caller_jid));
|
|
caller_info = get_caller_info(caller_jid, caller_defaults) or
|
|
caller_defaults;
|
|
muc = get_muc_info(stanza, caller_info);
|
|
silent = push_silent(msg.callee, msg.domain, muc['jid']);
|
|
else
|
|
msg.data_type = 'message';
|
|
caller_jid = format("%s@%s",
|
|
origin.username or caller.username,
|
|
origin.host or caller.host);
|
|
if is_invite(stanza) then
|
|
msg.data_type = 'invite';
|
|
caller_jid = jid_bare(stanza:get_child('invite').attr.from) or
|
|
caller_jid;
|
|
elseif is_attachment(stanza) then
|
|
msg.data_message = "new attachment";
|
|
end
|
|
caller_info = get_caller_info(caller_jid, caller_defaults) or
|
|
caller_defaults;
|
|
muc = get_muc_info(stanza, caller_info);
|
|
silent = push_silent(msg.callee, msg.domain, caller_jid);
|
|
end
|
|
if silent then
|
|
msg.data_silent = '1';
|
|
else
|
|
msg.data_silent = '0';
|
|
end
|
|
msg.data_sender_jid = caller_jid;
|
|
msg.data_sender_sip = jid_bare(caller_jid);
|
|
msg.push_id = uuid.generate();
|
|
msg = build_push_common_query(msg, muc);
|
|
if pushd_config.apns then
|
|
msg = build_push_apns_query(msg, muc, silent);
|
|
end
|
|
return msg;
|
|
end
|
|
|
|
module:log("debug", "stanza[%s]", tostring(stanza));
|
|
|
|
if from then
|
|
caller.username, caller.host = jid_split(from);
|
|
end
|
|
|
|
if to then
|
|
node, host = jid_split(to);
|
|
if from and is_pushd_blocked(from, to) then
|
|
module:log("debug", "skip pushd message from blocked jid [%s]", from);
|
|
return nil;
|
|
end
|
|
message = {
|
|
data_message = get_message(stanza),
|
|
data_full_message = tostring(stanza),
|
|
callee = node,
|
|
domain = host,
|
|
};
|
|
if message.data_message then
|
|
module:log("debug", "message OK");
|
|
if push_enable(node, host) then
|
|
message = build_push_query(message);
|
|
module:log("debug", "message: %s", ut.table.tostring(message));
|
|
http_options.body = http.formencode(message);
|
|
if http_options.body then
|
|
module:log("debug", "Sending http pushd request: %s data: %s",
|
|
pushd_config.url, http_options.body);
|
|
http.request(pushd_config.url, http_options, process_response);
|
|
end
|
|
else
|
|
module:log("debug", "no mobile_push_enable pref set for %s", to);
|
|
end
|
|
else
|
|
module:log("debug", "no message body");
|
|
end
|
|
end
|
|
end
|
|
|
|
local function fire_offline_message(event, muc_room, off_jid)
|
|
local stanza_c = st.clone(event.stanza);
|
|
local orig_from = event.stanza.attr.from;
|
|
stanza_c.attr.to = off_jid;
|
|
stanza_c.attr.from = muc_room:get_occupant_jid(orig_from);
|
|
|
|
module:log("debug", "stanza[%s] stanza_c[%s]",
|
|
tostring(event.stanza), tostring(stanza_c));
|
|
if not stanza_c.attr.from then
|
|
module:log("error", "original from[%s] not found at muc_room",
|
|
tostring(orig_from));
|
|
return nil;
|
|
end
|
|
if is_pushd_blocked(stanza_c.attr.from, stanza_c.attr.to) then
|
|
module:log("debug", "skip pushd message from blocked jid [%s]",
|
|
stanza_c.attr.from);
|
|
elseif is_pushd_blocked(orig_from, stanza_c.attr.to) then
|
|
module:log("debug", "skip pushd message from blocked jid [%s]",
|
|
orig_from);
|
|
else
|
|
module:fire_event('message/offline/handle', {
|
|
origin = event.origin,
|
|
stanza = stanza_c,
|
|
});
|
|
end
|
|
end
|
|
|
|
local function handle_muc_offline(event, room_jid)
|
|
local _, host = jid_split(room_jid);
|
|
local muc = hosts[host].modules.muc;
|
|
|
|
if muc then
|
|
local muc_room = muc.get_room_from_jid(room_jid);
|
|
if not muc_room then
|
|
module:log("debug", "muc room[%s] not here. Nothing to do",
|
|
room_jid);
|
|
return nil;
|
|
elseif ut.table.contains(pushd_config.muc_config.exclude, room_jid) then
|
|
module:log("debug", "muc room[%s] excluded from pushd", room_jid);
|
|
return nil;
|
|
end
|
|
|
|
local muc_members = get_members(muc_room);
|
|
local muc_occ_online = get_occupants(muc_room);
|
|
local muc_occ_offline = set.difference(muc_members,muc_occ_online);
|
|
module:log("debug", "muc_members[%s]", tostring(muc_members));
|
|
module:log("debug", "muc_occ_online[%s]", tostring(muc_occ_online));
|
|
module:log("debug", "muc_occ_offline[%s]", tostring(muc_occ_offline));
|
|
for off_jid in muc_occ_offline do
|
|
module:log("debug", "fire_offline_message[%s]", off_jid);
|
|
fire_offline_message(event, muc_room, off_jid);
|
|
end
|
|
end
|
|
end
|
|
|
|
local function handle_msg(event)
|
|
module:log("debug", "handle_msg");
|
|
local stanza = event.stanza;
|
|
local room_jid = stanza.attr.to;
|
|
|
|
if stanza.attr.type ~= 'groupchat' then
|
|
module:log("debug",
|
|
"message[%s] not of type groupchat. Nothing to do here",
|
|
tostring(stanza));
|
|
return nil;
|
|
end
|
|
module:log("debug", "handle_msg room_jid[%s]", tostring(room_jid));
|
|
return handle_muc_offline(event, room_jid);
|
|
end
|
|
|
|
local function handle_muc_created(event)
|
|
local room = event.room;
|
|
if pushd_config.muc_config.force_persistent then
|
|
room:set_persistent(true);
|
|
module:log("debug", "persistent room[%s] forced on creation",
|
|
room:get_name());
|
|
end
|
|
end
|
|
|
|
local function handle_muc_config(event)
|
|
local room, fields = event.room, event.fields;
|
|
local name = fields['muc#roomconfig_roomname'] or room:get_name();
|
|
local persistent = fields['muc#roomconfig_persistentroom'];
|
|
if pushd_config.muc_config.force_persistent and not persistent then
|
|
fields['muc#roomconfig_persistentroom'] = true;
|
|
event.changed = true;
|
|
module:log("debug", "persistent room[%s] forced", name);
|
|
end
|
|
end
|
|
|
|
local function handle_muc_presence(event)
|
|
if not pushd_config.muc_config.owner_on_join then return; end
|
|
local stanza = event.stanza;
|
|
if stanza.attr.type == "unavailable" then return; end
|
|
|
|
local room = get_muc_room(stanza.attr.to);
|
|
if not room then return; end
|
|
local from_jid = jid_bare(stanza.attr.from);
|
|
local affiliation = room:get_affiliation(from_jid);
|
|
if affiliation ~= "owner" then
|
|
room:set_affiliation(true, from_jid, "owner");
|
|
module:log("debug", "[%s] set affiliation to 'owner' for [%s]",
|
|
room:get_name(), from_jid);
|
|
end
|
|
end
|
|
|
|
local function handle_muc_presence_out(event)
|
|
local stanza = event.stanza;
|
|
local muc_stanza = stanza:get_child('x',
|
|
'http://jabber.org/protocol/muc#user');
|
|
|
|
if muc_stanza then
|
|
local item = muc_stanza:get_child('item');
|
|
if not item.nick and stanza.attr.from then
|
|
local nick = select(3, jid_split(stanza.attr.from));
|
|
if nick then
|
|
item.attr.nick = nick;
|
|
module:log("debug", "added nick[%s] tag", nick);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if module:get_host_type() == "component" then
|
|
module:hook("muc-room-created", handle_muc_created, 20);
|
|
module:hook("muc-config-submitted", handle_muc_config, 20);
|
|
module:hook("presence/full", handle_muc_presence, 501);
|
|
else
|
|
module:hook("presence/full", handle_muc_presence_out, 501);
|
|
end
|
|
module:hook("message/bare", handle_msg, 20);
|
|
module:hook("message/offline/handle", handle_offline, 20);
|
|
|
|
local function normalize_params(params)
|
|
assert(params.driver and params.database,
|
|
"Configuration error: Both the SQL driver and the database need to be specified");
|
|
return params;
|
|
end
|
|
|
|
function module.load()
|
|
if prosody.prosodyctl then return; end
|
|
local engines = module:shared("/*/sql/connections");
|
|
local params = normalize_params(module:get_option("auth_sql", default_params));
|
|
engine = engines[sql.db2uri(params)];
|
|
if not engine then
|
|
module:log("debug", "Creating new engine");
|
|
engine = sql:create_engine(params);
|
|
engines[sql.db2uri(params)] = engine;
|
|
end
|
|
engine:connect();
|
|
pushd_config = module:get_option("pushd_config", pushd_config);
|
|
if not pushd_config.muc_config then
|
|
pushd_config.muc_config = muc_config
|
|
end
|
|
|
|
module:log("debug", "load OK");
|
|
end
|