diff --git a/mod_mix/forms.lib.lua b/mod_mix/forms.lib.lua new file mode 100644 index 0000000..229255a --- /dev/null +++ b/mod_mix/forms.lib.lua @@ -0,0 +1,18 @@ +local dataforms = require("util.dataforms"); + +local namespaces = module:require("mix/namespaces"); + +return { + -- MIX + mix_info = dataforms.new({ + { name = "FORM_TYPE", type = "hidden", value = namespaces.mix_core }, + { name = "Name", type = "text-single" }, + { name = "Description", type = "text-single" }, + { name = "Contact", type = "jid-multi" }}); + -- MAM + mam_query = dataforms.new({ + { name = "FORM_TYPE", type = "hidden", value = namespaces.mam }, + { name = "with", type = "jid-single" }, + { name = "start", type = "text-single" }, + { name = "end", type = "text-single" }}); +}; diff --git a/mod_mix/mix.lib.lua b/mod_mix/mix.lib.lua index 199d447..fd9cd0c 100644 --- a/mod_mix/mix.lib.lua +++ b/mod_mix/mix.lib.lua @@ -2,11 +2,16 @@ local st = require("util.stanza"); local array = require("util.array"); local jid_lib = require("util.jid"); local uuid = require("util.uuid"); +local new_id = require("util.id").medium; local time = require("util.time"); +local datetime = require("util.datetime"); +local pubsub = require("util.pubsub"); +local lib_pubsub = module:require("pubsub"); +local storagemanager = require("core.storagemanager"); + local helpers = module:require("mix/helpers"); local namespaces = module:require("mix/namespaces"); -local pep = module:depends("pep"); - +local lib_forms = module:require("mix/forms"); local Participant = {}; Participant.__index = Participant; @@ -51,6 +56,106 @@ function Channel:from(config) return o; end +function Channel:get_broadcaster() + local function broadcast(kind, node, jids, item, _, node_obj) + if node == namespaces.presence then + -- NOTE: This assumes that we already added all necessary MIX data + -- before publishing this item + local presence = {}; + + if kind == "retract" then + presence = st.presence({ + type = "unavailable", + from = self:get_encoded_participant_jid(item.attr.from), + }); + presence:add_child(item:get_tag("mix", namespaces.mix_presence)); + else + presence = stanza.clone(item); + end + + for jid in pairs(jids) do + module:log("debug", "Sending presence notification to %s from %s", jid, item.attr.from); + message.attr.to = jid; + module:send(presence); + end + else + if node_obj then + if node_obj.config["notify_"..kind] == false then + return; + end + end + + if kind == "retract" then + kind = "items"; -- XEP-0060 signals retraction in an container + end + + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + + if kind == "items" then + if node_obj and node_obj.config.include_payload == false then + item:maptags(function () return nil; end); + end + end + end + + local id = new_id(); + local message = st.message({ from = self.jid, type = "headline", id = id }) + :tag("event", { xmlns = namespaces.pubsub_event }) + :tag(kind, { node = node }); + + if item then + message:add_child(item); + end + + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s from %s for node %s", jid, user_bare, node); + message.attr.to = jid; + module:send(message); + end + end + end + + return broadcast; +end + +-- PubSub stuff +local services = {}; -- room@server -> PubSub services + +local known_nodes = module:open_store("mix_pubsub"); +local node_config = module:open_store("mix_pubsub", "map"); + +local function itemstore(username) + local driver = storagemanager.get_driver(module.host, "mix_data"); + return function (config, node) + module:log("debug", "Creating new persistent item store for user %s, node %q", username, node); + local archive = driver:open("mix_pubsub_"..node, "archive"); + return lib_pubsub.archive_itemstore(archive, config, username, node, false); + end +end + +function Channel:get_pubsub_service() + local service = services[self.jid]; + if service then + return service; + end + + service = pubsub.new({ + node_defaults = { + ["persist_items"] = true; + ["access_model"] = "open"; -- TODO + ["max_items"] = 256; -- TODO: Once "max" is supported + }; + + broadcaster = self:get_broadcaster(); + nodestore = known_nodes; + itemstore = itemstore(self.jid); + }); + services[self.jid] = service; + return service; +end + function Channel:get_spid(jid) -- Returns the Stable Participant ID for the *BARE* jid return self.spid[jid]; @@ -64,13 +169,22 @@ end function Channel:find_participant(jid) -- Returns the index of a participant in a channel. Returns -1 -- if the participant is not found - return helpers.find(self.participants, function(p) return p.jid == jid end); + local function is_participant(p) + return p.jid == jid; + end + local _, participant = helpers.find(self.participants, is_participant); + return participant; end function Channel:is_participant(jid) -- Returns true if jid is a participant of the channel. False otherwise. - local i, _ = self:find_participant(jid); - return i ~= -1; + return self:find_participant(jid) ~= nil; +end + +function Channel:get_encoded_participant_jid(jid) + -- TODO: This assumes that jid is a participant + local spid = self:get_spid(jid_lib.bare(jid)); + return spid.."#"..self.jid; end function Channel:is_subscribed(jid, node) @@ -102,239 +216,6 @@ function Channel:debug_print() end end -function Channel:allow_jid(jid) - local srv = pep.get_pep_service(jid_lib.node(self.jid)); - array.push(self.allowed, jid); - srv:set_node_config(namespaces.allowed, - true, - { ["max_items"] = #self.allowed + 1 }); - srv:publish(namespaces.allowed, true, jid, nil); -end - -function Channel:ban_jid(jid) - local srv = pep.get_pep_service(jid_lib.node(self.jid)); - self.banned:push(jid); - srv:set_node_config(namespaces.banned, - true, - { ["max_items"] = #self.banned + 1 }); - srv:publish(namespaces.banned, true, jid, nil); - - local i = helpers.find_str(jid, "@"); - if i ~= nil then - -- "@" in JID => We're banning a real JID - if self:is_participant(jid) then - self:remove_participant(jid); - end - else - -- No "@" in JID => We're banning a host - for _, participant in self.participants do - if jid_lib.host(participant.jid) == jid then - self:remove_participant(participant.jid); - end - end - end -end - -function Channel:remove_participant(jid) - -- Removes a user form the channel. May be a kick, may be a leave. - local srv = pep.get_pep_service(jid_lib.node(self.jid)); - - -- Step 1: Unsubscribe from all subscribed nodes - for _, node in ipairs(self.subscriptions[jid]) do - srv:remove_subscription(node, true, jid); - end - self.subscriptions[jid] = nil; - - -- Step 2: Remove affiliations to all nodes - for _, node in ipairs(self.nodes) do - srv:set_affiliation(node, true, jid, "outcast"); - end - - -- Step 3: Remove jid as participant - local i, _ = self:find_participant(jid); - table.remove(self.participants, i); - - -- Step 4: Retract jid from participants node - local spid = self:get_spid(jid); - local notifier = st.stanza("retract", { id = spid }); - srv:retract(namespaces.participants, true, jid, notifier); - self:save_state(); -end - -function Channel:is_allowed(jid) - local i, _ = helpers.find(self.allowed, jid); - local j, _ = helpers.find(self.allowed, jid_lib.host(jid)); - - return i ~= -1 and j ~= -1; -end - -function Channel:is_admin(jid) - return helpers.in_array(self.administrators, jid); -end - -function Channel:is_owner(jid) - return helpers.in_array(self.owners, jid); -end - -function Channel:may_subscribe(jid, node, joining) - -- Returns true when a JID is allowed to subscribe to a node. - -- If joining is true and the node is set to "participants", then - -- true will be returned as the user will be a participant. - -- NOTE: When using this function directly, be careful to - -- check first whether the JID is allowed to join. - -- TODO: Presence - local group = ""; - if node == namespaces.info then - group = self.config["Information Node Subscription"]; - elseif node == namespaces.participants then - group = self.config["Participants Node Subscription"]; - elseif node == namespaces.messages then - group = self.config["Messages Node Subscription"]; - elseif node == namespaces.allowed then - group = self.config["Allowed Node Subscription"]; - elseif node == namespaces.banned then - group = self.config["Banned Node Subscription"]; - elseif node == namespaces.config then - group = "admins"; - elseif node == namespaces.avatar or node == namespaces.avatar_metadata then - group = "participants"; - end - - module:log("debug", "may_subscribe: Group: %s", group); - module:log("debug", "Is Owner: %s", self:is_owner(jid)); - module:log("debug", "Is Admin: %s", self:is_admin(jid)); - module:log("debug", "Is Participant: %s", self:is_participant(jid)); - module:log("debug", "Is allowed: %s", self:is_allowed(jid)); - - if group == "anyone" then - return true; - elseif group == "allowed" then - return self:is_allowed(jid); - elseif group == "nobody" then - return false; - elseif group == "admins" then - return self:is_admin(jid) or self:is_owner(jid); - elseif group == "owners" then - return self:is_owner(jid); - elseif group == "participants" then - return self:is_participant(jid) or self:is_admin(jid) or self:is_owner(jid) or joining; - end - - return false; -end - -function Channel:may_access(jid, node) - -- Access means read and subscribe - local group = ""; - if node == namespaces.config then - -- TODO: For some reason, this is failing - --group = self.config["Configuration Node Access"]; - group = "owners"; - end - - if group == "nobody" then - return false; - elseif group == "allowed" then - return self:is_allowed(jid); - elseif group == "admins" then - return self:is_admin(jid) or self:is_owner(jid); - elseif group == "owners" then - return self:is_owner(jid); - elseif group == "participants" then - return self:is_participant(jid) or self:is_admin(jid) or self:is_owner(jid); - end - - return false; -end - -function Channel:may_update(jid, node) - module:log("debug", "Update node %s", node); - local group = ""; - -- TODO: Deal with avatars - if node == namespaces.info then - group = self.config["Information Node Update Rights"]; - elseif node == namespaces.config then - -- TODO: Make this configurable - group = "admins"; - elseif node == namespaces.avatar or node == namespaces.avatar_metadata then - group = self.config["Avatar Nodes Update Rights"]; - end - - if group == "admins" then - return self:is_admin(jid) or self:is_owner(jid); - elseif group == "owners" then - return self:is_owner(jid); - elseif group == "participants" then - return self:is_participant(jid) or self:is_admin(jid) or self:is_owner(jid); - elseif group == "allowed" then - return self:is_allowed(jid); - end - - return false; -end - -function Channel:may_join(jid) - -- Does the banned node exist? - if helpers.in_array(self.nodes, namespaces.banned) then - local host = jid_lib.host(jid); - if helpers.in_array(self.banned, host) or helpers.in_array(self.banned, jid) then - -- User or host is banned - return false; - end - end - - -- Does the allowed node exist? - if helpers.in_array(self.nodes, namespaces.allowed) then - local host = jid_lib.host(jid); - if helpers.in_array(self.allowed, host) or helpers.in_array(self.allowed, jid) then - -- User or host is allowed - return true; - else - -- "Whitelist" is on but user or host is not on it. - return false; - end - end - - return true; -end - -function Channel:may(jid, action, data) - if action == "subscribe" then - return self:may_subscribe(jid, data, false); - elseif action == "access" then - return self:may_access(jid, data); - elseif action == "update" then - return self:may_update(jid, data); - elseif action == "pm" then - return self.config["Private Messages"]; - elseif action == "join" then - return self:may_join(jid); - elseif action == "invite" then - if not (self:is_admin(jid) or self:is_owner(jid)) then - return self.config["Participation Addition by Invitation from Participant"]; - else - return true; - end - elseif action == "retract" then - -- Is this the same user - -- TODO: jid == data may not work when using SPIDs - if jid == data then - return self.config["User Message Retraction"]; - else - local group = self.config["Administrator Message Retraction Rights"]; - if group == "nobody" then - return false; - elseif group == "admins" then - return self:is_admin(jid) or self:is_owner(jid); - elseif group == "owners" then - return self:is_owner(jid); - end - - return false; - end - end -end - function Channel:broadcast_message(message, participant, archive) -- Broadcast a message stanza according to rules layed out by -- XEP-0369 @@ -368,10 +249,10 @@ end function Channel:publish_participant(spid, participant) -- Publish a new participant on the service - local srv = pep.get_pep_service(jid_lib.node(self.jid)); - -- NOTE: This function has be to called *after* the new participant -- has been added to the channel.participants array + local srv = self:get_pubsub_service(); + srv:set_node_config(namespaces.participants, true, { ["max_items"] = #self.participants }); @@ -384,40 +265,135 @@ function Channel:publish_participant(spid, participant) :tag("jid"):text(participant["jid"])); end -local default_channel_configuration = { - ["Last Change Made By"] = ""; - ["Owner"] = {}; -- Filled in during creation - ["Administrator"] = {}; - ["End of Life"] = ""; - ["Nodes Present"] = {}; -- Filled in during creation - ["Messages Node Subscription"] = "participants"; - ["Presence Node Subscription"] = "participants"; - ["Participants Node Subscription"] = "participants"; - ["Information Node Subscription"] = "participants"; - ["Allowed Node Subscription"] = "admins"; - ["Banned Node Subscription"] = "admins"; - ["Configuration Node Access"] = "owners"; - ["Information Node Update Rights"] = "admins"; - ["Avatar Node Update Rights"] = "admins"; - ["Open Presence"] = false; - ["Participants Must Provide Presence"] = false; - ["User Message Retraction"] = false; - ["Administrator Message Retraction Rights"] = "owners"; - ["Participation Addition by Invitation from Participant"] = false; - ["Private Messages"] = true; - ["Mandatory Nicks"] = true; -}; +function Channel:remove_participant(jid) + -- Removes a user form the channel. May be a kick, may be a leave. + local srv = self:get_pubsub_service(); -local default_participant_configuration = { - ["JID Visibility"] = "never"; - ["Private Messages"] = "allow"; - ["Presence"] = "share"; - ["vCard"] = "block"; -}; + -- Step 1: Unsubscribe from all subscribed nodes + for _, node in ipairs(self.subscriptions[jid]) do + srv:remove_subscription(node, true, jid); + end + self.subscriptions[jid] = nil; + + -- Step 2: Remove affiliations to all nodes + for _, node in ipairs(self.nodes) do + srv:set_affiliation(node, true, jid, "outcast"); + end + + -- Step 3: Remove jid as participant + local participant = self:find_participant(jid); + self.participants = array.filter(self.participants, function (p) return p.jid ~= jid end); + + -- Step 4: Retract jid from participants node + local spid = self:get_spid(jid); + local notifier = st.stanza("retract", { id = spid }); + srv:retract(namespaces.participants, true, jid, notifier); + self:save_state(); +end + +function Channel:publish_info(srv) + local timestamp = datetime.datetime(time.now()); + local info = st.stanza("item", { id = timestamp, xmlns = namespaces.pubsub }) + :add_child(lib_forms.mix_info:form({ + ["FORM_TYPE"] = "hidden", + ["Name"] = self.name, + ["Description"] = self.description, + ["Contact"] = self.contacts + }, "result")); + srv:publish(namespaces.info, true, timestamp, info); +end + + +local function get_node_access_model(node, adhoc) + -- TODO + if adhoc then + return "whitelist"; + else + return "open"; + end +end + +local function get_node_max_items(node) + -- TODO: Would be nice if we could just return "max" + -- TODO: Handle all nodes + if node == namespaces.messages then + return 0; + elseif node == namespaces.info or + node == namespaces.config then + return 1; + end + + return 256; +end + +local 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 + +function Channel:set_affiliation(node, target, role) + -- Set the affiliation of target to node depending on what + -- node it is and whether target is the creator or not + local srv = self:get_pubsub_service(); + local affiliation = "member"; + if node == namespaces.presence then + affiliation = "none"; + end + + -- TODO(MIX-ADMIN): Also handle OWNER, ADMIN + if role == "creator" then + if node == namespaces.info or + node == namespaces.allowed or + node == namespaces.banned or + node == namespaces.config or + node == namespaces.avatar or + node == namespaces.avatar_metadata then + affiliation = "publisher"; + end + end + + srv:set_affiliation(node, true, target, affiliation); +end + +function Channel:may_subscribe(actor, node, joining) + if joining then + -- TODO: Is this true? + return true; + end + + if self:is_participant(actor) then + -- TODO(MIX-ADMIN): This is possible if the actor is an owner or admin + return node ~= namespaces.config; + end + + return false; +end + +function Channel:may_publish(actor, node) + return node ~= namespaces.presence and node ~= namespaces.messages; +end + +function Channel:may_retract(actor, node) + -- TODO: Maybe put may_{publish, retract} together + return node ~= namespaces.presence and node ~= namespaces.messages; +end + +function Channel:may_retrieve_items(actor, node) + return node ~= namespaces.presence and node ~= namespaces.messages; +end + +function Channel:may_join(actor) + -- TODO(MIX-ADMIN): Check the allowed and banned node + return true; +end return { - Channel = Channel, - Participant = Participant, - default_channel_configuration = default_channel_configuration, - default_participant_configuration = default_participant_configuration, + Channel = Channel; + Participant = Participant; + get_node_access_model = get_node_access_model; + get_node_max_items = get_node_max_items; + channel_not_found = channel_not_found; }; diff --git a/mod_mix/mod_mix.lua b/mod_mix/mod_mix.lua index b70e390..b94ce0e 100644 --- a/mod_mix/mod_mix.lua +++ b/mod_mix/mod_mix.lua @@ -1,6 +1,5 @@ --- TODO: Channel info's Contacts is empty --- TODO: Maybe reset affiliations to none instead of outcast -- TODO: Handle creation and deletion of avatar nodes when publishing to :config +-- TODO: Somehow make the hosts aware of our "presence" local host = module:get_host(); if module:get_host_type() ~= "component" then @@ -16,15 +15,15 @@ local time = require("util.time"); local dataforms = require("util.dataforms"); local array = require("util.array"); local set = require("util.set"); -local pep = module:depends("pep"); +local lib_pubsub = module:require("pubsub"); local helpers = module:require("mix/helpers"); local namespaces = module:require("mix/namespaces"); -local anon = module:require("mix/anon"); +local lib_forms = module:require("mix/forms"); +local lib_mix = module:require("mix/mix"); -local mixlib = module:require("mix/mix"); -local Channel = mixlib.Channel; -local Participant = mixlib.Participant; +local Channel = lib_mix.Channel; +local Participant = lib_mix.Participant; -- Persistent data local persistent_channels = module:open_store("mix_channels", "keyval"); @@ -38,84 +37,15 @@ local restrict_channel_creation = module:get_option("restrict_local_channels", " local service_name = module:get_option("service_name", "Prosody MIX service"); -- MIX configuration -local default_mix_nodes = array { namespaces.info, namespaces.participants, namespaces.messages, namespaces.config }; - --- Dataforms --- MAM -local mam_query_form = dataforms.new({ - { name = "FORM_TYPE", type = "hidden", value = namespaces.mam }, - { name = "with", type = "jid-single" }, - { name = "start", type = "text-single" }, - { name = "end", type = "text-single" }, -}); --- MIX Core -local mix_info_form = dataforms.new({ - { name = "FORM_TYPE", type = "hidden", value = namespaces.mix_core }, - { name = "Name", type = "text-single" }, - { name = "Description", type = "text-single" }, - { name = "Contact", type = "jid-multi" } -}); --- MIX-ADMIN stuff -local mix_config_form = dataforms.new({ - { name = "FORM_TYPE", type = "hidden", value = namespaces.mix_admin }, - { name = "Last Change Made By", type = "jid-single" }, - { name = "Owner", type = "jid-multi" }, - { name = "Administrator", type = "jid-multi" }, - { name = "End of Life", type = "text-single" }, - { name = "Nodes Present", type = "list-multi" }, - { name = "Message Node Subscription", type = "list-single" }, - { name = "Presence Node Subscription", type = "list-single" }, - { name = "Participants Node Subscription", type = "list-single" }, - { name = "Information Node Subscription", type = "list-single" }, - { name = "Allowed Node Subscription", type = "list-single" }, - { name = "Banned Node Subscription", type = "list-single" }, - { name = "Configuration Node Access", type = "list-single" }, - { name = "Information Node Update Rights", type = "list-single" }, - { name = "Avatar Node Update Rights", type = "list-single" }, - { name = "Open Presence", type = "boolean" }, - { name = "Participants Must Provide Presence", type = "boolean" }, - { name = "User Message Retraction", type = "boolean" }, - { name = "Administrator Message Retraction Rights", type = "list-single" }, - { name = "Participation Addition by Invitation from Participant", type = "boolean" }, - { name = "Private Messages", type = "boolean" }, - { name = "Mandatory Nicks", type = "boolean "}, -}); - -local function mix_admin_node_to_value(node) - local map = { - [namespaces.messages] = "messages"; - [namespaces.participants] = "participants"; - [namespaces.info] = "info"; - [namespaces.allowed] = "allowed"; - [namespaces.banned] = "banned"; - }; - - return "'"..map[node].."'"; -end - -local function mix_admin_value_to_node(value) - local map = { - ["'messages'"] = namespaces.messages; - ["'participants'"] = namespaces.participants; - ["'info'"] = namespaces.info; - ["'allowed'"] = namespaces.allowed; - ["'banned'"] = namespaces.banned; - }; - - return map[value] -end - -module:depends("disco"); -module:add_identity("conference", "mix", service_name); -module:add_feature("http://jabber.org/protocol/disco#info"); -module:add_feature(namespaces.mix_core); +local default_mix_nodes = array { namespaces.info, namespaces.participants, namespaces.messages }; local channels = {}; -local function get_channel(channel_jid) +local function find_channel(channel_jid) -- Return the channel object from the channels array for which the -- JID matches. If none is found, returns -1, nil - return helpers.find(channels, function(c) return c.jid == channel_jid; end); + local _, channel = helpers.find(channels, function(c) return c.jid == channel_jid; end); + return channel; end local function save_channels() @@ -154,56 +84,77 @@ function module.load() module:log("info", "Loading MIX channels done."); end -local 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"); +-- PubSub logic +local function pubsub_not_implemented(stanza, feature) + local reply = st.error_reply(stanza, "cancel", "feature-not-implemented"); + reply:tag("unsupported", { + xmlns = namespaces.pubsub_error; + feature = feature; + }); + + return reply; 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(); +local function handle_pubsub_iq(event) + local stanza, origin = event.stanza, event.origin; + local from = jid.bare(stanza.attr.from); + + local channel = find_channel(stanza.attr.to); + if not channel then + module:log("error", "PubSub was used for unknown channel"); + origin:send(st.error_reply(stanza, "cancel", "item-not-found")); + return; + end; + + -- Certain actions we do not want the user to perform, so we need to + -- catch them here. + local pubsub = stanza:get_child("pubsub", namespaces.pubsub); + if pubsub then + local items = pubsub:get_child("items"); + if items then + if not channel:may_retrieve_items(from, items.attr.node) then + origin:send(pubsub_not_implemented(stanza, "retrieve-items")); + return true; + end + end + + local publish = pubsub:get_child("publish"); + if publish then + if not channel:may_publish(from, publish.attr.node) then + origin:send(pubsub_not_implemented(stanza, "publish")); + return true; + end + end + + local retract = pubsub:get_child("retract"); + if retract then + if not channel:may_retract(from, retract.attr.node) then + origin:send(pubsub_not_implemented(stanza, "delete-items")); + return true; + end + end + + -- We generally do not allow deletion, creation or configuration of + -- nodes. (Un)Subscribing is not allowed as this is managed via + -- interaction with the MIX host. + -- NOTE: Checking for is not needed as no user is ever set as + -- owner + if pubsub:get_child("configure") then + origin:send(pubsub_not_implemented(stanza, "config-node")); + return true; + end + if pubsub:get_child("unsubscibe") or pubsub:get_child("subscribe") then + origin:send(st.error_reply(stanza, "auth", "forbidden")); + return true; end end -end); -module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event) - module:log("debug", "IQ-GET disco#items"); + -- TODO: Process publishing to :config - local origin, stanza = event.origin, event.stanza; - if stanza:get_child("query", "http://jabber.org/protocol/disco#items").attr.node ~= "mix" then - origin.send(st.error_reply(stanza, "modify", "bad-request")); - return true; - end - - -- TODO: Maybe here we should check if the user has permissions to get infos - -- about the channel before saying that it doesn't exist to prevent creating - -- an oracle. - local _, channel = get_channel(stanza.attr.to); - if not channel then - origin.send(channel_not_found(stanza)); - return true; - end - - if not channel:is_participant(jid.bare(stanza.attr.from)) then - origin.send(st.error_reply(stanza, "cancel", "forbidden")); - return true; - end - - local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" }); - for _, node in pairs(channel.nodes) do - reply:tag("item", { jid = channel.jid, node = node }):up(); - end - - origin.send(reply); - return true; -end); + local service = channel:get_pubsub_service(); + return lib_pubsub.handle_pubsub_iq(event, service); +end +module:hook("iq/bare/"..namespaces.pubsub..":pubsub", handle_pubsub_iq, 1000); local function can_create_channels(user) -- Returns true when the jid is allowed to create MIX channels. False otherwise. @@ -234,6 +185,52 @@ local function can_create_channels(user) return true; end + +-- Disco related functionality +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); + +local function handle_channel_disco_items(event) + module:log("debug", "IQ-GET disco#items"); + + local origin, stanza = event.origin, event.stanza; + if stanza:get_child("query", "http://jabber.org/protocol/disco#items").attr.node ~= "mix" then + origin.send(st.error_reply(stanza, "modify", "bad-request")); + return true; + end + + -- TODO: Maybe here we should check if the user has permissions to get infos + -- about the channel before saying that it doesn't exist to prevent creating + -- an oracle. + local channel = find_channel(stanza.attr.to); + if not channel then + origin.send(lib_mix.channel_not_found(stanza)); + return true; + end + + if not channel:is_participant(jid.bare(stanza.attr.from)) then + origin.send(st.error_reply(stanza, "cancel", "forbidden")); + return true; + end + + local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" }); + for _, node in pairs(channel.nodes) 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#items:query", handle_channel_disco_items); + module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event) module:log("debug", "IQ-GET host disco#info"); @@ -250,15 +247,15 @@ module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function( end origin.send(reply); return true; -end); +end, 1000); -module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event) +local function handle_channel_disco_info(event) module:log("debug", "IQ-GET disco#info"); local origin, stanza = event.origin, event.stanza; - local _, channel = get_channel(stanza.attr.to); + local channel = find_channel(stanza.attr.to); if not channel then - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }); @@ -270,15 +267,16 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( origin.send(reply); return true; -end); +end +module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", handle_channel_disco_info); module:hook("iq-set/bare/"..namespaces.mam..":query", function(event) local stanza, origin = event.stanza, event.origin; local channel_jid = stanza.attr.to; - local j, channel = get_channel(channel_jid); - if j == -1 then + local channel = find_channel(channel_jid); + if not channel then -- TODO: Is this correct? - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end @@ -294,7 +292,7 @@ module:hook("iq-set/bare/"..namespaces.mam..":query", function(event) local x = query:get_child("x", "jabber:x:data"); if x ~= nil then -- TODO: Error handling - local form, err = mam_query_form:data(x); + local form, err = lib_forms.mam_query:data(x); -- Validate if (form["start"] and not form["end"]) or (not form["start"] and form["end"]) then @@ -351,14 +349,14 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":leave", function(event) module:log("debug", "MIX leave received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); - local _, channel = get_channel(stanza.attr.to); + local channel = find_channel(stanza.attr.to); if not channel then - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end - local j, participant = channel:find_participant(from); - if j == -1 then + local participant = channel:find_participant(from); + if not participant then origin.send(st.error_reply(stanza, "cancel", "forbidden", @@ -381,21 +379,21 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); - local _, channel = get_channel(stanza.attr.to); + local channel = find_channel(stanza.attr.to); if not channel then - origin:send(channel_not_found(stanza)); + origin:send(lib_mix.channel_not_found(stanza)); return true; end -- Prevent the user from joining multiple times - local j, _ = channel:find_participant(from); - if j ~= -1 then + local participant = channel:find_participant(from); + if participant then module:send(st.error_reply(stanza, "cancel", "bad-request", "User already joined")); return true; end -- Is the user allowed to join? - if not channel:may(from, "join", nil) then + if not channel:may_join(from) then origin:send(st.error_reply(stanza, "cancel", "forbidden", "User or host is banned")); return true; end @@ -406,11 +404,7 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) local join = stanza:get_child("join", namespaces.mix_core); local nick_tag = join:get_child("nick"); - -- Check if the channel has mandatory nicks - if channel.config["Mandatory Nicks"] and not nick_tag then - origin:send(st.error_reply(stanza, "modify", "forbidden", "Nicks are mandatory")); - return true; - end + -- TODO: Check if the channel has mandatory nicks local nick; if not nick_tag then @@ -420,7 +414,7 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) end module:log("debug", "User joining as nick %s", nick); - local srv = pep.get_pep_service(jid.node(channel.jid)); + local srv = channel:get_pubsub_service(jid.node(channel.jid)); local nodes = {}; local has_subscribed_once = false; local first_error = nil; @@ -429,15 +423,6 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) -- May the user subscribe to the node? module:log("debug", "Subscribing user to node %s", subscribe.attr.node); if channel:may_subscribe(from, subscribe.attr.node, true) then - -- Set the correct affiliation - local affiliation = ""; - if owner_or_admin then - affiliation = "publisher"; - else - affiliation = "member"; - end - srv:set_affiliation(subscribe.attr.node, true, from, affiliation); - local ok, err = srv:add_subscription(subscribe.attr.node, true, from); if not ok then module:log("debug", "Error during subscription: %s", err); @@ -447,15 +432,14 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) if first_error ~= nil then first_error = err; end - - -- Reset affiliation - srv:set_affiliation(subscribe.attr.node, true, from, "outcast"); - else table.insert(nodes, subscribe.attr.node); reply:tag("subscribe", { node = subscribe.attr.node }):up(); has_subscribed_once = true; end + + -- Set the correct affiliation + channel:set_affiliation(subscribe.attr.node, from, "member"); else module:log("debug", "Error during subscription: may_subscribe returned false"); if first_error ~= nil then @@ -470,27 +454,9 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) return true; end - -- TODO: Make the default configurable - local config = mixlib.default_participant_configuration; - local x = join:get_child("x"); - if x ~= nil then - -- TODO: Error handling? - local form, err = anon.form:data(x); - if form["JID Visibility"] then - config["JID Visibility"] = form["JID Visibility"]; - end - if form["Private Messages"] then - config["Private Messages"] = form["Private Messages"]; - end - if form["Presence"] then - config["Presence"] = form["Presence"]; - end - if form["vCard"] then - config["vCard"] = form["vCard"]; - end - end + -- TODO: Participant configuration - local participant = Participant:new(jid.bare(from), nick, config); + local participant = Participant:new(jid.bare(from), nick, {}); channel.subscriptions[from] = nodes; table.insert(channel.participants, participant) channel:set_spid(jid.bare(stanza.attr.from), spid); @@ -510,14 +476,14 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":setnick", function(event) module:log("debug", "MIX setnick received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); - local _, channel = get_channel(stanza.attr.to); + local channel = find_channel(stanza.attr.to); if not channel then - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end - local j, participant = channel:find_participant(from); - if j == -1 then + local participant = channel:find_participant(from); + if not participant then channel:debug_print(); module:log("debug", "%s is not a participant in %s", from, channel.jid); return true; @@ -532,7 +498,7 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":setnick", function(event) end -- Change the nick - channel.participants[j].nick = nick:get_text(); + participant.nick = nick:get_text(); -- Inform all other members channel:publish_participant(channel:get_spid(participant.jid), participant); @@ -545,19 +511,6 @@ module:hook("iq-set/bare/"..namespaces.mix_core..":setnick", function(event) return true; end); -function Channel:publish_info(srv) - local timestamp = datetime.datetime(time.now()); - -- TODO: Check if this is correct - local info = st.stanza("item", { id = timestamp, xmlns = "http://jabber.org/protocol/pubsub" }) - :add_child(mix_info_form:form({ - ["FORM_TYPE"] = "hidden", - ["Name"] = self.name, - ["Description"] = self.description, - ["Contacts"] = self.contacts - }, "result")); - srv:publish(namespaces.info, true, timestamp, info); -end - local function create_channel(node, creator, adhoc) -- TODO: Now all properties from the admin dataform are covered local channel = Channel:new(string.format("%s@%s", node, host), -- Channel JID @@ -572,54 +525,23 @@ local function create_channel(node, creator, adhoc) adhoc, -- Is channel an AdHoc channel {}, -- Allowed {}, -- Banned - mixlib.default_channel_configuration, -- Channel config + lib_mix.default_channel_configuration, -- Channel config {}); -- Present nodes - -- Create the PEP nodes - local srv = pep.get_pep_service(node); - -- MIX-CORE + -- Create the PubSub nodes + local srv = channel:get_pubsub_service(); for _, psnode in ipairs(default_mix_nodes) do srv:create(psnode, true, { - ["access_model"] = "whitelist", - ["persist_items"] = true, + -- NOTE: Our custom PubSub service is persistent only, so we don't + -- need to explicitly set it + ["access_model"] = lib_mix.get_node_access_model(psnode, adhoc); + ["max_items"] = lib_mix.get_node_max_items(psnode); }); + channel:set_affiliation(psnode, creator, "creator"); + table.insert(channel.nodes, psnode); end + channel:publish_info(srv); - - -- MIX-ADMIN - local admin_nodes = array { namespaces.banned }; - if adhoc then - admin_nodes:push(namespaces.allowed); - end - for _, psnode in pairs(admin_nodes) do - srv:create(psnode, true, { - ["access_model"] = "whitelist", - ["persist_items"] = true, - }); - srv:set_affiliation(namespaces.allowed, true, creator, "publish"); - end - if adhoc then - -- Allow the creator to join - channel:allow_jid(creator); - end - - -- Some bookkeeping - local nodes = default_mix_nodes + admin_nodes; - local node_names = array(nodes):filter(function (element) - return element ~= namespaces.config; - end):map(mix_admin_node_to_value):push("'avatar'"); - -- NOTE: This is to prevent cycles during serialization (apparently) - channel.nodes = {table.unpack(nodes)}; - - local config = mixlib.default_channel_configuration; - config["Owner"] = { creator }; - config["Nodes Present"] = node_names; - channel.config = config; - srv:publish(namespaces.config, - true, - "", - st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub" }) - :add_child(mix_config_form:form(config, "result"))); table.insert(channels, channel); end @@ -640,9 +562,8 @@ module:hook("iq-set/host/"..namespaces.mix_core..":create", function(event) -- Create non-adhoc channel module:log("debug", "Attempting to create channel %s", create.attr.channel); node = create.attr.channel; - local i, _ = get_channel(create.attr.channel.."@"..stanza.attr.to); - module:log("debug", "Channel index %s", i); - if i ~= -1 then + local channel = find_channel(create.attr.channel.."@"..stanza.attr.to); + if channel then origin.send(st.error_reply(stanza, "cancel", "conflict", @@ -655,8 +576,8 @@ module:hook("iq-set/host/"..namespaces.mix_core..":create", function(event) -- Create adhoc channel while (true) do node = id.short(); - local i, _ = get_channel(string.format("%s@%s", node, host)); - if i == -1 then + local ch = find_channel(string.format("%s@%s", node, host)); + if not ch then break; end end @@ -680,25 +601,31 @@ module:hook("iq-set/host/"..namespaces.mix_core..":destroy", function(event) local destroy = stanza:get_child("destroy", namespaces.mix_core); local node = destroy.attr.channel; local node_jid = string.format("%s@%s", node, host); - local i, channel = get_channel(node_jid); + local channel = find_channel(node_jid); if not channel then - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end - if not channel:is_owner(from) then + -- TODO(MIX-ADMIN): Check if the user is the owner of the channel + -- Until then, we just check if the user is in the contacts + if helpers.find_str(channel.contacts, from) == -1 then origin.send(st.error_reply(stanza, "cancel", "forbidden")); return true; end - -- Remove all registered nodes - local srv = pep.get_pep_service(node); - for _, pep_node in pairs(channel.nodes) do - srv:delete(pep_node, true); + if module:fire_event("mix-destroy-channel", { channel = channel }) then + return true; end - table.remove(channels, i); - module:fire_event("mix-destroy-channel", { channel = channel }); + -- Remove all registered nodes + local srv = channel:get_pubsub_service(); + for _, psnode in pairs(channel.nodes) do + srv:delete(psnode, true); + end + channels = array.filter(channels, function (c) return c.jid ~= node_jid end); + + module:fire_event("mix-channel-destroyed", { channel = channel }); module:log("debug", "Channel %s destroyed", node); origin.send(st.reply(stanza)); @@ -715,14 +642,14 @@ module:hook("message/bare", function(event) end local from = jid.bare(stanza.attr.from); - local _, channel = get_channel(stanza.attr.to); + local channel = find_channel(stanza.attr.to); if not channel then - origin.send(channel_not_found(stanza)); + origin.send(lib_mix.channel_not_found(stanza)); return true; end - local j, participant = channel:find_participant(from); - if j == -1 then + local participant = channel:find_participant(from); + if not participant then origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not a participant")); return true; end @@ -733,274 +660,3 @@ module:hook("message/bare", function(event) channel:broadcast_message(stanza, participant, message_archive); return true; end); - -local function handle_new_node(srv, channel, node) - if node == namespaces.allowed then - -- If the allowed node is created, make sure to add the owners - -- as allowed - - channel.allowed = {}; - for _, owner in pairs(channel.owners) do - array.push(channel.allowed, owner) - srv:publish(namespaces.allowed, true, owner, nil); - end - - -- Remove all non-allowed participants - for _, participant in channel.participants do - if not channel:is_allowed(participant.jid) then - channel:remove_participant(participant.jid); - end - end - end -end - --- MIX Admin events -local function handle_pubsub_publish(event, publish, channel) - local node = publish.attr.node; - local from = jid.bare(event.stanza.attr.from); - local item = publish:get_child("item"); - - -- TODO: Deal with avatars - -- TODO: Should we trust the clients to behave? - -- local srv = pep.get_pep_service(jid.node(channel.jid)); - if node == namespaces.config then - if not channel:may(from, "update", namespaces.config) then - -- TODO: This does not work - event.origin:send(st.error_reply(publish, "auth", "forbidden")); - return true; - end - - local config, err = mix_config_form:data(item); - -- TODO: Error handling? - - local srv = pep.get_pep_service(jid.node(channel.jid)); - -- TODO: Check this over - local removed_admins = set.new(channel.administrators):difference(set.new(config["Administrator"])); - local removed_owners = set.new(channel.owners):difference(set.new(config["Owner"])); - local new_node_names = set.new(config["Nodes Present"]):difference(set.new(channel.nodes)); - local removed_node_names = set.new(channel.nodes):difference(set.new(config["Nodes Present"])); - local new_nodes = array.map(new_node_names, mix_admin_value_to_node); -- TODO - local removed_nodes = array.map(removed_node_names, mix_admin_value_to_node); -- TODO - - -- TODO: Verify that this is actually safe - for _, jid_ in pairs(array.append(removed_admins, removed_owners)) do - for _, psnode in pairs(channel.nodes) do - srv:set_affiliation(psnode, true, jid_, "member"); - end - end - - -- TODO: Apply this to the PubSub publish - config["Last Change Made By"] = jid.bare(event.stanza.attr.from); - channel.owners = config["Owner"]; - channel.administrators = config["Administrator"]; - channel.config = config; - channel.nodes = config["Nodes Present"] - - -- Remove all nodes that are not existant anymore... - for _, removed_node in pairs(removed_nodes) do - for participant, subscriptions in channel.participants do - local j, _ = helpers.find(subscriptions, removed_node); - if j ~= -1 then - srv:remove_subscription(removed_node, true, from); - table.remove(participant.subscriptions, j); - end - end - - if removed_node == namespaces.allowed then - channel.allowed = nil; - elseif removed_node == namespaces.banned then - channel.banned = nil; - end - end - -- ... and create all new ones. - for _, new_node in pairs(new_nodes) do - srv:create(new_node, true, { - ["access_model"] = "whitelist", - ["persist_items"] = true, - }); - for participant, _ in channel.participants do - local affiliation = ""; - if channel:is_admin(participant.jid) or channel:is_owner(participant.jid) then - affiliation = "publisher"; - else - affiliation = "member"; - end - - srv:set_affiliation(new_node, true, affiliation); - end - - handle_new_node(srv, channel, new_node); - end - - for _, jid_ in pairs(array.append(channel.owners, channel.administrators)) do - for _, psnode in pairs(channel.nodes) do - srv:set_affiliation(psnode, true, jid_, "publisher"); - end - end - elseif node == namespaces.info then - if not channel:may(from, "update", namespaces.info) then - -- TODO: This does not work - event.origin:send(st.error_reply(publish, "auth", "forbidden")); - return true; - end - - local info, err = mix_info_form:data(item); - -- TODO: Error handling - if info["Name"] then - channel.name = info["Name"]; - end - if info["Description"] then - channel.description = info["Description"]; - end - if info["Contact"] then - channel.contacts = info["Contact"]; - end - - module:log("debug", "Channel info updated"); - elseif node == namespaces.banned then - channel:ban_jid(publish:get_child("item").attr.id); - elseif node == namespaces.allowed then - channel:allow_jid(publish:get_child("item").attr.id); - elseif node == namespaces.participants or node == namespaces.messages then - -- We don't want admins or owners to publish to these nodes as they - -- are managed by the module - local reply = st.error_reply(event.stanza, "cancel", "feature-not-implemented"); - reply:tag("unsupported", { - xmlns = "http://jabber.org/protocol/pubsub#errors", - feature = "publish", - }); - event.origin:send(reply); - return true; - end - - -- Let the actual PubSub implementation handle the rest - channel:save_state(); -end - -local function handle_pubsub_retract(event, retract, channel) - local node = retract.attr.node; - local item = retract:get_child("item"); - if node == namespaces.banned then - channel.banned = array.filter(channel.banned, function (element) - return element ~= item.attr.id; - end); - elseif node == namespaces.allowed then - channel.allowed = array.filter(channel.allowed, function (element) - return element ~= item.attr.id; - end); - elseif node == namespaces.participants then - local reply = st.error_reply(event.stanza, "cancel", "feature-not-implemented"); - reply:tag("unsupported", { - xmlns = "http://jabber.org/protocol/pubsub#errors", - feature = "delete-items" - }); - event.origin:send(reply); - return true; - end - - channel:save_state(); -end - -local function handle_pubsub_subscribe(event, subscribe, channel) - local from = jid.bare(event.stanza.attr.from); - local origin = event.origin; - local node = subscribe.attr.node; - - -- Check for node existance - local i, _ = helpers.find(channel.nodes, node); - if i == -1 then - -- Let the actual PubSub implementation handle it - return; - end - - if not channel:may(from, "subscribe", node) then - origin:send(st.error_reply(event.stanza, "auth", "forbidden")); - return true; - end - - table.insert(channel.subscriptions[from], node); - channel:save_state(); - -- The rest is handled by the actual PubSub implementation -end - -local function handle_pubsub_unsubscribe(event, subscribe, channel) - local from = jid.bare(event.stanza.attr.from); - local origin = event.origin; - local node = subscribe.attr.node; - - -- Check for node existance - local i, _ = helpers.find(channel.nodes, node); - if i == -1 then - -- Let the actual PubSub implementation handle it - return; - end - - if not channel:may(from, "subscribe", node) then - origin:send(st.error_reply(event.stanza, "auth", "forbidden")); - return true; - end - - local j, _ = helpers.find(channel.subscriptions[from], node); - if j == -1 then - local errstanza = st.error_reply(event.stanza, "cancel", "unexpected-request"); - errstanza:tag("not-subscribed", { xmlns = "http://jabber.org/protocol/pubsub#errors" }); - origin:send(errstanza); - return true; - end - table.remove(channel.subscriptions[from], j); - channel:save_state(); - -- The rest is handled by the actual PubSub implementation -end - -local function handle_pubsub_items(event, items, channel) - local node = items.attr.node; - local from = jid.bare(event.stanza.attr.from); - if node == namespaces.config then - if not channel:may(from, "access", node) then - return true; - end - end -end - -module:hook("iq-get/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) - local stanza = event.stanza; - local _, channel = get_channel(stanza.attr.to); - if not channel then - return; - end - - local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); - local items = pubsub:get_child("items"); - if items then - return handle_pubsub_items(event, items, channel); - end -end, 1000); - -module:hook("iq-set/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) - local stanza = event.stanza; - local _, channel = get_channel(stanza.attr.to); - if not channel then - return; - end - - local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); - local publish = pubsub:get_child("publish"); - if publish then - return handle_pubsub_publish(event, publish, channel); - end - - local subscribe = pubsub:get_child("subscribe"); - if subscribe then - return handle_pubsub_subscribe(event, subscribe, channel); - end - - local unsubscribe = pubsub:get_child("unsubscribe"); - if unsubscribe then - return handle_pubsub_unsubscribe(event, subscribe, channel); - end - - local retract = pubsub:get_child("retract"); - if retract then - return handle_pubsub_retract(event, retract, channel); - end -end, 1000); diff --git a/mod_mix/namespaces.lib.lua b/mod_mix/namespaces.lib.lua index cc64f87..eebe9c3 100644 --- a/mod_mix/namespaces.lib.lua +++ b/mod_mix/namespaces.lib.lua @@ -4,6 +4,7 @@ return { mix_core = "urn:xmpp:mix:core:1"; mix_anon = "urn:xmpp:mix:anon:0"; mix_admin = "urn:xmpp:mix:admin:0"; + mix_presence = "urn:xmpp:mix:presence:0"; -- MAM mam = "urn:xmpp:mam:2"; -- User Avatar @@ -11,9 +12,14 @@ return { avatar_metadata = "urn:xmpp:avatar:metadata"; -- MIX PubSub nodes messages = "urn:xmpp:mix:nodes:messages"; + presence = "urn:xmpp:mix:nodes:presence"; participants = "urn:xmpp:mix:nodes:participants"; info = "urn:xmpp:mix:nodes:info"; allowed = "urn:xmpp:mix:nodes:allowed"; banned = "urn:xmpp:mix:nodes:banned"; config = "urn:xmpp:mix:nodes:config"; + -- PubSub + pubsub = "http://jabber.org/protocol/pubsub"; + pubsub_event = "http://jabber.org/protocol/pubsub#event"; + pubsub_error = "http://jabber.org/protocol/pubsub#errors"; };