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"); local Participant = {}; Participant.__index = Participant; function Participant:new(jid, nick, config) return setmetatable({ jid = jid, nick = nick, config = config, }, Participant); end function Participant:from(config) return setmetatable(config, Participant); end local Channel = {}; Channel.__index = Channel; function Channel:new(jid, name, description, participants, administrators, owners, subscriptions, spid, contacts, adhoc, allowed, banned, config, nodes) return setmetatable({ jid = jid, name = name, description = description, participants = participants, subscriptions = subscriptions, spid = spid, contacts = contacts, adhoc = adhoc, administrators = administrators, owners = owners, config = config, nodes = nodes, allowed = allowed, banned = banned, }, Channel); end function Channel:from(config) -- Turn a channel into a Channel object local o = setmetatable(config, Channel); for i, _ in pairs(o.participants) do o.participants[i] = Participant:from(o.participants[i]); end return o; end function Channel:get_spid(jid) -- Returns the Stable Participant ID for the *BARE* jid return self.spid[jid]; end function Channel:set_spid(jid, spid) -- Sets the Stable Participant ID for the *BARE* jid self.spid[jid] = spid; end function Channel:find_participant(jid) -- Returns the index of a participant in a channel. Returns -1 -- if the participant is not found return helpers.find(self.participants, function(p) return p.jid == jid end); end function Channel:is_participant(jid) -- Returns true if jid is a participant of the channel. False otherwise. local i, _ = self:find_participant(jid); return i ~= -1; end function Channel:is_subscribed(jid, node) -- Returns true of JID is subscribed to node on this channel. Returns false -- otherwise. return helpers.find_str(self.subscriptions[jid], node) ~= -1; end function Channel:debug_print() module:log("debug", "Channel %s (%s)", self.jid, self.name); module:log("debug", "'%s'", self.description); for _, p in pairs(self.participants) do module:log("debug", "=> %s (%s)", p.jid, p.nick); end module:log("debug", "Contacts:"); for _, c in pairs(self.contacts) do module:log("debug", "=> %s", c); end if self.subscriptions then module:log("debug", "Subscriptions:"); for user, subs in pairs(self.subscriptions) do module:log("debug", "[%s]", user); for _, sub in pairs(subs) do module:log("debug", "=> %s", sub); end end 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, default_channel_configuration = default_channel_configuration, default_participant_configuration = default_participant_configuration, };