264 lines
8.9 KiB
Lua
264 lines
8.9 KiB
Lua
|
if module:get_host() ~= "*" then
|
||
|
module:log("error", "mix_pam should be used on the user host!");
|
||
|
end
|
||
|
|
||
|
local jid = require("util.jid");
|
||
|
local st = require("util.stanza");
|
||
|
|
||
|
-- Persistent storage
|
||
|
local mix_pam = module:open_store("mix_pam", "keyval");
|
||
|
|
||
|
-- Runtime data
|
||
|
local mix_hosts = {};
|
||
|
|
||
|
-- Namespaceing
|
||
|
local mix_pam_xmlns = "urn:xmpp:mix:pam:2";
|
||
|
|
||
|
module:add_feature(mix_pam_xmlns);
|
||
|
-- NOTE: To show that we archive messages
|
||
|
-- module:add_feature(mix_pam_xmlns.."#archive");
|
||
|
|
||
|
function add_mix_host(host)
|
||
|
if mix_hosts[host] ~= nil then
|
||
|
mix_hosts[host] = mix_hosts[host] + 1;
|
||
|
module:log("debug", "Known MIX host has a new user");
|
||
|
else
|
||
|
module:log("debug", "Added %s as a new MIX host", host);
|
||
|
mix_hosts[host] = 1;
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function remove_mix_host(host)
|
||
|
if mix_hosts[host] ~= nil then
|
||
|
local count = mix_hosts[host];
|
||
|
if count == 1 then
|
||
|
mix_hosts[host] = nil;
|
||
|
module:log("debug", "Removing %s as a mix host", host);
|
||
|
else
|
||
|
mix_hosts[host] = count - 1;
|
||
|
module:log("debug", "Decrementing %s's user counter", host);
|
||
|
end
|
||
|
else
|
||
|
module:log("debug", "Attempt to remove unknown MIX host");
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function is_mix_host(host)
|
||
|
return mix_hosts[host] ~= nil;
|
||
|
end
|
||
|
|
||
|
function is_mix_message(stanza)
|
||
|
return stanza:get_child("mix", "urn:xmpp:mix:core:1") ~= nil;
|
||
|
end
|
||
|
|
||
|
function module.load()
|
||
|
mix_hosts = mix_pam:get("hosts");
|
||
|
module:log("info", "Loaded known MIX hosts");
|
||
|
|
||
|
if mix_hosts == nil then
|
||
|
module:log("info", "No known MIX hosts loaded");
|
||
|
mix_hosts = {};
|
||
|
return;
|
||
|
end
|
||
|
for _, host in pairs(mix_hosts) do
|
||
|
module:log("debug", "Known host: %s", host);
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local client_state_tracker = {}; -- [stanza ID] -> resource
|
||
|
function add_state(id, resource)
|
||
|
client_state_tracker[id] = resource;
|
||
|
module:log("debug", "Adding a resource %s for id %s", resource, id);
|
||
|
end
|
||
|
function has_state(id)
|
||
|
return client_state_tracker[id] ~= nil;
|
||
|
end
|
||
|
function pop_state(id)
|
||
|
module:log("debug", "Popping a resource for stanza id %s", id);
|
||
|
|
||
|
if has_state(id) then
|
||
|
local resource = client_state_tracker[id];
|
||
|
client_state_tracker[id] = nil;
|
||
|
return resource;
|
||
|
end
|
||
|
|
||
|
return nil;
|
||
|
end
|
||
|
|
||
|
module:hook("iq/self", function(event)
|
||
|
local stanza = event.stanza;
|
||
|
if #stanza.tags == 0 then return; end
|
||
|
|
||
|
if stanza:get_child("client-join", mix_pam_xmlns) ~= nil then
|
||
|
return handle_client_join(event);
|
||
|
elseif stanza:get_child("client-leave", mix_pam_xmlns) ~= nil then
|
||
|
return handle_client_leave(event);
|
||
|
end
|
||
|
end);
|
||
|
|
||
|
function handle_client_join(event)
|
||
|
-- Client requests to join
|
||
|
module:log("debug", "client-join received");
|
||
|
local stanza, origin = event.stanza, event.origin;
|
||
|
local from, to = jid.bare(stanza.attr.from), jid.bare(stanza.attr.to);
|
||
|
|
||
|
local client_join = stanza:get_child("client-join", mix_pam_xmlns);
|
||
|
if client_join.attr.channel == nil then
|
||
|
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No channel specified"));
|
||
|
return true;
|
||
|
end
|
||
|
local join = client_join:get_child("join", "urn:xmpp:mix:core:1");
|
||
|
if join == nil then
|
||
|
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No join stanza"));
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
-- Transform the client-join into a join
|
||
|
local join_iq = st.iq({ type = "set", from = jid.bare(stanza.attr.from), to = client_join.attr.channel, id = stanza.attr.id, xmlns = "jabber:client" });
|
||
|
join_iq:add_child(join);
|
||
|
|
||
|
add_state(stanza.attr.id, jid.resource(stanza.attr.from));
|
||
|
|
||
|
module:send(join_iq);
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
function handle_client_leave(event)
|
||
|
-- Client requests to leave
|
||
|
module:log("debug", "client-leave received");
|
||
|
local stanza, origin = event.stanza, event.origin;
|
||
|
local from, to = jid.bare(stanza.attr.from), jid.bare(stanza.attr.to);
|
||
|
|
||
|
local client_leave = stanza:get_child("client-leave", mix_pam_xmlns);
|
||
|
if client_leave.attr.channel == nil then
|
||
|
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No channel specified"));
|
||
|
return true;
|
||
|
end
|
||
|
local leave = client_leave:get_child("leave", "urn:xmpp:mix:core:1");
|
||
|
if leave == nil then
|
||
|
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No leave stanza"));
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
-- Transform the client-join into a join
|
||
|
local leave_iq = st.iq({ type = "set", from = jid.bare(stanza.attr.from), to = client_leave.attr.channel, id = stanza.attr.id });
|
||
|
leave_iq:add_child(leave);
|
||
|
|
||
|
add_state(stanza.attr.id, jid.resource(stanza.attr.from));
|
||
|
|
||
|
module:send(leave_iq);
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
module:hook("iq/bare", function(event)
|
||
|
-- We handle the MIX results here since IQ stanzas against bare JIDs
|
||
|
-- don't make a lot of sense. We first have to translate them into
|
||
|
-- full JIDs based on our stanza ID -> resource mapping
|
||
|
if event.stanza:get_child("leave", "urn:xmpp:mix:core:1") ~= nil then
|
||
|
return handle_mix_leave(event);
|
||
|
elseif event.stanza:get_child("join", "urn:xmpp:mix:core:1") ~= nil then
|
||
|
return handle_mix_join(event);
|
||
|
elseif has_state(event.stanza.attr.id) then
|
||
|
local tmp = st.clone(event.stanza);
|
||
|
tmp.attr.to = tmp.attr.to.."/"..pop_state(tmp.attr.id);
|
||
|
module:send(tmp);
|
||
|
return true;
|
||
|
end
|
||
|
end);
|
||
|
|
||
|
function handle_mix_join(event)
|
||
|
-- The MIX server responded
|
||
|
-- TODO: Do stuff to the user's roster
|
||
|
module:log("debug", "Received MIX-JOIN result");
|
||
|
add_mix_host(jid.host(event.stanza.attr.from));
|
||
|
mix_pam:set("hosts", mix_hosts);
|
||
|
|
||
|
local stanza = event.stanza;
|
||
|
local channel_jid = stanza:get_child("join", "urn:xmpp:mix:core:1").attr.id.."#"..stanza.attr.from;
|
||
|
local resource = pop_state(stanza.attr.id);
|
||
|
if resource == nil then
|
||
|
module:log("error", "Got a MIX join result for a not-requested id %s. Maybe the MIX server changed the stanza ID?", stanza.attr.id);
|
||
|
return false;
|
||
|
end
|
||
|
|
||
|
local client_join = st.iq({ type = "result", id = stanza.attr.id, from = jid.bare(stanza.attr.to), to = stanza.attr.to.."/"..resource })
|
||
|
:tag("client-join", { xmlns = mix_pam_xmlns, jid = channel_jid });
|
||
|
client_join:add_child(stanza:get_child("join", "urn:xmpp:mix:core:1"));
|
||
|
module:send(client_join);
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
function handle_mix_leave(event)
|
||
|
-- The MIX server responded
|
||
|
-- TODO: Do stuff to the user's roster
|
||
|
module:log("debug", "Received MIX-LEAVE result");
|
||
|
remove_mix_host(jid.host(event.stanza.attr.from));
|
||
|
mix_pam:set("hosts", mix_hosts);
|
||
|
|
||
|
local stanza = event.stanza;
|
||
|
local resource = pop_state(stanza.attr.id);
|
||
|
if resource == nil then
|
||
|
module:log("error", "Got a MIX leave result for a not-requested id %s. Maybe the MIX server changed the stanza ID?", stanza.attr.id);
|
||
|
return false;
|
||
|
end
|
||
|
|
||
|
local client_leave = st.iq({ type = "result", id = stanza.attr.id, from = jid.bare(stanza.attr.to), to = stanza.attr.to.."/"..resource })
|
||
|
:tag("client-leave", { xmlns = mix_pam_xmlns });
|
||
|
client_leave:add_child(stanza:get_child("leave", "urn:xmpp:mix:core:1"));
|
||
|
module:send(client_leave);
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
local user_resources = {}; -- [bare jid] -> array of bound resources
|
||
|
|
||
|
module:hook("resource-bind", function(event)
|
||
|
local jid, resource = jid.bare(event.session.full_jid), event.session.resource;
|
||
|
if user_resources[jid] ~= nil then
|
||
|
for _, r in pairs(user_resources) do
|
||
|
if r == resource then return; end
|
||
|
end
|
||
|
|
||
|
table.insert(user_resources[jid], resource);
|
||
|
else
|
||
|
user_resources[jid] = { resource };
|
||
|
end
|
||
|
|
||
|
module:log("debug", "Caught resource %s of %s", resource, jid);
|
||
|
end);
|
||
|
|
||
|
module:hook("resource-unbind", function(event)
|
||
|
local jid, resource = jid.bare(event.session.full_jid), event.session.resource;
|
||
|
for i, r in pairs(user_resources[jid]) do
|
||
|
if r == resource then
|
||
|
table.remove(user_resources[jid], i);
|
||
|
return;
|
||
|
end
|
||
|
end
|
||
|
|
||
|
module:log("debug", "Unbind of not recorded resource %s (%s)", resource, jid);
|
||
|
end);
|
||
|
|
||
|
module:hook("message/bare", function(event)
|
||
|
local stanza = event.stanza;
|
||
|
local host = jid.host(stanza.attr.from);
|
||
|
if not is_mix_host(host) then return; end
|
||
|
if not is_mix_message(stanza) then return; end
|
||
|
|
||
|
-- MIX-CORE says that if the message cannot be delivered, it should
|
||
|
-- just be dropped
|
||
|
if user_resources[stanza.attr.to] == nil then
|
||
|
module:log("debug", "Skipping %s: No resource bound", stanza.attr.to);
|
||
|
return true;
|
||
|
end
|
||
|
|
||
|
-- Per XEP we know that stanza.attr.to is the user's bare JID
|
||
|
for _, resource in pairs(user_resources[stanza.attr.to]) do
|
||
|
-- TODO: Only send to resources that advertise support for MIX (When MIX clients are available for testing)
|
||
|
local msg = st.clone(stanza);
|
||
|
msg.attr.to = stanza.attr.to.."/"..resource;
|
||
|
module:send(msg);
|
||
|
module:log("debug", "Sent message to %s", msg.attr.to);
|
||
|
end
|
||
|
return true;
|
||
|
end);
|