4 # :title: Bans Plugin v3 for rbot 0.9.11 and later
6 # Author:: Marco Gulino <marco@kmobiletools.org>
7 # Author:: kamu <mr.kamu@gmail.com>
8 # Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
10 # Copyright:: (C) 2006 Marco Gulino
11 # Copyright:: (C) 2007 kamu, Giuseppe Bilotta
15 # Managing kick and bans, automatically removing bans after timeouts, quiet
16 # bans, and kickban/quietban based on regexp
18 # v1 -> v2 (kamu's version, never released)
20 # * autoactions triggered on join
21 # * action on join or badword can be anything: kick, ban, kickban, quiet
24 # * remove the 'bans' prefix from most of the commands
25 # * (un)quiet has been renamed to (un)silence because 'quiet' was used to
26 # tell the bot to keep quiet
27 # * both (un)quiet and (un)silence are accepted as actions
28 # * use the more descriptive 'onjoin' term for autoactions
29 # * convert v1's (0.9.10) :bans and :bansmasks to BadWordActions and
31 # * enhanced list manipulation facilities
32 # * fixed regexp usage in requirements for plugin map
33 # * add proper auth management
35 define_structure :OnJoinAction, :host, :action, :channel, :reason
36 define_structure :BadWordAction, :regexp, :action, :channel, :timer, :reason
37 define_structure :WhitelistEntry, :host, :channel
38 define_structure :MassHlAction, :num, :perc, :action, :channel, :timer, :reason
40 class BansPlugin < Plugin
43 TimerRe = /^\d+[smhd]$/
44 ChannelRe = /^#+[^\s]+$/
45 ChannelAllRe = /^(?:all|#+[^\s]+)$/
46 ActionRe = /(?:ban|kick|kickban|silence|quiet)/
52 def make_badword_rx(txt)
53 return /\b(?:#{txt})\b/i
59 # Convert old BadWordActions, which were simpler and labelled :bans
60 if @registry.has_key? :bans
62 bans = @registry[:bans]
63 @registry[:bans].each { |ar|
71 warning "Unknown action in old data #{ar.inspect} -- entry ignored"
76 regexp = make_badword_rx(ar[2])
77 badwords << BadWordAction.new(regexp, action, chan, "0s", "")
79 @registry[:badwords] = badwords
81 # Store the ones we couldn't convert
82 @registry[:bans] = bans
84 @registry.delete(:bans)
87 @registry[:badwords] = Array.new unless @registry.has_key? :badwords
90 # Convert old WhitelistEntries, which were simpler and labelled :bansmasks
91 if @registry.has_key? :bans
93 @registry[:bansmasks].each { |mask|
94 badwords << WhitelistEntry.new(mask, "all")
96 @registry[:whitelist] = wl
97 @registry.delete(:bansmasks)
99 @registry[:whitelist] = Array.new unless @registry.has_key? :whitelist
102 @registry[:onjoin] = Array.new unless @registry.has_key? :onjoin
103 @registry[:masshl] = Array.new unless @registry.has_key? :masshl
106 def help(plugin, topic="")
109 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"
111 return "unban <nick/hostmask> [#channel]: unban a user from the given channel. defaults to the current channel"
113 return "kick <nick> [#channel] [reason ...]: kick a user from the given channel with the given reason. defaults to the current channel, no reason"
115 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"
117 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"
119 return "unsilence <nick/hostmask> [#channel]: allow the given user to talk on the given channel. defaults to the current channel"
123 return "bans add <onjoin|badword|whitelist|masshl>: add an automatic action for people that join or say some bad word, or a whitelist entry. further help available"
125 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"
127 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"
129 return "bans add whitelist <hostmask> [#channel|all]: add the given hostmask to the whitelist. no autoaction will be triggered by users on the whitelist"
131 return "masshl add <max_nicks|percentage> [action] [Xs/m/h/d] [#channel|all] [reason ...]: adds an massive highligh action. You can use both max and % in one trigger, the higher value will be taken. For two triggers in one channel, the one with higher requirements will be taken"
133 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. For masshl, bans rm masshl index [#channel|all]"
135 return"bans list <onjoin|badword|whitelist|masshl>: lists all onjoin or badwords or whitelist entries. For masshl, you can add [#channel|all]"
137 return "commands are: add, add onjoin, add badword, add whitelist, add masshl, rm, list"
140 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"
144 return unless m.channel
146 # check the whitelist first
147 @registry[:whitelist].each { |white|
148 next unless ['all', m.target.downcase].include?(white.channel)
149 return if m.source.matches?(white.host)
152 # check the badwords next
153 @registry[:badwords].each { |badword|
154 next unless ['all', m.target.downcase].include?(badword.channel)
155 next unless badword.regexp.match(m.plainmessage)
157 m.reply "bad word detected! #{badword.action} for #{badword.timer} because: #{badword.reason}"
158 do_cmd(badword.action.to_sym, m.source.nick, m.target, badword.timer, badword.reason)
162 # and finally, see if the user triggered masshl
163 mm = m.plainmessage.irc_downcase(m.server.casemap).split(/[\s\.,:]/)
164 nicks_said = (m.channel.users.map { |u| u.downcase} & mm).size
165 return unless nicks_said > 0 # not really needed, but saves some cycles
168 @registry[:masshl].each { |masshl|
169 next unless masshl.channel == m.channel.downcase or masshl.channel == "all"
170 needed = [masshl.num.to_i, (masshl.perc * m.channel.user_nicks.size / 100).to_i].max
171 next if needed > nicks_said or needed < got_nicks
172 masshl_action = masshl
175 return unless masshl_action
176 do_cmd masshl_action.action.intern, m.sourcenick, m.channel, masshl_action.timer, masshl_action.reason
180 @registry[:whitelist].each { |white|
181 next unless ['all', m.target.downcase].include?(white.channel)
182 return if m.source.matches?(white.host)
185 @registry[:onjoin].each { |auto|
186 next unless ['all', m.target.downcase].include?(auto.channel)
187 next unless m.source.matches? auto.host
189 do_cmd(auto.action.to_sym, m.source.nick, m.target, "0s", auto.reason)
194 def ban_user(m, params=nil)
195 nick, channel = params[:nick], check_channel(m, params[:channel])
196 timer = params[:timer]
197 do_cmd(:ban, nick, channel, timer)
200 def unban_user(m, params=nil)
201 nick, channel = params[:nick], check_channel(m, params[:channel])
202 do_cmd(:unban, nick, channel)
205 def kick_user(m, params=nil)
206 nick, channel = params[:nick], check_channel(m, params[:channel])
207 reason = params[:reason].to_s
208 do_cmd(:kick, nick, channel, "0s", reason)
211 def kickban_user(m, params=nil)
212 nick, channel, reason = params[:nick], check_channel(m, params[:channel])
213 timer, reason = params[:timer], params[:reason].to_s
214 do_cmd(:kickban, nick, channel, timer, reason)
217 def silence_user(m, params=nil)
218 nick, channel = params[:nick], check_channel(m, params[:channel])
219 timer = params[:timer]
220 do_cmd(:silence, nick, channel, timer)
223 def unsilence_user(m, params=nil)
224 nick, channel = params[:nick], check_channel(m, params[:channel])
225 do_cmd(:unsilence, nick, channel)
228 def add_masshl(m, params=nil)
229 num = params[:num].to_i
230 perc = params[:perc] ? /(\d{1,2})\%/.match(params[:perc])[1].to_i : 0
231 channel, action = params[:channel].downcase.dup, params[:action]
232 timer, reason = params[:timer].dup, params[:reason].to_s
233 if perc == 0 and num == 0
234 m.reply "both triggers 0, you don't want this."
238 masshl = @registry[:masshl]
239 masshl << MassHlAction.new(num, perc, action, channel, timer, reason)
240 @registry[:masshl] = masshl
245 def rm_masshl(m, params=nil)
246 masshl = @registry[:masshl]
247 masshl_w = params[:channel] ? masshl.select { |mh| mh.channel == params[:channel].downcase } : masshl
248 count = masshl_w.length
249 idx = params[:idx].to_i
252 m.reply "No such masshl \##{idx}"
255 masshl.delete(masshl_w[idx-1])
256 @registry[:masshl] = masshl
260 def list_masshl(m, params=nil)
261 masshl = @registry[:masshl]
262 masshl = masshl.select { |mh| mh.channel == params[:channel].downcase } if params[:channel]
263 m.reply params[:channel] ? "masshl rules: #{masshl.length} for #{params[:channel]}" : "masshl rules: #{masshl.length}"
264 masshl.each_with_index { |mh, idx|
265 m.reply "\##{idx+1}: #{mh.num} | #{mh.perc}% | #{mh.action} | #{mh.channel} | #{mh.timer} | #{mh.reason}"
269 def add_onjoin(m, params=nil)
271 host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase
272 action, reason = params[:action], params[:reason].to_s
274 autos = @registry[:onjoin]
275 autos << OnJoinAction.new(host, action, channel, reason.dup)
276 @registry[:onjoin] = autos
285 def list_onjoin(m, params=nil)
286 m.reply "onjoin rules: #{@registry[:onjoin].length}"
287 @registry[:onjoin].each_with_index { |auto, idx|
288 m.reply "\##{idx+1}: #{auto.host} | #{auto.action} | #{auto.channel} | '#{auto.reason}'"
292 def rm_onjoin(m, params=nil)
293 autos = @registry[:onjoin]
297 idx = params[:idx].to_i if params[:idx]
301 m.reply "No such onjoin \##{idx}"
304 autos.delete_at(idx-1)
307 host = m.server.new_netmask(params[:host])
308 channel = params[:channel].downcase
311 next unless ['all', rule.channel].include?(channel)
312 autos.delete rule if rule.host == host
319 @registry[:onjoin] = autos
320 if count > autos.length
323 m.reply "No matching onjoin rule for #{host} found"
327 def add_badword(m, params=nil)
328 regexp, channel = make_badword_rx(params[:regexp]), params[:channel].downcase.dup
329 action, timer, reason = params[:action], params[:timer].dup, params[:reason].to_s
331 badwords = @registry[:badwords]
332 badwords << BadWordAction.new(regexp, action, channel, timer, reason)
333 @registry[:badwords] = badwords
338 def list_badword(m, params=nil)
339 m.reply "badword rules: #{@registry[:badwords].length}"
341 @registry[:badwords].each_with_index { |badword, idx|
342 m.reply "\##{idx+1}: #{badword.regexp.source} | #{badword.action} | #{badword.channel} | #{badword.timer} | #{badword.reason}"
346 def rm_badword(m, params=nil)
347 badwords = @registry[:badwords]
348 count = badwords.length
351 idx = params[:idx].to_i if params[:idx]
355 m.reply "No such badword \##{idx}"
358 badwords.delete_at(idx-1)
360 channel = params[:channel].downcase
362 regexp = make_badword_rx(params[:regexp])
363 debug "Trying to remove #{regexp.inspect} from #{badwords.inspect}"
365 badwords.each { |badword|
366 next unless ['all', badword.channel].include?(channel)
367 debug "Removing #{badword.inspect}" if badword.regexp == regexp
368 badwords.delete(badword) if badword.regexp == regexp
372 @registry[:badwords] = badwords
373 if count > badwords.length
376 m.reply "No matching badword #{regexp} found"
380 def add_whitelist(m, params=nil)
382 host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase
384 # TODO check if a whitelist entry for this host already exists
385 whitelist = @registry[:whitelist]
386 whitelist << WhitelistEntry.new(host, channel)
387 @registry[:whitelist] = whitelist
396 def list_whitelist(m, params=nil)
397 m.reply "whitelist entries: #{@registry[:whitelist].length}"
398 @registry[:whitelist].each_with_index { |auto, idx|
399 m.reply "\##{idx+1}: #{auto.host} | #{auto.channel}"
403 def rm_whitelist(m, params=nil)
404 wl = @registry[:whitelist]
408 idx = params[:idx].to_i if params[:idx]
412 m.reply "No such whitelist entry \##{idx}"
418 host = m.server.new_netmask(params[:host])
419 channel = params[:channel].downcase
422 next unless ['all', rule.channel].include?(channel)
423 wl.delete rule if rule.host == host
430 @registry[:whitelist] = wl
431 if count > whitelist.length
434 m.reply "No host matching #{host}"
439 def check_channel(m, strchannel)
441 raise "must specify channel if using privmsg" if m.private? and not strchannel
442 channel = m.server.channel(strchannel) || m.target
443 raise "I am not in that channel" unless channel.has_user?(@bot.nick)
452 def do_cmd(action, nick, channel, timer_in=nil, reason=nil)
461 timer = $1.to_i * 60 * 60
463 timer = $1.to_i * 60 * 60 * 24
465 raise "Wrong time specifications"
470 set_temporary_mode(channel, 'b', nick, timer)
472 set_mode(channel, "-b", nick)
474 do_kick(channel, nick, reason)
476 set_temporary_mode(channel, 'b', nick, timer)
477 do_kick(channel, nick, reason)
478 when :silence, :quiet
479 set_mode(channel, "+q", nick)
480 @bot.timer.add_once(timer) { set_mode(channel, "-q", nick) } if timer > 0
481 when :unsilence, :unquiet
482 set_mode(channel, "-q", nick)
486 def set_mode(channel, mode, nick)
487 host = channel.has_user?(nick) ? "*!*@" + channel.get_user(nick).host : nick
488 @bot.mode(channel, mode, host)
491 def set_temporary_mode(channel, mode, nick, timer)
492 host = channel.has_user?(nick) ? "*!*@" + channel.users[nick].host : nick
493 @bot.mode(channel, "+#{mode}", host)
495 @bot.timer.add_once(timer) { @bot.mode(channel, "-#{mode}", host) }
498 def do_kick(channel, nick, reason="")
499 @bot.kick(channel, nick, reason)
503 plugin = BansPlugin.new
505 plugin.default_auth( 'act', false )
506 plugin.default_auth( 'edit', false )
507 plugin.default_auth( 'list', true )
509 plugin.map 'ban :nick :timer :channel', :action => 'ban_user',
510 :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
511 :defaults => {:timer => nil, :channel => nil},
513 plugin.map 'unban :nick :channel', :action => 'unban_user',
514 :requirements => {:channel => BansPlugin::ChannelRe},
515 :defaults => {:channel => nil},
517 plugin.map 'kick :nick :channel *reason', :action => 'kick_user',
518 :requirements => {:channel => BansPlugin::ChannelRe},
519 :defaults => {:channel => nil, :reason => 'requested'},
521 plugin.map 'kickban :nick :timer :channel *reason', :action => 'kickban_user',
522 :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
523 :defaults => {:timer => nil, :channel => nil, :reason => 'requested'},
525 plugin.map 'silence :nick :timer :channel', :action => 'silence_user',
526 :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
527 :defaults => {:timer => nil, :channel => nil},
529 plugin.map 'unsilence :nick :channel', :action => 'unsilence_user',
530 :requirements => {:channel => BansPlugin::ChannelRe},
531 :defaults => {:channel => nil},
534 plugin.map 'bans add onjoin :host :action :channel *reason', :action => 'add_onjoin',
535 :requirements => {:action => BansPlugin::ActionRe, :channel => BansPlugin::ChannelAllRe},
536 :defaults => {:action => 'kickban', :channel => 'all', :reason => 'netmask not welcome'},
537 :auth_path => 'edit::onjoin'
538 plugin.map 'bans rm onjoin index :idx', :action => 'rm_onjoin',
539 :requirements => {:num => BansPlugin::IdxRe},
540 :auth_path => 'edit::onjoin'
541 plugin.map 'bans rm onjoin :host :channel', :action => 'rm_onjoin',
542 :requirements => {:channel => BansPlugin::ChannelAllRe},
543 :defaults => {:channel => 'all'},
544 :auth_path => 'edit::onjoin'
545 plugin.map 'bans list onjoin[s]', :action => 'list_onjoin',
546 :auth_path => 'list::onjoin'
548 plugin.map 'bans add badword :regexp :action :timer :channel *reason', :action => 'add_badword',
549 :requirements => {:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
550 :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'bad word'},
551 :auth_path => 'edit::badword'
552 plugin.map 'bans rm badword index :idx', :action => 'rm_badword',
553 :requirements => {:num => BansPlugin::IdxRe},
554 :auth_path => 'edit::badword'
555 plugin.map 'bans rm badword :regexp :channel', :action => 'rm_badword',
556 :requirements => {:channel => BansPlugin::ChannelAllRe},
557 :defaults => {:channel => 'all'},
558 :auth_path => 'edit::badword'
559 plugin.map 'bans list badword[s]', :action => 'list_badword',
560 :auth_path => 'list::badword'
562 plugin.map 'bans add whitelist :host :channel', :action => 'add_whitelist',
563 :requirements => {:channel => BansPlugin::ChannelAllRe},
564 :defaults => {:channel => 'all'},
565 :auth_path => 'edit::whitelist'
566 plugin.map 'bans rm whitelist index :idx', :action => 'rm_whitelist',
567 :requirements => {:num => BansPlugin::IdxRe},
568 :auth_path => 'edit::whitelist'
569 plugin.map 'bans rm whitelist :host :channel', :action => 'rm_whitelist',
570 :requirements => {:channel => BansPlugin::ChannelAllRe},
571 :defaults => {:channel => 'all'},
572 :auth_path => 'edit::whitelist'
573 plugin.map 'bans list whitelist', :action => 'list_whitelist',
574 :auth_path => 'list::whitelist'
576 plugin.map 'bans add masshl :num :perc :action :timer :channel *reason', :action => 'add_masshl',
577 :requirements => {:num => /\d{1,2}/, :perc => /\d{1,2}\%/,:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
578 :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'masshl'},
579 :auth_path => 'edit::masshl'
580 plugin.map 'bans add masshl :perc :num :action :timer :channel *reason', :action => 'add_masshl',
581 :requirements => {:num => /\d{1,2}/, :perc => /\d{1,2}\%/,:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
582 :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'masshl'},
583 :auth_path => 'edit::masshl'
584 plugin.map 'bans add masshl :perc :action :timer :channel *reason', :action => 'add_masshl',
585 :requirements => {:num => /\d{1,2}/, :perc => /\d{1,2}\%/,:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
586 :defaults => {:num => 0, :action => 'silence', :timer => "0s", :channel => 'all', :reason => 'masshl'},
587 :auth_path => 'edit::masshl'
588 plugin.map 'bans add masshl :num :action :timer :channel *reason', :action => 'add_masshl',
589 :requirements => {:num => /\d{1,2}/, :perc => /\d{1,2}\%/,:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
590 :defaults => {:perc => "0%", :action => 'silence', :timer => "0s", :channel => 'all', :reason => 'masshl'},
591 :auth_path => 'edit::masshl'
592 plugin.map 'bans rm masshl :idx', :action => 'rm_masshl',
593 :requirements => {:channel => nil, :num => BansPlugin::IdxRe},
594 :auth_path => 'edit::masshl'
595 plugin.map 'bans rm masshl :idx :channel', :action => 'rm_masshl',
596 :requirements => {:channel => BansPlugin::ChannelAllRe},
597 :defaults => {:channel => nil},
598 :auth_path => 'edit::masshl'
599 plugin.map 'bans list masshl', :action => 'list_masshl',
600 :auth_path => 'list::masshl'
601 plugin.map 'bans list masshl :channel', :action => 'list_masshl',
602 :defaults => {:channel => nil},
603 :auth_path => 'list::masshl'