prosody-modules/mod_mix/mod_mix.lua
Alexander PapaTutuWawa fd9735faba mod_mix: Various changes
- Refactor and clean code
- Implement lots of MIX-ADMIN
- Beginning of MIX-ANON
2021-02-28 11:29:21 +01:00

1007 lines
37 KiB
Lua

-- 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
error("MIX should be loaded as a component", 0);
end
local st = require("util.stanza");
local jid = require("util.jid");
local uuid = require("util.uuid");
local id = require("util.id");
local datetime = require("util.datetime");
local time = require("util.time");
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");
local Channel = mixlib.Channel;
local Participant = mixlib.Participant;
-- Persistent data
local persistent_channels = module:open_store("mix_channels", "keyval");
local persistent_channel_data = module:open_store("mix_data", "keyval");
local message_archive = module:open_store("mix_log", "archive");
-- Configuration
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 = namespaces.mam },
{ name = "with", type = "jid-single" },
{ name = "start", type = "text-single" },
{ name = "end", 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 = namespaces.mix_admin },
{ name = "Last Change Made By", type = "jid-single" },
{ name = "Owner", type = "jid-multi" },
{ name = "Administrator", type = "jid-multi" },
{ name = "End of Life", type = "text-single" },
{ name = "Nodes Present", type = "list-multi" },
{ name = "Message Node Subscription", type = "list-single" },
{ name = "Presence Node Subscription", type = "list-single" },
{ name = "Participants Node Subscription", type = "list-single" },
{ name = "Information Node Subscription", type = "list-single" },
{ 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", service_name);
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature(namespaces.mix_core);
local channels = {};
local function get_channel(channel_jid)
-- Return the channel object from the channels array for which the
-- JID matches. If none is found, returns -1, nil
return helpers.find(channels, function(c) return c.jid == channel_jid; end);
end
local function save_channels()
module:log("debug", "Saving channel list...");
local channel_list = {};
for _, channel in pairs(channels) do
table.insert(channel_list, channel.jid);
persistent_channel_data:set(channel.jid, channel);
end
persistent_channels:set("channels", channel_list);
module:log("debug", "Saving channel list done.");
end
function Channel:save_state()
-- 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 done.", self.jid);
end
function module.load()
module:log("info", "Loading MIX channels...");
local channel_list = persistent_channels:get("channels");
if channel_list then
for _, channel_data in pairs(channel_list) do
local channel = Channel:from(persistent_channel_data:get(channel_data));
table.insert(channels, channel);
module:log("debug", "MIX channel %s loaded", channel.jid);
end
else
module:log("debug", "No MIX channels found.");
end
module:log("info", "Loading MIX channels done.");
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
module:hook("host-disco-items", function(event)
module:log("debug", "host-disco-items called");
local reply = event.reply;
for _, channel in pairs(channels) do
-- Adhoc channels are supposed to be invisible
if not channel.adhoc then
reply:tag("item", { jid = channel.jid }):up();
end
end
end);
module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event)
module:log("debug", "IQ-GET disco#items");
local origin, stanza = event.origin, event.stanza;
if stanza:get_child("query", "http://jabber.org/protocol/disco#items").attr.node ~= "mix" then
origin.send(st.error_reply(stanza, "modify", "bad-request"));
return true;
end
-- TODO: Maybe here we should check if the user has permissions to get infos
-- about the channel before saying that it doesn't exist to prevent creating
-- an oracle.
local _, channel = get_channel(stanza.attr.to);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
if not channel:is_participant(jid.bare(stanza.attr.from)) then
origin.send(st.error_reply(stanza, "cancel", "forbidden"));
return true;
end
local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" });
for _, node in pairs(channel.nodes) do
reply:tag("item", { jid = channel.jid, node = node }):up();
end
origin.send(reply);
return true;
end);
local function can_create_channels(user)
-- Returns true when the jid is allowed to create MIX channels. False otherwise.
-- NOTE: Taken from plugins/muc/mod_muc.lua
local host_suffix = host:gsub("^[^%.]+%.", "");
if restrict_channel_creation == "local" then
module:log("debug", "Comparing %s (Sender) to %s (Host)", jid.host(user), host_suffix);
if jid.host(user) == host_suffix then
return true;
else
return false;
end
elseif type(restrict_channel_creation) == "table" then
if helpers.find_str(restrict_channel_creation, user) ~= -1 then
-- User was specifically listed
return true;
elseif helpers.find_str(restrict_channel_creation, jid.host(user)) then
-- User's host was allowed
return true;
end
return false;
end
-- TODO: Handle also true/"admin" (See mod_muc)
return true;
end
module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
module:log("debug", "IQ-GET host disco#info");
local origin, stanza = event.origin, event.stanza;
local reply = st.reply(stanza)
:tag("query", { xmlns = "http://jabber.org/protocol/disco#info" })
: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 = namespaces.mix_core }):up();
if can_create_channels(stanza.attr.from) then
reply:tag("feature", { var = namespaces.mix_core.."#create-channel" }):up();
end
origin.send(reply);
return true;
end);
module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event)
module:log("debug", "IQ-GET disco#info");
local origin, stanza = event.origin, event.stanza;
local _, channel = get_channel(stanza.attr.to);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" });
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 = namespaces.mix_core }):up();
reply:tag("feature", { var = "urn:xmpp:mam:2" }):up();
origin.send(reply);
return true;
end);
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);
if j == -1 then
-- TODO: Is this correct?
origin.send(channel_not_found(stanza));
return true;
end
-- Check if the user is subscribed to the messages node
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", namespaces.mam);
local filter = {};
local query_id = query.attr.queryid;
local x = query:get_child("x", "jabber:x:data");
if x ~= nil then
-- TODO: Error handling
local form, err = mam_query_form:data(x);
-- Validate
if (form["start"] and not form["end"]) or (not form["start"] and form["end"]) then
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"));
return true;
end
module:log("debug", "Got a MAM query between %s and %s", form["start"], form["end"]);
filter = {
start = datetime.parse(form["start"]); ["end"] = datetime.parse(form["end"])
};
end
local data, err = message_archive:find(channel_jid, filter);
if not data then
module:log("debug", "MAM error: %s", err);
if err == "item-not-found" then
origin.send(st.error_reply(stanza, "modify", "item-not-found"));
else
origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
end
return true;
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 = 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);
origin.send(msg);
end
return true;
end);
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 = namespaces.mam })
:tag("x", { xmlns = "jabber:x:data", type = "form"})
:tag("field", { type = "hidden", var = "FORM_TYPE" })
: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" });
module:send(ret);
return true;
end);
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);
local _, channel = get_channel(stanza.attr.to);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
local j, participant = channel:find_participant(from);
if j == -1 then
origin.send(st.error_reply(stanza,
"cancel",
"forbidden",
"Not a participant"));
channel:debug_print();
module:log("debug", "%s is not a participant in %s", from, channel.jid);
return true;
end
channel:remove_participant(from);
module:fire_event("mix-channel-leave", { channel = channel, participant = participant });
origin.send(st.reply(stanza):tag("leave", { xmlns = namespaces.mix_core }));
return true;
end);
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));
return true;
end
-- Prevent the user from joining multiple times
local j, _ = channel:find_participant(from);
if j ~= -1 then
module:send(st.error_reply(stanza, "cancel", "bad-request", "User already joined"));
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 = 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);
else
nick = nick_tag:get_text();
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);
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);
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
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
-- TODO: This does not work
origin:send(st.error_reply(stanza, "cancel", first_error));
return true;
end
-- TODO: Make the default configurable
local config = mixlib.default_participant_configuration;
local x = join:get_child("x");
if x ~= nil then
-- TODO: Error handling?
local form, err = anon.form:data(x);
if form["JID Visibility"] then
config["JID Visibility"] = form["JID Visibility"];
end
if form["Private Messages"] then
config["Private Messages"] = form["Private Messages"];
end
if form["Presence"] then
config["Presence"] = form["Presence"];
end
if form["vCard"] then
config["vCard"] = form["vCard"];
end
end
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);
channel:publish_participant(spid, participant);
channel:save_state();
module:fire_event("mix-channel-join", { channel = channel, participant = participant });
-- 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/"..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);
local _, channel = get_channel(stanza.attr.to);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
local j, participant = channel:find_participant(from);
if j == -1 then
channel:debug_print();
module:log("debug", "%s is not a participant in %s", from, channel.jid);
return true;
end
-- NOTE: Prosody should guarantee us that the setnick stanza exists
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>"));
return true;
end
-- Change the nick
channel.participants[j].nick = nick:get_text();
-- Inform all other members
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 = namespaces.mix_core })
:tag("nick"):text(nick:get_text()));
channel:save_state();
return true;
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" })
: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)
-- 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,
{}, -- 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 ipairs(default_mix_nodes) do
srv:create(psnode, true, {
["access_model"] = "whitelist",
["persist_items"] = true,
});
end
channel:publish_info(srv);
-- MIX-ADMIN
local admin_nodes = array { namespaces.banned };
if adhoc then
admin_nodes:push(namespaces.allowed);
end
for _, psnode in pairs(admin_nodes) do
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
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/"..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);
-- Check permissions
if not can_create_channels(from) then
origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not authorized to create channels"));
return true;
end
local create = stanza:get_child("create", namespaces.mix_core);
local node;
if create.attr.channel ~= nil then
-- Create non-adhoc channel
module:log("debug", "Attempting to create channel %s", create.attr.channel);
node = create.attr.channel;
local i, _ = get_channel(create.attr.channel.."@"..stanza.attr.to);
module:log("debug", "Channel index %s", i);
if i ~= -1 then
origin.send(st.error_reply(stanza,
"cancel",
"conflict",
"Channel already exists"));
return true;
end
create_channel(create.attr.channel, from, false);
else
-- Create adhoc channel
while (true) do
node = id.short();
local i, _ = get_channel(string.format("%s@%s", node, host));
if i == -1 then
break;
end
end
create_channel(node, from, true);
end
module:log("debug", "Channel %s created with %s as owner", node, from);
-- TODO: Add an event
origin.send(st.reply(stanza)
:tag("create", { xmlns = namespaces.mix_core, channel = node }));
save_channels();
return true;
end);
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", namespaces.mix_core);
local node = destroy.attr.channel;
local node_jid = string.format("%s@%s", node, host);
local i, channel = get_channel(node_jid);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
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(channel.nodes) do
srv:delete(pep_node, true);
end
table.remove(channels, i);
module:fire_event("mix-destroy-channel", { channel = channel });
module:log("debug", "Channel %s destroyed", node);
origin.send(st.reply(stanza));
save_channels();
return true;
end);
module:hook("message/bare", function(event)
module:log("debug", "MIX message received");
local stanza, origin = event.stanza, event.origin;
if stanza.attr.type ~= "groupchat" then
origin.send(st.error_reply(stanza, "modify", "bad-request", "Non-groupchat message"));
return true;
end
local from = jid.bare(stanza.attr.from);
local _, channel = get_channel(stanza.attr.to);
if not channel then
origin.send(channel_not_found(stanza));
return true;
end
local j, participant = channel:find_participant(from);
if j == -1 then
origin.send(st.error_reply(stanza, "cancel", "forbidden", "Not a participant"));
return true;
end
-- 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);
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
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
-- 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
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);