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