local module_host = module:get_host(); --[[ if module_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"); local rm_remove_from_roster = require("core.rostermanager").remove_from_roster; local rm_add_to_roster = require("core.rostermanager").add_to_roster; local rm_roster_push = require("core.rostermanager").roster_push; -- Persistent storage local mix_pam = module:open_store("mix_pam", "keyval"); -- Runtime data local mix_hosts = {}; local mix_spids = {}; -- user -> channel -> spid -- Namespaceing local mix_pam_xmlns = "urn:xmpp:mix:pam:2"; local mix_roster_xmlns = "urn:xmpp:mix:roster:0"; module:add_feature(mix_pam_xmlns); -- NOTE: To show that we archive messages -- module:add_feature(mix_pam_xmlns.."#archive"); local function contains_key(table, key) -- Returns true if table contains the key key. Returns false -- otherwise. return table[key] ~= nil; end local function set_mix_spid(user, channel, spid) if mix_spids[user] ~= nil then mix_spids[user][channel] = spid; else mix_spids[user] = { [channel] = spid }; end mix_pam:set("spids", mix_spids); end local function get_mix_spid(user, channel) return mix_spids[user][channel]; end; local function remove_mix_spid(user, channel) mix_spids[user][channel] = nil; if mix_spids[user][channel] == {} then mix_spids[user][channel] = nil; end mix_pam:set("spids", mix_spids); end local 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 mix_pam:set("hosts", mix_hosts); end local 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 mix_pam:set("hosts", mix_hosts); end local function is_mix_host(host) return mix_hosts[host] ~= nil; end local 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 = {}; end for host, _ in pairs(mix_hosts) do module:log("debug", "Known host: %s", host); end mix_spids = mix_pam:get("spids"); module:log("info", "Loaded known MIX SPIDs"); if mix_spids == nil then module:log("info", "No known MIX SPIDs loaded"); mix_spids = {}; end for user, channel_map in pairs(mix_spids) do module:log("debug", "-- %s", user); if channel_map[user] then for channel, spid in pairs(channel_map[user]) do module:log("debug", "%s -> %s", channel, spid); end else module:log("warn", "User mapping empty"); end end end local client_state_tracker = {}; -- [stanza ID] -> resource local function add_state(id, resource) client_state_tracker[id] = resource; module:log("debug", "Adding a resource %s for id %s", resource, id); end local function has_state(id) return client_state_tracker[id] ~= nil; end local 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 local function handle_client_join(event) -- Client requests to join module:log("debug", "client-join received"); local stanza, origin = event.stanza, event.origin; local from = jid.bare(stanza.attr.from); 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(from)); module:send(join_iq); return true; end local function handle_client_leave(event) -- Client requests to leave module:log("debug", "client-leave received"); local stanza, origin = event.stanza, event.origin; local from = jid.bare(stanza.attr.from); 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(from)); module:send(leave_iq); return true; 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); local function handle_mix_join(event) -- The MIX server responded module:log("debug", "Received MIX-JOIN result"); module:log("debug", "Adding %s as a new MIX host", jid.host(event.stanza.attr.from)); add_mix_host(jid.host(event.stanza.attr.from)); mix_pam:set("hosts", mix_hosts); local stanza = event.stanza; local spid = stanza:get_child("join", "urn:xmpp:mix:core:1").attr.id; local channel_jid = spid.."#"..stanza.attr.from; local origin = pop_state(stanza.attr.id); local resource = origin.resource; if resource == nil then module:log("error", "Got a MIX join result for a not-requested id %s", stanza.attr.id); module:log("error", "Maybe the server changed the stanza ID?"); return false; end -- Keep track of the SPID set_mix_spid(stanza.attr.to, stanza.attr.from, spid); 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); -- TODO: Error handling? rm_add_to_roster(origin, stanza.attr.from, { subscription = "both", groups = {}, }); rm_roster_push(jid.node(stanza.attr.to), module_host, stanza.attr.from); return true; end local function handle_mix_leave(event) -- The MIX server responded 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 origin = pop_state(stanza.attr.id); local resource = origin.resource; if resource == nil then module:log("error", "Got a MIX leave result for a not-requested id %s", stanza.attr.id); module:log("error", "Maybe the MIX server changed the stanza ID?"); return false; end -- Keep track of the SPID set_mix_spid(stanza.attr.to, jid.bare(stanza.attr.from)); 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); -- Remove from roster -- TODO: Error handling rm_remove_from_roster(origin, jid.bare(stanza.attr.from)); rm_roster_push(jid.node(stanza.attr.to), module_host, jid.bare(stanza.attr.from)); 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); local user_resources = {}; -- [bare jid] -> array of bound resources module:hook("resource-bind", function(event) local session_jid, resource = jid.bare(event.session.full_jid), event.session.resource; if user_resources[session_jid] ~= nil then for _, r in pairs(user_resources) do if r == resource then return; end end table.insert(user_resources[session_jid], resource); else user_resources[session_jid] = { resource }; end module:log("debug", "Caught resource %s of %s", resource, session_jid); end); module:hook("resource-unbind", function(event) local session_jid, resource = jid.bare(event.session.full_jid), event.session.resource; for i, r in pairs(user_resources[session_jid]) do if r == resource then table.remove(user_resources[session_jid], i); return; end end module:log("debug", "Unbind of not recorded resource %s (%s)", resource, session_jid); end); module:hook("roster-get", function(event) -- NOTE: Currently this requires a patch to make mod_roster emit -- the roster-get event local annotate = event.stanza :get_child("query", "jabber:iq:roster") :get_child("annotate", mix_roster_xmlns); if not annotate then return; end local stanza = event.stanza; module:log("debug", "Annotated roster request received"); -- User requested the roster with an for item in event.reply:get_child("query", "jabber:iq:roster"):children() do if contains_key(mix_hosts, jid.host(item.attr.jid)) then local spid = get_mix_spid(jid.bare(stanza.attr.from), item.attr.jid); item:tag("channel", { xmlns = mix_roster_xmlns, ["participant-id"] = spid, }); end end end); module:hook("message/bare", function(event) local stanza = event.stanza; local jid_host = jid.host(stanza.attr.from); if not is_mix_host(jid_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);