clix/moderate.lua

1
local jid_split = require"prosody.util.jid".split;
2
local datetime = require "prosody.util.datetime";
3
local st = require "prosody.util.stanza";
4
local uuid = require"prosody.util.uuid".generate;
5
 
6
return function(opts, arg)
7
	if opts.help then
8
		print("clix moderate --room=room@muc.example.com")
9
		print("\t--start=timestamp")
10
		print("\t--end=timestamp")
11
		print("\t--from=nickname")
12
		print("\t--body-contains=\"some spam\"")
13
		print("\t--reason=\"spam\"")
14
		print("\t--dry-run")
15
		return 0;
16
	elseif opts.short_help or arg[1] or not opts.room then
17
		print("Remove messages from a MUC");
18
		return;
19
	end
20
 
21
	local function parse_datetime(s)
22
		if s:match("^%d%d:") then
23
			s = datetime.date().."T"..s;
24
		end
25
		if #s < 20 then
26
			s = s .. ("0000-01-01T00:00:00Z"):sub(#s+1)
27
		end
28
		return datetime.parse(s)
29
	end
30
 
31
	if opts.start then
32
		opts.start = parse_datetime(opts.start);
33
	end
34
	if opts["end"] then
35
		opts["end"] = parse_datetime(opts["end"]);
36
	end
37
 
38
	local function matches(message)
39
		local nick = select(3, jid_split(message.attr.from));
40
		if opts.from and opts.from ~= nick then return end
41
		local body = message:get_child_text("body");
42
		if opts.body_contains and not string.find(body or "", opts.body_contains, 1, true) then return end
43
		if opts.body_match and not string.find(body or "", opts.body_match) then return end
44
		return true;
45
	end
46
 
47
	local function on_connect(conn)
48
		local waiting = {}; -- to keep track of outstanding queries
49
 
50
		local function done(with)
51
			waiting[with] = nil;
52
			if next(waiting) == nil then
53
				conn:close();
54
			end
55
		end
56
 
57
		local function moderate(id)
58
			waiting[id] = true;
59
			-- TODO maybe queue and send the next request when a response comes in?
60
			local mod_iq = st.iq({ id = uuid(); type = "set"; to = opts.room })
61
				:tag("apply-to", { xmlns = "urn:xmpp:fasten:0"; id = id })
62
					:tag("moderate", { xmlns = "urn:xmpp:message-moderate:0" })
63
					:tag("retract", { xmlns = "urn:xmpp:message-retract:0" }):up()
64
			if opts.reason then
65
				mod_iq:tag("reason", { xmlns = "urn:xmpp:message-moderate:0" }):text(opts.reason):up()
66
			end
67
			mod_iq:reset();
68
 
69
			if opts.dry_run then
70
				done(id);
71
				conn:debug("Would send: %s", mod_iq);
72
				return;
73
			end
74
 
75
			return conn:send_iq(mod_iq, function (ret)
76
				if ret.attr.type == "error" then
77
					local t, cond, msg = ret:get_error();
78
					conn:error("Retracting message with id %s failed: %s(%s, %s)", msg or "", t, cond);
79
				end
80
				done(id);
81
			end);
82
		end
83
 
84
		local function handle_results(result, err)
85
			if not result then
86
				conn:error("Archive query failed: %s", err);
87
				return done(true);
88
			end
89
			for _, item in ipairs(result) do
90
				if matches(item.message) then
91
					conn:info("Moderate %s", item.message:top_tag())
92
					moderate(item.id);
93
				else
94
					conn:debug("Skip %s", item.message:top_tag())
95
				end
96
			end
97
 
98
			if result.complete == nil then -- COMPAT verse
99
				result.complete = opts.after == nil or result[1] == nil;
100
			end
101
 
102
			if not result.complete then
103
				-- Proceed to the next page
104
				opts.after = result.last;
105
				return conn:query_archive(opts.room, opts, handle_results);
106
			else
107
				-- All done, just wait for any outstanding moderation queries to complete
108
				done(true);
109
			end
110
		end
111
 
112
		waiting[true] = true; -- for the archive query
113
		conn:query_archive(opts.room, opts, handle_results);
114
	end
115
 
116
	clix_connect(opts, on_connect, { "archive" });
117
end