changeset 5653:62c6e17a5e9d

Merge
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Mon, 18 Sep 2023 08:24:19 -0500
parents eade7ff9f52c (diff) c217f4edfc4f (current diff)
children 31e56562f9bd
files
diffstat 4 files changed, 213 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_adhoc_bots/README.markdown	Mon Sep 18 08:24:19 2023 -0500
@@ -0,0 +1,22 @@
+---
+labels:
+- 'Stage-Alpha
+summary: Install adhoc command bots in MUCs
+---
+
+# Introduction
+
+This module allows you to "install" bots on a MUC service (via config for
+now, via adhoc command and on just one MUC to follow). All the adhoc commands
+defined on the bot become adhoc commands on the service's MUCs, and the bots
+can send XEP-0356 messages to the MUC to send messages as any participant.
+
+# Configuration
+
+List all bots to install. You must specify full JID.
+
+    adhoc_bots = { "some@bot.example.com/bot" }
+	
+And enable the module on the MUC service as usual
+
+    modules_enabled = { "muc_adhoc_bots" }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_adhoc_bots/mod_muc_adhoc_bots.lua	Mon Sep 18 08:24:19 2023 -0500
@@ -0,0 +1,122 @@
+local jid = require "util.jid";
+local json = require "util.json";
+local promise = require "util.promise";
+local st = require "util.stanza";
+local uuid = require "util.uuid";
+
+local xmlns_cmd = "http://jabber.org/protocol/commands";
+
+module:hook("muc-disco#info", function(event)
+	event.reply:tag("feature", {var = xmlns_cmd}):up();
+end);
+
+module:hook("iq-get/bare/http://jabber.org/protocol/disco#items:query", function (event)
+	local room = prosody.hosts[module:get_host()].modules.muc.get_room_from_jid(event.stanza.attr.to);
+	local occupant = room:get_occupant_by_real_jid(event.stanza.attr.from)
+	if event.stanza.tags[1].attr.node ~= xmlns_cmd or not occupant then
+		return
+	end
+
+	local bots = module:get_option_array("adhoc_bots", {})
+	bots:map(function(bot)
+		return module:send_iq(
+			st.iq({ type = "get", id = uuid.generate(), to = bot, from = room:get_occupant_jid(event.stanza.attr.from) })
+				:tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = xmlns_cmd }):up(),
+			nil,
+			5
+		)
+	end)
+
+	promise.all_settled(bots):next(function (bot_commands)
+		local reply = st.reply(event.stanza):query("http://jabber.org/protocol/disco#items")
+		for i, one_bot_reply in ipairs(bot_commands) do
+			if one_bot_reply.status == "fulfilled"	then
+			local query = one_bot_reply.value.stanza:get_child("query", "http://jabber.org/protocol/disco#items")
+				if query then
+					-- Should use query:childtags("item") but it doesn't work
+					for j,item in ipairs(query.tags) do
+						item.attr.node = json.encode({ jid = item.attr.jid, node = item.attr.node })
+						item.attr.jid = event.stanza.attr.to
+						reply:add_child(item):up()
+					end
+				end
+			end
+		end
+		event.origin.send(reply:up())
+	end):catch(function (e)
+		module:log("error", e)
+	end)
+
+	return true;
+end, 500);
+
+local function is_adhoc_bot(jid)
+	for i, bot_jid in ipairs(module:get_option_array("adhoc_bots", {})) do
+		if jid == bot_jid then
+			return true
+		end
+	end
+
+	return false
+end
+
+module:hook("iq-set/bare/"..xmlns_cmd..":command", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local node = stanza.tags[1].attr.node
+	local meta = json.decode(node)
+	local room = prosody.hosts[module:get_host()].modules.muc.get_room_from_jid(stanza.attr.to);
+	local occupant = room:get_occupant_by_real_jid(event.stanza.attr.from)
+	if meta and occupant and is_adhoc_bot(meta.jid) then
+		local fwd = st.clone(stanza)
+		fwd.attr.to = meta.jid
+		fwd.attr.from = room:get_occupant_jid(event.stanza.attr.from)
+		local command = fwd:get_child("command", "http://jabber.org/protocol/commands")
+		command.attr.node = meta.node
+		module:send_iq(fwd):next(function(response)
+			local response_command = response.stanza:get_child("command", "http://jabber.org/protocol/commands")
+			response.stanza.attr.from = stanza.attr.to
+			response.stanza.attr.to = stanza.attr.from
+			response_command.attr.node = node
+			origin.send(response.stanza)
+		end):catch(function (e)
+			module:log("error", e)
+		end)
+
+		return true
+	end
+
+	return
+end, 500);
+
+local function clean_xmlns(node)
+		-- Recursively remove "jabber:client" attribute from node.
+		-- In Prosody internal routing, xmlns should not be set.
+		-- Keeping xmlns would lead to issues like mod_smacks ignoring the outgoing stanza,
+		-- so we remove all xmlns attributes with a value of "jabber:client"
+		if node.attr.xmlns == 'jabber:client' then
+				for childnode in node:childtags() do
+						clean_xmlns(childnode)
+				end
+				node.attr.xmlns = nil
+		end
+end
+
+module:hook("message/bare", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	if not is_adhoc_bot(stanza.attr.from) then return; end
+	local room = prosody.hosts[module:get_host()].modules.muc.get_room_from_jid(stanza.attr.to);
+	if room == nil then return; end
+	local privilege = stanza:get_child("privilege", "urn:xmpp:privilege:2")
+	if privilege == nil then return; end
+	local fwd = privilege:get_child("forwarded", "urn:xmpp:forward:0")
+	if fwd == nil then return; end
+	local message = fwd:get_child("message", "jabber:client")
+	if message == nil then return; end
+	if message.attr.to ~= stanza.attr.to or jid.bare(message.attr.from) ~= stanza.attr.to then
+		return
+	end
+
+	clean_xmlns(message)
+	room:broadcast_message(message)
+	return true
+end)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_restrict_avatars/mod_muc_restrict_avatars.lua	Mon Sep 18 08:24:19 2023 -0500
@@ -0,0 +1,19 @@
+local bare_jid = require"util.jid".bare;
+local mod_muc = module:depends("muc");
+
+local function filter_avatar_advertisement(tag)
+	if tag.attr.xmlns == "vcard-temp:x:update" then
+		return nil;
+	end
+
+	return tag;
+end
+
+module:hook("presence/full", function(event)
+	local stanza = event.stanza;
+	local room = mod_muc.get_room_from_jid(bare_jid(stanza.attr.to));
+
+	if not room:get_affiliation(stanza.attr.from) then
+		stanza:maptags(filter_avatar_advertisement);
+	end
+end, 1);
--- a/mod_pubsub_subscription/mod_pubsub_subscription.lua	Sun Sep 17 13:36:30 2023 +0200
+++ b/mod_pubsub_subscription/mod_pubsub_subscription.lua	Mon Sep 18 08:24:19 2023 -0500
@@ -12,7 +12,7 @@
 
 local pending_subscription = cache.new(256); -- uuid → node
 local pending_unsubscription = cache.new(256); -- uuid → node
-local active_subscriptions = mt.new() -- service | node | uuid | { item }
+local active_subscriptions = mt.new() -- service | node | subscriber | uuid | { item }
 function module.save()
 	return { active_subscriptions = active_subscriptions.data }
 end
@@ -28,9 +28,10 @@
 	local item = item_event.item;
 	assert(item.service, "pubsub subscription item MUST have a 'service' field.");
 	assert(item.node, "pubsub subscription item MUST have a 'node' field.");
+	item.from = item.from or module.host;
 
 	local already_subscibed = false;
-	for _ in active_subscriptions:iter(item.service, item.node, nil) do -- luacheck: ignore 512
+	for _ in active_subscriptions:iter(item.service, item.node, item.from, nil) do -- luacheck: ignore 512
 		already_subscibed = true;
 		break
 	end
@@ -38,24 +39,30 @@
 	item._id = uuid.generate();
 	local iq_id = uuid.generate();
 	pending_subscription:set(iq_id, item._id);
-	active_subscriptions:set(item.service, item.node, item._id, item);
+	active_subscriptions:set(item.service, item.node, item.from, item._id, item);
 
 	if not already_subscibed then
-		module:send(st.iq({ type = "set", id = iq_id, from = module.host, to = item.service })
+		module:send(st.iq({ type = "set", id = iq_id, from = item.from, to = item.service })
 			:tag("pubsub", { xmlns = xmlns_pubsub })
-				:tag("subscribe", { jid = module.host, node = item.node }));
+				:tag("subscribe", { jid = item.from, node = item.node }));
 	end
 end
 
 for _, event_name in ipairs(valid_events) do
 	module:hook("pubsub-event/host/"..event_name, function (event)
-		for _, _, _, _, cb in active_subscriptions:iter(event.service, event.node, nil, "on_"..event_name) do
+		for _, _, _, _, _, cb in active_subscriptions:iter(event.service, event.node, event.stanza.attr.to, nil, "on_"..event_name) do
+			pcall(cb, event);
+		end
+	end);
+
+	module:hook("pubsub-event/bare/"..event_name, function (event)
+		for _, _, _, _, _, cb in active_subscriptions:iter(event.service, event.node, event.stanza.attr.to, nil, "on_"..event_name) do
 			pcall(cb, event);
 		end
 	end);
 end
 
-module:hook("iq/host", function (event)
+function handle_iq(context, event)
 	local stanza = event.stanza;
 	local service = stanza.attr.from;
 
@@ -79,7 +86,7 @@
 			what = "on_unsubscribed";
 		end
 		if not what then return end -- there are other states but we don't handle them
-		for _, _, _, _, cb in active_subscriptions:iter(service, node, nil, what) do
+		for _, _, _, _, _, cb in active_subscriptions:iter(service, node, stanza.attr.to, nil, what) do
 			cb(event);
 		end
 		return true;
@@ -89,40 +96,48 @@
 		local error_type, error_condition, reason, pubsub_error = stanza:get_error();
 		local err = { type = error_type, condition = error_condition, text = reason, extra = pubsub_error };
 		if active_subscriptions:get(service) then
-			for _, _, _, _, cb in active_subscriptions:iter(service, node, nil, "on_error") do
+			for _, _, _, _, _, cb in active_subscriptions:iter(service, node, stanza.attr.to, nil, "on_error") do
 				cb(err);
 			end
 			return true;
 		end
 	end
+end
+
+module:hook("iq/host", function (event)
+	handle_iq("host", event);
+end, 1);
+
+module:hook("iq/bare", function (event)
+	handle_iq("bare", event);
 end, 1);
 
 local function subscription_removed(item_event)
 	local item = item_event.item;
-	active_subscriptions:set(item.service, item.node, item._id, nil);
-	local node_subs = active_subscriptions:get(item.service, item.node);
+	active_subscriptions:set(item.service, item.node, item.from, item._id, nil);
+	local node_subs = active_subscriptions:get(item.service, item.node, item.from);
 	if node_subs and next(node_subs) then return end
 
 	local iq_id = uuid.generate();
 	pending_unsubscription:set(iq_id, item._id);
 
-	module:send(st.iq({ type = "set", id = iq_id, from = module.host, to = item.service })
+	module:send(st.iq({ type = "set", id = iq_id, from = item.from, to = item.service })
 		:tag("pubsub", { xmlns = xmlns_pubsub })
-			:tag("unsubscribe", { jid = module.host, node = item.node }))
+			:tag("unsubscribe", { jid = item.from, node = item.node }))
 end
 
 module:handle_items("pubsub-subscription", subscription_added, subscription_removed, true);
 
-module:hook("message/host", function(event)
+function handle_message(context, event)
 	local origin, stanza = event.origin, event.stanza;
 	local ret = nil;
 	local service = stanza.attr.from;
-	module:log("debug", "Got message/host: %s", stanza:top_tag());
+	module:log("debug", "Got message/%s: %s", context, stanza:top_tag());
 	for event_container in stanza:childtags("event", xmlns_pubsub_event) do
 		for pubsub_event in event_container:childtags() do
 			module:log("debug", "Got pubsub event %s", pubsub_event:top_tag());
 			local node = pubsub_event.attr.node;
-			module:fire_event("pubsub-event/host/"..pubsub_event.name, {
+			module:fire_event("pubsub-event/" .. context .. "/"..pubsub_event.name, {
 					stanza = stanza;
 					origin = origin;
 					event = pubsub_event;
@@ -133,13 +148,30 @@
 		end
 	end
 	return ret;
+end
+
+module:hook("message/host", function(event)
+	return handle_message("host", event);
 end);
 
-module:hook("pubsub-event/host/items", function (event)
+module:hook("message/bare", function(event)
+	return handle_message("bare", event);
+end);
+
+
+function handle_items(context, event)
 	for item in event.event:childtags() do
 		module:log("debug", "Got pubsub item event %s", item:top_tag());
 		event.item = item;
 		event.payload = item.tags[1];
-		module:fire_event("pubsub-event/host/"..item.name, event);
+		module:fire_event("pubsub-event/" .. context .. "/"..item.name, event);
 	end
+end
+
+module:hook("pubsub-event/host/items", function (event)
+	handle_items("host", event);
 end);
+
+module:hook("pubsub-event/bare/items", function (event)
+	handle_items("bare", event);
+end);