2020-11-02 16:07:50 +00:00
|
|
|
local st = require("util.stanza");
|
2021-02-28 10:29:21 +00:00
|
|
|
local array = require("util.array");
|
|
|
|
local jid_lib = require("util.jid");
|
|
|
|
local uuid = require("util.uuid");
|
2021-06-02 14:44:17 +00:00
|
|
|
local new_id = require("util.id").medium;
|
2021-02-28 10:29:21 +00:00
|
|
|
local time = require("util.time");
|
2021-06-02 14:44:17 +00:00
|
|
|
local datetime = require("util.datetime");
|
|
|
|
local pubsub = require("util.pubsub");
|
|
|
|
local lib_pubsub = module:require("pubsub");
|
|
|
|
local storagemanager = require("core.storagemanager");
|
|
|
|
|
2020-11-02 16:07:50 +00:00
|
|
|
local helpers = module:require("mix/helpers");
|
2021-02-28 10:29:21 +00:00
|
|
|
local namespaces = module:require("mix/namespaces");
|
2021-06-02 14:44:17 +00:00
|
|
|
local lib_forms = module:require("mix/forms");
|
2021-02-28 10:29:21 +00:00
|
|
|
|
|
|
|
local Participant = {};
|
2020-11-02 16:07:50 +00:00
|
|
|
Participant.__index = Participant;
|
2021-02-28 10:29:21 +00:00
|
|
|
function Participant:new(jid, nick, config)
|
2020-11-02 16:07:50 +00:00
|
|
|
return setmetatable({
|
|
|
|
jid = jid,
|
|
|
|
nick = nick,
|
2021-02-28 10:29:21 +00:00
|
|
|
config = config,
|
2020-11-02 16:07:50 +00:00
|
|
|
}, Participant);
|
|
|
|
end
|
|
|
|
|
|
|
|
function Participant:from(config)
|
|
|
|
return setmetatable(config, Participant);
|
|
|
|
end
|
|
|
|
|
2021-02-28 10:29:21 +00:00
|
|
|
local Channel = {};
|
2020-11-02 16:07:50 +00:00
|
|
|
Channel.__index = Channel;
|
2021-02-28 10:29:21 +00:00
|
|
|
function Channel:new(jid, name, description, participants, administrators, owners, subscriptions, spid, contacts, adhoc, allowed, banned, config, nodes)
|
2020-11-02 16:07:50 +00:00
|
|
|
return setmetatable({
|
|
|
|
jid = jid,
|
|
|
|
name = name,
|
|
|
|
description = description,
|
|
|
|
participants = participants,
|
|
|
|
subscriptions = subscriptions,
|
|
|
|
spid = spid,
|
|
|
|
contacts = contacts,
|
|
|
|
adhoc = adhoc,
|
2021-02-28 10:29:21 +00:00
|
|
|
administrators = administrators,
|
|
|
|
owners = owners,
|
|
|
|
config = config,
|
|
|
|
nodes = nodes,
|
|
|
|
allowed = allowed,
|
|
|
|
banned = banned,
|
2020-11-02 16:07:50 +00:00
|
|
|
}, 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
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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
|
|
|
|
module:log("debug", "Sending notification to %s from %s for node %s", jid, user_bare, 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
|
|
|
|
|
2020-11-02 16:07:50 +00:00
|
|
|
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
|
2021-06-02 14:44:17 +00:00
|
|
|
local function is_participant(p)
|
|
|
|
return p.jid == jid;
|
|
|
|
end
|
|
|
|
local _, participant = helpers.find(self.participants, is_participant);
|
|
|
|
return participant;
|
2020-11-02 16:07:50 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Channel:is_participant(jid)
|
|
|
|
-- Returns true if jid is a participant of the channel. False otherwise.
|
2021-06-02 14:44:17 +00:00
|
|
|
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;
|
2020-11-02 16:07:50 +00:00
|
|
|
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
|
|
|
|
2020-11-02 16:07:50 +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
|
|
|
|
2020-11-02 16:07:50 +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
|
2020-11-02 16:07:50 +00:00
|
|
|
module:log("debug", "=> %s", sub);
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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());
|
2021-02-28 10:29:21 +00:00
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
-- 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);
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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
|
|
|
|
|
2021-02-28 10:29:21 +00:00
|
|
|
function Channel:remove_participant(jid)
|
|
|
|
-- Removes a user form the channel. May be a kick, may be a leave.
|
2021-06-02 14:44:17 +00:00
|
|
|
local srv = self:get_pubsub_service();
|
2021-02-28 10:29:21 +00:00
|
|
|
|
|
|
|
-- 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
|
2021-06-02 14:44:17 +00:00
|
|
|
local participant = self:find_participant(jid);
|
|
|
|
self.participants = array.filter(self.participants, function (p) return p.jid ~= jid end);
|
2021-02-28 10:29:21 +00:00
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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);
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
local function get_node_access_model(node, adhoc)
|
|
|
|
-- TODO
|
|
|
|
if adhoc then
|
|
|
|
return "whitelist";
|
|
|
|
else
|
|
|
|
return "open";
|
|
|
|
end
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
return 256;
|
|
|
|
end
|
2021-02-28 10:29:21 +00:00
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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");
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
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";
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
-- 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
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
srv:set_affiliation(node, true, target, affiliation);
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
function Channel:may_subscribe(actor, node, joining)
|
|
|
|
if joining then
|
|
|
|
-- TODO: Is this true?
|
|
|
|
return true;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
if self:is_participant(actor) then
|
|
|
|
-- TODO(MIX-ADMIN): This is possible if the actor is an owner or admin
|
|
|
|
return node ~= namespaces.config;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return false;
|
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
function Channel:may_publish(actor, node)
|
|
|
|
return node ~= namespaces.presence and node ~= namespaces.messages;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
function Channel:may_retract(actor, node)
|
|
|
|
-- TODO: Maybe put may_{publish, retract} together
|
|
|
|
return node ~= namespaces.presence and node ~= namespaces.messages;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
function Channel:may_retrieve_items(actor, node)
|
|
|
|
return node ~= namespaces.presence and node ~= namespaces.messages;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2021-06-02 14:44:17 +00:00
|
|
|
function Channel:may_join(actor)
|
|
|
|
-- TODO(MIX-ADMIN): Check the allowed and banned node
|
|
|
|
return true;
|
2021-02-28 10:29:21 +00:00
|
|
|
end
|
|
|
|
|
2020-11-02 16:07:50 +00:00
|
|
|
return {
|
2021-06-02 14:44:17 +00:00
|
|
|
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;
|
2020-11-02 16:07:50 +00:00
|
|
|
};
|