]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/bans.rb
case fixes in bans
[user/henk/code/ruby/rbot.git] / 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 = params[:idx].to_i\r
223 \r
224     if idx\r
225       if idx > count\r
226         m.reply "No such onjoin \##{idx}"\r
227         return\r
228       end\r
229       autos.delete_at(idx-1)\r
230     else\r
231       begin\r
232         host = m.server.new_netmask(params[:host])\r
233         channel = params[:channel].downcase\r
234 \r
235         autos.each { |rule|\r
236           next unless ['all', rule.channel].include?(channel)\r
237           autos.delete rule if rule.host == host\r
238         }\r
239       rescue\r
240         error $!\r
241         m.reply $!\r
242       end\r
243     end\r
244     @registry[:onjoin] = autos\r
245     m.okay if count > autos.length\r
246   end\r
247 \r
248   def add_badword(m, params=nil)\r
249     regexp, channel = make_badword_rx(params[:regexp]), params[:channel].downcase.dup\r
250     action, timer, reason = params[:action], params[:timer].dup, params[:reason].to_s\r
251 \r
252     badwords = @registry[:badwords]\r
253     badwords << BadWordAction.new(regexp, action, channel, timer, reason)\r
254     @registry[:badwords] = badwords\r
255 \r
256     m.okay\r
257   end\r
258 \r
259   def list_badword(m, params=nil)\r
260     m.reply "badword rules: #{@registry[:badwords].length}"\r
261 \r
262     @registry[:badwords].each_with_index { |badword, idx|\r
263       m.reply "\##{idx+1}: #{badword.regexp.source} | #{badword.action} | #{badword.channel} | #{badword.timer} | #{badword.reason}"\r
264     }\r
265   end\r
266 \r
267   def rm_badword(m, params=nil)\r
268     badwords = @registry[:badwords]\r
269     count = badwords.length\r
270 \r
271     idx = params[:idx].to_i\r
272 \r
273     if idx\r
274       if idx > count\r
275         m.reply "No such badword \##{idx}"\r
276         return\r
277       end\r
278       badwords.delete_at(idx-1)\r
279     else\r
280       channel = params[:channel].downcase\r
281 \r
282       regexp = make_badword_rx(params[:regexp])\r
283       debug "Trying to remove #{regexp.inspect} from #{badwords.inspect}"\r
284 \r
285       badwords.each { |badword|\r
286         next unless ['all', badword.channel].include?(channel)\r
287         badwords.delete(badword) if badword == regexp\r
288       }\r
289     end\r
290 \r
291     @registry[:badwords] = badwords\r
292     m.okay if count > badwords.length\r
293   end\r
294 \r
295   def add_whitelist(m, params=nil)\r
296     begin\r
297       host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase\r
298 \r
299       # TODO check if a whitelist entry for this host already exists\r
300       whitelist = @registry[:whitelist]\r
301       whitelist << WhitelistEntry.new(host, channel)\r
302       @registry[:whitelist] = whitelist\r
303 \r
304       m.okay\r
305     rescue\r
306       error $!\r
307       m.reply $!\r
308     end\r
309   end\r
310 \r
311   def list_whitelist(m, params=nil)\r
312     m.reply "whitelist entries: #{@registry[:whitelist].length}"\r
313     @registry[:whitelist].each_with_index { |auto, idx|\r
314       m.reply "\##{idx+1}: #{auto.host} | #{auto.channel}"\r
315     }\r
316   end\r
317 \r
318   def rm_whitelist(m, params=nil)\r
319     wl = @registry[:whitelist]\r
320     count = wl.length\r
321 \r
322     idx = params[:idx].to_i\r
323 \r
324     if idx\r
325       if idx > count\r
326         m.reply "No such whitelist entry \##{idx}"\r
327         return\r
328       end\r
329       wl.delete_at(idx-1)\r
330     else\r
331       begin\r
332         host = m.server.new_netmask(params[:host])\r
333         channel = params[:channel].downcase\r
334 \r
335         wl.each { |rule|\r
336           next unless ['all', rule.channel].include?(channel)\r
337           wl.delete rule if rule.host == host\r
338         }\r
339       rescue\r
340         error $!\r
341         m.reply $!\r
342       end\r
343     end\r
344     @registry[:whitelist] = wl\r
345     m.okay if count > whitelist.length\r
346   end\r
347 \r
348   private\r
349   def check_channel(m, strchannel)\r
350     begin\r
351       raise "must specify channel if using privmsg" if m.private? and not strchannel\r
352       channel = m.server.channel(strchannel) || m.target\r
353       raise "I am not in that channel" unless channel.has_user?(@bot.nick)\r
354 \r
355       return channel\r
356     rescue\r
357       error $!\r
358       m.reply $!\r
359     end\r
360   end\r
361 \r
362   def do_cmd(action, nick, channel, timer_in=nil, reason=nil)\r
363     case timer_in\r
364     when nil\r
365       timer = 0\r
366     when /^(\d+)s$/\r
367       timer = $1.to_i\r
368     when /^(\d+)m$/\r
369       timer = $1.to_i * 60\r
370     when /^(\d+)h$/\r
371       timer = $1.to_i * 60 * 60 \r
372     when /^(\d+)d$/\r
373       timer = $1.to_i * 60 * 60 * 24\r
374     else\r
375       raise "Wrong time specifications"\r
376     end\r
377 \r
378     case action\r
379     when :ban\r
380       set_mode(channel, "+b", nick)\r
381       @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0\r
382     when :unban\r
383       set_mode(channel, "-b", nick)\r
384     when :kick\r
385       do_kick(channel, nick, reason)\r
386     when :kickban\r
387       set_mode(channel, "+b", nick)\r
388       @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0\r
389       do_kick(channel, nick, reason)\r
390     when :silence, :quiet\r
391       set_mode(channel, "+q", nick)\r
392       @bot.timer.add_once(timer) { set_mode(channel, "-q", nick) } if timer > 0\r
393     when :unsilence, :unquiet\r
394       set_mode(channel, "-q", nick)\r
395     end\r
396   end\r
397 \r
398   def set_mode(channel, mode, nick)\r
399     host = channel.has_user?(nick) ? "*!*@" + channel.get_user(nick).host : nick\r
400     @bot.mode(channel, mode, host)\r
401   end\r
402 \r
403   def do_kick(channel, nick, reason="")\r
404     @bot.kick(channel, nick, reason)\r
405   end\r
406 end\r
407 \r
408 plugin = BansPlugin.new\r
409 \r
410 plugin.default_auth( 'act', false )\r
411 plugin.default_auth( 'edit', false )\r
412 plugin.default_auth( 'list', true )\r
413 \r
414 plugin.map 'ban :nick :timer :channel', :action => 'ban_user',\r
415   :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},\r
416   :defaults => {:timer => nil, :channel => nil},\r
417   :auth_path => 'act'\r
418 plugin.map 'unban :nick :channel', :action => 'unban_user',\r
419   :requirements => {:channel => BansPlugin::ChannelRe},\r
420   :defaults => {:channel => nil},\r
421   :auth_path => 'act'\r
422 plugin.map 'kick :nick :channel *reason', :action => 'kick_user',\r
423   :requirements => {:channel => BansPlugin::ChannelRe},\r
424   :defaults => {:channel => nil, :reason => 'requested'},\r
425   :auth_path => 'act'\r
426 plugin.map 'kickban :nick :timer :channel *reason', :action => 'kickban_user',\r
427   :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},\r
428   :defaults => {:timer => nil, :channel => nil, :reason => 'requested'},\r
429   :auth_path => 'act'\r
430 plugin.map 'silence :nick :timer :channel', :action => 'silence_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 'unsilence :nick :channel', :action => 'unsilence_user',\r
435   :requirements => {:channel => BansPlugin::ChannelRe},\r
436   :defaults => {:channel => nil},\r
437   :auth_path => 'act'\r
438 \r
439 plugin.map 'bans add onjoin :host :action :channel *reason', :action => 'add_onjoin',\r
440   :requirements => {:action => BansPlugin::ActionRe, :channel => BansPlugin::ChannelAllRe},\r
441   :defaults => {:action => 'kickban', :channel => 'all', :reason => 'netmask not welcome'},\r
442   :auth_path => 'edit::onjoin'\r
443 plugin.map 'bans rm onjoin index :idx', :action => 'rm_onjoin',\r
444   :requirements => {:num => BansPlugin::IdxRe},\r
445   :auth_path => 'edit::onjoin'\r
446 plugin.map 'bans rm onjoin :host :channel', :action => 'rm_onjoin',\r
447   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
448   :defaults => {:channel => 'all'},\r
449   :auth_path => 'edit::onjoin'\r
450 plugin.map 'bans list onjoin[s]', :action => 'list_onjoin',\r
451   :auth_path => 'list::onjoin'\r
452 \r
453 plugin.map 'bans add badword :regexp :action :timer :channel *reason', :action => 'add_badword',\r
454   :requirements => {:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},\r
455   :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'bad word'},\r
456   :auth_path => 'edit::badword'\r
457 plugin.map 'bans rm badword index :idx', :action => 'rm_badword',\r
458   :requirements => {:num => BansPlugin::IdxRe},\r
459   :auth_path => 'edit::badword'\r
460 plugin.map 'bans rm badword :regexp :channel', :action => 'rm_badword',\r
461   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
462   :defaults => {:channel => 'all'},\r
463   :auth_path => 'edit::badword'\r
464 plugin.map 'bans list badword[s]', :action => 'list_badword',\r
465   :auth_path => 'list::badword'\r
466 \r
467 plugin.map 'bans add whitelist :host :channel', :action => 'add_whitelist',\r
468   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
469   :defaults => {:channel => 'all'},\r
470   :auth_path => 'edit::whitelist'\r
471 plugin.map 'bans rm whitelist index :idx', :action => 'rm_whitelist',\r
472   :requirements => {:num => BansPlugin::IdxRe},\r
473   :auth_path => 'edit::whitelist'\r
474 plugin.map 'bans rm whitelist :host :channel', :action => 'rm_whitelist',\r
475   :requirements => {:channel => BansPlugin::ChannelAllRe},\r
476   :defaults => {:channel => 'all'},\r
477   :auth_path => 'edit::whitelist'\r
478 plugin.map 'bans list whitelist', :action => 'list_whitelist',\r
479   :auth_path => 'list::whitelist'\r
480 \r