clix/moderate.lua

Sat, 24 Jun 2023 09:59:07 +0200

author
Kim Alvefur <zash@zash.se>
date
Sat, 24 Jun 2023 09:59:07 +0200
changeset 170
0d561f921c13
parent 168
75e8ca131178
permissions
-rw-r--r--

clix.adhoc: Move stanza to dataform converter here

Removes the need for verse to have a custom util.dataforms fork only for
this

local jid_split = require"prosody.util.jid".split;
local datetime = require "prosody.util.datetime";
local st = require "prosody.util.stanza";
local uuid = require"prosody.util.uuid".generate;

return function(opts, arg)
	if opts.help then
		print("clix moderate --room=room@muc.example.com")
		print("\t--start=timestamp")
		print("\t--end=timestamp")
		print("\t--from=nickname")
		print("\t--body-contains=\"some spam\"")
		print("\t--reason=\"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

mercurial