mod_mix: Various changes
- Refactor and clean code - Implement lots of MIX-ADMIN - Beginning of MIX-ANON
This commit is contained in:
		
							parent
							
								
									c7b9dc2432
								
							
						
					
					
						commit
						fd9735faba
					
				
							
								
								
									
										14
									
								
								mod_mix/anon.lib.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								mod_mix/anon.lib.lua
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| local dataforms = require("util.dataforms"); | ||||
| local namespaces = module:require("mix/namespaces"); | ||||
| 
 | ||||
| local mix_anon_form = dataforms.new({ | ||||
|     { name = "FORM_TYPE", type = "hidden", value = namespaces.anon }, | ||||
|     { name = "JID Visibility", type = "text-single" }, | ||||
|     { name = "Private Messages", type = "text-single" }, | ||||
|     { name = "Presence", type = "text-single" }, | ||||
|     { name = "vCard", type = "text-single" }, | ||||
| }); | ||||
| 
 | ||||
| return { | ||||
|     form = mix_anon_form; | ||||
| }; | ||||
| @ -13,11 +13,18 @@ end | ||||
| 
 | ||||
| local function find_str(array, str) | ||||
|     -- Returns the index of str in array. -1 if array does not contain str | ||||
|     local i, _ = find(array, function(v) return v == str end); | ||||
|     local i, _ = find(array, function(v) return v == str; end); | ||||
|     return i; | ||||
| end | ||||
| 
 | ||||
| local function in_array(array, element) | ||||
|     -- Returns true if element is in array. False otherwise. | ||||
|     local i, _ = find(array, function(v) return v == element; end); | ||||
|     return i ~= -1; | ||||
| end | ||||
| 
 | ||||
| return { | ||||
|     find_str = find_str, | ||||
|     find = find, | ||||
|     in_array = in_array, | ||||
| }; | ||||
|  | ||||
| @ -1,12 +1,20 @@ | ||||
| local st = require("util.stanza"); | ||||
| local array = require("util.array"); | ||||
| local jid_lib = require("util.jid"); | ||||
| local uuid = require("util.uuid"); | ||||
| local time = require("util.time"); | ||||
| local helpers = module:require("mix/helpers"); | ||||
| local namespaces = module:require("mix/namespaces"); | ||||
| local pep = module:depends("pep"); | ||||
| 
 | ||||
| Participant = {}; | ||||
| 
 | ||||
| local Participant = {}; | ||||
| Participant.__index = Participant; | ||||
| function Participant:new(jid, nick) | ||||
| function Participant:new(jid, nick, config) | ||||
|     return setmetatable({ | ||||
|         jid = jid, | ||||
|         nick = nick, | ||||
|         config = config, | ||||
|     }, Participant); | ||||
| end | ||||
| 
 | ||||
| @ -14,9 +22,9 @@ function Participant:from(config) | ||||
|     return setmetatable(config, Participant); | ||||
| end | ||||
| 
 | ||||
| Channel = {} | ||||
| local Channel = {}; | ||||
| Channel.__index = Channel; | ||||
| function Channel:new(jid, name, description, participants, subscriptions, spid, contacts, adhoc) | ||||
| function Channel:new(jid, name, description, participants, administrators, owners, subscriptions, spid, contacts, adhoc, allowed, banned, config, nodes) | ||||
|     return setmetatable({ | ||||
|         jid = jid, | ||||
|         name = name, | ||||
| @ -26,6 +34,12 @@ function Channel:new(jid, name, description, participants, subscriptions, spid, | ||||
|         spid = spid, | ||||
|         contacts = contacts, | ||||
|         adhoc = adhoc, | ||||
|         administrators = administrators, | ||||
|         owners = owners, | ||||
|         config = config, | ||||
|         nodes = nodes, | ||||
|         allowed = allowed, | ||||
|         banned = banned, | ||||
|     }, Channel); | ||||
| end | ||||
| function Channel:from(config) | ||||
| @ -88,7 +102,322 @@ 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 | ||||
|     local msg = st.clone(message); | ||||
|     msg:add_child(st.stanza("mix", { xmlns = namespaces.mix_core }) | ||||
|                     :tag("nick"):text(participant.nick):up() | ||||
|                     :tag("jid"):text(participant.jid):up()); | ||||
| 
 | ||||
|     -- Put the message into the archive | ||||
|     local mam_id = uuid.generate(); | ||||
|     msg.attr.id = mam_id; | ||||
|     -- NOTE: The spec says to do so | ||||
|     msg.attr.from = self.jid; | ||||
|     archive:append(message.attr.to, mam_id, msg, time.now()); | ||||
|     msg.attr.from = self.jid.."/"..self:get_spid(participant.jid); | ||||
| 
 | ||||
|     if module:fire_event("mix-broadcast-message", { message = msg, channel = self }) then | ||||
|         return; | ||||
|     end | ||||
| 
 | ||||
|     for _, p in pairs(self.participants) do | ||||
|         -- Only users who subscribed to the messages node should receive | ||||
|         -- messages | ||||
|         if self:is_subscribed(p.jid, namespaces.messages) then | ||||
|             local tmp = st.clone(msg); | ||||
|             tmp.attr.to = p.jid; | ||||
|             module:send(tmp); | ||||
|         end | ||||
|     end | ||||
| 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 | ||||
|     srv:set_node_config(namespaces.participants, | ||||
|                         true, | ||||
|                         { ["max_items"] = #self.participants }); | ||||
|     srv:publish(namespaces.participants, | ||||
|                 true, | ||||
|                 spid, | ||||
|                 st.stanza("item", { id = spid, xmlns = "http://jabber.org/protocol/pubsub" }) | ||||
|                     :tag("participant", { xmlns = namespaces.mix_core }) | ||||
|                         :tag("nick"):text(participant["nick"]):up() | ||||
|                         :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; | ||||
| }; | ||||
| 
 | ||||
| local default_participant_configuration = { | ||||
|     ["JID Visibility"] = "never"; | ||||
|     ["Private Messages"] = "allow"; | ||||
|     ["Presence"] = "share"; | ||||
|     ["vCard"] = "block"; | ||||
| }; | ||||
| 
 | ||||
| return { | ||||
|     Channel = Channel, | ||||
|     Participant = Participant | ||||
|     Participant = Participant, | ||||
|     default_channel_configuration = default_channel_configuration, | ||||
|     default_participant_configuration = default_participant_configuration, | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| -- Big TODOlist | ||||
| -- TODO: Channel:is_subscribed could be replaced by get_pep_service(...):get_subscription | ||||
| -- 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 | ||||
| @ -12,27 +13,18 @@ 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 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"); | ||||
| Channel = mixlib.Channel; | ||||
| Participant = mixlib.Participant; | ||||
| 
 | ||||
| -- XML namespaces | ||||
| local mix_core_xmlns = "urn:xmpp:mix:core:1"; | ||||
| local mix_anon_xmlns = "urn:xmpp:mix:anon:0"; | ||||
| --local mix_admin_xmlns = "urn:xmpp:mix:admin:0"; | ||||
| local mix_node_messages = "urn:xmpp:mix:nodes:messages"; | ||||
| local mix_node_participants = "urn:xmpp:mix:nodes:participants"; | ||||
| local mix_node_info = "urn:xmpp:mix:nodes:info"; | ||||
| --local mix_node_allowed = "urn:xmpp:mix:nodes:allowed"; | ||||
| --local mix_node_banned = "urn:xmpp:mix:nodes:banned"; | ||||
| --local mix_node_config = "urn:xmpp:mix:nodes:config"; | ||||
| 
 | ||||
| local mam_xmlns = "urn:xmpp:mam:2"; | ||||
| local Channel = mixlib.Channel; | ||||
| local Participant = mixlib.Participant; | ||||
| 
 | ||||
| -- Persistent data | ||||
| local persistent_channels = module:open_store("mix_channels", "keyval"); | ||||
| @ -43,29 +35,29 @@ local message_archive = module:open_store("mix_log", "archive"); | ||||
| 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 = mam_xmlns }, | ||||
|     { name = "FORM_TYPE", type = "hidden", value = namespaces.mam }, | ||||
|     { name = "with", type = "jid-single" }, | ||||
|     { name = "start", type = "text-single" }, | ||||
|     { name = "end", type = "text-single" }, | ||||
| }); | ||||
| -- MIX Anon | ||||
| local mix_anon_form = dataforms.new({ | ||||
|     { name = "FORM_TYPE", type = "hidden", value = mix_anon_xmlns }, | ||||
|     { name = "JID Visibility", type = "text-single" }, | ||||
|     { name = "Private Messages", type = "text-single" }, | ||||
|     { name = "Presence", type = "text-single" }, | ||||
|     { name = "vCard", 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 = mix_admin_xmlns }, | ||||
|     { 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" }, | ||||
| @ -78,13 +70,45 @@ local mix_config_form = dataforms.new({ | ||||
|     { 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", module:get_option("name", "Prosody MIX service")); | ||||
| module:add_identity("conference", "mix", service_name); | ||||
| module:add_feature("http://jabber.org/protocol/disco#info"); | ||||
| module:add_feature(mix_core_xmlns); | ||||
| module:add_feature(namespaces.mix_core); | ||||
| 
 | ||||
| local channels = {}; | ||||
| 
 | ||||
| @ -108,27 +132,10 @@ local function save_channels() | ||||
| 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); | ||||
|     -- 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 state done.", self.jid); | ||||
| end | ||||
| 
 | ||||
| local function publish_participant(service, channel, spid, participant) | ||||
|     -- Publish a new participant on the service | ||||
|     -- NOTE: This function has be to called *after* the new participant | ||||
|     --       has been added to the channel.participants attay | ||||
|     service:set_node_config(mix_node_participants, | ||||
|                             true, | ||||
|                             { ["max_items"] = #channel.participants }); | ||||
|     service:publish(mix_node_participants, | ||||
|                     true, | ||||
|                     spid, | ||||
|                     st.stanza("item", { id = spid, xmlns = "http://jabber.org/protocol/pubsub" }) | ||||
|                         :tag("participant", { xmlns = mix_core_xmlns }) | ||||
|                             :tag("nick"):text(participant["nick"]):up() | ||||
|                             :tag("jid"):text(participant["jid"])); | ||||
|     module:log("debug", "Saving done.", self.jid); | ||||
| end | ||||
| 
 | ||||
| function module.load() | ||||
| @ -190,7 +197,7 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve | ||||
|     end | ||||
| 
 | ||||
|     local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" }); | ||||
|     for _, node in pairs({mix_node_messages, mix_node_participants, mix_node_info}) do | ||||
|     for _, node in pairs(channel.nodes) do | ||||
|         reply:tag("item", { jid = channel.jid, node = node }):up(); | ||||
|     end | ||||
| 
 | ||||
| @ -233,14 +240,13 @@ module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function( | ||||
|     local origin, stanza = event.origin, event.stanza; | ||||
|     local reply = st.reply(stanza) | ||||
|                     :tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) | ||||
|                         -- TODO: Name | ||||
|                         :tag("identity", { category = "conference", type = "mix", name = "" }):up() | ||||
|                         :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 = mix_core_xmlns }):up(); | ||||
|                         :tag("feature", { var = namespaces.mix_core }):up(); | ||||
| 
 | ||||
|     if can_create_channels(stanza.attr.from) then | ||||
|         reply:tag("feature", { var = mix_core_xmlns.."#create-channel" }):up(); | ||||
|         reply:tag("feature", { var = namespaces.mix_core.."#create-channel" }):up(); | ||||
|     end | ||||
|     origin.send(reply); | ||||
|     return true; | ||||
| @ -259,14 +265,14 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( | ||||
|     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 = mix_core_xmlns }):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/"..mam_xmlns..":query", function(event) | ||||
| 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); | ||||
| @ -277,12 +283,12 @@ module:hook("iq-set/bare/"..mam_xmlns..":query", function(event) | ||||
|     end | ||||
| 
 | ||||
|     -- Check if the user is subscribed to the messages node | ||||
|     if not channel:is_subscribed(stanza.attr.from, mix_node_messages) then | ||||
|     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", mam_xmlns); | ||||
|     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"); | ||||
| @ -314,7 +320,7 @@ module:hook("iq-set/bare/"..mam_xmlns..":query", function(event) | ||||
|     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 = mam_xmlns, queryid = query_id, id = message_id }) | ||||
|                         :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); | ||||
| @ -323,16 +329,17 @@ module:hook("iq-set/bare/"..mam_xmlns..":query", function(event) | ||||
|     return true; | ||||
| end); | ||||
| 
 | ||||
| module:hook("iq-get/bare/"..mam_xmlns..":query", function(event) | ||||
| 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 = mam_xmlns }) | ||||
|                     :tag("query", { xmlns = namespaces.mam }) | ||||
|                         :tag("x", { xmlns = "jabber:x:data", type = "form"}) | ||||
|                             :tag("field", { type = "hidden", var = "FORM_TYPE" }) | ||||
|                                 :tag("value"):text(mam_xmlns):up():up() | ||||
|                                 :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" }); | ||||
| @ -340,7 +347,7 @@ module:hook("iq-get/bare/"..mam_xmlns..":query", function(event) | ||||
|     return true; | ||||
| end); | ||||
| 
 | ||||
| module:hook("iq-set/bare/"..mix_core_xmlns..":leave", function(event) | ||||
| 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); | ||||
| @ -361,38 +368,22 @@ module:hook("iq-set/bare/"..mix_core_xmlns..":leave", function(event) | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
|     -- Remove the user as a participant by... | ||||
|     -- Unsubscribing | ||||
|     local srv = pep.get_pep_service(jid.node(channel.jid)); | ||||
|     for _, node in pairs(channel.subscriptions[from]) do | ||||
|         srv:set_affiliation(node, true, from, "outcast"); | ||||
|         srv:remove_subscription(node, true, from); | ||||
|         module:log("debug", "Unsubscribed %s from %s on %s", from, node, channel.jid); | ||||
|     end | ||||
|     channel.subscriptions[from] = nil; | ||||
|     -- Retracting the participation | ||||
|     local spid = channel:get_spid(from); | ||||
|     local notifier = st.stanza("retract", { id = spid }); | ||||
|     -- TODO: Maybe error handling | ||||
|     srv:retract(mix_node_participants, true, spid, notifier); | ||||
|     -- Removing the user | ||||
|     table.remove(channel.participants, j); | ||||
|     channel:save_state(); | ||||
|     channel:remove_participant(from); | ||||
| 
 | ||||
|     module:fire_event("mix-channel-leave", { channel = channel, participant = participant }); | ||||
| 
 | ||||
|     origin.send(st.reply(stanza):tag("leave", { xmlns = mix_core_xmlns })); | ||||
|     origin.send(st.reply(stanza):tag("leave", { xmlns = namespaces.mix_core })); | ||||
|     return true; | ||||
| end); | ||||
| 
 | ||||
| module:hook("iq-set/bare/"..mix_core_xmlns..":join", function(event) | ||||
| 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)); | ||||
|         origin:send(channel_not_found(stanza)); | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
| @ -403,12 +394,24 @@ module:hook("iq-set/bare/"..mix_core_xmlns..":join", function(event) | ||||
|         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 = mix_core_xmlns, id = spid }); | ||||
|     local srv = pep.get_pep_service(jid.node(stanza.attr.to)); | ||||
|     local join = stanza:get_child("join", mix_core_xmlns); | ||||
|                     :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); | ||||
| @ -417,86 +420,93 @@ module:hook("iq-set/bare/"..mix_core_xmlns..":join", function(event) | ||||
|     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); | ||||
|         -- TODO: Once MIX-ADMIN is implemented, we should check here what | ||||
|         -- affiliation we set, e.g. if the JID is the owner, then set owner. | ||||
|         srv:set_affiliation(subscribe.attr.node, true, from, "member"); | ||||
|         local ok, err = srv:add_subscription(subscribe.attr.node, true, from); | ||||
|         if not ok then | ||||
|             module:log("debug", "Error during subscription: %s", err); | ||||
|         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); | ||||
| 
 | ||||
|             -- 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; | ||||
|             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 | ||||
|             table.insert(nodes, subscribe.attr.node); | ||||
|             reply:tag("subscribe", { node = subscribe.attr.node }):up(); | ||||
|             has_subscribed_once = true; | ||||
|             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 | ||||
|         origin.send(st.error_reply(stanza, "cancel", first_error)); | ||||
|         -- TODO: This does not work | ||||
|         origin:send(st.error_reply(stanza, "cancel", first_error)); | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
|     -- TODO: Make the default configurable | ||||
|     local jid_visibility = "never"; -- default | ||||
|     local allow_pms = "allow"; | ||||
|     local allow_vcards = "block"; | ||||
|     local share_presence = "share"; | ||||
|     local x = join:get_child("x", "jabber:x:data"); | ||||
|     local config = mixlib.default_participant_configuration; | ||||
|     local x = join:get_child("x"); | ||||
|     if x ~= nil then | ||||
|         -- TODO: Rethink naming | ||||
|         -- TODO: Error handling? | ||||
|         local form, err = mix_anon_form:data(x); | ||||
|         local form, err = anon.form:data(x); | ||||
|         if form["JID Visibility"] then | ||||
|             jid_visibility = form["JID Visibility"]; | ||||
|             config["JID Visibility"] = form["JID Visibility"]; | ||||
|         end | ||||
|         if form["Private Messages"] then | ||||
|             allow_pms = form["Private Messages"]; | ||||
|             config["Private Messages"] = form["Private Messages"]; | ||||
|         end | ||||
|         if form["Presence"] then | ||||
|             share_presence = form["Presence"]; | ||||
|             config["Presence"] = form["Presence"]; | ||||
|         end | ||||
|         if form["vCard"] then | ||||
|             allow_vcards = form["vCard"]; | ||||
|             config["vCard"] = form["vCard"]; | ||||
|         end | ||||
|     end | ||||
| 
 | ||||
|     local participant = Participant:new(jid.bare(from), nick, jid_visibility, allow_pms, share_presence, allow_vcards); | ||||
|     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); | ||||
|     publish_participant(srv, channel, spid, participant); | ||||
|     channel:publish_participant(spid, participant); | ||||
|     channel:save_state(); | ||||
| 
 | ||||
|     module:fire_event("mix-channel-join", { channel = channel, participant = participant }); | ||||
| 
 | ||||
|     reply:add_child(nick_tag); | ||||
|     reply:tag("x", { xmlns = "jabber:x:data", type = "result" }) | ||||
|         :tag("field", { var = "FORM_TYPE", type = "hidden" }) | ||||
|             :tag("value"):text(mix_anon_xmlns):up():up() | ||||
|         :tag("field", { var = "JID Visibility"}) | ||||
|             :tag("value"):text(jid_visibility):up():up() | ||||
|         :tag("field", { var = "Private Messages"}) | ||||
|             :tag("value"):text(allow_pms):up():up() | ||||
|         :tag("field", { var = "Presence"}) | ||||
|             :tag("value"):text(share_presence):up():up() | ||||
|         :tag("field", { var = "vCard"}) | ||||
|             :tag("value"):text(allow_vcards):up():up(); | ||||
| 
 | ||||
|     -- 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/"..mix_core_xmlns..":setnick", function(event) | ||||
| 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); | ||||
| @ -514,7 +524,7 @@ module:hook("iq-set/bare/"..mix_core_xmlns..":setnick", function(event) | ||||
|     end | ||||
| 
 | ||||
|     -- NOTE: Prosody should guarantee us that the setnick stanza exists | ||||
|     local setnick = stanza:get_child("setnick", mix_core_xmlns); | ||||
|     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 <nick>")); | ||||
| @ -524,14 +534,12 @@ module:hook("iq-set/bare/"..mix_core_xmlns..":setnick", function(event) | ||||
|     -- Change the nick | ||||
|     channel.participants[j].nick = nick:get_text(); | ||||
|     -- Inform all other members | ||||
|     local srv = pep.get_pep_service(jid.node(channel.jid)); | ||||
|     --local participant = channel.participants[participant_index]; | ||||
|     publish_participant(srv, channel, channel:get_spid(participant.jid), participant); | ||||
|     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 = mix_core_xmlns }) | ||||
|                     :tag("setnick", { xmlns = namespaces.mix_core }) | ||||
|                         :tag("nick"):text(nick:get_text())); | ||||
|     channel:save_state(); | ||||
|     return true; | ||||
| @ -539,32 +547,38 @@ 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" }) | ||||
|                     :tag("x", { xmlns = "jabber:x:data", type = "result" }) | ||||
|                         :tag("field", { var = "FORM_TYPE", type = "hidden" }) | ||||
|                             :tag("value"):text(mix_core_xmlns):up():up() | ||||
|                         :tag("field", { var = "Name" }) | ||||
|                             :tag("value"):text(self.name):up() | ||||
|                         :tag("field", { var = "Contact" }); | ||||
|     for _, contact in pairs(self.contacts) do | ||||
|         info:add_child(st.stanza("value"):text(contact)); | ||||
|     end | ||||
|     srv:publish(mix_node_info, true, timestamp, info); | ||||
|                     :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) | ||||
|     local channel = Channel:new(string.format("%s@%s", node, host), | ||||
|     -- 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, | ||||
|                                 {}, | ||||
|                                 {}, | ||||
|                                 {}, | ||||
|                                 { creator }, | ||||
|                                 adhoc); | ||||
|                                 {},             -- 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 pairs({ mix_node_info, mix_node_participants, mix_node_messages }) do | ||||
|     for _, psnode in ipairs(default_mix_nodes) do | ||||
|         srv:create(psnode, true, { | ||||
|             ["access_model"] = "whitelist", | ||||
|             ["persist_items"] = true, | ||||
| @ -572,28 +586,44 @@ local function create_channel(node, creator, adhoc) | ||||
|     end | ||||
|     channel:publish_info(srv); | ||||
| 
 | ||||
|     --[[ | ||||
|     -- MIX-ADMIN | ||||
|     local admin_nodes = { mix_node_banned, mix_node_config }; | ||||
|     local admin_nodes = array { namespaces.banned }; | ||||
|     if adhoc then | ||||
|         table.insert(admin_nodes, mix_node_allowed); | ||||
|         admin_nodes:push(namespaces.allowed); | ||||
|     end | ||||
|     for _, psnode in pairs(admin_nodes) do | ||||
|         srv:create(mix_node_allowed, true, { ["access_model"] = "whitelist" }); | ||||
|         srv:set_affiliation(mix_node_allowed, true, creator, "owner"); | ||||
|         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 | ||||
|         srv:publish(mix_node_allowed, | ||||
|                     true, | ||||
|                     nil, | ||||
|                     st.stanza("item", { id = creator })); | ||||
|         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/"..mix_core_xmlns..":create", function(event) | ||||
| 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); | ||||
| @ -604,7 +634,7 @@ module:hook("iq-set/host/"..mix_core_xmlns..":create", function(event) | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
|     local create = stanza:get_child("create", mix_core_xmlns); | ||||
|     local create = stanza:get_child("create", namespaces.mix_core); | ||||
|     local node; | ||||
|     if create.attr.channel ~= nil then | ||||
|         -- Create non-adhoc channel | ||||
| @ -637,17 +667,17 @@ module:hook("iq-set/host/"..mix_core_xmlns..":create", function(event) | ||||
|     -- TODO: Add an event | ||||
| 
 | ||||
|     origin.send(st.reply(stanza) | ||||
|                 :tag("create", { xmlns = mix_core_xmlns, channel = node })); | ||||
|                 :tag("create", { xmlns = namespaces.mix_core, channel = node })); | ||||
|     save_channels(); | ||||
|     return true; | ||||
| end); | ||||
| 
 | ||||
| module:hook("iq-set/host/"..mix_core_xmlns..":destroy", function(event) | ||||
| 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", mix_core_xmlns); | ||||
|     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); | ||||
| @ -656,15 +686,14 @@ module:hook("iq-set/host/"..mix_core_xmlns..":destroy", function(event) | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
|     -- TODO: Permission checks | ||||
|     -- TODO: Maybe make this configurable | ||||
|     if helpers.find_str(channel.contacts, from) == -1 then | ||||
|     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({ mix_node_participants, mix_node_info, mix_node_messages }) do | ||||
|     for _, pep_node in pairs(channel.nodes) do | ||||
|         srv:delete(pep_node, true); | ||||
|     end | ||||
|     table.remove(channels, i); | ||||
| @ -698,31 +727,280 @@ module:hook("message/bare", function(event) | ||||
|         return true; | ||||
|     end | ||||
| 
 | ||||
|     local msg = st.clone(stanza); | ||||
|     msg:add_child(st.stanza("mix", { xmlns = mix_core_xmlns }) | ||||
|                     :tag("nick"):text(participant.nick):up() | ||||
|                     :tag("jid"):text(participant.jid):up()); | ||||
|     -- 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); | ||||
| 
 | ||||
|     -- Put the message into the archive | ||||
|     local mam_id = uuid.generate(); | ||||
|     msg.attr.id = mam_id; | ||||
|     -- NOTE: The spec says to do so | ||||
|     msg.attr.from = channel.jid; | ||||
|     message_archive:append(stanza.attr.to, mam_id, msg, time.now()); | ||||
|     msg.attr.from = channel.jid.."/"..channel:get_spid(from); | ||||
| 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 | ||||
| 
 | ||||
|     if module:fire_event("mix-broadcast-message", { message = msg, channel = channel }) then | ||||
|         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 | ||||
| 
 | ||||
|     for _, p in pairs(channel.participants) do | ||||
|         -- Only users who subscribed to the messages node should receive | ||||
|         -- messages | ||||
|         if channel:is_subscribed(p.jid, mix_node_messages) then | ||||
|             local tmp = st.clone(msg); | ||||
|             tmp.attr.to = p.jid; | ||||
|             module:send(tmp); | ||||
|     -- 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 | ||||
|     return true; | ||||
| 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); | ||||
|  | ||||
							
								
								
									
										19
									
								
								mod_mix/namespaces.lib.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								mod_mix/namespaces.lib.lua
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| return { | ||||
|     -- XMLNS | ||||
|     -- MIX | ||||
|     mix_core = "urn:xmpp:mix:core:1"; | ||||
|     mix_anon = "urn:xmpp:mix:anon:0"; | ||||
|     mix_admin = "urn:xmpp:mix:admin:0"; | ||||
|     -- MAM | ||||
|     mam = "urn:xmpp:mam:2"; | ||||
|     -- User Avatar | ||||
|     avatar = "urn:xmpp:avatar:data"; | ||||
|     avatar_metadata = "urn:xmpp:avatar:metadata"; | ||||
|     -- MIX PubSub nodes | ||||
|     messages = "urn:xmpp:mix:nodes:messages"; | ||||
|     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"; | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user