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