Changeset

160:6c1953fbe0fa

clix.moderate: New command to do MUC moderation (XEP-0425)
author Kim Alvefur <zash@zash.se>
date Sat, 06 Nov 2021 18:12:22 +0100
parents 159:68e09745d928
children 161:64cb732f67e4
files clix/moderate.lua squishy
diffstat 2 files changed, 117 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/clix/moderate.lua	Sat Nov 06 18:12:22 2021 +0100
@@ -0,0 +1,116 @@
+local jid_split = require"util.jid".split;
+local datetime = require "util.datetime";
+local st = require "util.stanza";
+local uuid = require"util.uuid".generate;
+
+return function(opts, arg)
+	if opts.help then
+		print("clix moderate --to=room@muc.example.com")
+		print("\t--start=timestamp")
+		print("\t--end=timestamp")
+		print("\t--from=nickname")
+		print("\t--body-contains=\"some spam\"")
+		print("\t--dry-run")
+		return 0;
+	elseif opts.short_help or arg[1] or not opts.room then
+		print("Remove messages from a MUC");
+		return;
+	end
+
+	local function parse_datetime(s)
+		if s:match("^%d%d:") then
+			s = datetime.date().."T"..s;
+		end
+		if #s < 20 then
+			s = s .. ("0000-01-01T00:00:00Z"):sub(#s+1)
+		end
+		return datetime.parse(s)
+	end
+
+	if opts.start then
+		opts.start = parse_datetime(opts.start);
+	end
+	if opts["end"] then
+		opts["end"] = parse_datetime(opts["end"]);
+	end
+
+	local function matches(message)
+		local nick = select(3, jid_split(message.attr.from));
+		if opts.from and opts.from ~= nick then return end
+		local body = message:get_child_text("body");
+		if opts.body_contains and not string.find(body or "", opts.body_contains, 1, true) then return end
+		if opts.body_match and not string.find(body or "", opts.body_match) then return end
+		return true;
+	end
+
+	local function on_connect(conn)
+		local waiting = {}; -- to keep track of outstanding queries
+
+		local function done(with)
+			waiting[with] = nil;
+			if next(waiting) == nil then
+				conn:close();
+			end
+		end
+
+		local function moderate(id)
+			waiting[id] = true;
+			-- TODO maybe queue and send the next request when a response comes in?
+			local mod_iq = st.iq({ id = uuid(); type = "set"; to = opts.room })
+				:tag("apply-to", { xmlns = "urn:xmpp:fasten:0"; id = id })
+					:tag("moderate", { xmlns = "urn:xmpp:message-moderate:0" })
+					:tag("retract", { xmlns = "urn:xmpp:message-retract:0" }):up()
+			if opts.reason then
+				mod_iq:tag("reason", { xmlns = "urn:xmpp:message-moderate:0" }):text(opts.reason):up()
+			end
+			mod_iq:reset();
+
+			if opts.dry_run then
+				done(id);
+				conn:debug("Would send: %s", mod_iq);
+				return;
+			end
+
+			return conn:send_iq(mod_iq, function (ret)
+				if ret.attr.type == "error" then
+					local t, cond, msg = ret:get_error();
+					conn:error("Retracting message with id %s failed: %s(%s, %s)", msg or "", t, cond);
+				end
+				done(id);
+			end);
+		end
+
+		local function handle_results(result, err)
+			if not result then
+				conn:error("Archive query failed: %s", err);
+				return done(true);
+			end
+			for _, item in ipairs(result) do
+				if matches(item.message) then
+					conn:info("Moderate %s", item.message:top_tag())
+					moderate(item.id);
+				else
+					conn:debug("Skip %s", item.message:top_tag())
+				end
+			end
+
+			if result.complete == nil then -- COMPAT verse
+				result.complete = opts.after == nil or result[1] == nil;
+			end
+
+			if not result.complete then
+				-- Proceed to the next page
+				opts.after = result.last;
+				return conn:query_archive(opts.room, opts, handle_results);
+			else
+				-- All done, just wait for any outstanding moderation queries to complete
+				done(true);
+			end
+		end
+
+		waiting[true] = true; -- for the archive query
+		conn:query_archive(opts.room, opts, handle_results);
+	end
+
+	clix_connect(opts, on_connect, { "archive" });
+end
--- a/squishy	Sat Nov 06 16:25:50 2021 +0100
+++ b/squishy	Sat Nov 06 18:12:22 2021 +0100
@@ -16,6 +16,7 @@
 	"presence";
 	"watch_pep";
 	"avatar";
+	"moderate";
 }
 
 for _, cmd in ipairs(commands) do