Fix rm methods in bans plugin
[rbot] / data / rbot / plugins / bans.rb
1 # BansPlugin v3 for 0.9.11\r
2 #\r
3 # Managing kick and bans, automatically removing bans after timeouts, quiet bans, and kickban/quietban based on regexp\r
4 #\r
5 # (c) 2006 Marco Gulino <marco@kmobiletools.org>\r
6 # (c) 2007 kamu <mr.kamu@gmail.com>\r
7 # (c) 2007 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>\r
8 #\r
9 # v1 -> v2 (kamu's version, never released)\r
10 #   * reworked\r
11 #   * autoactions triggered on join\r
12 #   * action on join or badword can be anything: kick, ban, kickban, quiet\r
13 #\r
14 # v2 -> v3 (GB)\r
15 #   * remove the 'bans' prefix from most of the commands\r
16 #   * (un)quiet has been renamed to (un)silence because 'quiet' was used to tell the bot to keep quiet\r
17 #   * both (un)quiet and (un)silence are accepted as actions\r
18 #   * use the more descriptive 'onjoin' term for autoactions\r
19 #   * convert v1's (0.9.10) :bans and :bansmasks to BadWordActions and WhitelistEntries\r
20 #   * enhanced list manipulation facilities\r
21 #   * fixed regexp usage in requirements for plugin map\r
22 #   * add proper auth management\r
23 #\r
24 # Licensed under GPL V2.\r
25 \r
26 OnJoinAction = Struct.new("OnJoinAction", :host, :action, :channel, :reason)\r
27 BadWordAction = Struct.new("BadWordAction", :regexp, :action, :channel, :timer, :reason)\r
28 WhitelistEntry = Struct.new("WhitelistEntry", :host, :channel)\r
29 \r
30 \r
31 class BansPlugin < Plugin\r
32 \r
33   IdxRe = /^\d+$/\r
34   TimerRe = /^\d+[smhd]$/\r
35   ChannelRe = /^#+[^\s]+$/\r
36   ChannelAllRe = /^(?:all|#+[^\s]+)$/\r
37   ActionRe = /(?:ban|kick|kickban|silence|quiet)/\r
38 \r
39   def name\r
40     "bans"\r
41   end\r
42 \r
43   def make_badword_rx(txt)\r
44     return /\b#{txt}\b/i\r
45   end\r
46 \r
47   def initialize\r
48     super\r
49 \r
50     # Convert old BadWordActions, which were simpler and labelled :bans\r
51     if @registry.has_key? :bans\r
52       badwords = Array.new\r
53       bans = @registry[:bans]\r
54       @registry[:bans].each { |ar|\r
55         case ar[0]\r
56         when "quietban"\r
57           action = :silence\r
58         when "kickban"\r
59           action = :kickban\r
60         else\r
61           # Shouldn't happen\r
62           warn "Unknown action in old data #{ar.inspect} -- entry ignored"\r
63           next\r
64         end\r
65         bans.delete(ar)\r
66         chan = ar[1].downcase\r
67         regexp = make_badword_rx(ar[2])\r
68         badwords << BadWordAction.new(regexp, action, chan, "0s", "")\r
69       }\r
70       @registry[:badwords] = badwords\r
71       if bans.length > 0\r
72         # Store the ones we couldn't convert\r
73         @registry[:bans] = bans\r
74       else\r
75         @registry.delete(:bans)\r
76       end\r
77     else\r
78       @registry[:badwords] = Array.new unless @registry.has_key? :badwords\r
79     end\r
80 \r
81     # Convert old WhitelistEntries, which were simpler and labelled :bansmasks\r
82     if @registry.has_key? :bans\r
83       wl = Array.new\r
84       @registry[:bansmasks].each { |mask|\r
85         badwords << WhitelistEntry.new(mask, "all")\r
86       }\r
87       @registry[:whitelist] = wl\r
88       @registry.delete(:bansmasks)\r
89     else\r
90       @registry[:whitelist] = Array.new unless @registry.has_key? :whitelist\r
91     end\r
92 \r
93     @registry[:onjoin] = Array.new unless @registry.has_key? :onjoin\r
94   end\r
95 \r
96   def help(plugin, topic="")\r
97     case plugin\r
98     when "ban"\r
99       return "ban <nick/hostmask> [Xs/m/h/d] [#channel]: ban a user from the given channel for the given amount of time. default is forever, on the current channel"\r
100     when "unban"\r
101       return "unban <nick/hostmask> [#channel]: unban a user from the given channel. defaults to the current channel"\r
102     when "kick"\r
103       return "kick <nick> [#channel] [reason ...]: kick a user from the given channel with the given reason. defaults to the current channel, no reason"\r
104     when "kickban"\r
105       return "kickban <nick> [Xs/m/h/d] [#channel] [reason ...]: kicks and bans a user from the given channel for the given amount of time, with the given reason. default is forever, on the current channel, with no reason"\r
106     when "silence"\r
107       return "silence <nick/hostmask> [Xs/m/h/d] [#channel]: silence a user on the given channel for the given time. default is forever, on the current channel. not all servers support silencing users"\r
108     when "unsilence"\r
109       return "unsilence <nick/hostmask> [#channel]: allow the given user to talk on the given channel. defaults to the current channel"\r
110     when "bans"\r
111       case topic\r
112       when "add"\r
113         return "bans add <onjoin|badword|whitelist>: add an automatic action for people that join or say some bad word, or a whitelist entry. further help available"\r
114       when "add onjoin"\r
115         return "bans add onjoin <hostmask> [action] [#channel] [reason ...]: will add an autoaction for any one who joins with hostmask. default action is silence, default channel is all"\r
116       when "add badword"\r
117         return "bans add badword <regexp> [action] [Xs/m/h/d] [#channel|all] [reason ...]: adds a badword regexp, if a user sends a message that matches regexp, the action will be invoked. default action is silence, default channel is all"\r
118       when "add whitelist"\r
119         return "bans add whitelist <hostmask> [#channel|all]: add the given hostmask to the whitelist. no autoaction will be triggered by users on the whitelist"\r
120       when "rm"\r
121         return "bans rm <onjoin|badword|whitelist> <hostmask/regexp> [#channel], or bans rm <onjoin|badword|whitelist> index <num>: removes the specified onjoin or badword rule or whitelist entry."\r
122       when "list"\r
123         return"bans list <onjoin|badword|whitelist>: lists all onjoin or badwords or whitelist entries"\r
124       end\r
125     end\r
126     return "bans <command>: allows a user of the bot to do a range of bans and unbans. commands are: [un]ban, kick[ban], [un]silence, add, rm and list"\r
127   end\r
128 \r
129   def listen(m)\r
130     return unless m.respond_to?(:public?) and m.public?\r
131     @registry[:whitelist].each { |white|\r
132       next unless ['all', m.target.downcase].include?(white.channel)\r
133       return if m.source.matches?(white.host)\r
134     }\r
135 \r
136     @registry[:badwords].each { |badword|\r
137       next unless ['all', m.target.downcase].include?(badword.channel)\r
138       next unless badword.regexp.match(m.message)\r
139 \r
140       do_cmd(badword.action.to_sym, m.source.nick, m.target, badword.timer, badword.reason)\r
141       m.reply "bad word detected! #{badword.action} for #{badword.timer} because: #{badword.reason}"\r
142       return\r
143     }\r
144   end\r
145 \r
146   def join(m)\r
147     @registry[:whitelist].each { |white|\r
148       next unless ['all', m.target.downcase].include?(white.channel)\r
149       return if m.source.matches?(white.host)\r
150     }\r
151 \r
152     @registry[:onjoin].each { |auto|\r
153       next unless ['all', m.target.downcase].include?(auto.channel)\r
154       next unless m.source.matches? auto.host\r
155 \r
156       do_cmd(auto.action.to_sym, m.source.nick, m.target, "0s", auto.reason)\r
157       return\r
158     }\r
159   end\r
160 \r
161   def ban_user(m, params=nil)\r
162     nick, channel = params[:nick], check_channel(m, params[:channel])\r
163     timer = params[:timer]\r
164     do_cmd(:ban, nick, channel, timer)\r
165   end\r
166 \r
167   def unban_user(m, params=nil)\r
168     nick, channel = params[:nick], check_channel(m, params[:channel])\r
169     do_cmd(:unban, nick, channel)\r
170   end\r
171 \r
172   def kick_user(m, params=nil)\r
173     nick, channel = params[:nick], check_channel(m, params[:channel])\r
174     reason = params[:reason].to_s\r
175     do_cmd(:kick, nick, channel, "0s", reason)\r
176   end\r
177 \r
178   def kickban_user(m, params=nil)\r
179     nick, channel, reason = params[:nick], check_channel(m, params[:channel])\r
180     timer, reason = params[:timer], params[:reason].to_s\r
181     do_cmd(:kickban, nick, channel, timer, reason)\r
182   end\r
183 \r
184   def silence_user(m, params=nil)\r
185     nick, channel = params[:nick], check_channel(m, params[:channel])\r
186     timer = params[:timer]\r
187     do_cmd(:silence, nick, channel, timer)\r
188   end\r
189 \r
190   def unsilence_user(m, params=nil)\r
191     nick, channel = params[:nick], check_channel(m, params[:channel])\r
192     do_cmd(:unsilence, nick, channel)\r
193   end\r
194 \r
195   def add_onjoin(m, params=nil)\r
196     begin\r
197       host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase\r
198       action, reason = params[:action], params[:reason].to_s\r
199 \r
200       autos = @registry[:onjoin]\r
201       autos << OnJoinAction.new(host, action, channel, reason.dup)\r
202       @registry[:onjoin] = autos\r
203 \r
204       m.okay\r
205     rescue\r
206       error $!\r
207       m.reply $!\r
208     end\r
209   end\r
210 \r
211   def list_onjoin(m, params=nil)\r
212     m.reply "onjoin rules: #{@registry[:onjoin].length}"\r
213     @registry[:onjoin].each_with_index { |auto, idx|\r
214       m.reply "\##{idx+1}: #{auto.host} | #{auto.action} | #{auto.channel} | '#{auto.reason}'"\r
215     }\r
216   end\r
217 \r
218   def rm_onjoin(m, params=nil)\r
219     autos = @registry[:onjoin]\r
220     count = autos.length\r
221 \r
222     idx = nil\r
223     idx = params[:idx].to_i if params[:idx]\r
224 \r
225     if idx\r
226       if idx > count\r
227         m.reply "No such onjoin \##{idx}"\r
228         return\r
229       end\r
230       autos.delete_at(idx-1)\r
231     else\r
232       begin\r
233         host = m.server.new_netmask(params[:host])\r
234         channel = params[:channel].downcase\r
235 \r
236         autos.each { |rule|\r
237           next unless ['all', rule.channel].include?(channel)\r
238           autos.delete rule if rule.host == host\r
239         }\r
240       rescue\r
241         error $!\r
242         m.reply $!\r
243       end\r
244     end\r
245     @registry[:onjoin] = autos\r
246     if count > autos.length\r
247       m.okay\r
248     else\r
249       m.reply "No matching onjoin rule for #{host} found"\r
250     end\r
251   end\r
252 \r
253   def add_badword(m, params=nil)\r
254     regexp, channel = make_badword_rx(params[:regexp]), params[:channel].downcase.dup\r
255     action, timer, reason = params[:action], params[:timer].dup, params[:reason].to_s\r
256 \r
257     badwords = @registry[:badwords]\r
258     badwords << BadWordAction.new(regexp, action, channel, timer, reason)\r
259     @registry[:badwords] = badwords\r
260 \r
261     m.okay\r
262   end\r
263 \r
264   def list_badword(m, params=nil)\r
265     m.reply "badword rules: #{@registry[:badwords].length}"\r
266 \r
267     @registry[:badwords].each_with_index { |badword, idx|\r
268       m.reply "\##{idx+1}: #{badword.regexp.source} | #{badword.action} | #{badword.channel} | #{badword.timer} | #{badword.reason}"\r
269     }\r
270   end\r
271 \r
272   def rm_badword(m, params=nil)\r
273     badwords = @registry[:badwords]\r
274     count = badwords.length\r
275 \r
276     idx = nil\r
277     idx = params[:idx].to_i if params[:idx]\r
278 \r
279     if idx\r
280       if idx > count\r
281         m.reply "No such badword \##{idx}"\r
282         return\r
283       end\r
284       badwords.delete_at(idx-1)\r
285     else\r
286       channel = params[:channel].downcase\r
287 \r
288       regexp = make_badword_rx(params[:regexp])\r
289       debug "Trying to remove #{regexp.inspect} from #{badwords.inspect}"\r
290 \r
291       badwords.each { |badword|\r
292         next unless ['all', badword.channel].include?(channel)\r
293         debug "Removing #{badword.inspect}" if badword == regexp\r
294         badwords.delete(badword) if badword == regexp\r
295       }\r
296     end\r
297 \r
298     @registry[:badwords] = badwords\r
299     if count > badwords.length\r
300       m.okay\r
301     else\r
302       m.reply "No matching badword #{regexp} found"\r
303     end\r
304   end\r
305 \r
306   def add_whitelist(m, params=nil)\r
307     begin\r
308       host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase\r
309 \r
310       # TODO check if a whitelist entry for this host already exists\r
311       whitelist = @registry[:whitelist]\r
312       whitelist << WhitelistEntry.new(host, channel)\r
313       @registry[:whitelist] = whitelist\r
314 \r
315       m.okay\r
316     rescue\r
317       error $!\r
318       m.reply $!\r
319     end\r
320   end\r
321 \r
322   def list_whitelist(m, params=nil)\r
323     m.reply "whitelist entries: #{@registry[:whitelist].length}"\r
324     @registry[:whitelist].each_with_index { |auto, idx|\r
325       m.reply "\##{idx+1}: #{auto.host} | #{auto.channel}"\r
326     }\r
327   end\r
328 \r
329   def rm_whitelist(m, params=nil)\r
330     wl = @registry[:whitelist]\r
331     count = wl.length\r
332 \r
333     idx = nil\r
334     idx = params[:idx].to_i if params[:idx]\r
335 \r
336     if idx\r
337       if idx > count\r
338         m.reply "No such whitelist entry \##{idx}"\r
339         return\r
340       end\r
341       wl.delete_at(idx-1)\r
342     else\r
343       begin\r
344         host = m.server.new_netmask(params[:host])\r
345         channel = params[:channel].downcase\r
346 \r
347         wl.each { |rule|\r
348           next unless ['all', rule.channel].include?(channel)\r
349           wl.delete rule if rule.host == host\r
350         }\r
351       rescue\r
352         error $!\r
353         m.reply $!\r
354       end\r
355     end\r
356     @registry[:whitelist] = wl\r
357     if count > whitelist.length\r
358       m.okay\r
359     else\r
360       m.reply "No host matching #{host}"\r
361     end\r
362   end\r
363 \r
364   private\r
365   def check_channel(m, strchannel)\r
366     begin\r
367       raise "must specify channel if using privmsg" if m.private? and not strchannel\r
368       channel = m.server.channel(strchannel) || m.target\r
369       raise "I am not in that channel" unless channel.has_user?(@bot.nick)\r
370 \r
371       return channel\r
372     rescue\r
373       error $!\r
374       m.reply $!\r
375     end\r
376   end\r
377 \r
378   def do_cmd(action, nick, channel, timer_in=nil, reason=nil)\r
379     case timer_in\r
380     when nil\r
381       timer = 0\r
382     when /^(\d+)s$/\r
383       timer = $1.to_i\r
384     when /^(\d+)m$/\r
385       timer = $1.to_i * 60\r
386     when /^(\d+)h$/\r
387       timer = $1.to_i * 60 * 60 \r
388     when /^(\d+)d$/\r
389       timer = $1.to_i * 60 * 60 * 24\r
390     else\r
391       raise "Wrong time specifications"\r
392     end\r
393 \r
394     case action\r
395     when :ban\r
396       set_mode(channel, "+b", nick)\r
397       @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0\r
398     when :unban\r
399       set_mode(channel, "-b", nick)\r
400     when :kick\r
401       do_kick(channel, nick, reason)\r
402     when :kickban\r
403       set_mode(channel, "+b", nick)\r
404       @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0\r
405       do_kick(channel, nick, reason)\r
406     when :silence, :quiet\r
407       set_mode(channel, "+q", nick)\r
408       @bot.timer.add_once(timer) { set_mode(channel, "-q", nick) } if timer > 0\r
409     when :unsilence, :unquiet\r
410       set_mode(channel, "-q", nick)\r
411     end\r
412   end\r
413 \r
414   def set_mode(channel, mode, nick)\r
415     host = channel.has_user?(nick) ? "*!*@" + channel.get_user(nick).host : nick\r
416     @bot.mode(channel, mode, host)\r
417   end\r
418 \r
419   def do_kick(channel, nick, reason="")\r
420     @bot.kick(channel, nick, reason)\r
421   end\r
422 end\r
423 \r
424 plugin = BansPlugin.new\r
425 \r
426 plugin.default_auth( 'act', false )\r
427 plugin.default_auth( 'edit', false )\r
428 plugin.default_auth( 'list', true )\r
429 \r
430 plugin.map 'ban :nick :timer :channel', :action => 'ban_user',\r
431   :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},\r
432   :defaults => {:timer => nil, :channel => nil},\r
433   :auth_path => 'act'\r
434 plugin.map 'unban :nick :channel', :action => 'unban_user',\r
435   :requirements => {:channel => BansPlugin::ChannelRe},\r
436   :defaults => {:channel => nil},\r
437   :auth_path => 'act'\r
438 plugin.map 'kick :nick :channel *reason', :action => 'kick_user',\r
439   :requirements => {:channel => BansPlugin::ChannelRe},\r
440   :defaults => {:channel => nil, :reason => 'requested'},\r
441   :auth_path => 'act'\r
442 plugin.map 'kickban :nick :timer :channel *reason', :action => 'kickban_user',\r
443   :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},\r
444   :defaults => {:timer => nil, :channel => nil, :reason => 'requested'},\r
445   :auth_path => 'act'\r
446 plugin.map 'silence :nick :timer :channel', :action => 'silence_user',\r
447   :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},\r
448   :defaults => {:timer => nil, :channel => nil},\r
449   :auth_path => 'act'\r
450 plugin.map 'unsilence :nick :channel', :action => 'unsilence_user',\r
451   :requirements => {:channel => BansPlugin::ChannelRe},\r
452   :defaults => {:channel => nil},\r
453   :auth_path => 'act'\r
454 \r
455 plugin.map 'bans add onjoin :host :action :channel *reason', :action => 'add_onjoin',\r
456   :requirements => {:action => BansPlugin::ActionRe, :channel => BansPlugin::ChannelAllRe},\r
457   :defaults => {:action => 'kickban', :channel => 'all', :reason => 'netmask not welcome'},\r
458   :auth_path => 'edit::onjoin'\r
459 plugin.map 'bans rm onjoin index :idx', :action => 'rm_onjoin',\r
460   :requirements => {:num => BansPlugin::IdxRe},\r
461   :auth_path => 'edit::onjoin'\r
462 plugin.map 'bans rm onjoin :host :channel', :action => 'rm_onjoin',\r
463   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
464   :defaults => {:channel => 'all'},\r
465   :auth_path => 'edit::onjoin'\r
466 plugin.map 'bans list onjoin[s]', :action => 'list_onjoin',\r
467   :auth_path => 'list::onjoin'\r
468 \r
469 plugin.map 'bans add badword :regexp :action :timer :channel *reason', :action => 'add_badword',\r
470   :requirements => {:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},\r
471   :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'bad word'},\r
472   :auth_path => 'edit::badword'\r
473 plugin.map 'bans rm badword index :idx', :action => 'rm_badword',\r
474   :requirements => {:num => BansPlugin::IdxRe},\r
475   :auth_path => 'edit::badword'\r
476 plugin.map 'bans rm badword :regexp :channel', :action => 'rm_badword',\r
477   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
478   :defaults => {:channel => 'all'},\r
479   :auth_path => 'edit::badword'\r
480 plugin.map 'bans list badword[s]', :action => 'list_badword',\r
481   :auth_path => 'list::badword'\r
482 \r
483 plugin.map 'bans add whitelist :host :channel', :action => 'add_whitelist',\r
484   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
485   :defaults => {:channel => 'all'},\r
486   :auth_path => 'edit::whitelist'\r
487 plugin.map 'bans rm whitelist index :idx', :action => 'rm_whitelist',\r
488   :requirements => {:num => BansPlugin::IdxRe},\r
489   :auth_path => 'edit::whitelist'\r
490 plugin.map 'bans rm whitelist :host :channel', :action => 'rm_whitelist',\r
491   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
492   :defaults => {:channel => 'all'},\r
493   :auth_path => 'edit::whitelist'\r
494 plugin.map 'bans list whitelist', :action => 'list_whitelist',\r
495   :auth_path => 'list::whitelist'\r
496 \r