# HG changeset patch # User Kim Alvefur # Date 1636218742 -3600 # Node ID 6c1953fbe0fa0bb11d99be45242c78e917f1f27e # Parent 68e09745d92872bca386e5e9ab5f9d4336cd0cd8 clix.moderate: New command to do MUC moderation (XEP-0425) diff -r 68e09745d928 -r 6c1953fbe0fa clix/moderate.lua --- /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 diff -r 68e09745d928 -r 6c1953fbe0fa squishy --- 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