prosody-modules/mod_mix/mix.lib.lua

400 lines
12 KiB
Lua
Raw Permalink 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 new_id = require("util.id").medium;
local time = require("util.time");
local datetime = require("util.datetime");
local pubsub = require("util.pubsub");
local lib_pubsub = module:require("pubsub");
local storagemanager = require("core.storagemanager");
local helpers = module:require("mix/helpers");
local namespaces = module:require("mix/namespaces");
local lib_forms = module:require("mix/forms");
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_broadcaster()
local function broadcast(kind, node, jids, item, _, node_obj)
if node == namespaces.presence then
-- NOTE: This assumes that we already added all necessary MIX data
-- before publishing this item
local presence = {};
if kind == "retract" then
presence = st.presence({
type = "unavailable",
from = self:get_encoded_participant_jid(item.attr.from),
});
presence:add_child(item:get_tag("mix", namespaces.mix_presence));
else
presence = stanza.clone(item);
end
for jid in pairs(jids) do
module:log("debug", "Sending presence notification to %s from %s", jid, item.attr.from);
message.attr.to = jid;
module:send(presence);
end
else
if node_obj then
if node_obj.config["notify_"..kind] == false then
return;
end
end
if kind == "retract" then
kind = "items"; -- XEP-0060 signals retraction in an <items> container
end
if item then
item = st.clone(item);
item.attr.xmlns = nil; -- Clear the pubsub namespace
if kind == "items" then
if node_obj and node_obj.config.include_payload == false then
item:maptags(function () return nil; end);
end
end
end
local id = new_id();
local message = st.message({ from = self.jid, type = "headline", id = id })
:tag("event", { xmlns = namespaces.pubsub_event })
:tag(kind, { node = node });
if item then
message:add_child(item);
end
for jid in pairs(jids) do
2021-06-03 21:07:41 +00:00
module:log("debug", "Sending notification to %s for node %s", jid, node);
message.attr.to = jid;
module:send(message);
end
end
end
return broadcast;
end
-- PubSub stuff
local services = {}; -- room@server -> PubSub services
local known_nodes = module:open_store("mix_pubsub");
local node_config = module:open_store("mix_pubsub", "map");
local function itemstore(username)
local driver = storagemanager.get_driver(module.host, "mix_data");
return function (config, node)
module:log("debug", "Creating new persistent item store for user %s, node %q", username, node);
local archive = driver:open("mix_pubsub_"..node, "archive");
return lib_pubsub.archive_itemstore(archive, config, username, node, false);
end
end
function Channel:get_pubsub_service()
local service = services[self.jid];
if service then
return service;
end
service = pubsub.new({
node_defaults = {
["persist_items"] = true;
["access_model"] = "open"; -- TODO
["max_items"] = 256; -- TODO: Once "max" is supported
};
broadcaster = self:get_broadcaster();
nodestore = known_nodes;
itemstore = itemstore(self.jid);
});
services[self.jid] = service;
return service;
end
function Channel:get_spid(jid)
-- Returns the Stable Participant ID for the *BARE* jid
return self.spid[jid];
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
local function is_participant(p)
return p.jid == jid;
end
local _, participant = helpers.find(self.participants, is_participant);
return participant;
end
function Channel:is_participant(jid)
-- Returns true if jid is a participant of the channel. False otherwise.
return self:find_participant(jid) ~= nil;
end
function Channel:get_encoded_participant_jid(jid)
-- TODO: This assumes that jid is a participant
local spid = self:get_spid(jid_lib.bare(jid));
return spid.."#"..self.jid;
end
function Channel:is_subscribed(jid, node)
-- 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: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
-- NOTE: This function has be to called *after* the new participant
-- has been added to the channel.participants array
local srv = self:get_pubsub_service();
srv:set_node_config(namespaces.participants,
true,
{ ["max_items"] = #self.participants });
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
function Channel:remove_participant(jid)
-- Removes a user form the channel. May be a kick, may be a leave.
local srv = self:get_pubsub_service();
-- Step 1: Unsubscribe from all subscribed nodes
for _, node in ipairs(self.subscriptions[jid]) do
srv:remove_subscription(node, true, jid);
end
self.subscriptions[jid] = nil;
-- Step 2: Remove affiliations to all nodes
for _, node in ipairs(self.nodes) do
srv:set_affiliation(node, true, jid, "outcast");
end
-- Step 3: Remove jid as participant
local participant = self:find_participant(jid);
self.participants = array.filter(self.participants, function (p) return p.jid ~= jid end);
-- Step 4: Retract jid from participants node
local spid = self:get_spid(jid);
local notifier = st.stanza("retract", { id = spid });
srv:retract(namespaces.participants, true, jid, notifier);
self:save_state();
end
function Channel:publish_info(srv)
local timestamp = datetime.datetime(time.now());
local info = st.stanza("item", { id = timestamp, xmlns = namespaces.pubsub })
:add_child(lib_forms.mix_info:form({
["FORM_TYPE"] = "hidden",
["Name"] = self.name,
["Description"] = self.description,
["Contact"] = self.contacts
}, "result"));
srv:publish(namespaces.info, true, timestamp, info);
end
local function get_node_access_model(node, adhoc)
-- TODO
if adhoc then
return "whitelist";
else
return "open";
end
end
local function get_node_max_items(node)
-- TODO: Would be nice if we could just return "max"
-- TODO: Handle all nodes
if node == namespaces.messages then
return 0;
elseif node == namespaces.info or
node == namespaces.config then
return 1;
end
return 256;
end
local function channel_not_found(stanza)
-- Wrapper for returning a "Channel-not-found" error stanza
return st.error_reply(stanza,
"cancel",
"item-not-found",
"The MIX channel was not found");
end
function Channel:set_affiliation(node, target, role)
-- Set the affiliation of target to node depending on what
-- node it is and whether target is the creator or not
local srv = self:get_pubsub_service();
local affiliation = "member";
if node == namespaces.presence then
affiliation = "none";
end
-- TODO(MIX-ADMIN): Also handle OWNER, ADMIN
if role == "creator" then
if node == namespaces.info or
node == namespaces.allowed or
node == namespaces.banned or
node == namespaces.config or
node == namespaces.avatar or
node == namespaces.avatar_metadata then
affiliation = "publisher";
end
end
srv:set_affiliation(node, true, target, affiliation);
end
function Channel:may_subscribe(actor, node, joining)
if joining then
-- TODO: Is this true?
return true;
end
if self:is_participant(actor) then
-- TODO(MIX-ADMIN): This is possible if the actor is an owner or admin
return node ~= namespaces.config;
end
return false;
end
function Channel:may_publish(actor, node)
return node ~= namespaces.presence and node ~= namespaces.messages;
end
function Channel:may_retract(actor, node)
-- TODO: Maybe put may_{publish, retract} together
return node ~= namespaces.presence and node ~= namespaces.messages;
end
function Channel:may_retrieve_items(actor, node)
return node ~= namespaces.presence and node ~= namespaces.messages;
end
function Channel:may_join(actor)
-- TODO(MIX-ADMIN): Check the allowed and banned node
return true;
end
return {
Channel = Channel;
Participant = Participant;
get_node_access_model = get_node_access_model;
get_node_max_items = get_node_max_items;
channel_not_found = channel_not_found;
};