1 --luacheck: globals meta idsafe
2 local condition_handlers
= {};
4 local jid
= require
"util.jid";
6 -- Helper to convert user-input strings (yes/true//no/false) to a bool
7 local function string_to_boolean(s
)
9 return s
== "yes" or s
== "true";
12 -- Return a code string for a condition that checks whether the contents
13 -- of variable with the name 'name' matches any of the values in the
14 -- comma/space/pipe delimited list 'values'.
15 local function compile_comparison_list(name
, values
)
16 local conditions
= {};
17 for value
in values
:gmatch("[^%s,|]+") do
18 table.insert(conditions
, ("%s == %q"):format(name
, value
));
20 return table.concat(conditions
, " or ");
23 function condition_handlers
.KIND(kind
)
24 assert(kind
, "Expected stanza kind to match against");
25 return compile_comparison_list("name", kind
), { "name" };
28 local wildcard_equivs
= { ["*"] = ".*", ["?"] = "." };
30 local function compile_jid_match_part(part
, match
)
32 return part
.." == nil";
34 local pattern
= match
:match("^<(.*)>$");
36 if pattern
== "*" then
39 if pattern
:find("^<.*>$") then
40 pattern
= pattern
:match("^<(.*)>$");
42 pattern
= pattern
:gsub("%p", "%%%0"):gsub("%%(%p)", wildcard_equivs
);
44 return ("(%s and %s:find(%q))"):format(part
, part
, "^"..pattern
.."$");
46 return ("%s == %q"):format(part
, match
);
50 local function compile_jid_match(which
, match_jid
)
51 local match_node
, match_host
, match_resource
= jid
.split(match_jid
);
52 local conditions
= {};
53 conditions
[#conditions
+1] = compile_jid_match_part(which
.."_node", match_node
);
54 conditions
[#conditions
+1] = compile_jid_match_part(which
.."_host", match_host
);
55 if match_resource
then
56 conditions
[#conditions
+1] = compile_jid_match_part(which
.."_resource", match_resource
);
58 return table.concat(conditions
, " and ");
61 function condition_handlers
.TO(to
)
62 return compile_jid_match("to", to
), { "split_to" };
65 function condition_handlers
.FROM(from
)
66 return compile_jid_match("from", from
), { "split_from" };
69 function condition_handlers
.FROM_EXACTLY(from
)
71 return ("from == %s"):format(metaq(from
, metadeps
)), { "from", unpack(metadeps
) };
74 function condition_handlers
.TO_EXACTLY(to
)
76 return ("to == %s"):format(metaq(to
, metadeps
)), { "to", unpack(metadeps
) };
79 function condition_handlers
.TO_SELF()
80 -- Intentionally not using 'to' here, as that defaults to bare JID when nil
81 return ("stanza.attr.to == nil");
84 function condition_handlers
.TYPE(type)
85 assert(type, "Expected 'type' value to match against");
86 return compile_comparison_list("(type or (name == 'message' and 'normal') or (name == 'presence' and 'available'))", type), { "type", "name" };
89 local function zone_check(zone
, which
)
90 local zone_var
= zone
;
91 if zone
== "$local" then zone_var
= "_local" end
92 local which_not
= which
== "from" and "to" or "from";
93 return ("(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s]) "
94 .."and not(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s])"
96 :format(zone_var
, which
, zone_var
, which
, zone_var
, which
,
97 zone_var
, which_not
, zone_var
, which_not
, zone_var
, which_not
), {
98 "split_to", "split_from", "bare_to", "bare_from", "zone:"..zone
102 function condition_handlers
.ENTERING(zone
)
103 return zone_check(zone
, "to");
106 function condition_handlers
.LEAVING(zone
)
107 return zone_check(zone
, "from");
110 -- IN ROSTER? (parameter is deprecated)
111 function condition_handlers
.IN_ROSTER(yes_no
)
112 local in_roster_requirement
= string_to_boolean(yes_no
or "yes"); -- COMPAT w/ older scripts
113 return "not "..(in_roster_requirement
and "not" or "").." roster_entry", { "roster_entry" };
116 function condition_handlers
.IN_ROSTER_GROUP(group
)
117 return ("not not (roster_entry and roster_entry.groups[%q])"):format(group
), { "roster_entry" };
120 function condition_handlers
.SUBSCRIBED()
121 return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
122 { "rostermanager", "split_to", "bare_to", "bare_from" };
125 function condition_handlers
.PENDING_SUBSCRIPTION_FROM_SENDER()
126 return "(bare_to == bare_from or to_node and rostermanager.is_contact_pending_in(to_node, to_host, bare_from))",
127 { "rostermanager", "split_to", "bare_to", "bare_from" };
130 function condition_handlers
.PAYLOAD(payload_ns
)
131 return ("stanza:get_child(nil, %q)"):format(payload_ns
);
134 function condition_handlers
.INSPECT(path
)
135 if path
:find("=") then
136 local query
, match_type
, value
= path
:match("(.-)([~/$]*)=(.*)");
137 if not(query
:match("#$") or query
:match("@[^/]+")) then
138 error("Stanza path does not return a string (append # for text content or @name for value of named attribute)", 0);
140 local meta_deps
= {};
141 local quoted_value
= ("%q"):format(value
);
142 if match_type
:find("$", 1, true) then
143 match_type
= match_type
:gsub("%$", "");
144 quoted_value
= meta(quoted_value
, meta_deps
);
146 if match_type
== "~" then -- Lua pattern match
147 return ("(stanza:find(%q) or ''):match(%s)"):format(query
, quoted_value
), meta_deps
;
148 elseif match_type
== "/" then -- find literal substring
149 return ("(stanza:find(%q) or ''):find(%s, 1, true)"):format(query
, quoted_value
), meta_deps
;
150 elseif match_type
== "" then -- exact match
151 return ("stanza:find(%q) == %s"):format(query
, quoted_value
), meta_deps
;
153 error("Unrecognised comparison '"..match_type
.."='", 0);
156 return ("stanza:find(%q)"):format(path
);
159 function condition_handlers
.FROM_GROUP(group_name
)
160 return ("group_contains(%q, bare_from)"):format(group_name
), { "group_contains", "bare_from" };
163 function condition_handlers
.TO_GROUP(group_name
)
164 return ("group_contains(%q, bare_to)"):format(group_name
), { "group_contains", "bare_to" };
167 function condition_handlers
.CROSSING_GROUPS(group_names
)
169 for group_name
in group_names
:gmatch("([^, ][^,]+)") do
170 group_name
= group_name
:match("^%s*(.-)%s*$"); -- Trim leading/trailing whitespace
171 -- Just check that's it is crossing from outside group to inside group
172 table.insert(code
, ("(group_contains(%q, bare_to) and group_contains(%q, bare_from))"):format(group_name
, group_name
))
174 return "not "..table.concat(code
, " or "), { "group_contains", "bare_to", "bare_from" };
177 function condition_handlers
.FROM_ADMIN_OF(host
)
178 return ("is_admin(bare_from, %s)"):format(host
~= "*" and metaq(host
) or nil), { "is_admin", "bare_from" };
181 function condition_handlers
.TO_ADMIN_OF(host
)
182 return ("is_admin(bare_to, %s)"):format(host
~= "*" and metaq(host
) or nil), { "is_admin", "bare_to" };
185 function condition_handlers
.FROM_ADMIN()
186 return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
189 function condition_handlers
.TO_ADMIN()
190 return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
193 local day_numbers
= { sun
= 0, mon
= 2, tue
= 3, wed
= 4, thu
= 5, fri
= 6, sat
= 7 };
195 local function current_time_check(op
, hour
, minute
)
196 hour
, minute
= tonumber(hour
), tonumber(minute
);
197 local adj_op
= op
== "<" and "<" or ">="; -- Start time inclusive, end time exclusive
199 return "(current_hour"..adj_op
..hour
..")";
201 return "((current_hour"..op
..hour
..") or (current_hour == "..hour
.." and current_minute"..adj_op
..minute
.."))";
205 local function resolve_day_number(day_name
)
206 return assert(day_numbers
[day_name
:sub(1,3):lower()], "Unknown day name: "..day_name
);
209 function condition_handlers
.DAY(days
)
210 local conditions
= {};
211 for day_range
in days
:gmatch("[^,]+") do
212 local day_start
, day_end
= day_range
:match("(%a+)%s*%-%s*(%a+)");
213 if day_start
and day_end
then
214 local day_start_num
, day_end_num
= resolve_day_number(day_start
), resolve_day_number(day_end
);
216 if day_end_num
< day_start_num
then
219 table.insert(conditions
, ("current_day >= %d %s current_day <= %d"):format(day_start_num
, op
, day_end_num
));
220 elseif day_range
:find("%a") then
221 local day
= resolve_day_number(day_range
:match("%a+"));
222 table.insert(conditions
, "current_day == "..day
);
224 error("Unable to parse day/day range: "..day_range
);
227 assert(#conditions
>0, "Expected a list of days or day ranges");
228 return "("..table.concat(conditions
, ") or (")..")", { "time:day" };
231 function condition_handlers
.TIME(ranges
)
232 local conditions
= {};
233 for range
in ranges
:gmatch("([^,]+)") do
235 range
= range
:lower()
236 :gsub("(%d+):?(%d*) *am", function (h
, m
) return tostring(tonumber(h
)%12)..":"..(tonumber(m
) or "00"); end)
237 :gsub("(%d+):?(%d*) *pm", function (h
, m
) return tostring(tonumber(h
)%12+12)..":"..(tonumber(m
) or "00"); end);
238 local start_hour
, start_minute
= range
:match("(%d+):(%d+) *%-");
239 local end_hour
, end_minute
= range
:match("%- *(%d+):(%d+)");
240 local op
= tonumber(start_hour
) > tonumber(end_hour
) and " or " or " and ";
241 if start_hour
and end_hour
then
242 table.insert(clause
, current_time_check(">", start_hour
, start_minute
));
243 table.insert(clause
, current_time_check("<", end_hour
, end_minute
));
246 error("Unable to parse time range: "..range
);
248 table.insert(conditions
, "("..table.concat(clause
, " "..op
.." ")..")");
250 return table.concat(conditions
, " or "), { "time:hour,min" };
253 function condition_handlers
.LIMIT(spec
)
254 local name
, param
= spec
:match("^(%w+) on (.+)$");
255 local meta_deps
= {};
258 name
= spec
:match("^%w+$");
260 error("Unable to parse LIMIT specification");
263 param
= meta(("%q"):format(param
), meta_deps
);
267 return ("not global_throttle_%s:poll(1)"):format(name
), { "globalthrottle:"..name
, unpack(meta_deps
) };
269 return ("not multi_throttle_%s:poll_on(%s, 1)"):format(name
, param
), { "multithrottle:"..name
, unpack(meta_deps
) };
272 function condition_handlers
.ORIGIN_MARKED(name_and_time
)
273 local name
, time
= name_and_time
:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
275 name
= name_and_time
:match("^%s*([%w_]+)%s*$");
278 error("Error parsing mark name, see documentation for usage examples");
281 return ("(current_timestamp - (session.firewall_marked_%s or 0)) < %d"):format(idsafe(name
), tonumber(time
)), { "timestamp" };
283 return ("not not session.firewall_marked_"..idsafe(name
));
286 function condition_handlers
.USER_MARKED(name_and_time
)
287 local name
, time
= name_and_time
:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
289 name
= name_and_time
:match("^%s*([%w_]+)%s*$");
292 error("Error parsing mark name, see documentation for usage examples");
295 return ("(current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)) < %d"):format(idsafe(name
), tonumber(time
)), { "timestamp" };
297 return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name
)..")");
300 function condition_handlers
.SENT_DIRECTED_PRESENCE_TO_SENDER()
301 return "not not (session.directed and session.directed[from])", { "from" };
305 function condition_handlers
.TO_FULL_JID()
306 return "not not full_sessions[to]", { "to" };
309 -- CHECK LIST: spammers contains $<@from>
310 function condition_handlers
.CHECK_LIST(list_condition
)
311 local list_name
, expr
= list_condition
:match("(%S+) contains (.+)$");
312 if not (list_name
and expr
) then
313 error("Error parsing list check, syntax: LISTNAME contains EXPRESSION");
315 local meta_deps
= {};
316 expr
= meta(("%q"):format(expr
), meta_deps
);
317 return ("list_%s:contains(%s) == true"):format(list_name
, expr
), { "list:"..list_name
, unpack(meta_deps
) };
320 -- SCAN: body for word in badwords
321 function condition_handlers
.SCAN(scan_expression
)
322 local search_name
, pattern_name
, list_name
= scan_expression
:match("(%S+) for (%S+) in (%S+)$");
323 if not (search_name
) then
324 error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST");
326 return ("scan_list(list_%s, %s)"):format(list_name
, "tokens_"..search_name
.."_"..pattern_name
), { "scan_list", "tokens:"..search_name
.."-"..pattern_name
, "list:"..list_name
};
329 -- COUNT: lines in body < 10
330 local valid_comp_ops
= { [">"] = ">", ["<"] = "<", ["="] = "==", ["=="] = "==", ["<="] = "<=", [">="] = ">=" };
331 function condition_handlers
.COUNT(count_expression
)
332 local pattern_name
, search_name
, comparator_expression
= count_expression
:match("(%S+) in (%S+) (.+)$");
333 if not (pattern_name
) then
334 error("Error parsing COUNT expression, syntax: PATTERN in SEARCH COMPARATOR");
337 comparator_expression
= comparator_expression
:gsub("%d+", function (value_string
)
338 value
= tonumber(value_string
);
342 error("Error parsing COUNT expression, expected value");
344 local comp_op
= comparator_expression
:gsub("%s+", "");
345 assert(valid_comp_ops
[comp_op
], "Error parsing COUNT expression, unknown comparison operator: "..comp_op
);
346 return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(search_name
, pattern_name
, comp_op
, value
), { "it_count", "search:"..search_name
, "pattern:"..pattern_name
};
349 return condition_handlers
;