-- 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 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 dataforms = require("util.dataforms"); local array = require("util.array"); local set = require("util.set"); local pep = module:depends("pep"); local helpers = module:require("mix/helpers"); local namespaces = module:require("mix/namespaces"); local anon = module:require("mix/anon"); local mixlib = module:require("mix/mix"); local Channel = mixlib.Channel; local Participant = mixlib.Participant; -- Persistent data local persistent_channels = module:open_store("mix_channels", "keyval"); local persistent_channel_data = module:open_store("mix_data", "keyval"); local message_archive = module:open_store("mix_log", "archive"); -- Configuration 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"); local restrict_channel_creation = module:get_option("restrict_local_channels", "local"); 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 channels = {}; local function get_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); end local 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() -- Store the channel in the persistent channel store module:log("debug", "Saving channel %s...", self.jid); persistent_channel_data:set(self.jid, self); module:log("debug", "Saving 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_data in pairs(channel_list) do local channel = Channel:from(persistent_channel_data:get(channel_data)); table.insert(channels, channel); module:log("debug", "MIX channel %s loaded", channel.jid); end else module:log("debug", "No MIX channels found."); end 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"); 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; 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 function can_create_channels(user) -- Returns true when the jid is allowed to create MIX channels. False otherwise. -- NOTE: Taken from plugins/muc/mod_muc.lua local host_suffix = host:gsub("^[^%.]+%.", ""); if restrict_channel_creation == "local" then module:log("debug", "Comparing %s (Sender) to %s (Host)", jid.host(user), host_suffix); if jid.host(user) == host_suffix then return true; else return false; end elseif type(restrict_channel_creation) == "table" then if helpers.find_str(restrict_channel_creation, user) ~= -1 then -- User was specifically listed return true; elseif helpers.find_str(restrict_channel_creation, jid.host(user)) then -- User's host was allowed return true; end return false; end -- TODO: Handle also true/"admin" (See mod_muc) return true; end module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event) module:log("debug", "IQ-GET host disco#info"); local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza) :tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) :tag("identity", { category = "conference", type = "mix", name = service_name }):up() :tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up() :tag("feature", { var = "http://jabber.org/protocol/disco#items" }):up() :tag("feature", { var = namespaces.mix_core }):up(); if can_create_channels(stanza.attr.from) then reply:tag("feature", { var = namespaces.mix_core.."#create-channel" }):up(); end origin.send(reply); return true; end); module:hook("iq-get/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 true; 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 = namespaces.mix_core }):up(); reply:tag("feature", { var = "urn:xmpp:mam:2" }):up(); origin.send(reply); return true; end); 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 -- TODO: Is this correct? origin.send(channel_not_found(stanza)); return true; end -- Check if the user is subscribed to the messages node if not channel:is_subscribed(stanza.attr.from, namespaces.messages) then origin.send(st.error_reply(stanza, "cancel", "forbidden")); return true; end local query = stanza:get_child("query", namespaces.mam); local filter = {}; local query_id = query.attr.queryid; local x = query:get_child("x", "jabber:x:data"); if x ~= nil then -- TODO: Error handling local form, err = mam_query_form:data(x); -- Validate if (form["start"] and not form["end"]) or (not form["start"] and form["end"]) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp")); return true; end module:log("debug", "Got a MAM query between %s and %s", form["start"], form["end"]); filter = { start = datetime.parse(form["start"]); ["end"] = datetime.parse(form["end"]) }; end local data, err = message_archive:find(channel_jid, filter); if not data then module:log("debug", "MAM error: %s", err); if err == "item-not-found" then origin.send(st.error_reply(stanza, "modify", "item-not-found")); else origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); end return true; end for message_id, item, when in data do local msg = st.stanza("message", { from = channel_jid, to = stanza.attr.from, type = "groupchat" }) :tag("result", { xmlns = namespaces.mam, queryid = query_id, id = message_id }) :tag("forwarded", { xmlns = "urn:xmpp:forward:0" }) :tag("delay", { xmlns = "urn:xmpp:delay", stamp = datetime.datetime(when) }):up(); msg:add_child(item); origin.send(msg); end return true; end); module:hook("iq-get/bare/"..namespaces.mam..":query", function(event) if event.stanza.attr.id ~= "form1" then return; end module:log("debug", "Got a MAM query for supported fields"); -- TODO: Use dataforms:... local ret = st.reply(event.stanza) :tag("query", { xmlns = namespaces.mam }) :tag("x", { xmlns = "jabber:x:data", type = "form"}) :tag("field", { type = "hidden", var = "FORM_TYPE" }) :tag("value"):text(namespaces.mam):up():up() :tag("field", { type = "jid-single", var = "with" }):up() :tag("field", { type = "text-single", var = "start" }):up() :tag("field", { type = "text-single", var = "end" }); module:send(ret); return true; end); 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); if not channel then origin.send(channel_not_found(stanza)); return true; end local j, participant = channel:find_participant(from); if j == -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 true; end channel:remove_participant(from); module:fire_event("mix-channel-leave", { channel = channel, participant = participant }); origin.send(st.reply(stanza):tag("leave", { xmlns = namespaces.mix_core })); return true; end); module:hook("iq-set/bare/"..namespaces.mix_core..":join", function(event) module:log("debug", "MIX join received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); local _, channel = get_channel(stanza.attr.to); if not channel then origin:send(channel_not_found(stanza)); return true; end -- Prevent the user from joining multiple times local j, _ = channel:find_participant(from); if j ~= -1 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 origin:send(st.error_reply(stanza, "cancel", "forbidden", "User or host is banned")); return true; end local spid = channel:get_spid(from) or uuid.generate(); -- Stable Participant ID local reply = st.reply(stanza) :tag("join", { xmlns = namespaces.mix_core, id = spid }); 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 local nick; if not nick_tag then nick = jid.node(from); else nick = nick_tag:get_text(); end module:log("debug", "User joining as nick %s", nick); local srv = pep.get_pep_service(jid.node(channel.jid)); local nodes = {}; local has_subscribed_once = false; local first_error = nil; local owner_or_admin = channel:is_admin(from) or channel:is_owner(from); for subscribe in join:childtags("subscribe") do -- 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); -- MIX-CORE says that the first error should be returned when -- no of the requested nodes could be subscribed to 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 else module:log("debug", "Error during subscription: may_subscribe returned false"); if first_error ~= nil then first_error = "Channel does not allow subscribing"; end end end if not has_subscribed_once then -- TODO: This does not work origin:send(st.error_reply(stanza, "cancel", first_error)); 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 local participant = Participant:new(jid.bare(from), nick, config); channel.subscriptions[from] = nodes; table.insert(channel.participants, participant) channel:set_spid(jid.bare(stanza.attr.from), spid); channel:publish_participant(spid, participant); channel:save_state(); module:fire_event("mix-channel-join", { channel = channel, participant = participant }); -- We do not reuse nick_tag as it might be nil reply:tag("nick"):text(nick):up(); reply:add_child(anon.form:form(config, "result")); origin.send(reply); return true end); 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); if not channel then origin.send(channel_not_found(stanza)); return true; end local j, participant = channel:find_participant(from); if j == -1 then channel:debug_print(); module:log("debug", "%s is not a participant in %s", from, channel.jid); return true; end -- NOTE: Prosody should guarantee us that the setnick stanza exists local setnick = stanza:get_child("setnick", namespaces.mix_core); local nick = setnick:get_child("nick"); if nick == nil then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Missing ")); return true; end -- Change the nick channel.participants[j].nick = nick:get_text(); -- Inform all other members channel:publish_participant(channel:get_spid(participant.jid), participant); module:fire_event("mix-change-nick", { channel = channel, participant = participant }); origin.send(st.reply(stanza) :tag("setnick", { xmlns = namespaces.mix_core }) :tag("nick"):text(nick:get_text())); channel:save_state(); 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 default_channel_name, default_channel_description, {}, -- Participants {}, -- Administrators { creator }, -- Owners {}, -- Subscriptions {}, -- SPID mapping { creator }, -- Contacts adhoc, -- Is channel an AdHoc channel {}, -- Allowed {}, -- Banned mixlib.default_channel_configuration, -- Channel config {}); -- Present nodes -- Create the PEP nodes local srv = pep.get_pep_service(node); -- MIX-CORE for _, psnode in ipairs(default_mix_nodes) do srv:create(psnode, true, { ["access_model"] = "whitelist", ["persist_items"] = true, }); 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 module:hook("iq-set/host/"..namespaces.mix_core..":create", function(event) module:log("debug", "MIX create received"); local origin, stanza = event.origin, event.stanza; local from = jid.bare(stanza.attr.from); -- Check permissions if not can_create_channels(from) then origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not authorized to create channels")); return true; end local create = stanza:get_child("create", namespaces.mix_core); local node; if create.attr.channel ~= nil then -- 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 origin.send(st.error_reply(stanza, "cancel", "conflict", "Channel already exists")); return true; end create_channel(create.attr.channel, from, false); else -- Create adhoc channel while (true) do node = id.short(); local i, _ = get_channel(string.format("%s@%s", node, host)); if i == -1 then break; end end create_channel(node, from, true); end module:log("debug", "Channel %s created with %s as owner", node, from); -- TODO: Add an event origin.send(st.reply(stanza) :tag("create", { xmlns = namespaces.mix_core, channel = node })); save_channels(); return true; end); module:hook("iq-set/host/"..namespaces.mix_core..":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("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); if not channel then origin.send(channel_not_found(stanza)); return true; end if not channel:is_owner(from) 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); end table.remove(channels, i); module:fire_event("mix-destroy-channel", { channel = channel }); 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 received"); local stanza, origin = event.stanza, event.origin; if stanza.attr.type ~= "groupchat" then origin.send(st.error_reply(stanza, "modify", "bad-request", "Non-groupchat message")); return true; end local from = jid.bare(stanza.attr.from); local _, channel = get_channel(stanza.attr.to); if not channel then origin.send(channel_not_found(stanza)); return true; end local j, participant = channel:find_participant(from); if j == -1 then origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not a participant")); return true; end -- Handles sending the message accordingly, firing an event and -- even doing nothing if an event handler for "mix-broadcast-message" -- returns true. 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);