local host = module:get_host(); if module:get_host_type() ~= "component" then error("MIX should be loaded as a component", 0); end local st = require("util.stanza"); local jid = require("util.jid"); local uuid = require("util.uuid"); local id = require("util.id"); local datetime = require("util.datetime"); local time = require("util.time"); local serialization = require("util.serialization"); local pep = module:depends("pep"); local persistent_channels = module:open_store("mix_channels", "keyval"); local persistent_channel_data = module:open_store("mix_data", "keyval"); -- Default values local default_channel_description = module:get_option("default_description", "A MIX channel for chatting"); local default_channel_name = module:get_option("default_name", "MIX channel"); module:depends("disco"); module:add_identity("conference", "mix", module:get_option("name", "Prosody MIX service"); module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("urn:xmpp:mix:core:1"); Participant = {}; Participant.__index = Participant; function Participant:new(jid, nick) return setmetatable({ jid = jid, nick = nick, }, Participant); end Channel = {} Channel.__index = Channel; function Channel:new(jid, name, description, participants, subscriptions, spid, contacts, adhoc) return setmetatable({ jid = jid, name = name, description = description, participants = participants, subscriptions = subscriptions, spid = spid, contacts = contacts, adhoc = adhoc, }, Channel); end function Channel:from(config) return setmetatable(config, Channel); end function Channel:get_spid(jid) return self.spid[jid]; end function Channel:debug_print() module:log("debug", "Channel %s (%s)", self.jid, self.name); module:log("debug", "'%s'", self.description); for _, p in pairs(self.participants) do module:log("debug", "=> %s (%s)", p.jid, p.nick); end module:log("debug", "Contacts:"); for _, c in pairs(self.contacts) do module:log("debug", "=> %s", c); end if self.subscriptions then module:log("debug", "Subscriptions:"); for user, subs in pairs(self.subscriptions) do module:log("debug", "[%s]", user); for _, sub in pairs(self.subscriptions[user]) do module:log("debug", "=> %s", sub); end end end end local channels = {}; function get_channel(jid) for i, channel in pairs(channels) do if channel.jid == jid then return i, channel end end end function publish_participant(service, spid, participant) service:publish("urn:xmpp:mix:nodes:participants", true, spid, st.stanza("item", { id = spid, xmlns = "http://jabber.org/protocol/pubsub" }) :tag("participant", { xmlns = "urn:xmpp:mix:core:1" }) :tag("nick"):text(participant["nick"]):up() :tag("jid"):text(participant["jid"])); end function save_channels() module:log("debug", "Saving channel list..."); local channel_list = {}; for _, channel in pairs(channels) do table.insert(channel_list, channel.jid); persistent_channel_data:set(channel.jid, channel); end persistent_channels:set("channels", channel_list); module:log("debug", "Saving channel list done."); end function Channel:save_state() -- Saving the entire state everything one channel seems stupid, -- so we just save the changed channel module:log("debug", "Saving state of channel %s...", self.jid); persistent_channel_data:set(self.jid, self); module:log("debug", "Saving state done.", self.jid); end function module.load() module:log("info", "Loading MIX channels..."); local channel_list = persistent_channels:get("channels"); if channel_list then for _, channel in pairs(channel_list) do table.insert(channels, Channel:from(persistent_channel_data:get(channel))); module:log("debug", "MIX channel %s loaded", channel); end else module:log("debug", "No MIX channels found."); end module:log("debug", "Loading MIX channels done."); end function channel_not_found(stanza) -- Wrapper for returning a "Channel-not-found" error stanza return st.error_reply(stanza, "cancel", "item-not-found", "The MIX channel was not found"); end module:hook("host-disco-items", function(event) module:log("debug", "host-disco-items called"); local reply = event.reply; for _, channel in pairs(channels) do -- Adhoc channels are supposed to be invisible if not channel.adhoc then reply:tag("item", { jid = channel.jid }):up(); end end end); module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event) module:log("debug", "IQ-GET disco#items"); local origin, stanza = event.origin, event.stanza; local _, channel = get_channel(staza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end -- TODO: Maybe check permissions local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" }); for _, node in pairs({"urn:xmpp:mix:nodes:messages", "urn:xmpp:mix:nodes:participants", "urn:xmpp:mix:nodes:info"}) do reply:tag("item", { jid = channel.jid, node = node }):up(); end origin.send(reply); return true; end); module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event) module:log("debug", "IQ-GET disco#info"); local origin, stanza = event.origin, event.stanza; local _, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }); reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); reply:tag("identity", { category = "conference", name = channel.name, type = "mix" }):up(); reply:tag("feature", { var = "urn:xmpp:mix:core:1" }):up(); -- TODO: Check permissions reply:tag("feature", { var = "urn:xmpp:mix:core:1#create-channel" }):up(); origin.send(reply); return true; end); function find_participant(table, jid) for i, v in pairs(table) do if v.jid == jid then return i; end end return -1; end module:hook("iq-set/bare/urn:xmpp:mix:core:1:leave", function(event) module:log("debug", "MIX leave received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); local i, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end local participant_index = find_participant(channel.participants, from); if participant_index == -1 then origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not a participant")); channel:debug_print(); module:log("debug", "%s is not a participant in %s", from, channel.jid); return; end -- Remove the user as a participant by... -- Unsubscribing local srv = pep.get_pep_service(channel.jid); for _, node in pairs(channel.subscriptions[from]) do srv:remove_subscription(node, true, from); module:log("debug", "Unsubscribed %s from %s on %s", from, node, channel.jid); end channels[i].subscriptions[from] = nil; -- Retracting the participation srv:retract("urn:xmpp:mix:nodes:participants", true, channel.spid[from], true); -- Removing the user table.remove(channels[i].participants, participant_index); channel:save_state(); origin.send(st.reply(stanza):tag("leave", { xmlns = "urn:xmpp:mix:core:1" })); return true; end); module:hook("iq-set/bare/urn:xmpp:mix:core:1:join", function(event) module:log("debug", "MIX join received"); local origin, stanza = event.origin, event.stanza; local i, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end local from = jid.bare(stanza.attr.from); local spid = channel.spid[from] or uuid.generate(); -- Stable Participant ID local reply = st.reply(stanza) :tag("join", { xmlns = "urn:xmpp:mix:core:1", id = spid }); local srv = pep.get_pep_service(jid.node(stanza.attr.to)); local join = stanza:get_child("join", "urn:xmpp:mix:core:1"); local nick = join:get_child("nick"); module:log("debug", "User joining as nick %s", nick:get_text()); local nodes = {}; -- TODO: The spec says that an error sould be returned when no nodes can be subscribed to for subscribe in join:childtags("subscribe") do module:log("debug", "Subscribing user to node %s", subscribe.attr.node); local ok, err = srv:add_subscription(subscribe.attr.node, true, from); if not ok then module:log("debug", "Error during subscription: %s", err); else table.insert(nodes, subscribe.attr.node); reply:tag("subscribe", { node = subscribe.attr.node }):up(); end end channels[i].subscriptions[from] = nodes; local participant = Participant:new(jid.bare(from), nick:get_text()) table.insert(channels[i].participants, participant) channels[i].spid[jid.bare(stanza.attr.from)] = spid; publish_participant(srv, spid, participant); channels[i]:save_state(); reply:add_child(nick); origin.send(reply); return true end); module:hook("iq-set/bare/urn:xmpp:mix:core:1:setnick", function(event) module:log("debug", "MIX setnick received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); local i, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end local participant_index = find_participant(channel.participants, from); if participant_index == -1 then channel:debug_print(); module:log("debug", "%s is not a participant in %s", from, channel.jid); return; end local setnick = stanza:get_child("setnick", "urn:xmpp:mix:core:1"); -- TODO: Error handling local nick = setnick:get_child("nick"):get_text(); -- Change the nick channels[i].participants[participant_index].nick = nick; -- Inform all other members local srv = pep.get_pep_service(channel.jid); local participant = channel.participants[participant_index]; publish_participant(srv, channel:get_spid(participant.jid), participant); origin.send(st.reply(stanza) :tag("setnick", { xmlns = "urn:xmpp:mix:core:1" }) :tag("nick"):text(nick)); channel:save_state(); return true; end); function Channel:publish_info(srv) srv:publish("urn:xmpp:mix:nodes:info", true, timestamp, st.stanza("item", { id = timestamp, xmlns = "http://jabber.org/protocol/pubsub" }) :tag("x", { xmlns = "jabber:x:data", type = "result" }) :tag("field", { var = "FORM_TYPE", type = "hidden" }) :tag("value"):text("urn:xmpp:mix:core:1"):up():up() :tag("field", { var = "Name" }) :tag("value"):text(self.name):up() :tag("field", { var = "Contact" }) :tag("value"):text(self.contacts)); end function create_channel(node, creator, adhoc) local channel = Channel:new(string.format("%s@%s", node, host), default_channel_name, default_channel_description, {}, {}, {}, { creator }, adhoc); -- Create the PEP nodes local srv = pep.get_pep_service(node); local timestamp = datetime.datetime(time.now()); srv:create("urn:xmpp:mix:nodes:info", true, { ["access_model"] = "open" }); channel:publish_info(srv); -- TODO: This seems bad srv:create("urn:xmpp:mix:nodes:participants", true, { ["access_model"] = "open"}); table.insert(channels, channel); end module:hook("iq-set/host/urn:xmpp:mix:core:1:create", function(event) module:log("debug", "MIX create received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); local create = stanza:get_child("create", "urn:xmpp:mix:core:1"); local node; if create.attr.channel ~= nil then -- Create non-adhoc channel node = create.attr.channel; local _, channel = get_channel(stanza.attr.to); if channel then origin.send(st.error_reply(stanza, "cancel", "conflict", "Channel already exists")); return; end create_channel(create.attr.channel, from, false); else -- Create adhoc channel -- TODO: Check for a collision node = id.short(); create_channel(node, from, true); end module:log("debug", "Channel %s created with %s as owner", node, from); origin.send(st.reply(stanza) :tag("create", { xmlns = "urn:xmpp:mix:core:1", channel = node })); save_channels(); return true; end); module:hook("iq-set/host/urn:xmpp:mix:core:1:destroy", function(event) module:log("debug", "MIX destroy received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); local destroy = stanza:get_child("create", "urn:xmpp:mix:core:1"); local node = destory.attr.channel; local node_jid = string.format("%s@%s", node, host); local i, channel = get_channel(node_jid); if not channel then origin.send(channel_not_found(stanza)); return; end -- TODO: Check permissions -- Remove all registered nodes local srv = pep.get_pep_service(node); for _, pep_node in pairs({ "urn:xmpp:mix:nodes:participants", "urn:xmpp:mix:nodes:info" }) do srv:delete(pep_node, true); end table.remove(channels, i); module:log("debug", "Channel %s destroyed", node); origin.send(st.reply(stanza)); save_channels(); return true; end); module:hook("message/bare", function(event) module:log("debug", "MIX message detected"); local stanza, origin = event.stanza, event.origin; if stanza.attr.type ~= "groupchat" then -- TODO: Is this correct? origin.send(st.error_reply(stanza, "cancel", "bad-request", "Non-groupchat message")); return; end local from = jid.bare(stanza.attr.from); local i, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return; end local participant_index = find_participant(channel.participants, from); if participant_index == -1 then origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not a participant")); return; end local participant = channel.participants[participant_index]; local msg = st.clone(stanza); msg:add_child(st.stanza("mix", { xmlns = "urn:xmpp:mix:core:1" }) :tag("nick"):text(participant.nick):up() :tag("jid"):text(participant.jid):up()); msg.attr.from = channel.jid.."/"..channel.spid[from]; for _, p in pairs(channel.participants) do if p.jid ~= participant.jid then msg.attr.to = p.jid; module:send(msg); module:log("debug", msg:pretty_print()); module:log("debug", "Message from %s sent to %s", participant.jid, p.jid); end end return true; end);