prosody-modules/mod_mix/mix.lib.lua

424 lines
14 KiB
Lua
Raw Normal View History

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
2021-02-21 17:38:21 +00:00
module:log("debug", "Contacts:");
for _, c in pairs(self.contacts) do
module:log("debug", "=> %s", c);
end
2021-02-21 17:38:21 +00:00
if self.subscriptions then
module:log("debug", "Subscriptions:");
for user, subs in pairs(self.subscriptions) do
module:log("debug", "[%s]", user);
2021-02-21 17:38:21 +00:00
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,
};