diff options
author | Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | 2008-03-04 18:24:57 +0100 |
---|---|---|
committer | Giuseppe Bilotta <giuseppe.bilotta@gmail.com> | 2008-03-04 18:24:57 +0100 |
commit | 6f9bfa43ac907700fcba394e0f6b9d987b1192fb (patch) | |
tree | 90058ef94a1cdf86b51b70bfdafd17b8f26ea33a /data | |
parent | 02e8b672cdf3bf8c67f78a2a7fc028c4c6304a30 (diff) |
Unixify all line endings.
Some files had DOS-style line endings. Change all of them to Unix-style.
Diffstat (limited to 'data')
-rw-r--r-- | data/rbot/plugins/bans.rb | 1008 | ||||
-rw-r--r-- | data/rbot/plugins/deepthoughts.rb | 178 | ||||
-rw-r--r-- | data/rbot/plugins/dict.rb | 348 | ||||
-rw-r--r-- | data/rbot/plugins/games/azgame.rb | 1132 | ||||
-rw-r--r-- | data/rbot/plugins/hl2.rb | 118 | ||||
-rw-r--r-- | data/rbot/plugins/rss.rb | 2214 | ||||
-rw-r--r-- | data/rbot/plugins/salut.rb | 452 |
7 files changed, 2725 insertions, 2725 deletions
diff --git a/data/rbot/plugins/bans.rb b/data/rbot/plugins/bans.rb index ac5262b7..3818ce22 100644 --- a/data/rbot/plugins/bans.rb +++ b/data/rbot/plugins/bans.rb @@ -1,504 +1,504 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: Bans Plugin v3 for rbot 0.9.11 and later
-#
-# Author:: Marco Gulino <marco@kmobiletools.org>
-# Author:: kamu <mr.kamu@gmail.com>
-# Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
-#
-# Copyright:: (C) 2006 Marco Gulino
-# Copyright:: (C) 2007 kamu, Giuseppe Bilotta
-#
-# License:: GPL V2.
-#
-# Managing kick and bans, automatically removing bans after timeouts, quiet
-# bans, and kickban/quietban based on regexp
-#
-# v1 -> v2 (kamu's version, never released)
-# * reworked
-# * autoactions triggered on join
-# * action on join or badword can be anything: kick, ban, kickban, quiet
-#
-# v2 -> v3 (GB)
-# * remove the 'bans' prefix from most of the commands
-# * (un)quiet has been renamed to (un)silence because 'quiet' was used to
-# tell the bot to keep quiet
-# * both (un)quiet and (un)silence are accepted as actions
-# * use the more descriptive 'onjoin' term for autoactions
-# * convert v1's (0.9.10) :bans and :bansmasks to BadWordActions and
-# WhitelistEntries
-# * enhanced list manipulation facilities
-# * fixed regexp usage in requirements for plugin map
-# * add proper auth management
-
-define_structure :OnJoinAction, :host, :action, :channel, :reason
-define_structure :BadWordAction, :regexp, :action, :channel, :timer, :reason
-define_structure :WhitelistEntry, :host, :channel
-
-class BansPlugin < Plugin
-
- IdxRe = /^\d+$/
- TimerRe = /^\d+[smhd]$/
- ChannelRe = /^#+[^\s]+$/
- ChannelAllRe = /^(?:all|#+[^\s]+)$/
- ActionRe = /(?:ban|kick|kickban|silence|quiet)/
-
- def name
- "bans"
- end
-
- def make_badword_rx(txt)
- return /\b(?:#{txt})\b/i
- end
-
- def initialize
- super
-
- # Convert old BadWordActions, which were simpler and labelled :bans
- if @registry.has_key? :bans
- badwords = Array.new
- bans = @registry[:bans]
- @registry[:bans].each { |ar|
- case ar[0]
- when "quietban"
- action = :silence
- when "kickban"
- action = :kickban
- else
- # Shouldn't happen
- warning "Unknown action in old data #{ar.inspect} -- entry ignored"
- next
- end
- bans.delete(ar)
- chan = ar[1].downcase
- regexp = make_badword_rx(ar[2])
- badwords << BadWordAction.new(regexp, action, chan, "0s", "")
- }
- @registry[:badwords] = badwords
- if bans.length > 0
- # Store the ones we couldn't convert
- @registry[:bans] = bans
- else
- @registry.delete(:bans)
- end
- else
- @registry[:badwords] = Array.new unless @registry.has_key? :badwords
- end
-
- # Convert old WhitelistEntries, which were simpler and labelled :bansmasks
- if @registry.has_key? :bans
- wl = Array.new
- @registry[:bansmasks].each { |mask|
- badwords << WhitelistEntry.new(mask, "all")
- }
- @registry[:whitelist] = wl
- @registry.delete(:bansmasks)
- else
- @registry[:whitelist] = Array.new unless @registry.has_key? :whitelist
- end
-
- @registry[:onjoin] = Array.new unless @registry.has_key? :onjoin
- end
-
- def help(plugin, topic="")
- case plugin
- when "ban"
- 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"
- when "unban"
- return "unban <nick/hostmask> [#channel]: unban a user from the given channel. defaults to the current channel"
- when "kick"
- return "kick <nick> [#channel] [reason ...]: kick a user from the given channel with the given reason. defaults to the current channel, no reason"
- when "kickban"
- 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"
- when "silence"
- 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"
- when "unsilence"
- return "unsilence <nick/hostmask> [#channel]: allow the given user to talk on the given channel. defaults to the current channel"
- when "bans"
- case topic
- when "add"
- 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"
- when "add onjoin"
- 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"
- when "add badword"
- 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"
- when "add whitelist"
- return "bans add whitelist <hostmask> [#channel|all]: add the given hostmask to the whitelist. no autoaction will be triggered by users on the whitelist"
- when "rm"
- 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."
- when "list"
- return"bans list <onjoin|badword|whitelist>: lists all onjoin or badwords or whitelist entries"
- end
- end
- 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"
- end
-
- def listen(m)
- return unless m.respond_to?(:public?) and m.public?
- @registry[:whitelist].each { |white|
- next unless ['all', m.target.downcase].include?(white.channel)
- return if m.source.matches?(white.host)
- }
-
- @registry[:badwords].each { |badword|
- next unless ['all', m.target.downcase].include?(badword.channel)
- next unless badword.regexp.match(m.message)
-
- do_cmd(badword.action.to_sym, m.source.nick, m.target, badword.timer, badword.reason)
- m.reply "bad word detected! #{badword.action} for #{badword.timer} because: #{badword.reason}"
- return
- }
- end
-
- def join(m)
- @registry[:whitelist].each { |white|
- next unless ['all', m.target.downcase].include?(white.channel)
- return if m.source.matches?(white.host)
- }
-
- @registry[:onjoin].each { |auto|
- next unless ['all', m.target.downcase].include?(auto.channel)
- next unless m.source.matches? auto.host
-
- do_cmd(auto.action.to_sym, m.source.nick, m.target, "0s", auto.reason)
- return
- }
- end
-
- def ban_user(m, params=nil)
- nick, channel = params[:nick], check_channel(m, params[:channel])
- timer = params[:timer]
- do_cmd(:ban, nick, channel, timer)
- end
-
- def unban_user(m, params=nil)
- nick, channel = params[:nick], check_channel(m, params[:channel])
- do_cmd(:unban, nick, channel)
- end
-
- def kick_user(m, params=nil)
- nick, channel = params[:nick], check_channel(m, params[:channel])
- reason = params[:reason].to_s
- do_cmd(:kick, nick, channel, "0s", reason)
- end
-
- def kickban_user(m, params=nil)
- nick, channel, reason = params[:nick], check_channel(m, params[:channel])
- timer, reason = params[:timer], params[:reason].to_s
- do_cmd(:kickban, nick, channel, timer, reason)
- end
-
- def silence_user(m, params=nil)
- nick, channel = params[:nick], check_channel(m, params[:channel])
- timer = params[:timer]
- do_cmd(:silence, nick, channel, timer)
- end
-
- def unsilence_user(m, params=nil)
- nick, channel = params[:nick], check_channel(m, params[:channel])
- do_cmd(:unsilence, nick, channel)
- end
-
- def add_onjoin(m, params=nil)
- begin
- host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase
- action, reason = params[:action], params[:reason].to_s
-
- autos = @registry[:onjoin]
- autos << OnJoinAction.new(host, action, channel, reason.dup)
- @registry[:onjoin] = autos
-
- m.okay
- rescue
- error $!
- m.reply $!
- end
- end
-
- def list_onjoin(m, params=nil)
- m.reply "onjoin rules: #{@registry[:onjoin].length}"
- @registry[:onjoin].each_with_index { |auto, idx|
- m.reply "\##{idx+1}: #{auto.host} | #{auto.action} | #{auto.channel} | '#{auto.reason}'"
- }
- end
-
- def rm_onjoin(m, params=nil)
- autos = @registry[:onjoin]
- count = autos.length
-
- idx = nil
- idx = params[:idx].to_i if params[:idx]
-
- if idx
- if idx > count
- m.reply "No such onjoin \##{idx}"
- return
- end
- autos.delete_at(idx-1)
- else
- begin
- host = m.server.new_netmask(params[:host])
- channel = params[:channel].downcase
-
- autos.each { |rule|
- next unless ['all', rule.channel].include?(channel)
- autos.delete rule if rule.host == host
- }
- rescue
- error $!
- m.reply $!
- end
- end
- @registry[:onjoin] = autos
- if count > autos.length
- m.okay
- else
- m.reply "No matching onjoin rule for #{host} found"
- end
- end
-
- def add_badword(m, params=nil)
- regexp, channel = make_badword_rx(params[:regexp]), params[:channel].downcase.dup
- action, timer, reason = params[:action], params[:timer].dup, params[:reason].to_s
-
- badwords = @registry[:badwords]
- badwords << BadWordAction.new(regexp, action, channel, timer, reason)
- @registry[:badwords] = badwords
-
- m.okay
- end
-
- def list_badword(m, params=nil)
- m.reply "badword rules: #{@registry[:badwords].length}"
-
- @registry[:badwords].each_with_index { |badword, idx|
- m.reply "\##{idx+1}: #{badword.regexp.source} | #{badword.action} | #{badword.channel} | #{badword.timer} | #{badword.reason}"
- }
- end
-
- def rm_badword(m, params=nil)
- badwords = @registry[:badwords]
- count = badwords.length
-
- idx = nil
- idx = params[:idx].to_i if params[:idx]
-
- if idx
- if idx > count
- m.reply "No such badword \##{idx}"
- return
- end
- badwords.delete_at(idx-1)
- else
- channel = params[:channel].downcase
-
- regexp = make_badword_rx(params[:regexp])
- debug "Trying to remove #{regexp.inspect} from #{badwords.inspect}"
-
- badwords.each { |badword|
- next unless ['all', badword.channel].include?(channel)
- debug "Removing #{badword.inspect}" if badword == regexp
- badwords.delete(badword) if badword == regexp
- }
- end
-
- @registry[:badwords] = badwords
- if count > badwords.length
- m.okay
- else
- m.reply "No matching badword #{regexp} found"
- end
- end
-
- def add_whitelist(m, params=nil)
- begin
- host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase
-
- # TODO check if a whitelist entry for this host already exists
- whitelist = @registry[:whitelist]
- whitelist << WhitelistEntry.new(host, channel)
- @registry[:whitelist] = whitelist
-
- m.okay
- rescue
- error $!
- m.reply $!
- end
- end
-
- def list_whitelist(m, params=nil)
- m.reply "whitelist entries: #{@registry[:whitelist].length}"
- @registry[:whitelist].each_with_index { |auto, idx|
- m.reply "\##{idx+1}: #{auto.host} | #{auto.channel}"
- }
- end
-
- def rm_whitelist(m, params=nil)
- wl = @registry[:whitelist]
- count = wl.length
-
- idx = nil
- idx = params[:idx].to_i if params[:idx]
-
- if idx
- if idx > count
- m.reply "No such whitelist entry \##{idx}"
- return
- end
- wl.delete_at(idx-1)
- else
- begin
- host = m.server.new_netmask(params[:host])
- channel = params[:channel].downcase
-
- wl.each { |rule|
- next unless ['all', rule.channel].include?(channel)
- wl.delete rule if rule.host == host
- }
- rescue
- error $!
- m.reply $!
- end
- end
- @registry[:whitelist] = wl
- if count > whitelist.length
- m.okay
- else
- m.reply "No host matching #{host}"
- end
- end
-
- private
- def check_channel(m, strchannel)
- begin
- raise "must specify channel if using privmsg" if m.private? and not strchannel
- channel = m.server.channel(strchannel) || m.target
- raise "I am not in that channel" unless channel.has_user?(@bot.nick)
-
- return channel
- rescue
- error $!
- m.reply $!
- end
- end
-
- def do_cmd(action, nick, channel, timer_in=nil, reason=nil)
- case timer_in
- when nil
- timer = 0
- when /^(\d+)s$/
- timer = $1.to_i
- when /^(\d+)m$/
- timer = $1.to_i * 60
- when /^(\d+)h$/
- timer = $1.to_i * 60 * 60
- when /^(\d+)d$/
- timer = $1.to_i * 60 * 60 * 24
- else
- raise "Wrong time specifications"
- end
-
- case action
- when :ban
- set_mode(channel, "+b", nick)
- @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0
- when :unban
- set_mode(channel, "-b", nick)
- when :kick
- do_kick(channel, nick, reason)
- when :kickban
- set_mode(channel, "+b", nick)
- @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0
- do_kick(channel, nick, reason)
- when :silence, :quiet
- set_mode(channel, "+q", nick)
- @bot.timer.add_once(timer) { set_mode(channel, "-q", nick) } if timer > 0
- when :unsilence, :unquiet
- set_mode(channel, "-q", nick)
- end
- end
-
- def set_mode(channel, mode, nick)
- host = channel.has_user?(nick) ? "*!*@" + channel.get_user(nick).host : nick
- @bot.mode(channel, mode, host)
- end
-
- def do_kick(channel, nick, reason="")
- @bot.kick(channel, nick, reason)
- end
-end
-
-plugin = BansPlugin.new
-
-plugin.default_auth( 'act', false )
-plugin.default_auth( 'edit', false )
-plugin.default_auth( 'list', true )
-
-plugin.map 'ban :nick :timer :channel', :action => 'ban_user',
- :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
- :defaults => {:timer => nil, :channel => nil},
- :auth_path => 'act'
-plugin.map 'unban :nick :channel', :action => 'unban_user',
- :requirements => {:channel => BansPlugin::ChannelRe},
- :defaults => {:channel => nil},
- :auth_path => 'act'
-plugin.map 'kick :nick :channel *reason', :action => 'kick_user',
- :requirements => {:channel => BansPlugin::ChannelRe},
- :defaults => {:channel => nil, :reason => 'requested'},
- :auth_path => 'act'
-plugin.map 'kickban :nick :timer :channel *reason', :action => 'kickban_user',
- :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
- :defaults => {:timer => nil, :channel => nil, :reason => 'requested'},
- :auth_path => 'act'
-plugin.map 'silence :nick :timer :channel', :action => 'silence_user',
- :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe},
- :defaults => {:timer => nil, :channel => nil},
- :auth_path => 'act'
-plugin.map 'unsilence :nick :channel', :action => 'unsilence_user',
- :requirements => {:channel => BansPlugin::ChannelRe},
- :defaults => {:channel => nil},
- :auth_path => 'act'
-
-plugin.map 'bans add onjoin :host :action :channel *reason', :action => 'add_onjoin',
- :requirements => {:action => BansPlugin::ActionRe, :channel => BansPlugin::ChannelAllRe},
- :defaults => {:action => 'kickban', :channel => 'all', :reason => 'netmask not welcome'},
- :auth_path => 'edit::onjoin'
-plugin.map 'bans rm onjoin index :idx', :action => 'rm_onjoin',
- :requirements => {:num => BansPlugin::IdxRe},
- :auth_path => 'edit::onjoin'
-plugin.map 'bans rm onjoin :host :channel', :action => 'rm_onjoin',
- :requirements => {:channel => BansPlugin::ChannelAllRe},
- :defaults => {:channel => 'all'},
- :auth_path => 'edit::onjoin'
-plugin.map 'bans list onjoin[s]', :action => 'list_onjoin',
- :auth_path => 'list::onjoin'
-
-plugin.map 'bans add badword :regexp :action :timer :channel *reason', :action => 'add_badword',
- :requirements => {:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe},
- :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'bad word'},
- :auth_path => 'edit::badword'
-plugin.map 'bans rm badword index :idx', :action => 'rm_badword',
- :requirements => {:num => BansPlugin::IdxRe},
- :auth_path => 'edit::badword'
-plugin.map 'bans rm badword :regexp :channel', :action => 'rm_badword',
- :requirements => {:channel => BansPlugin::ChannelAllRe},
- :defaults => {:channel => 'all'},
- :auth_path => 'edit::badword'
-plugin.map 'bans list badword[s]', :action => 'list_badword',
- :auth_path => 'list::badword'
-
-plugin.map 'bans add whitelist :host :channel', :action => 'add_whitelist',
- :requirements => {:channel => BansPlugin::ChannelAllRe},
- :defaults => {:channel => 'all'},
- :auth_path => 'edit::whitelist'
-plugin.map 'bans rm whitelist index :idx', :action => 'rm_whitelist',
- :requirements => {:num => BansPlugin::IdxRe},
- :auth_path => 'edit::whitelist'
-plugin.map 'bans rm whitelist :host :channel', :action => 'rm_whitelist',
- :requirements => {:channel => BansPlugin::ChannelAllRe},
- :defaults => {:channel => 'all'},
- :auth_path => 'edit::whitelist'
-plugin.map 'bans list whitelist', :action => 'list_whitelist',
- :auth_path => 'list::whitelist'
-
+#-- vim:sw=2:et +#++ +# +# :title: Bans Plugin v3 for rbot 0.9.11 and later +# +# Author:: Marco Gulino <marco@kmobiletools.org> +# Author:: kamu <mr.kamu@gmail.com> +# Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com> +# +# Copyright:: (C) 2006 Marco Gulino +# Copyright:: (C) 2007 kamu, Giuseppe Bilotta +# +# License:: GPL V2. +# +# Managing kick and bans, automatically removing bans after timeouts, quiet +# bans, and kickban/quietban based on regexp +# +# v1 -> v2 (kamu's version, never released) +# * reworked +# * autoactions triggered on join +# * action on join or badword can be anything: kick, ban, kickban, quiet +# +# v2 -> v3 (GB) +# * remove the 'bans' prefix from most of the commands +# * (un)quiet has been renamed to (un)silence because 'quiet' was used to +# tell the bot to keep quiet +# * both (un)quiet and (un)silence are accepted as actions +# * use the more descriptive 'onjoin' term for autoactions +# * convert v1's (0.9.10) :bans and :bansmasks to BadWordActions and +# WhitelistEntries +# * enhanced list manipulation facilities +# * fixed regexp usage in requirements for plugin map +# * add proper auth management + +define_structure :OnJoinAction, :host, :action, :channel, :reason +define_structure :BadWordAction, :regexp, :action, :channel, :timer, :reason +define_structure :WhitelistEntry, :host, :channel + +class BansPlugin < Plugin + + IdxRe = /^\d+$/ + TimerRe = /^\d+[smhd]$/ + ChannelRe = /^#+[^\s]+$/ + ChannelAllRe = /^(?:all|#+[^\s]+)$/ + ActionRe = /(?:ban|kick|kickban|silence|quiet)/ + + def name + "bans" + end + + def make_badword_rx(txt) + return /\b(?:#{txt})\b/i + end + + def initialize + super + + # Convert old BadWordActions, which were simpler and labelled :bans + if @registry.has_key? :bans + badwords = Array.new + bans = @registry[:bans] + @registry[:bans].each { |ar| + case ar[0] + when "quietban" + action = :silence + when "kickban" + action = :kickban + else + # Shouldn't happen + warning "Unknown action in old data #{ar.inspect} -- entry ignored" + next + end + bans.delete(ar) + chan = ar[1].downcase + regexp = make_badword_rx(ar[2]) + badwords << BadWordAction.new(regexp, action, chan, "0s", "") + } + @registry[:badwords] = badwords + if bans.length > 0 + # Store the ones we couldn't convert + @registry[:bans] = bans + else + @registry.delete(:bans) + end + else + @registry[:badwords] = Array.new unless @registry.has_key? :badwords + end + + # Convert old WhitelistEntries, which were simpler and labelled :bansmasks + if @registry.has_key? :bans + wl = Array.new + @registry[:bansmasks].each { |mask| + badwords << WhitelistEntry.new(mask, "all") + } + @registry[:whitelist] = wl + @registry.delete(:bansmasks) + else + @registry[:whitelist] = Array.new unless @registry.has_key? :whitelist + end + + @registry[:onjoin] = Array.new unless @registry.has_key? :onjoin + end + + def help(plugin, topic="") + case plugin + when "ban" + 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" + when "unban" + return "unban <nick/hostmask> [#channel]: unban a user from the given channel. defaults to the current channel" + when "kick" + return "kick <nick> [#channel] [reason ...]: kick a user from the given channel with the given reason. defaults to the current channel, no reason" + when "kickban" + 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" + when "silence" + 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" + when "unsilence" + return "unsilence <nick/hostmask> [#channel]: allow the given user to talk on the given channel. defaults to the current channel" + when "bans" + case topic + when "add" + 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" + when "add onjoin" + 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" + when "add badword" + 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" + when "add whitelist" + return "bans add whitelist <hostmask> [#channel|all]: add the given hostmask to the whitelist. no autoaction will be triggered by users on the whitelist" + when "rm" + 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." + when "list" + return"bans list <onjoin|badword|whitelist>: lists all onjoin or badwords or whitelist entries" + end + end + 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" + end + + def listen(m) + return unless m.respond_to?(:public?) and m.public? + @registry[:whitelist].each { |white| + next unless ['all', m.target.downcase].include?(white.channel) + return if m.source.matches?(white.host) + } + + @registry[:badwords].each { |badword| + next unless ['all', m.target.downcase].include?(badword.channel) + next unless badword.regexp.match(m.message) + + do_cmd(badword.action.to_sym, m.source.nick, m.target, badword.timer, badword.reason) + m.reply "bad word detected! #{badword.action} for #{badword.timer} because: #{badword.reason}" + return + } + end + + def join(m) + @registry[:whitelist].each { |white| + next unless ['all', m.target.downcase].include?(white.channel) + return if m.source.matches?(white.host) + } + + @registry[:onjoin].each { |auto| + next unless ['all', m.target.downcase].include?(auto.channel) + next unless m.source.matches? auto.host + + do_cmd(auto.action.to_sym, m.source.nick, m.target, "0s", auto.reason) + return + } + end + + def ban_user(m, params=nil) + nick, channel = params[:nick], check_channel(m, params[:channel]) + timer = params[:timer] + do_cmd(:ban, nick, channel, timer) + end + + def unban_user(m, params=nil) + nick, channel = params[:nick], check_channel(m, params[:channel]) + do_cmd(:unban, nick, channel) + end + + def kick_user(m, params=nil) + nick, channel = params[:nick], check_channel(m, params[:channel]) + reason = params[:reason].to_s + do_cmd(:kick, nick, channel, "0s", reason) + end + + def kickban_user(m, params=nil) + nick, channel, reason = params[:nick], check_channel(m, params[:channel]) + timer, reason = params[:timer], params[:reason].to_s + do_cmd(:kickban, nick, channel, timer, reason) + end + + def silence_user(m, params=nil) + nick, channel = params[:nick], check_channel(m, params[:channel]) + timer = params[:timer] + do_cmd(:silence, nick, channel, timer) + end + + def unsilence_user(m, params=nil) + nick, channel = params[:nick], check_channel(m, params[:channel]) + do_cmd(:unsilence, nick, channel) + end + + def add_onjoin(m, params=nil) + begin + host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase + action, reason = params[:action], params[:reason].to_s + + autos = @registry[:onjoin] + autos << OnJoinAction.new(host, action, channel, reason.dup) + @registry[:onjoin] = autos + + m.okay + rescue + error $! + m.reply $! + end + end + + def list_onjoin(m, params=nil) + m.reply "onjoin rules: #{@registry[:onjoin].length}" + @registry[:onjoin].each_with_index { |auto, idx| + m.reply "\##{idx+1}: #{auto.host} | #{auto.action} | #{auto.channel} | '#{auto.reason}'" + } + end + + def rm_onjoin(m, params=nil) + autos = @registry[:onjoin] + count = autos.length + + idx = nil + idx = params[:idx].to_i if params[:idx] + + if idx + if idx > count + m.reply "No such onjoin \##{idx}" + return + end + autos.delete_at(idx-1) + else + begin + host = m.server.new_netmask(params[:host]) + channel = params[:channel].downcase + + autos.each { |rule| + next unless ['all', rule.channel].include?(channel) + autos.delete rule if rule.host == host + } + rescue + error $! + m.reply $! + end + end + @registry[:onjoin] = autos + if count > autos.length + m.okay + else + m.reply "No matching onjoin rule for #{host} found" + end + end + + def add_badword(m, params=nil) + regexp, channel = make_badword_rx(params[:regexp]), params[:channel].downcase.dup + action, timer, reason = params[:action], params[:timer].dup, params[:reason].to_s + + badwords = @registry[:badwords] + badwords << BadWordAction.new(regexp, action, channel, timer, reason) + @registry[:badwords] = badwords + + m.okay + end + + def list_badword(m, params=nil) + m.reply "badword rules: #{@registry[:badwords].length}" + + @registry[:badwords].each_with_index { |badword, idx| + m.reply "\##{idx+1}: #{badword.regexp.source} | #{badword.action} | #{badword.channel} | #{badword.timer} | #{badword.reason}" + } + end + + def rm_badword(m, params=nil) + badwords = @registry[:badwords] + count = badwords.length + + idx = nil + idx = params[:idx].to_i if params[:idx] + + if idx + if idx > count + m.reply "No such badword \##{idx}" + return + end + badwords.delete_at(idx-1) + else + channel = params[:channel].downcase + + regexp = make_badword_rx(params[:regexp]) + debug "Trying to remove #{regexp.inspect} from #{badwords.inspect}" + + badwords.each { |badword| + next unless ['all', badword.channel].include?(channel) + debug "Removing #{badword.inspect}" if badword == regexp + badwords.delete(badword) if badword == regexp + } + end + + @registry[:badwords] = badwords + if count > badwords.length + m.okay + else + m.reply "No matching badword #{regexp} found" + end + end + + def add_whitelist(m, params=nil) + begin + host, channel = m.server.new_netmask(params[:host]), params[:channel].downcase + + # TODO check if a whitelist entry for this host already exists + whitelist = @registry[:whitelist] + whitelist << WhitelistEntry.new(host, channel) + @registry[:whitelist] = whitelist + + m.okay + rescue + error $! + m.reply $! + end + end + + def list_whitelist(m, params=nil) + m.reply "whitelist entries: #{@registry[:whitelist].length}" + @registry[:whitelist].each_with_index { |auto, idx| + m.reply "\##{idx+1}: #{auto.host} | #{auto.channel}" + } + end + + def rm_whitelist(m, params=nil) + wl = @registry[:whitelist] + count = wl.length + + idx = nil + idx = params[:idx].to_i if params[:idx] + + if idx + if idx > count + m.reply "No such whitelist entry \##{idx}" + return + end + wl.delete_at(idx-1) + else + begin + host = m.server.new_netmask(params[:host]) + channel = params[:channel].downcase + + wl.each { |rule| + next unless ['all', rule.channel].include?(channel) + wl.delete rule if rule.host == host + } + rescue + error $! + m.reply $! + end + end + @registry[:whitelist] = wl + if count > whitelist.length + m.okay + else + m.reply "No host matching #{host}" + end + end + + private + def check_channel(m, strchannel) + begin + raise "must specify channel if using privmsg" if m.private? and not strchannel + channel = m.server.channel(strchannel) || m.target + raise "I am not in that channel" unless channel.has_user?(@bot.nick) + + return channel + rescue + error $! + m.reply $! + end + end + + def do_cmd(action, nick, channel, timer_in=nil, reason=nil) + case timer_in + when nil + timer = 0 + when /^(\d+)s$/ + timer = $1.to_i + when /^(\d+)m$/ + timer = $1.to_i * 60 + when /^(\d+)h$/ + timer = $1.to_i * 60 * 60 + when /^(\d+)d$/ + timer = $1.to_i * 60 * 60 * 24 + else + raise "Wrong time specifications" + end + + case action + when :ban + set_mode(channel, "+b", nick) + @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0 + when :unban + set_mode(channel, "-b", nick) + when :kick + do_kick(channel, nick, reason) + when :kickban + set_mode(channel, "+b", nick) + @bot.timer.add_once(timer) { set_mode(channel, "-b", nick) } if timer > 0 + do_kick(channel, nick, reason) + when :silence, :quiet + set_mode(channel, "+q", nick) + @bot.timer.add_once(timer) { set_mode(channel, "-q", nick) } if timer > 0 + when :unsilence, :unquiet + set_mode(channel, "-q", nick) + end + end + + def set_mode(channel, mode, nick) + host = channel.has_user?(nick) ? "*!*@" + channel.get_user(nick).host : nick + @bot.mode(channel, mode, host) + end + + def do_kick(channel, nick, reason="") + @bot.kick(channel, nick, reason) + end +end + +plugin = BansPlugin.new + +plugin.default_auth( 'act', false ) +plugin.default_auth( 'edit', false ) +plugin.default_auth( 'list', true ) + +plugin.map 'ban :nick :timer :channel', :action => 'ban_user', + :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe}, + :defaults => {:timer => nil, :channel => nil}, + :auth_path => 'act' +plugin.map 'unban :nick :channel', :action => 'unban_user', + :requirements => {:channel => BansPlugin::ChannelRe}, + :defaults => {:channel => nil}, + :auth_path => 'act' +plugin.map 'kick :nick :channel *reason', :action => 'kick_user', + :requirements => {:channel => BansPlugin::ChannelRe}, + :defaults => {:channel => nil, :reason => 'requested'}, + :auth_path => 'act' +plugin.map 'kickban :nick :timer :channel *reason', :action => 'kickban_user', + :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe}, + :defaults => {:timer => nil, :channel => nil, :reason => 'requested'}, + :auth_path => 'act' +plugin.map 'silence :nick :timer :channel', :action => 'silence_user', + :requirements => {:timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelRe}, + :defaults => {:timer => nil, :channel => nil}, + :auth_path => 'act' +plugin.map 'unsilence :nick :channel', :action => 'unsilence_user', + :requirements => {:channel => BansPlugin::ChannelRe}, + :defaults => {:channel => nil}, + :auth_path => 'act' + +plugin.map 'bans add onjoin :host :action :channel *reason', :action => 'add_onjoin', + :requirements => {:action => BansPlugin::ActionRe, :channel => BansPlugin::ChannelAllRe}, + :defaults => {:action => 'kickban', :channel => 'all', :reason => 'netmask not welcome'}, + :auth_path => 'edit::onjoin' +plugin.map 'bans rm onjoin index :idx', :action => 'rm_onjoin', + :requirements => {:num => BansPlugin::IdxRe}, + :auth_path => 'edit::onjoin' +plugin.map 'bans rm onjoin :host :channel', :action => 'rm_onjoin', + :requirements => {:channel => BansPlugin::ChannelAllRe}, + :defaults => {:channel => 'all'}, + :auth_path => 'edit::onjoin' +plugin.map 'bans list onjoin[s]', :action => 'list_onjoin', + :auth_path => 'list::onjoin' + +plugin.map 'bans add badword :regexp :action :timer :channel *reason', :action => 'add_badword', + :requirements => {:action => BansPlugin::ActionRe, :timer => BansPlugin::TimerRe, :channel => BansPlugin::ChannelAllRe}, + :defaults => {:action => 'silence', :timer => "0s", :channel => 'all', :reason => 'bad word'}, + :auth_path => 'edit::badword' +plugin.map 'bans rm badword index :idx', :action => 'rm_badword', + :requirements => {:num => BansPlugin::IdxRe}, + :auth_path => 'edit::badword' +plugin.map 'bans rm badword :regexp :channel', :action => 'rm_badword', + :requirements => {:channel => BansPlugin::ChannelAllRe}, + :defaults => {:channel => 'all'}, + :auth_path => 'edit::badword' +plugin.map 'bans list badword[s]', :action => 'list_badword', + :auth_path => 'list::badword' + +plugin.map 'bans add whitelist :host :channel', :action => 'add_whitelist', + :requirements => {:channel => BansPlugin::ChannelAllRe}, + :defaults => {:channel => 'all'}, + :auth_path => 'edit::whitelist' +plugin.map 'bans rm whitelist index :idx', :action => 'rm_whitelist', + :requirements => {:num => BansPlugin::IdxRe}, + :auth_path => 'edit::whitelist' +plugin.map 'bans rm whitelist :host :channel', :action => 'rm_whitelist', + :requirements => {:channel => BansPlugin::ChannelAllRe}, + :defaults => {:channel => 'all'}, + :auth_path => 'edit::whitelist' +plugin.map 'bans list whitelist', :action => 'list_whitelist', + :auth_path => 'list::whitelist' + diff --git a/data/rbot/plugins/deepthoughts.rb b/data/rbot/plugins/deepthoughts.rb index 87ccab2d..b1171bae 100644 --- a/data/rbot/plugins/deepthoughts.rb +++ b/data/rbot/plugins/deepthoughts.rb @@ -1,89 +1,89 @@ -THOUGHTS = [
- 'It takes a big man to cry, but it takes a bigger man to laugh at that man.',
- "If I ever get real rich, I hope I'm not real mean to poor people, like I am now.",
- 'To me, it\'s always a good idea to always carry two sacks of something when you walk around. That way, if anybody says, "Hey, can you give me a hand?," you can say, "Sorry, got these sacks."',
- 'Maybe in order to understand mankind we have to look at that word itself. MANKIND. Basically, it\'s made up of two separate words "mank" and "ind." What do these words mean? It\'s a mystery and that\'s why so is mankind.',
- "Anytime I see something screech across a room and latch onto someone's neck, and the guy screams and tries to get it off, I have to laugh, because what is that thing.",
- "If you're in a war, instead of throwing a hand grenade at the enemy, throw one of those small pumpkins. Maybe it'll make everyone think how stupid war is, and while they are thinking, you can throw a real grenade at them.",
- 'One thing kids like is to be tricked. For instance, I was going to take my nephew to Disneyland, but instead I drove him to an old burned-out warehouse. "Oh no," I said, "Disneyland burned down." He cried and cried, but I think that deep down he thought it was a pretty good joke. I started to drive over to the real Disneyland, but it was getting pretty late.',
- "To me, clowns aren't funny. In fact, they're kinda scary. I've wondered where this started and I think it goes back to the time I went to the circus and a clown killed my dad.",
- "I can picture in my mind a world without war, a world without hate. And I can picture us attacking that world, because they'd never expect it.",
- "When I was a kid my favorite relative was Uncle Caveman. After school we'd all go play in his cave, and every once in a while he would eat one of us. It wasn't until later that I found out that Uncle Caveman was a bear.",
- 'Instead of trying to build newer and bigger weapons of destruction, we should be thinking about getting more use out of the ones we already have.',
- 'If a kid asks where rain comes from, I think a cute thing to tell him is, "God is crying." And if he asks why God is crying, another cute thing to tell him is, "Probably because of something you did."',
- 'When I found the skull in the woods, the first thing I did was call the police. But then I got curious about it. I picked it up, and started wondering who this person was, and why he had deer horns.',
- "Instead of a trap door, what about a trap window? The guy looks out it, and if he leans too far, he falls out. Wait. I guess that's like a regular window.",
- 'Laurie got offended that I used the word "puke." But to me, that\'s what her dinner tasted like.',
- 'If you want to be the popular one at a party, here\'s a good thing to do: Go up to some people who are talking and laughing and say, "Well, technically that\'s illegal." It might fit in with what somebody just said. And even if it doesn\'t, so what, I hate this stupid party.',
- "If you ever drop your keys into a river of molten lava, let'em go, because, man, they're gone.",
- "I bet a funny thing about driving a car off a cliff is, while you're in midair, you still hit those brakes! Hey, better try the emergency brake!",
- "When you die, if you get a choice between going to regular heaven or pie heaven, choose pie heaven. It might be a trick, but if it's not, mmmmmmm, boy.",
- 'Dad always thought laughter was the best medicine, which I guess is why several of us died of tuberculosis.',
- "If you ever fall off the Sears Tower, just go real limp, because maybe you'll look like a dummy and people will try to catch you because, hey, free dummy.",
- 'I think a good product would be "Baby Duck Hat." It\'s a fake baby duck, which you strap on top of your head. Then you go swimming underwater until you find a mommy duck and her babies, and you join them. Then all of the sudden, you stand up out of the water and roar like Godzilla. Man those ducks really take off! Also Baby Duck Hat is good for parties.',
- "Sometimes I think I'd be better off dead. No, wait, not me, you.",
- 'I hope that someday we will be able to put away our fears and prejudices and just laugh at people.',
- "I believe in making the world safe for our children, but not our children's children, because I don't think children should be having sex.",
- 'I hope that after I die, people will say of me: "That guy sure owed me a lot of money."',
- "One thing vampire children have to be taught early on is, don't run with a wooden stake.",
- 'It makes me mad when people say I turned and ran like a scared rabbit. Maybe it was like an angry rabbit, who was going to fight in another fight, away from the first fight.',
- "Broken promises don't upset me. I just think, why did they believe me?",
- "How come the dove gets to be the peace symbol? How about the pillow? It has more feathers than the dove, and it doesn't have that dangerous beak.",
- "If you define cowardice as running away at the first sign of danger, screaming and tripping and begging for mercy, then yes, Mr. Brave man, I guess I'm a coward.",
- 'During the Middle Ages, probably one of the biggest mistakes was not putting on your armor because you were "just going down to the corner."',
- 'If I lived back in the wild west days, instead of carrying a six-gun in my holster, I\'d carry a soldering iron. That way, if some smart-aleck cowboy said something like "Hey, look. He\'s carrying a soldering iron!" and started laughing, and everybody else started laughing, I could just say, "That\'s right, it\'s a soldering iron. The soldering iron of justice." Then everybody would get real quiet and ashamed, because they had made fun of the soldering iron of justice, and I could probably hit them up for a free drink.',
- "I hope if dogs ever take over the world and they choose a king, they don't just go by size, because I bet there are some Chihuahuas with some good ideas.",
- "Sometimes I think you have to march right in and demand your rights, even if you don't know what your rights are, or who the person is you're talking to. Then on the way out, slam the door.",
- "Is there anything more beautiful than a beautiful, beautiful flamingo, flying across in front of a beautiful sunset? And he's carrying a beautiful rose in his beak, and also he's carrying a very beautiful painting with his feet. And also, you're drunk.",
- "I wish I had a dollar for every time I spent a dollar, because then, yahoo!, I'd have all my money back.",
- 'If I was the head of a country that lost a war, and I had to sign a peace treaty, just as I was signing, I\'d glance over the treaty and then suddenly act surprised. "Wait a minute! I thought we won!"',
- "Once while walking through the mall a guy came up to me and said, 'Hey, how's it going?' So I grabbed his arm and twisted it up behind his head and said 'Now who's asking the questions?'",
- 'The face of a child can say it all, especially the mouth part of the face.',
- 'Why do the caterpillar and the ant have to be enemies? One eats leaves, and the other eats caterpillars. Oh, I see now.',
- "We used to laugh at Grandpa when he'd head off and go fishing. But we wouldn't be laughing that evening when he'd come back with some whore he picked up in town.",
- 'I can\'t stand cheap people. It makes me real mad when someone says something like, "Hey, when are you going to pay me that $100 you owe me?" or "Do you have that $50 you borrowed?" Man, quit being so cheap!',
- "If you think a weakness can be turned into a strength, I hate to tell you this, but that's another weakness.",
- "I'd like to see a nature film where an eagle swoops down and pulls a fish out of a lake, and then maybe he's flying along, low to the ground, and the fish pulls a worm out of the ground. Now that's a documentary!",
- 'I hope they never find out that lightning has a lot of vitamins in it, because do you hide from it or not?',
- 'Many people never stop to realize that a tree is a living thing, not that different from a tall, leafy dog that has roots and is very quiet.',
- 'If you get invited to your first orgy, don\'t just show up nude. That\'s a common mistake. You have to let nudity "happen."',
- "To me, boxing is like a ballet, except there's no music, no choreography and the dancers hit each other.",
- "One thing a computer can do that most humans can't is be sealed up in a cardboard box and sit in a warehouse.",
- "Whenever you read a good book, it's like the author is right there, in the room talking to you, which is why I don't like to read good books.",
- "Contrary to what most people say, the most dangerous animal in the world is not the lion or the tiger or even the elephant. It's a shark riding on an elephant's back, just trampling and eating everything they see.",
- 'Somebody told me how frightening it was how much topsoil we are losing each year, but I told that story around the campfire and nobody got scared.',
- "If you go parachuting, and your parachute doesn't open, and you friends are all watching you fall, I think a funny gag would be to pretend you were swimming.",
- "Whenever I see an old lady slip and fall on a wet sidewalk, my first instinct is to laugh. But then I think, what if I was an ant and she fell on me. Then it wouldn't seem quite so funny.",
- "Consider the daffodil. And while you're doing that, I'll be over here, looking through your stuff.",
- 'I think there should be something in science called the "reindeer effect." I don\'t know what it would be, but I think it\'d be good to hear someone say, "Gentlemen, what we have here is a terrifying example of the reindeer effect."',
- "If you go through a lot of hammers each month, I don't think it necessarily means you're a hard worker. It may just mean that you have a lot to learn about proper hammer maintenance.",
- "You know what would make a good story? Something about a clown who make people happy, but inside he's real sad. Also, he has severe diarrhea.",
- 'I wish I had a Kryptonite cross, because then you could keep both Dracula AND Superman away.',
- 'Once when I was in Hawaii, on the island of Kauai, I met a mysterious old stranger. He said he was about to die and wanted to tell someone about the treasure. I said, "Okay, as long as it\'s not a long story. Some of us have a plane to catch, you know." He told us about his life and all, and I thought: "This story isn\'t too long." But then, he kept going, and I started thinking, "Uh-oh, this story is getting long." But then the story was over, and I said to myself: "You know, that story wasn\'t too long after all." I forget what the story was about, but there was a good movie on the plane. It was a little long, though.',
- "A man doesn't automatically get my respect. He has to get down in the dirt and beg for it.",
- "Love can sweep you off your feet and carry you along in a way you've never known before. But the ride always ends, and you end up feeling lonely and bitter. Wait. It's not love I'm describing. I'm thinking of a monorail.",
- "For mad scientists who keep brains in jars, here's a tip: Why not add a slice of lemon to each jar, for freshness.",
- 'Probably the earliest fly swatters were nothing more than some sort of striking surface attached to the end of a long stick.',
- 'You know something that would really make me applaud? A guy gets stuck in quicksand, then sinks, then suddenly comes shooting out, riding on water skis! How do they do that?!',
- 'I think somebody should come up with a way to breed a very large shrimp. That way, you could ride him, then after you camped at night, you could eat him. How about it, science?',
- "I wouldn't be surprised if someday some fishermen caught a big shark and cut it open, and there inside was a whole person. Then they cut the person open, and in him is a little baby shark. And in the baby shark there isn't a person, because it would be too small. But there's a little doll or something, like a Johnny Combat little toy guy---something like that.",
- 'We like to praise birds for flying. But how much of it is actually flying, and how much of it is just sort of coasting from the previous flap?',
- 'Something tells me that the first mousetrap wasn\'t designed to catch mice at all, but to protect little cheese "gems" from burglars.',
- 'I guess we were all guilty, in a way. We all shot him, we all skinned him, and we all got a complimentary bumper sticker that said, "I helped skin Bob."',
- "If you ever reach total enlightenment while you're drinking a beer, I bet it makes beer shoot out your nose.",
- "I guess I kinda lost control, because in the middle of the play I ran up and lit the evil puppet villain on fire. No, I didn't. Just kidding. I just said that to help illustrate one of the human emotions, which is freaking out. Another emotion is greed, as when you kill someone for money, or something like that. Another emotion is generosity, as when you pay someone double what he paid for his stupid puppet.",
- 'Of all the tall tales, I think my favorite is the one about Eli Whitney and the interchangeable parts.',
-]
-
-class DeepThoughts < Plugin
- def help(plugin, topic="")
- "deepthought => think a deep thought."
- end
-
- def deepthought(m, params)
- m.reply THOUGHTS.pick_one
- end
-end
-
-plugin = DeepThoughts.new
-plugin.map 'deepthought'
+THOUGHTS = [ + 'It takes a big man to cry, but it takes a bigger man to laugh at that man.', + "If I ever get real rich, I hope I'm not real mean to poor people, like I am now.", + 'To me, it\'s always a good idea to always carry two sacks of something when you walk around. That way, if anybody says, "Hey, can you give me a hand?," you can say, "Sorry, got these sacks."', + 'Maybe in order to understand mankind we have to look at that word itself. MANKIND. Basically, it\'s made up of two separate words "mank" and "ind." What do these words mean? It\'s a mystery and that\'s why so is mankind.', + "Anytime I see something screech across a room and latch onto someone's neck, and the guy screams and tries to get it off, I have to laugh, because what is that thing.", + "If you're in a war, instead of throwing a hand grenade at the enemy, throw one of those small pumpkins. Maybe it'll make everyone think how stupid war is, and while they are thinking, you can throw a real grenade at them.", + 'One thing kids like is to be tricked. For instance, I was going to take my nephew to Disneyland, but instead I drove him to an old burned-out warehouse. "Oh no," I said, "Disneyland burned down." He cried and cried, but I think that deep down he thought it was a pretty good joke. I started to drive over to the real Disneyland, but it was getting pretty late.', + "To me, clowns aren't funny. In fact, they're kinda scary. I've wondered where this started and I think it goes back to the time I went to the circus and a clown killed my dad.", + "I can picture in my mind a world without war, a world without hate. And I can picture us attacking that world, because they'd never expect it.", + "When I was a kid my favorite relative was Uncle Caveman. After school we'd all go play in his cave, and every once in a while he would eat one of us. It wasn't until later that I found out that Uncle Caveman was a bear.", + 'Instead of trying to build newer and bigger weapons of destruction, we should be thinking about getting more use out of the ones we already have.', + 'If a kid asks where rain comes from, I think a cute thing to tell him is, "God is crying." And if he asks why God is crying, another cute thing to tell him is, "Probably because of something you did."', + 'When I found the skull in the woods, the first thing I did was call the police. But then I got curious about it. I picked it up, and started wondering who this person was, and why he had deer horns.', + "Instead of a trap door, what about a trap window? The guy looks out it, and if he leans too far, he falls out. Wait. I guess that's like a regular window.", + 'Laurie got offended that I used the word "puke." But to me, that\'s what her dinner tasted like.', + 'If you want to be the popular one at a party, here\'s a good thing to do: Go up to some people who are talking and laughing and say, "Well, technically that\'s illegal." It might fit in with what somebody just said. And even if it doesn\'t, so what, I hate this stupid party.', + "If you ever drop your keys into a river of molten lava, let'em go, because, man, they're gone.", + "I bet a funny thing about driving a car off a cliff is, while you're in midair, you still hit those brakes! Hey, better try the emergency brake!", + "When you die, if you get a choice between going to regular heaven or pie heaven, choose pie heaven. It might be a trick, but if it's not, mmmmmmm, boy.", + 'Dad always thought laughter was the best medicine, which I guess is why several of us died of tuberculosis.', + "If you ever fall off the Sears Tower, just go real limp, because maybe you'll look like a dummy and people will try to catch you because, hey, free dummy.", + 'I think a good product would be "Baby Duck Hat." It\'s a fake baby duck, which you strap on top of your head. Then you go swimming underwater until you find a mommy duck and her babies, and you join them. Then all of the sudden, you stand up out of the water and roar like Godzilla. Man those ducks really take off! Also Baby Duck Hat is good for parties.', + "Sometimes I think I'd be better off dead. No, wait, not me, you.", + 'I hope that someday we will be able to put away our fears and prejudices and just laugh at people.', + "I believe in making the world safe for our children, but not our children's children, because I don't think children should be having sex.", + 'I hope that after I die, people will say of me: "That guy sure owed me a lot of money."', + "One thing vampire children have to be taught early on is, don't run with a wooden stake.", + 'It makes me mad when people say I turned and ran like a scared rabbit. Maybe it was like an angry rabbit, who was going to fight in another fight, away from the first fight.', + "Broken promises don't upset me. I just think, why did they believe me?", + "How come the dove gets to be the peace symbol? How about the pillow? It has more feathers than the dove, and it doesn't have that dangerous beak.", + "If you define cowardice as running away at the first sign of danger, screaming and tripping and begging for mercy, then yes, Mr. Brave man, I guess I'm a coward.", + 'During the Middle Ages, probably one of the biggest mistakes was not putting on your armor because you were "just going down to the corner."', + 'If I lived back in the wild west days, instead of carrying a six-gun in my holster, I\'d carry a soldering iron. That way, if some smart-aleck cowboy said something like "Hey, look. He\'s carrying a soldering iron!" and started laughing, and everybody else started laughing, I could just say, "That\'s right, it\'s a soldering iron. The soldering iron of justice." Then everybody would get real quiet and ashamed, because they had made fun of the soldering iron of justice, and I could probably hit them up for a free drink.', + "I hope if dogs ever take over the world and they choose a king, they don't just go by size, because I bet there are some Chihuahuas with some good ideas.", + "Sometimes I think you have to march right in and demand your rights, even if you don't know what your rights are, or who the person is you're talking to. Then on the way out, slam the door.", + "Is there anything more beautiful than a beautiful, beautiful flamingo, flying across in front of a beautiful sunset? And he's carrying a beautiful rose in his beak, and also he's carrying a very beautiful painting with his feet. And also, you're drunk.", + "I wish I had a dollar for every time I spent a dollar, because then, yahoo!, I'd have all my money back.", + 'If I was the head of a country that lost a war, and I had to sign a peace treaty, just as I was signing, I\'d glance over the treaty and then suddenly act surprised. "Wait a minute! I thought we won!"', + "Once while walking through the mall a guy came up to me and said, 'Hey, how's it going?' So I grabbed his arm and twisted it up behind his head and said 'Now who's asking the questions?'", + 'The face of a child can say it all, especially the mouth part of the face.', + 'Why do the caterpillar and the ant have to be enemies? One eats leaves, and the other eats caterpillars. Oh, I see now.', + "We used to laugh at Grandpa when he'd head off and go fishing. But we wouldn't be laughing that evening when he'd come back with some whore he picked up in town.", + 'I can\'t stand cheap people. It makes me real mad when someone says something like, "Hey, when are you going to pay me that $100 you owe me?" or "Do you have that $50 you borrowed?" Man, quit being so cheap!', + "If you think a weakness can be turned into a strength, I hate to tell you this, but that's another weakness.", + "I'd like to see a nature film where an eagle swoops down and pulls a fish out of a lake, and then maybe he's flying along, low to the ground, and the fish pulls a worm out of the ground. Now that's a documentary!", + 'I hope they never find out that lightning has a lot of vitamins in it, because do you hide from it or not?', + 'Many people never stop to realize that a tree is a living thing, not that different from a tall, leafy dog that has roots and is very quiet.', + 'If you get invited to your first orgy, don\'t just show up nude. That\'s a common mistake. You have to let nudity "happen."', + "To me, boxing is like a ballet, except there's no music, no choreography and the dancers hit each other.", + "One thing a computer can do that most humans can't is be sealed up in a cardboard box and sit in a warehouse.", + "Whenever you read a good book, it's like the author is right there, in the room talking to you, which is why I don't like to read good books.", + "Contrary to what most people say, the most dangerous animal in the world is not the lion or the tiger or even the elephant. It's a shark riding on an elephant's back, just trampling and eating everything they see.", + 'Somebody told me how frightening it was how much topsoil we are losing each year, but I told that story around the campfire and nobody got scared.', + "If you go parachuting, and your parachute doesn't open, and you friends are all watching you fall, I think a funny gag would be to pretend you were swimming.", + "Whenever I see an old lady slip and fall on a wet sidewalk, my first instinct is to laugh. But then I think, what if I was an ant and she fell on me. Then it wouldn't seem quite so funny.", + "Consider the daffodil. And while you're doing that, I'll be over here, looking through your stuff.", + 'I think there should be something in science called the "reindeer effect." I don\'t know what it would be, but I think it\'d be good to hear someone say, "Gentlemen, what we have here is a terrifying example of the reindeer effect."', + "If you go through a lot of hammers each month, I don't think it necessarily means you're a hard worker. It may just mean that you have a lot to learn about proper hammer maintenance.", + "You know what would make a good story? Something about a clown who make people happy, but inside he's real sad. Also, he has severe diarrhea.", + 'I wish I had a Kryptonite cross, because then you could keep both Dracula AND Superman away.', + 'Once when I was in Hawaii, on the island of Kauai, I met a mysterious old stranger. He said he was about to die and wanted to tell someone about the treasure. I said, "Okay, as long as it\'s not a long story. Some of us have a plane to catch, you know." He told us about his life and all, and I thought: "This story isn\'t too long." But then, he kept going, and I started thinking, "Uh-oh, this story is getting long." But then the story was over, and I said to myself: "You know, that story wasn\'t too long after all." I forget what the story was about, but there was a good movie on the plane. It was a little long, though.', + "A man doesn't automatically get my respect. He has to get down in the dirt and beg for it.", + "Love can sweep you off your feet and carry you along in a way you've never known before. But the ride always ends, and you end up feeling lonely and bitter. Wait. It's not love I'm describing. I'm thinking of a monorail.", + "For mad scientists who keep brains in jars, here's a tip: Why not add a slice of lemon to each jar, for freshness.", + 'Probably the earliest fly swatters were nothing more than some sort of striking surface attached to the end of a long stick.', + 'You know something that would really make me applaud? A guy gets stuck in quicksand, then sinks, then suddenly comes shooting out, riding on water skis! How do they do that?!', + 'I think somebody should come up with a way to breed a very large shrimp. That way, you could ride him, then after you camped at night, you could eat him. How about it, science?', + "I wouldn't be surprised if someday some fishermen caught a big shark and cut it open, and there inside was a whole person. Then they cut the person open, and in him is a little baby shark. And in the baby shark there isn't a person, because it would be too small. But there's a little doll or something, like a Johnny Combat little toy guy---something like that.", + 'We like to praise birds for flying. But how much of it is actually flying, and how much of it is just sort of coasting from the previous flap?', + 'Something tells me that the first mousetrap wasn\'t designed to catch mice at all, but to protect little cheese "gems" from burglars.', + 'I guess we were all guilty, in a way. We all shot him, we all skinned him, and we all got a complimentary bumper sticker that said, "I helped skin Bob."', + "If you ever reach total enlightenment while you're drinking a beer, I bet it makes beer shoot out your nose.", + "I guess I kinda lost control, because in the middle of the play I ran up and lit the evil puppet villain on fire. No, I didn't. Just kidding. I just said that to help illustrate one of the human emotions, which is freaking out. Another emotion is greed, as when you kill someone for money, or something like that. Another emotion is generosity, as when you pay someone double what he paid for his stupid puppet.", + 'Of all the tall tales, I think my favorite is the one about Eli Whitney and the interchangeable parts.', +] + +class DeepThoughts < Plugin + def help(plugin, topic="") + "deepthought => think a deep thought." + end + + def deepthought(m, params) + m.reply THOUGHTS.pick_one + end +end + +plugin = DeepThoughts.new +plugin.map 'deepthought' diff --git a/data/rbot/plugins/dict.rb b/data/rbot/plugins/dict.rb index ca5f2588..db779889 100644 --- a/data/rbot/plugins/dict.rb +++ b/data/rbot/plugins/dict.rb @@ -1,174 +1,174 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: Dictionary lookup plugin for rbot
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006-2007 Giuseppe Bilotta
-# License:: GPL v2
-#
-# Provides a link to the definition of a word in one of the supported
-# dictionaries. Currently available are
-# * the Oxford dictionary for (British) English
-# * the De Mauro/Paravia dictionary for Italian
-# * the Chambers dictionary for English (accepts both US and UK)
-#
-# Other plugins can use this one to check if a given word is valid in italian
-# or english by using the is_italian?/is_british?/is_english? methods
-#
-# TODO: cache results and reuse them if get_cached returns a cache copy
-
-DEMAURO_LEMMA = /<anchor>(.*?)(?: - (.*?))<go href="lemma.php\?ID=(\d+)"\/><\/anchor>/
-CHAMBERS_LEMMA = /<p><span class="hwd">(.*?)<\/span> <span class="psa">(.*?)<\/span>(.*?)<\/p>/
-
-class DictPlugin < Plugin
- Config.register Config::IntegerValue.new('dict.hits',
- :default => 3,
- :desc => "Number of hits to return from a dictionary lookup")
- Config.register Config::IntegerValue.new('dict.first_par',
- :default => 0,
- :desc => "When set to n > 0, the bot will return the first paragraph from the first n dictionary hits")
-
- def initialize
- super
- @dmurl = "http://www.demauroparavia.it/"
- @dmwapurl = "http://wap.demauroparavia.it/index.php?lemma=%s"
- @dmwaplemma = "http://wap.demauroparavia.it/lemma.php?ID=%s"
- @oxurl = "http://www.askoxford.com/concise_oed/%s"
- @chambersurl = "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st"
- end
-
-
- def help(plugin, topic="")
- case topic
- when "demauro"
- return "demauro <word> => provides a link to the definition of <word> from the De Mauro/Paravia dictionary"
- when "oxford"
- return "oxford <word> => provides a link to the definition of <word> (it can also be an expression) from the Concise Oxford dictionary"
- when "chambers"
- return "chambers <word> => provides a link to the definition of <word> (it can also be an expression) from the Chambers 21st Century Dictionary"
- end
- return "<dictionary> <word>: check for <word> on <dictionary> where <dictionary> can be one of: demauro, oxford, chambers"
- end
-
- def demauro(m, params)
- justcheck = params[:justcheck]
-
- word = params[:word].downcase
- url = @dmwapurl % CGI.escape(word)
- xml = nil
- info = @bot.httputil.get_response(url) rescue nil
- xml = info.body if info
- if xml.nil?
- info = info ? " (#{info.code} - #{info.message})" : ""
- return false if justcheck
- m.reply "An error occurred while looking for #{word}#{info}"
- return
- end
- if xml=~ /Non ho trovato occorrenze per/
- return false if justcheck
- m.reply "Nothing found for #{word}"
- return
- end
- entries = xml.scan(DEMAURO_LEMMA)
- text = word
- urls = []
- if not entries.transpose.first.grep(/\b#{word}\b/)
- return false if justcheck
- text += " not found. Similar words"
- end
- return true if justcheck
- text += ": "
- n = 0
- hits = @bot.config['dict.hits']
- text += entries[0...hits].map { |ar|
- n += 1
- urls << @dmwaplemma % ar[2]
- "#{n}. #{Bold}#{ar[0]}#{Bold} - #{ar[1].gsub(/<\/?em>/,'')}: #{@dmurl}#{ar[2]}"
- }.join(" | ")
- m.reply text
-
- first_pars = @bot.config['dict.first_par']
-
- return unless first_pars > 0
-
- Utils.get_first_pars urls, first_pars, :message => m,
- :strip => /^.+?\s+-\s+/
-
- end
-
- def is_italian?(word)
- return demauro(nil, :word => word, :justcheck => true)
- end
-
-
- def oxford(m, params)
- justcheck = params[:justcheck]
-
- word = params[:word].join
- [word, word + "_1"].each { |check|
- url = @oxurl % CGI.escape(check)
- if params[:british]
- url << "?view=uk"
- end
- h = @bot.httputil.get(url, :max_redir => 5)
- if h and h.match(%r!<h2>#{word}(?:<sup>1</sup>)?</h2>!)
- m.reply("#{word} : #{url}") unless justcheck
- defn = $'
- m.reply("#{Bold}%s#{Bold}: %s" % [word, defn.ircify_html(:nbsp => :space)], :overlong => :truncate)
- return true
- end
- }
- return false if justcheck
- m.reply "#{word} not found"
- end
-
- def is_british?(word)
- return oxford(nil, :word => word, :justcheck => true, :british => true)
- end
-
-
- def chambers(m, params)
- justcheck = params[:justcheck]
-
- word = params[:word].to_s.downcase
- url = @chambersurl % CGI.escape(word)
- xml = nil
- info = @bot.httputil.get_response(url) rescue nil
- xml = info.body if info
- case xml
- when nil
- info = info ? " (#{info.code} - #{info.message})" : ""
- return false if justcheck
- m.reply "An error occurred while looking for #{word}#{info}"
- return
- when /Sorry, no entries for <b>.*?<\/b> were found./
- return false if justcheck
- m.reply "Nothing found for #{word}"
- return
- when /No exact matches for <b>.*?<\/b>, but the following may be helpful./
- return false if justcheck
- m.reply "Nothing found for #{word}, but see #{url} for possible suggestions"
- return
- end
- # Else, we have a hit
- return true if justcheck
- m.reply "#{word}: #{url}"
- entries = xml.scan(CHAMBERS_LEMMA)
- hits = @bot.config['dict.hits']
- entries[0...hits].map { |ar|
- m.reply(("#{Bold}%s#{Bold} #{Underline}%s#{Underline}%s" % ar).ircify_html, :overlong => :truncate)
- }
- end
-
- def is_english?(word)
- return chambers(nil, :word => word, :justcheck => true)
- end
-
-end
-
-plugin = DictPlugin.new
-plugin.map 'demauro :word', :action => 'demauro'
-plugin.map 'oxford *word', :action => 'oxford'
-plugin.map 'chambers *word', :action => 'chambers'
-
+#-- vim:sw=2:et +#++ +# +# :title: Dictionary lookup plugin for rbot +# +# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com> +# Copyright:: (C) 2006-2007 Giuseppe Bilotta +# License:: GPL v2 +# +# Provides a link to the definition of a word in one of the supported +# dictionaries. Currently available are +# * the Oxford dictionary for (British) English +# * the De Mauro/Paravia dictionary for Italian +# * the Chambers dictionary for English (accepts both US and UK) +# +# Other plugins can use this one to check if a given word is valid in italian +# or english by using the is_italian?/is_british?/is_english? methods +# +# TODO: cache results and reuse them if get_cached returns a cache copy + +DEMAURO_LEMMA = /<anchor>(.*?)(?: - (.*?))<go href="lemma.php\?ID=(\d+)"\/><\/anchor>/ +CHAMBERS_LEMMA = /<p><span class="hwd">(.*?)<\/span> <span class="psa">(.*?)<\/span>(.*?)<\/p>/ + +class DictPlugin < Plugin + Config.register Config::IntegerValue.new('dict.hits', + :default => 3, + :desc => "Number of hits to return from a dictionary lookup") + Config.register Config::IntegerValue.new('dict.first_par', + :default => 0, + :desc => "When set to n > 0, the bot will return the first paragraph from the first n dictionary hits") + + def initialize + super + @dmurl = "http://www.demauroparavia.it/" + @dmwapurl = "http://wap.demauroparavia.it/index.php?lemma=%s" + @dmwaplemma = "http://wap.demauroparavia.it/lemma.php?ID=%s" + @oxurl = "http://www.askoxford.com/concise_oed/%s" + @chambersurl = "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st" + end + + + def help(plugin, topic="") + case topic + when "demauro" + return "demauro <word> => provides a link to the definition of <word> from the De Mauro/Paravia dictionary" + when "oxford" + return "oxford <word> => provides a link to the definition of <word> (it can also be an expression) from the Concise Oxford dictionary" + when "chambers" + return "chambers <word> => provides a link to the definition of <word> (it can also be an expression) from the Chambers 21st Century Dictionary" + end + return "<dictionary> <word>: check for <word> on <dictionary> where <dictionary> can be one of: demauro, oxford, chambers" + end + + def demauro(m, params) + justcheck = params[:justcheck] + + word = params[:word].downcase + url = @dmwapurl % CGI.escape(word) + xml = nil + info = @bot.httputil.get_response(url) rescue nil + xml = info.body if info + if xml.nil? + info = info ? " (#{info.code} - #{info.message})" : "" + return false if justcheck + m.reply "An error occurred while looking for #{word}#{info}" + return + end + if xml=~ /Non ho trovato occorrenze per/ + return false if justcheck + m.reply "Nothing found for #{word}" + return + end + entries = xml.scan(DEMAURO_LEMMA) + text = word + urls = [] + if not entries.transpose.first.grep(/\b#{word}\b/) + return false if justcheck + text += " not found. Similar words" + end + return true if justcheck + text += ": " + n = 0 + hits = @bot.config['dict.hits'] + text += entries[0...hits].map { |ar| + n += 1 + urls << @dmwaplemma % ar[2] + "#{n}. #{Bold}#{ar[0]}#{Bold} - #{ar[1].gsub(/<\/?em>/,'')}: #{@dmurl}#{ar[2]}" + }.join(" | ") + m.reply text + + first_pars = @bot.config['dict.first_par'] + + return unless first_pars > 0 + + Utils.get_first_pars urls, first_pars, :message => m, + :strip => /^.+?\s+-\s+/ + + end + + def is_italian?(word) + return demauro(nil, :word => word, :justcheck => true) + end + + + def oxford(m, params) + justcheck = params[:justcheck] + + word = params[:word].join + [word, word + "_1"].each { |check| + url = @oxurl % CGI.escape(check) + if params[:british] + url << "?view=uk" + end + h = @bot.httputil.get(url, :max_redir => 5) + if h and h.match(%r!<h2>#{word}(?:<sup>1</sup>)?</h2>!) + m.reply("#{word} : #{url}") unless justcheck + defn = $' + m.reply("#{Bold}%s#{Bold}: %s" % [word, defn.ircify_html(:nbsp => :space)], :overlong => :truncate) + return true + end + } + return false if justcheck + m.reply "#{word} not found" + end + + def is_british?(word) + return oxford(nil, :word => word, :justcheck => true, :british => true) + end + + + def chambers(m, params) + justcheck = params[:justcheck] + + word = params[:word].to_s.downcase + url = @chambersurl % CGI.escape(word) + xml = nil + info = @bot.httputil.get_response(url) rescue nil + xml = info.body if info + case xml + when nil + info = info ? " (#{info.code} - #{info.message})" : "" + return false if justcheck + m.reply "An error occurred while looking for #{word}#{info}" + return + when /Sorry, no entries for <b>.*?<\/b> were found./ + return false if justcheck + m.reply "Nothing found for #{word}" + return + when /No exact matches for <b>.*?<\/b>, but the following may be helpful./ + return false if justcheck + m.reply "Nothing found for #{word}, but see #{url} for possible suggestions" + return + end + # Else, we have a hit + return true if justcheck + m.reply "#{word}: #{url}" + entries = xml.scan(CHAMBERS_LEMMA) + hits = @bot.config['dict.hits'] + entries[0...hits].map { |ar| + m.reply(("#{Bold}%s#{Bold} #{Underline}%s#{Underline}%s" % ar).ircify_html, :overlong => :truncate) + } + end + + def is_english?(word) + return chambers(nil, :word => word, :justcheck => true) + end + +end + +plugin = DictPlugin.new +plugin.map 'demauro :word', :action => 'demauro' +plugin.map 'oxford *word', :action => 'oxford' +plugin.map 'chambers *word', :action => 'chambers' + diff --git a/data/rbot/plugins/games/azgame.rb b/data/rbot/plugins/games/azgame.rb index 495173b3..b1bb09e5 100644 --- a/data/rbot/plugins/games/azgame.rb +++ b/data/rbot/plugins/games/azgame.rb @@ -1,566 +1,566 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: A-Z Game Plugin for rbot
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Author:: Yaohan Chen <yaohan.chen@gmail.com>: Japanese support
-#
-# Copyright:: (C) 2006 Giuseppe Bilotta
-# Copyright:: (C) 2007 GIuseppe Bilotta, Yaohan Chen
-#
-# License:: GPL v2
-#
-# A-Z Game: guess the word by reducing the interval of allowed ones
-#
-# TODO allow manual addition of words
-
-class AzGame
-
- attr_reader :range, :word
- attr_reader :lang, :rules, :listener
- attr_accessor :tries, :total_tries, :total_failed, :failed, :winner
- def initialize(plugin, lang, rules, word)
- @plugin = plugin
- @lang = lang.to_sym
- @word = word.downcase
- @rules = rules
- @range = [@rules[:first].dup, @rules[:last].dup]
- @listener = @rules[:listener]
- @total_tries = 0
- @total_failed = 0 # not used, reported, updated
- @tries = Hash.new(0)
- @failed = Hash.new(0) # not used, not reported, updated
- @winner = nil
- def @range.to_s
- return "%s -- %s" % self
- end
- end
-
- def check(word)
- w = word.downcase
- debug "checking #{w} for #{@word} in #{@range}"
- # Since we're called threaded, bail out early if a winner
- # was assigned already
- return [:ignore, nil] if @winner
- return [:bingo, nil] if w == @word
- return [:out, @range] if w < @range.first or w > @range.last
- return [:ignore, @range] if w == @range.first or w == @range.last
- # This is potentially slow (for languages that check online)
- return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w)
- debug "we like it"
- # Check again if there was a winner in the mean time,
- # and bail out if there was
- return [:ignore, nil] if @winner
- if w < @word and w > @range.first
- @range.first.replace(w)
- return [:in, @range]
- elsif w > @word and w < @range.last
- @range.last.replace(w)
- return [:in, @range]
- end
- return [:out, @range]
- end
-
-# TODO scoring: base score is t = ceil(100*exp(-((n-1)^2)/(50^2)))+p for n attempts
-# done by p players; players that didn't win but contributed
-# with a attempts will get t*a/n points
-
- include Math
-
- def score
- n = @total_tries
- p = @tries.keys.length
- t = (100*exp(-((n-1)**2)/(50.0**2))).ceil + p
- debug "Total score: #{t}"
- ret = Hash.new
- @tries.each { |k, a|
- ret[k] = [t*a/n, n_("%{count} try", "%{count} tries", a) % {:count => a}]
- }
- if @winner
- debug "replacing winner score of %d with %d" % [ret[@winner].first, t]
- tries = ret[@winner].last
- ret[@winner] = [t, _("winner, %{tries}") % {:tries => tries}]
- end
- return ret.sort_by { |h| h.last.first }.reverse
- end
-
-end
-
-class AzGamePlugin < Plugin
-
- def initialize
- super
- # if @registry.has_key?(:games)
- # @games = @registry[:games]
- # else
- @games = Hash.new
- # end
- if @registry.has_key?(:wordcache) and @registry[:wordcache]
- @wordcache = @registry[:wordcache]
- else
- @wordcache = Hash.new
- end
- debug "A-Z wordcache: #{@wordcache.pretty_inspect}"
-
- @rules = {
- :italian => {
- :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\.
- :bad => /var\./,
- :first => 'abaco',
- :last => 'zuzzurellone',
- :url => "http://www.demauroparavia.it/%s",
- :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s",
- :listener => /^[a-z]+$/
- },
- :english => {
- :good => /(?:singular )?noun|verb|adj/,
- :first => 'abacus',
- :last => 'zuni',
- :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st",
- :listener => /^[a-z]+$/
- },
- }
-
- japanese_wordlist = "#{@bot.botclass}/azgame/wordlist-japanese"
- if File.exist?(japanese_wordlist)
- words = File.readlines(japanese_wordlist) \
- .map {|line| line.strip} .uniq
- if(words.length >= 4) # something to guess
- @rules[:japanese] = {
- :good => /^\S+$/,
- :list => words,
- :first => words[0],
- :last => words[-1],
- :listener => /^\S+$/
- }
- debug "Japanese wordlist loaded, #{@rules[:japanese][:list].length} lines; first word: #{@rules[:japanese][:first]}, last word: #{@rules[:japanese][:last]}"
- end
- end
- end
-
- def save
- # @registry[:games] = @games
- @registry[:wordcache] = @wordcache
- end
-
- def listen(m)
- return unless m.kind_of?(PrivMessage)
- return if m.channel.nil? or m.address?
- k = m.channel.downcase.to_s # to_sym?
- return unless @games.key?(k)
- return if m.params
- word = m.plugin.downcase
- return unless word =~ @games[k].listener
- word_check(m, k, word)
- end
-
- def word_check(m, k, word)
- # Not really safe ... what happens
- Thread.new {
- isit = @games[k].check(word)
- case isit.first
- when :bingo
- m.reply _("%{bold}BINGO!%{bold} the word was %{underline}%{word}%{underline}. Congrats, %{bold}%{player}%{bold}!") % {:bold => Bold, :underline => Underline, :word => word, :player => m.sourcenick}
- @games[k].total_tries += 1
- @games[k].tries[m.source] += 1
- @games[k].winner = m.source
- ar = @games[k].score.inject([]) { |res, kv|
- res.push("%s: %d (%s)" % kv.flatten)
- }
- m.reply _("The game was won after %{tries} tries. Scores for this game: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}
- @games.delete(k)
- when :out
- m.reply _("%{word} is not in the range %{bold}%{range}%{bold}") % {:word => word, :bold => Bold, :range => isit.last} if m.address?
- when :noexist
- # bail out early if the game was won in the mean time
- return if !@games[k] or @games[k].winner
- m.reply _("%{word} doesn't exist or is not acceptable for the game") % {:word => word}
- @games[k].total_failed += 1
- @games[k].failed[m.source] += 1
- when :in
- # bail out early if the game was won in the mean time
- return if !@games[k] or @games[k].winner
- m.reply _("close, but no cigar. New range: %{bold}%{range}%{bold}") % {:bold => Bold, :range => isit.last}
- @games[k].total_tries += 1
- @games[k].tries[m.source] += 1
- when :ignore
- m.reply _("%{word} is already one of the range extrema: %{range}") % {:word => word, :range => isit.last} if m.address?
- else
- m.reply _("hm, something went wrong while verifying %{word}")
- end
- }
- end
-
- def manual_word_check(m, params)
- k = m.channel.downcase.to_s
- word = params[:word].downcase
- if not @games.key?(k)
- m.reply _("no A-Z game running here, can't check if %{word} is valid, can I?")
- return
- end
- if word !~ /^\S+$/
- m.reply _("I only accept single words composed by letters only, sorry")
- return
- end
- word_check(m, k, word)
- end
-
- def stop_game(m, params)
- return if m.channel.nil? # Shouldn't happen, but you never know
- k = m.channel.downcase.to_s # to_sym?
- if @games.key?(k)
- m.reply _("the word in %{bold}%{range}%{bold} was: %{bold}%{word}%{bold}") % {:bold => Bold, :range => @games[k].range, :word => @games[k].word}
- ar = @games[k].score.inject([]) { |res, kv|
- res.push("%s: %d (%s)" % kv.flatten)
- }
- m.reply _("The game was cancelled after %{tries} tries. Scores for this game would have been: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}
- @games.delete(k)
- else
- m.reply _("no A-Z game running in this channel ...")
- end
- end
-
- def start_game(m, params)
- return if m.channel.nil? # Shouldn't happen, but you never know
- k = m.channel.downcase.to_s # to_sym?
- unless @games.key?(k)
- lang = (params[:lang] || @bot.config['core.language']).to_sym
- method = 'random_pick_'+lang.to_s
- m.reply _("let me think ...")
- if @rules.has_key?(lang) and self.respond_to?(method)
- word = self.send(method)
- if word.empty?
- m.reply _("couldn't think of anything ...")
- return
- end
- else
- m.reply _("I can't play A-Z in %{lang}, sorry") % {:lang => lang}
- return
- end
- m.reply _("got it!")
- @games[k] = AzGame.new(self, lang, @rules[lang], word)
- end
- tr = @games[k].total_tries
- # this message building code is rewritten to make translation easier
- if tr == 0
- tr_msg = ''
- else
- f_tr = @games[k].total_failed
- if f_tr > 0
- tr_msg = _(" (after %{total_tries} and %{invalid_tries})") %
- { :total_tries => n_("%{count} try", "%{count} tries", tr) %
- {:count => tr},
- :invalid_tries => n_("%{count} invalid try", "%{count} invalid tries", tr) %
- {:count => f_tr} }
- else
- tr_msg = _(" (after %{total_tries})") %
- { :total_tries => n_("%{count} try", "%{count} tries", tr) %
- {:count => tr}}
- end
- end
-
- m.reply _("A-Z: %{bold}%{range}%{bold}") % {:bold => Bold, :range => @games[k].range} + tr_msg
- return
- end
-
- def wordlist(m, params)
- pars = params[:params]
- lang = (params[:lang] || @bot.config['core.language']).to_sym
- wc = @wordcache[lang] || Hash.new rescue Hash.new
- cmd = params[:cmd].to_sym rescue :count
- case cmd
- when :count
- m.reply n_("I have %{count} %{lang} word in my cache", "I have %{count} %{lang} words in my cache", wc.size) % {:count => wc.size, :lang => lang}
- when :show, :list
- if pars.empty?
- m.reply _("provide a regexp to match")
- return
- end
- begin
- regex = /#{pars[0]}/
- matches = wc.keys.map { |k|
- k.to_s
- }.grep(regex)
- rescue
- matches = []
- end
- if matches.size == 0
- m.reply _("no %{lang} word I know match %{pattern}") % {:lang => lang, :pattern => pars[0]}
- elsif matches.size > 25
- m.reply _("more than 25 %{lang} words I know match %{pattern}, try a stricter matching") % {:lang => lang, :pattern => pars[0]}
- else
- m.reply "#{matches.join(', ')}"
- end
- when :info
- if pars.empty?
- m.reply _("provide a word")
- return
- end
- word = pars[0].downcase.to_sym
- if not wc.key?(word)
- m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}
- return
- end
- if wc[word].key?(:when)
- tr = _("%{word} learned from %{user} on %{date}") % {:word => word, :user => wc[word][:who], :date => wc[word][:when]}
- else
- tr = _("%{word} learned from %{user}") % {:word => word, :user => wc[word][:who]}
- end
- m.reply tr
- when :delete
- if pars.empty?
- m.reply _("provide a word")
- return
- end
- word = pars[0].downcase.to_sym
- if not wc.key?(word)
- m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}
- return
- end
- wc.delete(word)
- @bot.okay m.replyto
- when :add
- if pars.empty?
- m.reply _("provide a word")
- return
- end
- word = pars[0].downcase.to_sym
- if wc.key?(word)
- m.reply _("I already know the %{lang} word %{word}")
- return
- end
- wc[word] = { :who => m.sourcenick, :when => Time.now }
- @bot.okay m.replyto
- else
- end
- end
-
- def is_japanese?(word)
- @rules[:japanese][:list].include?(word)
- end
-
- # return integer between min and max, inclusive
- def rand_between(min, max)
- rand(max - min + 1) + min
- end
-
- def random_pick_japanese(min=nil, max=nil)
- rules = @rules[:japanese]
- min = rules[:first] if min.nil_or_empty?
- max = rules[:last] if max.nil_or_empty?
- debug "Randomly picking word between #{min} and #{max}"
- min_index = rules[:list].index(min)
- max_index = rules[:list].index(max)
- debug "Index between #{min_index} and #{max_index}"
- index = rand_between(min_index + 1, max_index - 1)
- debug "Index generated: #{index}"
- word = rules[:list][index]
- debug "Randomly picked #{word}"
- word
- end
-
- def is_italian?(word)
- unless @wordcache.key?(:italian)
- @wordcache[:italian] = Hash.new
- end
- wc = @wordcache[:italian]
- return true if wc.key?(word.to_sym)
- rules = @rules[:italian]
- p = @bot.httputil.get(rules[:wapurl] % word, :open_timeout => 60, :read_timeout => 60)
- if not p
- error "could not connect!"
- return false
- end
- debug p
- p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|
- debug "new word #{word} of type #{qual}"
- if qual =~ rules[:good] and qual !~ rules[:bad]
- wc[word.to_sym] = {:who => :dict}
- return true
- end
- next
- }
- return false
- end
-
- def random_pick_italian(min=nil,max=nil)
- # Try to pick a random word between min and max
- word = String.new
- min = min.to_s
- max = max.to_s
- if min > max
- m.reply "#{min} > #{max}"
- return word
- end
- rules = @rules[:italian]
- min = rules[:first] if min.empty?
- max = rules[:last] if max.empty?
- debug "looking for word between #{min.inspect} and #{max.inspect}"
- return word if min.empty? or max.empty?
- begin
- while (word <= min or word >= max or word !~ /^[a-z]+$/)
- debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
- # TODO for the time being, skip words with extended characters
- unless @wordcache.key?(:italian)
- @wordcache[:italian] = Hash.new
- end
- wc = @wordcache[:italian]
-
- if wc.size > 0
- cache_or_url = rand(2)
- if cache_or_url == 0
- debug "getting word from wordcache"
- word = wc.keys[rand(wc.size)].to_s
- next
- end
- end
-
- # TODO when doing ranges, adapt this choice
- l = ('a'..'z').to_a[rand(26)]
- debug "getting random word from dictionary, starting with letter #{l}"
- first = rules[:url] % "lettera_#{l}_0_50"
- p = @bot.httputil.get(first)
- max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i
- pp = rand(max_page)+1
- debug "getting random word from dictionary, starting with letter #{l}, page #{pp}"
- p = @bot.httputil.get(first+"&pagina=#{pp}") if pp > 1
- lemmi = Array.new
- good = rules[:good]
- bad = rules[:bad]
- # We look for a lemma composed by a single word and of length at least two
- p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*? (.+?)<\/li>/) { |url, prelemma, tipo|
- lemma = prelemma.downcase.to_sym
- debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}"
- next if wc.key?(lemma)
- case tipo
- when good
- if tipo =~ bad
- debug "refusing, #{bad}"
- next
- end
- debug "good one"
- lemmi << lemma
- wc[lemma] = {:who => :dict}
- else
- debug "refusing, not #{good}"
- end
- }
- word = lemmi[rand(lemmi.length)].to_s
- end
- rescue => e
- error "error #{e.inspect} while looking up a word"
- error e.backtrace.join("\n")
- end
- return word
- end
-
- def is_english?(word)
- unless @wordcache.key?(:english)
- @wordcache[:english] = Hash.new
- end
- wc = @wordcache[:english]
- return true if wc.key?(word.to_sym)
- rules = @rules[:english]
- p = @bot.httputil.get(rules[:url] % CGI.escape(word))
- if not p
- error "could not connect!"
- return false
- end
- debug p
- if p =~ /<span class="(?:hwd|srch)">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i
- debug "new word #{word}"
- wc[word.to_sym] = {:who => :dict}
- return true
- end
- return false
- end
-
- def random_pick_english(min=nil,max=nil)
- # Try to pick a random word between min and max
- word = String.new
- min = min.to_s
- max = max.to_s
- if min > max
- m.reply "#{min} > #{max}"
- return word
- end
- rules = @rules[:english]
- min = rules[:first] if min.empty?
- max = rules[:last] if max.empty?
- debug "looking for word between #{min.inspect} and #{max.inspect}"
- return word if min.empty? or max.empty?
- begin
- while (word <= min or word >= max or word !~ /^[a-z]+$/)
- debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
- # TODO for the time being, skip words with extended characters
- unless @wordcache.key?(:english)
- @wordcache[:english] = Hash.new
- end
- wc = @wordcache[:english]
-
- if wc.size > 0
- cache_or_url = rand(2)
- if cache_or_url == 0
- debug "getting word from wordcache"
- word = wc.keys[rand(wc.size)].to_s
- next
- end
- end
-
- # TODO when doing ranges, adapt this choice
- l = ('a'..'z').to_a[rand(26)]
- ll = ('a'..'z').to_a[rand(26)]
- random = [l,ll].join('*') + '*'
- debug "getting random word from dictionary, matching #{random}"
- p = @bot.httputil.get(rules[:url] % CGI.escape(random))
- debug p
- lemmi = Array.new
- good = rules[:good]
- # We look for a lemma composed by a single word and of length at least two
- p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|
- lemma = prelemma.downcase
- debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}"
- next if wc.key?(lemma.to_sym)
- if lemma =~ /^[a-z]+$/
- debug "good one"
- lemmi << lemma
- wc[lemma.to_sym] = {:who => :dict}
- else
- debug "funky characters, not good"
- end
- }
- next if lemmi.empty?
- word = lemmi[rand(lemmi.length)]
- end
- rescue => e
- error "error #{e.inspect} while looking up a word"
- error e.backtrace.join("\n")
- end
- return word
- end
-
- def help(plugin, topic="")
- case topic
- when 'manage'
- return _("az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)")
- when 'cancel'
- return _("az cancel => abort current game")
- when 'check'
- return _('az check <word> => checks <word> against current game')
- when 'rules'
- return _("try to guess the word the bot is thinking of; if you guess wrong, the bot will use the new word to restrict the range of allowed words: eventually, the range will be so small around the correct word that you can't miss it")
- when 'play'
- return _("az => start a game if none is running, show the current word range otherwise; you can say 'az <language>' if you want to play in a language different from the current bot default")
- end
- return _("az topics: play, rules, cancel, manage, check")
- end
-
-end
-
-plugin = AzGamePlugin.new
-plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!'
-plugin.map 'az cancel', :action=>'stop_game', :private => false
-plugin.map 'az check :word', :action => 'manual_word_check', :private => false
-plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil }
-
+#-- vim:sw=2:et +#++ +# +# :title: A-Z Game Plugin for rbot +# +# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com> +# Author:: Yaohan Chen <yaohan.chen@gmail.com>: Japanese support +# +# Copyright:: (C) 2006 Giuseppe Bilotta +# Copyright:: (C) 2007 GIuseppe Bilotta, Yaohan Chen +# +# License:: GPL v2 +# +# A-Z Game: guess the word by reducing the interval of allowed ones +# +# TODO allow manual addition of words + +class AzGame + + attr_reader :range, :word + attr_reader :lang, :rules, :listener + attr_accessor :tries, :total_tries, :total_failed, :failed, :winner + def initialize(plugin, lang, rules, word) + @plugin = plugin + @lang = lang.to_sym + @word = word.downcase + @rules = rules + @range = [@rules[:first].dup, @rules[:last].dup] + @listener = @rules[:listener] + @total_tries = 0 + @total_failed = 0 # not used, reported, updated + @tries = Hash.new(0) + @failed = Hash.new(0) # not used, not reported, updated + @winner = nil + def @range.to_s + return "%s -- %s" % self + end + end + + def check(word) + w = word.downcase + debug "checking #{w} for #{@word} in #{@range}" + # Since we're called threaded, bail out early if a winner + # was assigned already + return [:ignore, nil] if @winner + return [:bingo, nil] if w == @word + return [:out, @range] if w < @range.first or w > @range.last + return [:ignore, @range] if w == @range.first or w == @range.last + # This is potentially slow (for languages that check online) + return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w) + debug "we like it" + # Check again if there was a winner in the mean time, + # and bail out if there was + return [:ignore, nil] if @winner + if w < @word and w > @range.first + @range.first.replace(w) + return [:in, @range] + elsif w > @word and w < @range.last + @range.last.replace(w) + return [:in, @range] + end + return [:out, @range] + end + +# TODO scoring: base score is t = ceil(100*exp(-((n-1)^2)/(50^2)))+p for n attempts +# done by p players; players that didn't win but contributed +# with a attempts will get t*a/n points + + include Math + + def score + n = @total_tries + p = @tries.keys.length + t = (100*exp(-((n-1)**2)/(50.0**2))).ceil + p + debug "Total score: #{t}" + ret = Hash.new + @tries.each { |k, a| + ret[k] = [t*a/n, n_("%{count} try", "%{count} tries", a) % {:count => a}] + } + if @winner + debug "replacing winner score of %d with %d" % [ret[@winner].first, t] + tries = ret[@winner].last + ret[@winner] = [t, _("winner, %{tries}") % {:tries => tries}] + end + return ret.sort_by { |h| h.last.first }.reverse + end + +end + +class AzGamePlugin < Plugin + + def initialize + super + # if @registry.has_key?(:games) + # @games = @registry[:games] + # else + @games = Hash.new + # end + if @registry.has_key?(:wordcache) and @registry[:wordcache] + @wordcache = @registry[:wordcache] + else + @wordcache = Hash.new + end + debug "A-Z wordcache: #{@wordcache.pretty_inspect}" + + @rules = { + :italian => { + :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\. + :bad => /var\./, + :first => 'abaco', + :last => 'zuzzurellone', + :url => "http://www.demauroparavia.it/%s", + :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s", + :listener => /^[a-z]+$/ + }, + :english => { + :good => /(?:singular )?noun|verb|adj/, + :first => 'abacus', + :last => 'zuni', + :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st", + :listener => /^[a-z]+$/ + }, + } + + japanese_wordlist = "#{@bot.botclass}/azgame/wordlist-japanese" + if File.exist?(japanese_wordlist) + words = File.readlines(japanese_wordlist) \ + .map {|line| line.strip} .uniq + if(words.length >= 4) # something to guess + @rules[:japanese] = { + :good => /^\S+$/, + :list => words, + :first => words[0], + :last => words[-1], + :listener => /^\S+$/ + } + debug "Japanese wordlist loaded, #{@rules[:japanese][:list].length} lines; first word: #{@rules[:japanese][:first]}, last word: #{@rules[:japanese][:last]}" + end + end + end + + def save + # @registry[:games] = @games + @registry[:wordcache] = @wordcache + end + + def listen(m) + return unless m.kind_of?(PrivMessage) + return if m.channel.nil? or m.address? + k = m.channel.downcase.to_s # to_sym? + return unless @games.key?(k) + return if m.params + word = m.plugin.downcase + return unless word =~ @games[k].listener + word_check(m, k, word) + end + + def word_check(m, k, word) + # Not really safe ... what happens + Thread.new { + isit = @games[k].check(word) + case isit.first + when :bingo + m.reply _("%{bold}BINGO!%{bold} the word was %{underline}%{word}%{underline}. Congrats, %{bold}%{player}%{bold}!") % {:bold => Bold, :underline => Underline, :word => word, :player => m.sourcenick} + @games[k].total_tries += 1 + @games[k].tries[m.source] += 1 + @games[k].winner = m.source + ar = @games[k].score.inject([]) { |res, kv| + res.push("%s: %d (%s)" % kv.flatten) + } + m.reply _("The game was won after %{tries} tries. Scores for this game: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')} + @games.delete(k) + when :out + m.reply _("%{word} is not in the range %{bold}%{range}%{bold}") % {:word => word, :bold => Bold, :range => isit.last} if m.address? + when :noexist + # bail out early if the game was won in the mean time + return if !@games[k] or @games[k].winner + m.reply _("%{word} doesn't exist or is not acceptable for the game") % {:word => word} + @games[k].total_failed += 1 + @games[k].failed[m.source] += 1 + when :in + # bail out early if the game was won in the mean time + return if !@games[k] or @games[k].winner + m.reply _("close, but no cigar. New range: %{bold}%{range}%{bold}") % {:bold => Bold, :range => isit.last} + @games[k].total_tries += 1 + @games[k].tries[m.source] += 1 + when :ignore + m.reply _("%{word} is already one of the range extrema: %{range}") % {:word => word, :range => isit.last} if m.address? + else + m.reply _("hm, something went wrong while verifying %{word}") + end + } + end + + def manual_word_check(m, params) + k = m.channel.downcase.to_s + word = params[:word].downcase + if not @games.key?(k) + m.reply _("no A-Z game running here, can't check if %{word} is valid, can I?") + return + end + if word !~ /^\S+$/ + m.reply _("I only accept single words composed by letters only, sorry") + return + end + word_check(m, k, word) + end + + def stop_game(m, params) + return if m.channel.nil? # Shouldn't happen, but you never know + k = m.channel.downcase.to_s # to_sym? + if @games.key?(k) + m.reply _("the word in %{bold}%{range}%{bold} was: %{bold}%{word}%{bold}") % {:bold => Bold, :range => @games[k].range, :word => @games[k].word} + ar = @games[k].score.inject([]) { |res, kv| + res.push("%s: %d (%s)" % kv.flatten) + } + m.reply _("The game was cancelled after %{tries} tries. Scores for this game would have been: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')} + @games.delete(k) + else + m.reply _("no A-Z game running in this channel ...") + end + end + + def start_game(m, params) + return if m.channel.nil? # Shouldn't happen, but you never know + k = m.channel.downcase.to_s # to_sym? + unless @games.key?(k) + lang = (params[:lang] || @bot.config['core.language']).to_sym + method = 'random_pick_'+lang.to_s + m.reply _("let me think ...") + if @rules.has_key?(lang) and self.respond_to?(method) + word = self.send(method) + if word.empty? + m.reply _("couldn't think of anything ...") + return + end + else + m.reply _("I can't play A-Z in %{lang}, sorry") % {:lang => lang} + return + end + m.reply _("got it!") + @games[k] = AzGame.new(self, lang, @rules[lang], word) + end + tr = @games[k].total_tries + # this message building code is rewritten to make translation easier + if tr == 0 + tr_msg = '' + else + f_tr = @games[k].total_failed + if f_tr > 0 + tr_msg = _(" (after %{total_tries} and %{invalid_tries})") % + { :total_tries => n_("%{count} try", "%{count} tries", tr) % + {:count => tr}, + :invalid_tries => n_("%{count} invalid try", "%{count} invalid tries", tr) % + {:count => f_tr} } + else + tr_msg = _(" (after %{total_tries})") % + { :total_tries => n_("%{count} try", "%{count} tries", tr) % + {:count => tr}} + end + end + + m.reply _("A-Z: %{bold}%{range}%{bold}") % {:bold => Bold, :range => @games[k].range} + tr_msg + return + end + + def wordlist(m, params) + pars = params[:params] + lang = (params[:lang] || @bot.config['core.language']).to_sym + wc = @wordcache[lang] || Hash.new rescue Hash.new + cmd = params[:cmd].to_sym rescue :count + case cmd + when :count + m.reply n_("I have %{count} %{lang} word in my cache", "I have %{count} %{lang} words in my cache", wc.size) % {:count => wc.size, :lang => lang} + when :show, :list + if pars.empty? + m.reply _("provide a regexp to match") + return + end + begin + regex = /#{pars[0]}/ + matches = wc.keys.map { |k| + k.to_s + }.grep(regex) + rescue + matches = [] + end + if matches.size == 0 + m.reply _("no %{lang} word I know match %{pattern}") % {:lang => lang, :pattern => pars[0]} + elsif matches.size > 25 + m.reply _("more than 25 %{lang} words I know match %{pattern}, try a stricter matching") % {:lang => lang, :pattern => pars[0]} + else + m.reply "#{matches.join(', ')}" + end + when :info + if pars.empty? + m.reply _("provide a word") + return + end + word = pars[0].downcase.to_sym + if not wc.key?(word) + m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word} + return + end + if wc[word].key?(:when) + tr = _("%{word} learned from %{user} on %{date}") % {:word => word, :user => wc[word][:who], :date => wc[word][:when]} + else + tr = _("%{word} learned from %{user}") % {:word => word, :user => wc[word][:who]} + end + m.reply tr + when :delete + if pars.empty? + m.reply _("provide a word") + return + end + word = pars[0].downcase.to_sym + if not wc.key?(word) + m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word} + return + end + wc.delete(word) + @bot.okay m.replyto + when :add + if pars.empty? + m.reply _("provide a word") + return + end + word = pars[0].downcase.to_sym + if wc.key?(word) + m.reply _("I already know the %{lang} word %{word}") + return + end + wc[word] = { :who => m.sourcenick, :when => Time.now } + @bot.okay m.replyto + else + end + end + + def is_japanese?(word) + @rules[:japanese][:list].include?(word) + end + + # return integer between min and max, inclusive + def rand_between(min, max) + rand(max - min + 1) + min + end + + def random_pick_japanese(min=nil, max=nil) + rules = @rules[:japanese] + min = rules[:first] if min.nil_or_empty? + max = rules[:last] if max.nil_or_empty? + debug "Randomly picking word between #{min} and #{max}" + min_index = rules[:list].index(min) + max_index = rules[:list].index(max) + debug "Index between #{min_index} and #{max_index}" + index = rand_between(min_index + 1, max_index - 1) + debug "Index generated: #{index}" + word = rules[:list][index] + debug "Randomly picked #{word}" + word + end + + def is_italian?(word) + unless @wordcache.key?(:italian) + @wordcache[:italian] = Hash.new + end + wc = @wordcache[:italian] + return true if wc.key?(word.to_sym) + rules = @rules[:italian] + p = @bot.httputil.get(rules[:wapurl] % word, :open_timeout => 60, :read_timeout => 60) + if not p + error "could not connect!" + return false + end + debug p + p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url| + debug "new word #{word} of type #{qual}" + if qual =~ rules[:good] and qual !~ rules[:bad] + wc[word.to_sym] = {:who => :dict} + return true + end + next + } + return false + end + + def random_pick_italian(min=nil,max=nil) + # Try to pick a random word between min and max + word = String.new + min = min.to_s + max = max.to_s + if min > max + m.reply "#{min} > #{max}" + return word + end + rules = @rules[:italian] + min = rules[:first] if min.empty? + max = rules[:last] if max.empty? + debug "looking for word between #{min.inspect} and #{max.inspect}" + return word if min.empty? or max.empty? + begin + while (word <= min or word >= max or word !~ /^[a-z]+$/) + debug "looking for word between #{min} and #{max} (prev: #{word.inspect})" + # TODO for the time being, skip words with extended characters + unless @wordcache.key?(:italian) + @wordcache[:italian] = Hash.new + end + wc = @wordcache[:italian] + + if wc.size > 0 + cache_or_url = rand(2) + if cache_or_url == 0 + debug "getting word from wordcache" + word = wc.keys[rand(wc.size)].to_s + next + end + end + + # TODO when doing ranges, adapt this choice + l = ('a'..'z').to_a[rand(26)] + debug "getting random word from dictionary, starting with letter #{l}" + first = rules[:url] % "lettera_#{l}_0_50" + p = @bot.httputil.get(first) + max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i + pp = rand(max_page)+1 + debug "getting random word from dictionary, starting with letter #{l}, page #{pp}" + p = @bot.httputil.get(first+"&pagina=#{pp}") if pp > 1 + lemmi = Array.new + good = rules[:good] + bad = rules[:bad] + # We look for a lemma composed by a single word and of length at least two + p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*? (.+?)<\/li>/) { |url, prelemma, tipo| + lemma = prelemma.downcase.to_sym + debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}" + next if wc.key?(lemma) + case tipo + when good + if tipo =~ bad + debug "refusing, #{bad}" + next + end + debug "good one" + lemmi << lemma + wc[lemma] = {:who => :dict} + else + debug "refusing, not #{good}" + end + } + word = lemmi[rand(lemmi.length)].to_s + end + rescue => e + error "error #{e.inspect} while looking up a word" + error e.backtrace.join("\n") + end + return word + end + + def is_english?(word) + unless @wordcache.key?(:english) + @wordcache[:english] = Hash.new + end + wc = @wordcache[:english] + return true if wc.key?(word.to_sym) + rules = @rules[:english] + p = @bot.httputil.get(rules[:url] % CGI.escape(word)) + if not p + error "could not connect!" + return false + end + debug p + if p =~ /<span class="(?:hwd|srch)">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i + debug "new word #{word}" + wc[word.to_sym] = {:who => :dict} + return true + end + return false + end + + def random_pick_english(min=nil,max=nil) + # Try to pick a random word between min and max + word = String.new + min = min.to_s + max = max.to_s + if min > max + m.reply "#{min} > #{max}" + return word + end + rules = @rules[:english] + min = rules[:first] if min.empty? + max = rules[:last] if max.empty? + debug "looking for word between #{min.inspect} and #{max.inspect}" + return word if min.empty? or max.empty? + begin + while (word <= min or word >= max or word !~ /^[a-z]+$/) + debug "looking for word between #{min} and #{max} (prev: #{word.inspect})" + # TODO for the time being, skip words with extended characters + unless @wordcache.key?(:english) + @wordcache[:english] = Hash.new + end + wc = @wordcache[:english] + + if wc.size > 0 + cache_or_url = rand(2) + if cache_or_url == 0 + debug "getting word from wordcache" + word = wc.keys[rand(wc.size)].to_s + next + end + end + + # TODO when doing ranges, adapt this choice + l = ('a'..'z').to_a[rand(26)] + ll = ('a'..'z').to_a[rand(26)] + random = [l,ll].join('*') + '*' + debug "getting random word from dictionary, matching #{random}" + p = @bot.httputil.get(rules[:url] % CGI.escape(random)) + debug p + lemmi = Array.new + good = rules[:good] + # We look for a lemma composed by a single word and of length at least two + p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard| + lemma = prelemma.downcase + debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}" + next if wc.key?(lemma.to_sym) + if lemma =~ /^[a-z]+$/ + debug "good one" + lemmi << lemma + wc[lemma.to_sym] = {:who => :dict} + else + debug "funky characters, not good" + end + } + next if lemmi.empty? + word = lemmi[rand(lemmi.length)] + end + rescue => e + error "error #{e.inspect} while looking up a word" + error e.backtrace.join("\n") + end + return word + end + + def help(plugin, topic="") + case topic + when 'manage' + return _("az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)") + when 'cancel' + return _("az cancel => abort current game") + when 'check' + return _('az check <word> => checks <word> against current game') + when 'rules' + return _("try to guess the word the bot is thinking of; if you guess wrong, the bot will use the new word to restrict the range of allowed words: eventually, the range will be so small around the correct word that you can't miss it") + when 'play' + return _("az => start a game if none is running, show the current word range otherwise; you can say 'az <language>' if you want to play in a language different from the current bot default") + end + return _("az topics: play, rules, cancel, manage, check") + end + +end + +plugin = AzGamePlugin.new +plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!' +plugin.map 'az cancel', :action=>'stop_game', :private => false +plugin.map 'az check :word', :action => 'manual_word_check', :private => false +plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil } + diff --git a/data/rbot/plugins/hl2.rb b/data/rbot/plugins/hl2.rb index d6444143..5023e01c 100644 --- a/data/rbot/plugins/hl2.rb +++ b/data/rbot/plugins/hl2.rb @@ -1,59 +1,59 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: Half-Life 2 plugin for rbot
-#
-# Author:: Ole Christian Rynning <oc@rynning.no>
-# Copyright:: (C) 2006 Ole Christian Rynning
-# License:: GPL v2
-#
-# Simple Half-Life 2 (Source Engine) plugin to query online
-# servers to see if its online and kicking and how many users.
-#
-# Added 2 seconds timeout to the response. And sockets are now
-# closing properly.
-
-require 'socket'
-require 'timeout'
-
-class HL2Plugin < Plugin
-
- A2S_INFO = "\xFF\xFF\xFF\xFF\x54\x53\x6F\x75\x72\x63\x65\x20\x45\x6E\x67\x69\x6E\x65\x20\x51\x75\x65\x72\x79\x00"
-
- TIMEOUT = 2
-
- def a2s_info(addr, port)
- socket = UDPSocket.new()
- socket.send(A2S_INFO, 0, addr, port.to_i)
- response = nil
-
- begin
- timeout(TIMEOUT) do
- response = socket.recvfrom(1400,0)
- end
- rescue Exception
- end
-
- socket.close()
- response ? response.first.unpack("iACZ*Z*Z*Z*sCCCaaCCZ*") : nil
- end
-
- def help(plugin, topic="")
- "hl2 'server:port' => show basic information about the given server"
- end
-
- def hl2(m, params)
- addr, port = params[:conn_str].split(':')
- info = a2s_info(addr, port)
- if info != nil
- m.reply "#{info[3]} is online with #{info[8]}/#{info[9]} players."
- else
- m.reply "Couldn't connect to #{params[:conn_str]}"
- end
- end
-
-end
-
-plugin = HL2Plugin.new
-plugin.map 'hl2 :conn_str', :thread => true
-
+#-- vim:sw=2:et +#++ +# +# :title: Half-Life 2 plugin for rbot +# +# Author:: Ole Christian Rynning <oc@rynning.no> +# Copyright:: (C) 2006 Ole Christian Rynning +# License:: GPL v2 +# +# Simple Half-Life 2 (Source Engine) plugin to query online +# servers to see if its online and kicking and how many users. +# +# Added 2 seconds timeout to the response. And sockets are now +# closing properly. + +require 'socket' +require 'timeout' + +class HL2Plugin < Plugin + + A2S_INFO = "\xFF\xFF\xFF\xFF\x54\x53\x6F\x75\x72\x63\x65\x20\x45\x6E\x67\x69\x6E\x65\x20\x51\x75\x65\x72\x79\x00" + + TIMEOUT = 2 + + def a2s_info(addr, port) + socket = UDPSocket.new() + socket.send(A2S_INFO, 0, addr, port.to_i) + response = nil + + begin + timeout(TIMEOUT) do + response = socket.recvfrom(1400,0) + end + rescue Exception + end + + socket.close() + response ? response.first.unpack("iACZ*Z*Z*Z*sCCCaaCCZ*") : nil + end + + def help(plugin, topic="") + "hl2 'server:port' => show basic information about the given server" + end + + def hl2(m, params) + addr, port = params[:conn_str].split(':') + info = a2s_info(addr, port) + if info != nil + m.reply "#{info[3]} is online with #{info[8]}/#{info[9]} players." + else + m.reply "Couldn't connect to #{params[:conn_str]}" + end + end + +end + +plugin = HL2Plugin.new +plugin.map 'hl2 :conn_str', :thread => true + diff --git a/data/rbot/plugins/rss.rb b/data/rbot/plugins/rss.rb index f6e0b98a..3b8e8c7d 100644 --- a/data/rbot/plugins/rss.rb +++ b/data/rbot/plugins/rss.rb @@ -1,1107 +1,1107 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: RSS feed plugin for rbot
-#
-# Author:: Stanislav Karchebny <berkus@madfire.net>
-# Author:: Ian Monroe <ian@monroe.nu>
-# Author:: Mark Kretschmann <markey@web.de>
-# Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
-#
-# Copyright:: (C) 2004 Stanislav Karchebny
-# Copyright:: (C) 2005 Ian Monroe, Mark Kretschmann
-# Copyright:: (C) 2006-2007 Giuseppe Bilotta
-#
-# License:: MIT license
-
-require 'rss'
-
-# Try to load rss/content/2.0 so we can access the data in <content:encoded>
-# tags.
-begin
- require 'rss/content/2.0'
-rescue LoadError
-end
-
-module ::RSS
-
- # Make an 'unique' ID for a given item, based on appropriate bot options
- # Currently only suppored is bot.config['rss.show_updated']: when true, the
- # description is included in the uid hashing, otherwise it's not
- #
- def RSS.item_uid_for_bot(item, opts={})
- options = { :show_updated => true}.merge(opts)
- desc = nil
- if options[:show_updated]
- desc = item.content.content rescue item.description rescue nil
- end
- [(item.title.content rescue item.title rescue nil),
- (item.link.href rescue item.link),
- desc].hash
- end
-
- # Add support for Slashdot namespace in RDF. The code is just an adaptation
- # of the DublinCore code.
- unless defined?(SLASH_PREFIX)
- SLASH_PREFIX = 'slash'
- SLASH_URI = "http://purl.org/rss/1.0/modules/slash/"
-
- RDF.install_ns(SLASH_PREFIX, SLASH_URI)
-
- module BaseSlashModel
- def append_features(klass)
- super
-
- return if klass.instance_of?(Module)
- SlashModel::ELEMENT_NAME_INFOS.each do |name, plural_name|
- plural = plural_name || "#{name}s"
- full_name = "#{SLASH_PREFIX}_#{name}"
- full_plural_name = "#{SLASH_PREFIX}_#{plural}"
- klass_name = "Slash#{Utils.to_class_name(name)}"
-
- # This will fail with older version of the Ruby RSS module
- begin
- klass.install_have_children_element(name, SLASH_URI, "*",
- full_name, full_plural_name)
- klass.install_must_call_validator(SLASH_PREFIX, SLASH_URI)
- rescue ArgumentError
- klass.module_eval("install_have_children_element(#{full_name.dump}, #{full_plural_name.dump})")
- end
-
- klass.module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- remove_method :#{full_name} if method_defined? :#{full_name}
- remove_method :#{full_name}= if method_defined? :#{full_name}=
- remove_method :set_#{full_name} if method_defined? :set_#{full_name}
-
- def #{full_name}
- @#{full_name}.first and @#{full_name}.first.value
- end
-
- def #{full_name}=(new_value)
- @#{full_name}[0] = Utils.new_with_value_if_need(#{klass_name}, new_value)
- end
- alias set_#{full_name} #{full_name}=
- EOC
- end
- end
- end
-
- module SlashModel
- extend BaseModel
- extend BaseSlashModel
-
- TEXT_ELEMENTS = {
- "department" => nil,
- "section" => nil,
- "comments" => nil,
- "hit_parade" => nil
- }
-
- ELEMENT_NAME_INFOS = SlashModel::TEXT_ELEMENTS.to_a
-
- ELEMENTS = TEXT_ELEMENTS.keys
-
- ELEMENTS.each do |name, plural_name|
- module_eval(<<-EOC, *get_file_and_line_from_caller(0))
- class Slash#{Utils.to_class_name(name)} < Element
- include RSS10
-
- content_setup
-
- class << self
- def required_prefix
- SLASH_PREFIX
- end
-
- def required_uri
- SLASH_URI
- end
- end
-
- @tag_name = #{name.dump}
-
- alias_method(:value, :content)
- alias_method(:value=, :content=)
-
- def initialize(*args)
- begin
- if Utils.element_initialize_arguments?(args)
- super
- else
- super()
- self.content = args[0]
- end
- # Older Ruby RSS module
- rescue NoMethodError
- super()
- self.content = args[0]
- end
- end
-
- def full_name
- tag_name_with_prefix(SLASH_PREFIX)
- end
-
- def maker_target(target)
- target.new_#{name}
- end
-
- def setup_maker_attributes(#{name})
- #{name}.content = content
- end
- end
- EOC
- end
- end
-
- class RDF
- class Item; include SlashModel; end
- end
-
- SlashModel::ELEMENTS.each do |name|
- class_name = Utils.to_class_name(name)
- BaseListener.install_class_name(SLASH_URI, name, "Slash#{class_name}")
- end
-
- SlashModel::ELEMENTS.collect! {|name| "#{SLASH_PREFIX}_#{name}"}
- end
-end
-
-
-class ::RssBlob
- attr_accessor :url, :handle, :type, :refresh_rate, :xml, :title, :items,
- :mutex, :watchers, :last_fetched
-
- def initialize(url,handle=nil,type=nil,watchers=[], xml=nil, lf = nil)
- @url = url
- if handle
- @handle = handle
- else
- @handle = url
- end
- @type = type
- @watchers=[]
- @refresh_rate = nil
- @xml = xml
- @title = nil
- @items = nil
- @mutex = Mutex.new
- @last_fetched = lf
- sanitize_watchers(watchers)
- end
-
- def dup
- @mutex.synchronize do
- self.class.new(@url,
- @handle,
- @type ? @type.dup : nil,
- @watchers.dup,
- @xml ? @xml.dup : nil,
- @last_fetched)
- end
- end
-
- # Downcase all watchers, possibly turning them into Strings if they weren't
- def sanitize_watchers(list=@watchers)
- ls = list.dup
- @watchers.clear
- ls.each { |w|
- add_watch(w)
- }
- end
-
- def watched?
- !@watchers.empty?
- end
-
- def watched_by?(who)
- @watchers.include?(who.downcase)
- end
-
- def add_watch(who)
- if watched_by?(who)
- return nil
- end
- @mutex.synchronize do
- @watchers << who.downcase
- end
- return who
- end
-
- def rm_watch(who)
- @mutex.synchronize do
- @watchers.delete(who.downcase)
- end
- end
-
- def to_a
- [@handle,@url,@type,@refresh_rate,@watchers]
- end
-
- def to_s(watchers=false)
- if watchers
- a = self.to_a.flatten
- else
- a = self.to_a[0,3]
- end
- a.compact.join(" | ")
- end
-end
-
-class RSSFeedsPlugin < Plugin
- Config.register Config::IntegerValue.new('rss.head_max',
- :default => 100, :validate => Proc.new{|v| v > 0 && v < 200},
- :desc => "How many characters to use of a RSS item header")
-
- Config.register Config::IntegerValue.new('rss.text_max',
- :default => 200, :validate => Proc.new{|v| v > 0 && v < 400},
- :desc => "How many characters to use of a RSS item text")
-
- Config.register Config::IntegerValue.new('rss.thread_sleep',
- :default => 300, :validate => Proc.new{|v| v > 30},
- :desc => "How many seconds to sleep before checking RSS feeds again")
-
- Config.register Config::BooleanValue.new('rss.show_updated',
- :default => true,
- :desc => "Whether feed items for which the description was changed should be shown as new")
-
- Config.register Config::BooleanValue.new('rss.show_links',
- :default => true,
- :desc => "Whether to display links from the text of a feed item.")
-
- # We used to save the Mutex with the RssBlob, which was idiotic. And
- # since Mutexes dumped in one version might not be resotrable in another,
- # we need a few tricks to be able to restore data from other versions of Ruby
- #
- # When migrating 1.8.6 => 1.8.5, all we need to do is define an empty
- # #marshal_load() method for Mutex. For 1.8.5 => 1.8.6 we need something
- # dirtier, as seen later on in the initialization code.
- unless Mutex.new.respond_to?(:marshal_load)
- class ::Mutex
- def marshal_load(str)
- return
- end
- end
- end
-
- attr_reader :feeds
-
- def initialize
- super
- if @registry.has_key?(:feeds)
- # When migrating from Ruby 1.8.5 to 1.8.6, dumped Mutexes may render the
- # data unrestorable. If this happens, we patch the data, thus allowing
- # the restore to work.
- #
- # This is actually pretty safe for a number of reasons:
- # * the code is only called if standard marshalling fails
- # * the string we look for is quite unlikely to appear randomly
- # * if the string appears somewhere and the patched string isn't recoverable
- # either, we'll get another (unrecoverable) error, which makes the rss
- # plugin unsable, just like it was if no recovery was attempted
- # * if the string appears somewhere and the patched string is recoverable,
- # we may get a b0rked feed, which is eventually overwritten by a clean
- # one, so the worst thing that can happen is that a feed update spams
- # the watchers once
- @registry.recovery = Proc.new { |val|
- patched = val.sub(":\v@mutexo:\nMutex", ":\v@mutexo:\vObject")
- ret = Marshal.restore(patched)
- ret.each_value { |blob|
- blob.mutex = nil
- blob
- }
- }
-
- @feeds = @registry[:feeds]
- raise unless @feeds
-
- @registry.recovery = nil
-
- @feeds.keys.grep(/[A-Z]/) { |k|
- @feeds[k.downcase] = @feeds[k]
- @feeds.delete(k)
- }
- @feeds.each { |k, f|
- f.mutex = Mutex.new
- f.sanitize_watchers
- parseRss(f) if f.xml
- }
- else
- @feeds = Hash.new
- end
- @watch = Hash.new
- rewatch_rss
- end
-
- def name
- "rss"
- end
-
- def watchlist
- @feeds.select { |h, f| f.watched? }
- end
-
- def cleanup
- stop_watches
- super
- end
-
- def save
- unparsed = Hash.new()
- @feeds.each { |k, f|
- unparsed[k] = f.dup
- # we don't want to save the mutex
- unparsed[k].mutex = nil
- }
- @registry[:feeds] = unparsed
- end
-
- def stop_watch(handle)
- if @watch.has_key?(handle)
- begin
- debug "Stopping watch #{handle}"
- @bot.timer.remove(@watch[handle])
- @watch.delete(handle)
- rescue Exception => e
- report_problem("Failed to stop watch for #{handle}", e, nil)
- end
- end
- end
-
- def stop_watches
- @watch.each_key { |k|
- stop_watch(k)
- }
- end
-
- def help(plugin,topic="")
- case topic
- when "show"
- "rss show #{Bold}handle#{Bold} [#{Bold}limit#{Bold}] : show #{Bold}limit#{Bold} (default: 5, max: 15) entries from rss #{Bold}handle#{Bold}; #{Bold}limit#{Bold} can also be in the form a..b, to display a specific range of items"
- when "list"
- "rss list [#{Bold}handle#{Bold}] : list all rss feeds (matching #{Bold}handle#{Bold})"
- when "watched"
- "rss watched [#{Bold}handle#{Bold}] [in #{Bold}chan#{Bold}]: list all watched rss feeds (matching #{Bold}handle#{Bold}) (in channel #{Bold}chan#{Bold})"
- when "who", "watches", "who watches"
- "rss who watches [#{Bold}handle#{Bold}]]: list all watchers for rss feeds (matching #{Bold}handle#{Bold})"
- when "add"
- "rss add #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : add a new rss called #{Bold}handle#{Bold} from url #{Bold}url#{Bold} (of type #{Bold}type#{Bold})"
- when "change"
- "rss change #{Bold}what#{Bold} of #{Bold}handle#{Bold} to #{Bold}new#{Bold} : change the #{Underline}handle#{Underline}, #{Underline}url#{Underline}, #{Underline}type#{Underline} or #{Underline}refresh#{Underline} rate of rss called #{Bold}handle#{Bold} to value #{Bold}new#{Bold}"
- when /^(del(ete)?|rm)$/
- "rss del(ete)|rm #{Bold}handle#{Bold} : delete rss feed #{Bold}handle#{Bold}"
- when "replace"
- "rss replace #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : try to replace the url of rss called #{Bold}handle#{Bold} with #{Bold}url#{Bold} (of type #{Bold}type#{Bold}); only works if nobody else is watching it"
- when "forcereplace"
- "rss forcereplace #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : replace the url of rss called #{Bold}handle#{Bold} with #{Bold}url#{Bold} (of type #{Bold}type#{Bold})"
- when "watch"
- "rss watch #{Bold}handle#{Bold} [#{Bold}url#{Bold} [#{Bold}type#{Bold}]] [in #{Bold}chan#{Bold}]: watch rss #{Bold}handle#{Bold} for changes (in channel #{Bold}chan#{Bold}); when the other parameters are present, the feed will be created if it doesn't exist yet"
- when /(un|rm)watch/
- "rss unwatch|rmwatch #{Bold}handle#{Bold} [in #{Bold}chan#{Bold}]: stop watching rss #{Bold}handle#{Bold} (in channel #{Bold}chan#{Bold}) for changes"
- when /who(?: watche?s?)?/
- "rss who watches #{Bold}handle#{Bold}: lists watches for rss #{Bold}handle#{Bold}"
- when "rewatch"
- "rss rewatch : restart threads that watch for changes in watched rss"
- else
- "manage RSS feeds: rss show|list|watched|add|change|del(ete)|rm|(force)replace|watch|unwatch|rmwatch|rewatch|who watches"
- end
- end
-
- def report_problem(report, e=nil, m=nil)
- if m && m.respond_to?(:reply)
- m.reply report
- else
- warning report
- end
- if e
- debug e.inspect
- debug e.backtrace.join("\n") if e.respond_to?(:backtrace)
- end
- end
-
- def show_rss(m, params)
- handle = params[:handle]
- lims = params[:limit].to_s.match(/(\d+)(?:..(\d+))?/)
- debug lims.to_a.inspect
- if lims[2]
- ll = [[lims[1].to_i-1,lims[2].to_i-1].min, 0].max
- ul = [[lims[1].to_i-1,lims[2].to_i-1].max, 14].min
- rev = lims[1].to_i > lims[2].to_i
- else
- ll = 0
- ul = [[lims[1].to_i-1, 0].max, 14].min
- rev = false
- end
-
- feed = @feeds.fetch(handle.downcase, nil)
- unless feed
- m.reply "I don't know any feeds named #{handle}"
- return
- end
-
- m.reply "lemme fetch it..."
- title = items = nil
- we_were_watching = false
-
- if @watch.key?(feed.handle)
- # If a feed is being watched, we run the watcher thread
- # so that all watchers can be informed of changes to
- # the feed. Before we do that, though, we remove the
- # show requester from the watchlist, if present, lest
- # he gets the update twice.
- if feed.watched_by?(m.replyto)
- we_were_watching = true
- feed.rm_watch(m.replyto)
- end
- @bot.timer.reschedule(@watch[feed.handle], 0)
- if we_were_watching
- feed.add_watch(m.replyto)
- end
- else
- fetched = fetchRss(feed, m, false)
- end
- return unless fetched or feed.xml
- if not fetched and feed.items
- m.reply "using old data"
- else
- parsed = parseRss(feed, m)
- m.reply "using old data" unless parsed
- end
- return unless feed.items
- title = feed.title
- items = feed.items
-
- # We sort the feeds in freshness order (newer ones first)
- items = freshness_sort(items)
- disp = items[ll..ul]
- disp.reverse! if rev
-
- m.reply "Channel : #{title}"
- disp.each do |item|
- printFormattedRss(feed, item, {:places=>[m.replyto],:handle=>nil,:date=>true})
- end
- end
-
- def itemDate(item,ex=nil)
- return item.pubDate if item.respond_to?(:pubDate) and item.pubDate
- return item.date if item.respond_to?(:date) and item.date
- return ex
- end
-
- def freshness_sort(items)
- notime = Time.at(0)
- items.sort { |a, b|
- itemDate(b, notime) <=> itemDate(a, notime)
- }
- end
-
- def list_rss(m, params)
- wanted = params[:handle]
- reply = String.new
- @feeds.each { |handle, feed|
- next if wanted and !handle.match(/#{wanted}/i)
- reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
- (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate
- (reply << " (watched)") if feed.watched_by?(m.replyto)
- reply << "\n"
- }
- if reply.empty?
- reply = "no feeds found"
- reply << " matching #{wanted}" if wanted
- end
- m.reply reply, :max_lines => reply.length
- end
-
- def watched_rss(m, params)
- wanted = params[:handle]
- chan = params[:chan] || m.replyto
- reply = String.new
- watchlist.each { |handle, feed|
- next if wanted and !handle.match(/#{wanted}/i)
- next unless feed.watched_by?(chan)
- reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
- (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate
- reply << "\n"
- }
- if reply.empty?
- reply = "no watched feeds"
- reply << " matching #{wanted}" if wanted
- end
- m.reply reply
- end
-
- def who_watches(m, params)
- wanted = params[:handle]
- reply = String.new
- watchlist.each { |handle, feed|
- next if wanted and !handle.match(/#{wanted}/i)
- reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
- (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate
- reply << ": watched by #{feed.watchers.join(', ')}"
- reply << "\n"
- }
- if reply.empty?
- reply = "no watched feeds"
- reply << " matching #{wanted}" if wanted
- end
- m.reply reply
- end
-
- def add_rss(m, params, force=false)
- handle = params[:handle]
- url = params[:url]
- unless url.match(/https?/)
- m.reply "I only deal with feeds from HTTP sources, so I can't use #{url} (maybe you forgot the handle?)"
- return
- end
- type = params[:type]
- if @feeds.fetch(handle.downcase, nil) && !force
- m.reply "There is already a feed named #{handle} (URL: #{@feeds[handle.downcase].url})"
- return
- end
- unless url
- m.reply "You must specify both a handle and an url to add an RSS feed"
- return
- end
- @feeds[handle.downcase] = RssBlob.new(url,handle,type)
- reply = "Added RSS #{url} named #{handle}"
- if type
- reply << " (format: #{type})"
- end
- m.reply reply
- return handle
- end
-
- def change_rss(m, params)
- handle = params[:handle].downcase
- feed = @feeds.fetch(handle, nil)
- unless feed
- m.reply "No such feed with handle #{handle}"
- return
- end
- case params[:what].intern
- when :handle
- new = params[:new].downcase
- if @feeds.key?(new) and @feeds[new]
- m.reply "There already is a feed with handle #{new}"
- return
- else
- feed.mutex.synchronize do
- @feeds[new] = feed
- @feeds.delete(handle)
- feed.handle = new
- end
- handle = new
- end
- when :url
- new = params[:new]
- feed.mutex.synchronize do
- feed.url = new
- end
- when :format, :type
- new = params[:new]
- new = nil if new == 'default'
- feed.mutex.synchronize do
- feed.type = new
- end
- when :refresh
- new = params[:new].to_i
- new = nil if new == 0
- feed.mutex.synchronize do
- feed.refresh_rate = new
- end
- else
- m.reply "Don't know how to change #{params[:what]} for feeds"
- return
- end
- m.reply "Feed changed:"
- list_rss(m, {:handle => handle})
- end
-
- def del_rss(m, params, pass=false)
- feed = unwatch_rss(m, params, true)
- return unless feed
- if feed.watched?
- m.reply "someone else is watching #{feed.handle}, I won't remove it from my list"
- return
- end
- @feeds.delete(feed.handle.downcase)
- m.okay unless pass
- return
- end
-
- def replace_rss(m, params)
- handle = params[:handle]
- if @feeds.key?(handle.downcase)
- del_rss(m, {:handle => handle}, true)
- end
- if @feeds.key?(handle.downcase)
- m.reply "can't replace #{feed.handle}"
- else
- add_rss(m, params, true)
- end
- end
-
- def forcereplace_rss(m, params)
- add_rss(m, params, true)
- end
-
- def watch_rss(m, params)
- handle = params[:handle]
- chan = params[:chan] || m.replyto
- url = params[:url]
- type = params[:type]
- if url
- add_rss(m, params)
- end
- feed = @feeds.fetch(handle.downcase, nil)
- if feed
- if feed.add_watch(chan)
- watchRss(feed, m)
- m.okay
- else
- m.reply "Already watching #{feed.handle} in #{chan}"
- end
- else
- m.reply "Couldn't watch feed #{handle} (no such feed found)"
- end
- end
-
- def unwatch_rss(m, params, pass=false)
- handle = params[:handle].downcase
- chan = params[:chan] || m.replyto
- unless @feeds.has_key?(handle)
- m.reply("dunno that feed")
- return
- end
- feed = @feeds[handle]
- if feed.rm_watch(chan)
- m.reply "#{chan} has been removed from the watchlist for #{feed.handle}"
- else
- m.reply("#{chan} wasn't watching #{feed.handle}") unless pass
- end
- if !feed.watched?
- stop_watch(handle)
- end
- return feed
- end
-
- def rewatch_rss(m=nil, params=nil)
- if params and handle = params[:handle]
- feed = @feeds.fetch(handle.downcase, nil)
- if feed
- @bot.timer.reschedule(@watch[feed.handle], 0)
- m.okay if m
- else
- m.reply _("no such feed %{handle}") % { :handle => handle } if m
- end
- else
- stop_watches
-
- # Read watches from list.
- watchlist.each{ |handle, feed|
- watchRss(feed, m)
- }
- m.okay if m
- end
- end
-
- private
- def watchRss(feed, m=nil)
- if @watch.has_key?(feed.handle)
- report_problem("watcher thread for #{feed.handle} is already running", nil, m)
- return
- end
- status = Hash.new
- status[:failures] = 0
- tmout = 0
- if feed.last_fetched
- tmout = feed.last_fetched + calculate_timeout(feed) - Time.now
- tmout = 0 if tmout < 0
- end
- debug "scheduling a watcher for #{feed} in #{tmout} seconds"
- @watch[feed.handle] = @bot.timer.add(tmout) {
- debug "watcher for #{feed} wakes up"
- failures = status[:failures]
- begin
- debug "fetching #{feed}"
- first_run = !feed.last_fetched
- oldxml = feed.xml ? feed.xml.dup : nil
- unless fetchRss(feed)
- failures += 1
- else
- if first_run
- debug "first run for #{feed}, getting items"
- parseRss(feed)
- elsif oldxml and oldxml == feed.xml
- debug "xml for #{feed} didn't change"
- failures -= 1 if failures > 0
- else
- if not feed.items
- debug "no previous items in feed #{feed}"
- parseRss(feed)
- failures -= 1 if failures > 0
- else
- # This one is used for debugging
- otxt = []
-
- # These are used for checking new items vs old ones
- uid_opts = { :show_updated => @bot.config['rss.show_updated'] }
- oids = Set.new feed.items.map { |item|
- uid = RSS.item_uid_for_bot(item, uid_opts)
- otxt << item.to_s
- debug [uid, item].inspect
- debug [uid, otxt.last].inspect
- uid
- }
-
- unless parseRss(feed)
- debug "no items in feed #{feed}"
- failures += 1
- else
- debug "Checking if new items are available for #{feed}"
- failures -= 1 if failures > 0
- # debug "Old:"
- # debug oldxml
- # debug "New:"
- # debug feed.xml
-
- dispItems = feed.items.reject { |item|
- uid = RSS.item_uid_for_bot(item, uid_opts)
- txt = item.to_s
- if oids.include?(uid)
- debug "rejecting old #{uid} #{item.inspect}"
- debug [uid, txt].inspect
- true
- else
- debug "accepting new #{uid} #{item.inspect}"
- debug [uid, txt].inspect
- warning "same text! #{txt}" if otxt.include?(txt)
- false
- end
- }
-
- if dispItems.length > 0
- debug "Found #{dispItems.length} new items in #{feed}"
- # When displaying watched feeds, publish them from older to newer
- dispItems.reverse.each { |item|
- printFormattedRss(feed, item)
- }
- else
- debug "No new items found in #{feed}"
- end
- end
- end
- end
- end
- rescue Exception => e
- error "Error watching #{feed}: #{e.inspect}"
- debug e.backtrace.join("\n")
- failures += 1
- end
-
- status[:failures] = failures
-
- seconds = calculate_timeout(feed, failures)
- debug "watcher for #{feed} going to sleep #{seconds} seconds.."
- begin
- @bot.timer.reschedule(@watch[feed.handle], seconds)
- rescue
- warning "watcher for #{feed} failed to reschedule: #{$!.inspect}"
- end
- }
- debug "watcher for #{feed} added"
- end
-
- def calculate_timeout(feed, failures = 0)
- seconds = @bot.config['rss.thread_sleep']
- feed.mutex.synchronize do
- seconds = feed.refresh_rate if feed.refresh_rate
- end
- seconds *= failures + 1
- seconds += seconds * (rand(100)-50)/100
- return seconds
- end
-
- def select_nonempty(*ar)
- debug ar
- ret = ar.map { |i| (i && i.empty?) ? nil : i }.compact.first
- (ret && ret.empty?) ? nil : ret
- end
-
- def printFormattedRss(feed, item, opts=nil)
- debug item
- places = feed.watchers
- handle = "::#{feed.handle}:: "
- date = String.new
- if opts
- places = opts[:places] if opts.key?(:places)
- handle = opts[:handle].to_s if opts.key?(:handle)
- if opts.key?(:date) && opts[:date]
- if item.respond_to?(:updated)
- if item.updated.content.class <= Time
- date = item.updated.content.strftime("%Y/%m/%d %H:%M")
- else
- date = item.updated.content.to_s
- end
- elsif item.respond_to?(:source) and item.source.respond_to?(:updated)
- if item.source.updated.content.class <= Time
- date = item.source.updated.content.strftime("%Y/%m/%d %H:%M")
- else
- date = item.source.updated.content.to_s
- end
- elsif item.respond_to?(:pubDate)
- if item.pubDate.class <= Time
- date = item.pubDate.strftime("%Y/%m/%d %H:%M")
- else
- date = item.pubDate.to_s
- end
- elsif item.respond_to?(:date)
- if item.date.class <= Time
- date = item.date.strftime("%Y/%m/%d %H:%M")
- else
- date = item.date.to_s
- end
- else
- date = "(no date)"
- end
- date += " :: "
- end
- end
-
- tit_opt = {}
- # Twitters don't need a cap on the title length since they have a hard
- # limit to 160 characters, and most of them are under 140 characters
- tit_opt[:limit] = @bot.config['rss.head_max'] unless feed.type == 'twitter'
-
- if item.title
- base_title = item.title.to_s.dup
- # git changesets are SHA1 hashes (40 hex digits), way too long, get rid of them, as they are
- # visible in the URL anyway
- # TODO make this optional?
- base_title.sub!(/^Changeset \[([\da-f]{40})\]:/) { |c| "(git commit)"} if feed.type == 'trac'
- title = "#{Bold}#{base_title.ircify_html(tit_opt)}#{Bold}"
- end
-
- desc_opt = {}
- desc_opt[:limit] = @bot.config['rss.text_max']
- desc_opt[:a_href] = :link_out if @bot.config['rss.show_links']
-
- # We prefer content_encoded here as it tends to provide more html formatting
- # for use with ircify_html.
- if item.respond_to?(:content_encoded) && item.content_encoded
- desc = item.content_encoded.ircify_html(desc_opt)
- elsif item.respond_to?(:description) && item.description
- desc = item.description.ircify_html(desc_opt)
- else
- if item.content.type == "html"
- desc = item.content.content.ircify_html(desc_opt)
- else
- desc = item.content.content
- if desc.size > desc_opt[:limit]
- desc = desc.slice(0, desc_opt[:limit]) + "#{Reverse}...#{Reverse}"
- end
- end
- end
-
- link = item.link.href rescue item.link.chomp rescue nil
-
- category = select_nonempty((item.category.content rescue nil), (item.dc_subject rescue nil))
- author = select_nonempty((item.author.name.content rescue nil), (item.dc_creator rescue nil), (item.author rescue nil))
-
- line1 = nil
- line2 = nil
-
- at = ((item.title && item.link) ? ' @ ' : '')
-
- case feed.type
- when 'blog'
- author += " " if author
- abt = category ? "about #{category} " : ""
- line1 = "#{handle}#{date}#{author}blogged #{abt}at #{link}"
- line2 = "#{handle}#{title} - #{desc}"
- when 'photoblog'
- author += " " if author
- abt = category ? "under #{category} " : ""
- line1 = "#{handle}#{date}#{author}added an image #{abt}at #{link}"
- line2 = "#{handle}#{title} - #{desc}"
- when 'news'
- line1 = "#{handle}#{date}#{title} @ #{link}"
- line2 = line2 = "#{handle}#{date}#{desc}"
- when 'git'
- author += " " if author
- line1 = "#{handle}#{date}#{author}commited #{title} @ #{link}"
- when 'forum'
- line1 = "#{handle}#{date}#{title}#{at}#{link}"
- when 'wiki'
- line1 = "#{handle}#{date}#{title}#{at}#{link} has been edited by #{author}. #{desc}"
- when 'gmane'
- line1 = "#{handle}#{date}Message #{title} sent by #{author}. #{desc}"
- when 'trac'
- line1 = "#{handle}#{date}#{title} @ #{link}"
- unless item.title =~ /^(?:Changeset \[(?:[\da-f]+)\]|\(git commit\))/
- line2 = "#{handle}#{date}#{desc}"
- end
- when '/.'
- dept = "(from the #{item.slash_department} dept) " rescue nil
- sec = " in section #{item.slash_section}" rescue nil
-
- line1 = "#{handle}#{date}#{dept}#{title}#{at}#{link} (posted by #{author}#{sec})"
- else
- line1 = "#{handle}#{date}#{title}#{at}#{link}"
- line1 << " (by #{author})" if author
- end
- places.each { |loc|
- @bot.say loc, line1, :overlong => :truncate
- next unless line2
- @bot.say loc, line2, :overlong => :truncate
- }
- end
-
- def fetchRss(feed, m=nil, cache=true)
- feed.last_fetched = Time.now
- begin
- # Use 60 sec timeout, cause the default is too low
- xml = @bot.httputil.get(feed.url,
- :read_timeout => 60,
- :open_timeout => 60,
- :cache => cache)
- rescue URI::InvalidURIError, URI::BadURIError => e
- report_problem("invalid rss feed #{feed.url}", e, m)
- return nil
- rescue => e
- report_problem("error getting #{feed.url}", e, m)
- return nil
- end
- debug "fetched #{feed}"
- unless xml
- report_problem("reading feed #{feed} failed", nil, m)
- return nil
- end
- # Ok, 0.9 feeds are not supported, maybe because
- # Netscape happily removed the DTD. So what we do is just to
- # reassign the 0.9 RDFs to 1.0, and hope it goes right.
- xml.gsub!("xmlns=\"http://my.netscape.com/rdf/simple/0.9/\"",
- "xmlns=\"http://purl.org/rss/1.0/\"")
- feed.mutex.synchronize do
- feed.xml = xml
- end
- return true
- end
-
- def parseRss(feed, m=nil)
- return nil unless feed.xml
- feed.mutex.synchronize do
- xml = feed.xml
- begin
- ## do validate parse
- rss = RSS::Parser.parse(xml)
- debug "parsed and validated #{feed}"
- rescue RSS::InvalidRSSError
- ## do non validate parse for invalid RSS 1.0
- begin
- rss = RSS::Parser.parse(xml, false)
- debug "parsed but not validated #{feed}"
- rescue RSS::Error => e
- report_problem("parsing rss stream failed, whoops =(", e, m)
- return nil
- end
- rescue RSS::Error => e
- report_problem("parsing rss stream failed, oioi", e, m)
- return nil
- rescue => e
- report_problem("processing error occured, sorry =(", e, m)
- return nil
- end
- items = []
- if rss.nil?
- report_problem("#{feed} does not include RSS 1.0 or 0.9x/2.0", nil, m)
- else
- begin
- rss.output_encoding = 'UTF-8'
- rescue RSS::UnknownConvertMethod => e
- report_problem("bah! something went wrong =(", e, m)
- return nil
- end
- if rss.respond_to? :channel
- rss.channel.title ||= "Unknown"
- title = rss.channel.title
- else
- title = rss.title.content
- end
- rss.items.each do |item|
- item.title ||= "Unknown"
- items << item
- end
- end
-
- if items.empty?
- report_problem("no items found in the feed, maybe try weed?", e, m)
- return nil
- end
- feed.title = title
- feed.items = items
- return true
- end
- end
-end
-
-plugin = RSSFeedsPlugin.new
-
-plugin.default_auth( 'edit', false )
-plugin.default_auth( 'edit:add', true)
-
-plugin.map 'rss show :handle :limit',
- :action => 'show_rss',
- :requirements => {:limit => /^\d+(?:\.\.\d+)?$/},
- :defaults => {:limit => 5}
-plugin.map 'rss list :handle',
- :action => 'list_rss',
- :defaults => {:handle => nil}
-plugin.map 'rss watched :handle [in :chan]',
- :action => 'watched_rss',
- :defaults => {:handle => nil}
-plugin.map 'rss who watches :handle',
- :action => 'who_watches',
- :defaults => {:handle => nil}
-plugin.map 'rss add :handle :url :type',
- :action => 'add_rss',
- :auth_path => 'edit',
- :defaults => {:type => nil}
-plugin.map 'rss change :what of :handle to :new',
- :action => 'change_rss',
- :auth_path => 'edit',
- :requirements => { :what => /handle|url|format|type|refresh/ }
-plugin.map 'rss change :what for :handle to :new',
- :action => 'change_rss',
- :auth_path => 'edit',
- :requirements => { :what => /handle|url|format|type|refesh/ }
-plugin.map 'rss del :handle',
- :auth_path => 'edit:rm!',
- :action => 'del_rss'
-plugin.map 'rss delete :handle',
- :auth_path => 'edit:rm!',
- :action => 'del_rss'
-plugin.map 'rss rm :handle',
- :auth_path => 'edit:rm!',
- :action => 'del_rss'
-plugin.map 'rss replace :handle :url :type',
- :auth_path => 'edit',
- :action => 'replace_rss',
- :defaults => {:type => nil}
-plugin.map 'rss forcereplace :handle :url :type',
- :auth_path => 'edit',
- :action => 'forcereplace_rss',
- :defaults => {:type => nil}
-plugin.map 'rss watch :handle [in :chan]',
- :action => 'watch_rss',
- :defaults => {:url => nil, :type => nil}
-plugin.map 'rss watch :handle :url :type [in :chan]',
- :action => 'watch_rss',
- :defaults => {:url => nil, :type => nil}
-plugin.map 'rss unwatch :handle [in :chan]',
- :action => 'unwatch_rss'
-plugin.map 'rss rmwatch :handle [in :chan]',
- :action => 'unwatch_rss'
-plugin.map 'rss rewatch [:handle]',
- :action => 'rewatch_rss'
+#-- vim:sw=2:et +#++ +# +# :title: RSS feed plugin for rbot +# +# Author:: Stanislav Karchebny <berkus@madfire.net> +# Author:: Ian Monroe <ian@monroe.nu> +# Author:: Mark Kretschmann <markey@web.de> +# Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com> +# +# Copyright:: (C) 2004 Stanislav Karchebny +# Copyright:: (C) 2005 Ian Monroe, Mark Kretschmann +# Copyright:: (C) 2006-2007 Giuseppe Bilotta +# +# License:: MIT license + +require 'rss' + +# Try to load rss/content/2.0 so we can access the data in <content:encoded> +# tags. +begin + require 'rss/content/2.0' +rescue LoadError +end + +module ::RSS + + # Make an 'unique' ID for a given item, based on appropriate bot options + # Currently only suppored is bot.config['rss.show_updated']: when true, the + # description is included in the uid hashing, otherwise it's not + # + def RSS.item_uid_for_bot(item, opts={}) + options = { :show_updated => true}.merge(opts) + desc = nil + if options[:show_updated] + desc = item.content.content rescue item.description rescue nil + end + [(item.title.content rescue item.title rescue nil), + (item.link.href rescue item.link), + desc].hash + end + + # Add support for Slashdot namespace in RDF. The code is just an adaptation + # of the DublinCore code. + unless defined?(SLASH_PREFIX) + SLASH_PREFIX = 'slash' + SLASH_URI = "http://purl.org/rss/1.0/modules/slash/" + + RDF.install_ns(SLASH_PREFIX, SLASH_URI) + + module BaseSlashModel + def append_features(klass) + super + + return if klass.instance_of?(Module) + SlashModel::ELEMENT_NAME_INFOS.each do |name, plural_name| + plural = plural_name || "#{name}s" + full_name = "#{SLASH_PREFIX}_#{name}" + full_plural_name = "#{SLASH_PREFIX}_#{plural}" + klass_name = "Slash#{Utils.to_class_name(name)}" + + # This will fail with older version of the Ruby RSS module + begin + klass.install_have_children_element(name, SLASH_URI, "*", + full_name, full_plural_name) + klass.install_must_call_validator(SLASH_PREFIX, SLASH_URI) + rescue ArgumentError + klass.module_eval("install_have_children_element(#{full_name.dump}, #{full_plural_name.dump})") + end + + klass.module_eval(<<-EOC, *get_file_and_line_from_caller(0)) + remove_method :#{full_name} if method_defined? :#{full_name} + remove_method :#{full_name}= if method_defined? :#{full_name}= + remove_method :set_#{full_name} if method_defined? :set_#{full_name} + + def #{full_name} + @#{full_name}.first and @#{full_name}.first.value + end + + def #{full_name}=(new_value) + @#{full_name}[0] = Utils.new_with_value_if_need(#{klass_name}, new_value) + end + alias set_#{full_name} #{full_name}= + EOC + end + end + end + + module SlashModel + extend BaseModel + extend BaseSlashModel + + TEXT_ELEMENTS = { + "department" => nil, + "section" => nil, + "comments" => nil, + "hit_parade" => nil + } + + ELEMENT_NAME_INFOS = SlashModel::TEXT_ELEMENTS.to_a + + ELEMENTS = TEXT_ELEMENTS.keys + + ELEMENTS.each do |name, plural_name| + module_eval(<<-EOC, *get_file_and_line_from_caller(0)) + class Slash#{Utils.to_class_name(name)} < Element + include RSS10 + + content_setup + + class << self + def required_prefix + SLASH_PREFIX + end + + def required_uri + SLASH_URI + end + end + + @tag_name = #{name.dump} + + alias_method(:value, :content) + alias_method(:value=, :content=) + + def initialize(*args) + begin + if Utils.element_initialize_arguments?(args) + super + else + super() + self.content = args[0] + end + # Older Ruby RSS module + rescue NoMethodError + super() + self.content = args[0] + end + end + + def full_name + tag_name_with_prefix(SLASH_PREFIX) + end + + def maker_target(target) + target.new_#{name} + end + + def setup_maker_attributes(#{name}) + #{name}.content = content + end + end + EOC + end + end + + class RDF + class Item; include SlashModel; end + end + + SlashModel::ELEMENTS.each do |name| + class_name = Utils.to_class_name(name) + BaseListener.install_class_name(SLASH_URI, name, "Slash#{class_name}") + end + + SlashModel::ELEMENTS.collect! {|name| "#{SLASH_PREFIX}_#{name}"} + end +end + + +class ::RssBlob + attr_accessor :url, :handle, :type, :refresh_rate, :xml, :title, :items, + :mutex, :watchers, :last_fetched + + def initialize(url,handle=nil,type=nil,watchers=[], xml=nil, lf = nil) + @url = url + if handle + @handle = handle + else + @handle = url + end + @type = type + @watchers=[] + @refresh_rate = nil + @xml = xml + @title = nil + @items = nil + @mutex = Mutex.new + @last_fetched = lf + sanitize_watchers(watchers) + end + + def dup + @mutex.synchronize do + self.class.new(@url, + @handle, + @type ? @type.dup : nil, + @watchers.dup, + @xml ? @xml.dup : nil, + @last_fetched) + end + end + + # Downcase all watchers, possibly turning them into Strings if they weren't + def sanitize_watchers(list=@watchers) + ls = list.dup + @watchers.clear + ls.each { |w| + add_watch(w) + } + end + + def watched? + !@watchers.empty? + end + + def watched_by?(who) + @watchers.include?(who.downcase) + end + + def add_watch(who) + if watched_by?(who) + return nil + end + @mutex.synchronize do + @watchers << who.downcase + end + return who + end + + def rm_watch(who) + @mutex.synchronize do + @watchers.delete(who.downcase) + end + end + + def to_a + [@handle,@url,@type,@refresh_rate,@watchers] + end + + def to_s(watchers=false) + if watchers + a = self.to_a.flatten + else + a = self.to_a[0,3] + end + a.compact.join(" | ") + end +end + +class RSSFeedsPlugin < Plugin + Config.register Config::IntegerValue.new('rss.head_max', + :default => 100, :validate => Proc.new{|v| v > 0 && v < 200}, + :desc => "How many characters to use of a RSS item header") + + Config.register Config::IntegerValue.new('rss.text_max', + :default => 200, :validate => Proc.new{|v| v > 0 && v < 400}, + :desc => "How many characters to use of a RSS item text") + + Config.register Config::IntegerValue.new('rss.thread_sleep', + :default => 300, :validate => Proc.new{|v| v > 30}, + :desc => "How many seconds to sleep before checking RSS feeds again") + + Config.register Config::BooleanValue.new('rss.show_updated', + :default => true, + :desc => "Whether feed items for which the description was changed should be shown as new") + + Config.register Config::BooleanValue.new('rss.show_links', + :default => true, + :desc => "Whether to display links from the text of a feed item.") + + # We used to save the Mutex with the RssBlob, which was idiotic. And + # since Mutexes dumped in one version might not be resotrable in another, + # we need a few tricks to be able to restore data from other versions of Ruby + # + # When migrating 1.8.6 => 1.8.5, all we need to do is define an empty + # #marshal_load() method for Mutex. For 1.8.5 => 1.8.6 we need something + # dirtier, as seen later on in the initialization code. + unless Mutex.new.respond_to?(:marshal_load) + class ::Mutex + def marshal_load(str) + return + end + end + end + + attr_reader :feeds + + def initialize + super + if @registry.has_key?(:feeds) + # When migrating from Ruby 1.8.5 to 1.8.6, dumped Mutexes may render the + # data unrestorable. If this happens, we patch the data, thus allowing + # the restore to work. + # + # This is actually pretty safe for a number of reasons: + # * the code is only called if standard marshalling fails + # * the string we look for is quite unlikely to appear randomly + # * if the string appears somewhere and the patched string isn't recoverable + # either, we'll get another (unrecoverable) error, which makes the rss + # plugin unsable, just like it was if no recovery was attempted + # * if the string appears somewhere and the patched string is recoverable, + # we may get a b0rked feed, which is eventually overwritten by a clean + # one, so the worst thing that can happen is that a feed update spams + # the watchers once + @registry.recovery = Proc.new { |val| + patched = val.sub(":\v@mutexo:\nMutex", ":\v@mutexo:\vObject") + ret = Marshal.restore(patched) + ret.each_value { |blob| + blob.mutex = nil + blob + } + } + + @feeds = @registry[:feeds] + raise unless @feeds + + @registry.recovery = nil + + @feeds.keys.grep(/[A-Z]/) { |k| + @feeds[k.downcase] = @feeds[k] + @feeds.delete(k) + } + @feeds.each { |k, f| + f.mutex = Mutex.new + f.sanitize_watchers + parseRss(f) if f.xml + } + else + @feeds = Hash.new + end + @watch = Hash.new + rewatch_rss + end + + def name + "rss" + end + + def watchlist + @feeds.select { |h, f| f.watched? } + end + + def cleanup + stop_watches + super + end + + def save + unparsed = Hash.new() + @feeds.each { |k, f| + unparsed[k] = f.dup + # we don't want to save the mutex + unparsed[k].mutex = nil + } + @registry[:feeds] = unparsed + end + + def stop_watch(handle) + if @watch.has_key?(handle) + begin + debug "Stopping watch #{handle}" + @bot.timer.remove(@watch[handle]) + @watch.delete(handle) + rescue Exception => e + report_problem("Failed to stop watch for #{handle}", e, nil) + end + end + end + + def stop_watches + @watch.each_key { |k| + stop_watch(k) + } + end + + def help(plugin,topic="") + case topic + when "show" + "rss show #{Bold}handle#{Bold} [#{Bold}limit#{Bold}] : show #{Bold}limit#{Bold} (default: 5, max: 15) entries from rss #{Bold}handle#{Bold}; #{Bold}limit#{Bold} can also be in the form a..b, to display a specific range of items" + when "list" + "rss list [#{Bold}handle#{Bold}] : list all rss feeds (matching #{Bold}handle#{Bold})" + when "watched" + "rss watched [#{Bold}handle#{Bold}] [in #{Bold}chan#{Bold}]: list all watched rss feeds (matching #{Bold}handle#{Bold}) (in channel #{Bold}chan#{Bold})" + when "who", "watches", "who watches" + "rss who watches [#{Bold}handle#{Bold}]]: list all watchers for rss feeds (matching #{Bold}handle#{Bold})" + when "add" + "rss add #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : add a new rss called #{Bold}handle#{Bold} from url #{Bold}url#{Bold} (of type #{Bold}type#{Bold})" + when "change" + "rss change #{Bold}what#{Bold} of #{Bold}handle#{Bold} to #{Bold}new#{Bold} : change the #{Underline}handle#{Underline}, #{Underline}url#{Underline}, #{Underline}type#{Underline} or #{Underline}refresh#{Underline} rate of rss called #{Bold}handle#{Bold} to value #{Bold}new#{Bold}" + when /^(del(ete)?|rm)$/ + "rss del(ete)|rm #{Bold}handle#{Bold} : delete rss feed #{Bold}handle#{Bold}" + when "replace" + "rss replace #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : try to replace the url of rss called #{Bold}handle#{Bold} with #{Bold}url#{Bold} (of type #{Bold}type#{Bold}); only works if nobody else is watching it" + when "forcereplace" + "rss forcereplace #{Bold}handle#{Bold} #{Bold}url#{Bold} [#{Bold}type#{Bold}] : replace the url of rss called #{Bold}handle#{Bold} with #{Bold}url#{Bold} (of type #{Bold}type#{Bold})" + when "watch" + "rss watch #{Bold}handle#{Bold} [#{Bold}url#{Bold} [#{Bold}type#{Bold}]] [in #{Bold}chan#{Bold}]: watch rss #{Bold}handle#{Bold} for changes (in channel #{Bold}chan#{Bold}); when the other parameters are present, the feed will be created if it doesn't exist yet" + when /(un|rm)watch/ + "rss unwatch|rmwatch #{Bold}handle#{Bold} [in #{Bold}chan#{Bold}]: stop watching rss #{Bold}handle#{Bold} (in channel #{Bold}chan#{Bold}) for changes" + when /who(?: watche?s?)?/ + "rss who watches #{Bold}handle#{Bold}: lists watches for rss #{Bold}handle#{Bold}" + when "rewatch" + "rss rewatch : restart threads that watch for changes in watched rss" + else + "manage RSS feeds: rss show|list|watched|add|change|del(ete)|rm|(force)replace|watch|unwatch|rmwatch|rewatch|who watches" + end + end + + def report_problem(report, e=nil, m=nil) + if m && m.respond_to?(:reply) + m.reply report + else + warning report + end + if e + debug e.inspect + debug e.backtrace.join("\n") if e.respond_to?(:backtrace) + end + end + + def show_rss(m, params) + handle = params[:handle] + lims = params[:limit].to_s.match(/(\d+)(?:..(\d+))?/) + debug lims.to_a.inspect + if lims[2] + ll = [[lims[1].to_i-1,lims[2].to_i-1].min, 0].max + ul = [[lims[1].to_i-1,lims[2].to_i-1].max, 14].min + rev = lims[1].to_i > lims[2].to_i + else + ll = 0 + ul = [[lims[1].to_i-1, 0].max, 14].min + rev = false + end + + feed = @feeds.fetch(handle.downcase, nil) + unless feed + m.reply "I don't know any feeds named #{handle}" + return + end + + m.reply "lemme fetch it..." + title = items = nil + we_were_watching = false + + if @watch.key?(feed.handle) + # If a feed is being watched, we run the watcher thread + # so that all watchers can be informed of changes to + # the feed. Before we do that, though, we remove the + # show requester from the watchlist, if present, lest + # he gets the update twice. + if feed.watched_by?(m.replyto) + we_were_watching = true + feed.rm_watch(m.replyto) + end + @bot.timer.reschedule(@watch[feed.handle], 0) + if we_were_watching + feed.add_watch(m.replyto) + end + else + fetched = fetchRss(feed, m, false) + end + return unless fetched or feed.xml + if not fetched and feed.items + m.reply "using old data" + else + parsed = parseRss(feed, m) + m.reply "using old data" unless parsed + end + return unless feed.items + title = feed.title + items = feed.items + + # We sort the feeds in freshness order (newer ones first) + items = freshness_sort(items) + disp = items[ll..ul] + disp.reverse! if rev + + m.reply "Channel : #{title}" + disp.each do |item| + printFormattedRss(feed, item, {:places=>[m.replyto],:handle=>nil,:date=>true}) + end + end + + def itemDate(item,ex=nil) + return item.pubDate if item.respond_to?(:pubDate) and item.pubDate + return item.date if item.respond_to?(:date) and item.date + return ex + end + + def freshness_sort(items) + notime = Time.at(0) + items.sort { |a, b| + itemDate(b, notime) <=> itemDate(a, notime) + } + end + + def list_rss(m, params) + wanted = params[:handle] + reply = String.new + @feeds.each { |handle, feed| + next if wanted and !handle.match(/#{wanted}/i) + reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})" + (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate + (reply << " (watched)") if feed.watched_by?(m.replyto) + reply << "\n" + } + if reply.empty? + reply = "no feeds found" + reply << " matching #{wanted}" if wanted + end + m.reply reply, :max_lines => reply.length + end + + def watched_rss(m, params) + wanted = params[:handle] + chan = params[:chan] || m.replyto + reply = String.new + watchlist.each { |handle, feed| + next if wanted and !handle.match(/#{wanted}/i) + next unless feed.watched_by?(chan) + reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})" + (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate + reply << "\n" + } + if reply.empty? + reply = "no watched feeds" + reply << " matching #{wanted}" if wanted + end + m.reply reply + end + + def who_watches(m, params) + wanted = params[:handle] + reply = String.new + watchlist.each { |handle, feed| + next if wanted and !handle.match(/#{wanted}/i) + reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})" + (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate + reply << ": watched by #{feed.watchers.join(', ')}" + reply << "\n" + } + if reply.empty? + reply = "no watched feeds" + reply << " matching #{wanted}" if wanted + end + m.reply reply + end + + def add_rss(m, params, force=false) + handle = params[:handle] + url = params[:url] + unless url.match(/https?/) + m.reply "I only deal with feeds from HTTP sources, so I can't use #{url} (maybe you forgot the handle?)" + return + end + type = params[:type] + if @feeds.fetch(handle.downcase, nil) && !force + m.reply "There is already a feed named #{handle} (URL: #{@feeds[handle.downcase].url})" + return + end + unless url + m.reply "You must specify both a handle and an url to add an RSS feed" + return + end + @feeds[handle.downcase] = RssBlob.new(url,handle,type) + reply = "Added RSS #{url} named #{handle}" + if type + reply << " (format: #{type})" + end + m.reply reply + return handle + end + + def change_rss(m, params) + handle = params[:handle].downcase + feed = @feeds.fetch(handle, nil) + unless feed + m.reply "No such feed with handle #{handle}" + return + end + case params[:what].intern + when :handle + new = params[:new].downcase + if @feeds.key?(new) and @feeds[new] + m.reply "There already is a feed with handle #{new}" + return + else + feed.mutex.synchronize do + @feeds[new] = feed + @feeds.delete(handle) + feed.handle = new + end + handle = new + end + when :url + new = params[:new] + feed.mutex.synchronize do + feed.url = new + end + when :format, :type + new = params[:new] + new = nil if new == 'default' + feed.mutex.synchronize do + feed.type = new + end + when :refresh + new = params[:new].to_i + new = nil if new == 0 + feed.mutex.synchronize do + feed.refresh_rate = new + end + else + m.reply "Don't know how to change #{params[:what]} for feeds" + return + end + m.reply "Feed changed:" + list_rss(m, {:handle => handle}) + end + + def del_rss(m, params, pass=false) + feed = unwatch_rss(m, params, true) + return unless feed + if feed.watched? + m.reply "someone else is watching #{feed.handle}, I won't remove it from my list" + return + end + @feeds.delete(feed.handle.downcase) + m.okay unless pass + return + end + + def replace_rss(m, params) + handle = params[:handle] + if @feeds.key?(handle.downcase) + del_rss(m, {:handle => handle}, true) + end + if @feeds.key?(handle.downcase) + m.reply "can't replace #{feed.handle}" + else + add_rss(m, params, true) + end + end + + def forcereplace_rss(m, params) + add_rss(m, params, true) + end + + def watch_rss(m, params) + handle = params[:handle] + chan = params[:chan] || m.replyto + url = params[:url] + type = params[:type] + if url + add_rss(m, params) + end + feed = @feeds.fetch(handle.downcase, nil) + if feed + if feed.add_watch(chan) + watchRss(feed, m) + m.okay + else + m.reply "Already watching #{feed.handle} in #{chan}" + end + else + m.reply "Couldn't watch feed #{handle} (no such feed found)" + end + end + + def unwatch_rss(m, params, pass=false) + handle = params[:handle].downcase + chan = params[:chan] || m.replyto + unless @feeds.has_key?(handle) + m.reply("dunno that feed") + return + end + feed = @feeds[handle] + if feed.rm_watch(chan) + m.reply "#{chan} has been removed from the watchlist for #{feed.handle}" + else + m.reply("#{chan} wasn't watching #{feed.handle}") unless pass + end + if !feed.watched? + stop_watch(handle) + end + return feed + end + + def rewatch_rss(m=nil, params=nil) + if params and handle = params[:handle] + feed = @feeds.fetch(handle.downcase, nil) + if feed + @bot.timer.reschedule(@watch[feed.handle], 0) + m.okay if m + else + m.reply _("no such feed %{handle}") % { :handle => handle } if m + end + else + stop_watches + + # Read watches from list. + watchlist.each{ |handle, feed| + watchRss(feed, m) + } + m.okay if m + end + end + + private + def watchRss(feed, m=nil) + if @watch.has_key?(feed.handle) + report_problem("watcher thread for #{feed.handle} is already running", nil, m) + return + end + status = Hash.new + status[:failures] = 0 + tmout = 0 + if feed.last_fetched + tmout = feed.last_fetched + calculate_timeout(feed) - Time.now + tmout = 0 if tmout < 0 + end + debug "scheduling a watcher for #{feed} in #{tmout} seconds" + @watch[feed.handle] = @bot.timer.add(tmout) { + debug "watcher for #{feed} wakes up" + failures = status[:failures] + begin + debug "fetching #{feed}" + first_run = !feed.last_fetched + oldxml = feed.xml ? feed.xml.dup : nil + unless fetchRss(feed) + failures += 1 + else + if first_run + debug "first run for #{feed}, getting items" + parseRss(feed) + elsif oldxml and oldxml == feed.xml + debug "xml for #{feed} didn't change" + failures -= 1 if failures > 0 + else + if not feed.items + debug "no previous items in feed #{feed}" + parseRss(feed) + failures -= 1 if failures > 0 + else + # This one is used for debugging + otxt = [] + + # These are used for checking new items vs old ones + uid_opts = { :show_updated => @bot.config['rss.show_updated'] } + oids = Set.new feed.items.map { |item| + uid = RSS.item_uid_for_bot(item, uid_opts) + otxt << item.to_s + debug [uid, item].inspect + debug [uid, otxt.last].inspect + uid + } + + unless parseRss(feed) + debug "no items in feed #{feed}" + failures += 1 + else + debug "Checking if new items are available for #{feed}" + failures -= 1 if failures > 0 + # debug "Old:" + # debug oldxml + # debug "New:" + # debug feed.xml + + dispItems = feed.items.reject { |item| + uid = RSS.item_uid_for_bot(item, uid_opts) + txt = item.to_s + if oids.include?(uid) + debug "rejecting old #{uid} #{item.inspect}" + debug [uid, txt].inspect + true + else + debug "accepting new #{uid} #{item.inspect}" + debug [uid, txt].inspect + warning "same text! #{txt}" if otxt.include?(txt) + false + end + } + + if dispItems.length > 0 + debug "Found #{dispItems.length} new items in #{feed}" + # When displaying watched feeds, publish them from older to newer + dispItems.reverse.each { |item| + printFormattedRss(feed, item) + } + else + debug "No new items found in #{feed}" + end + end + end + end + end + rescue Exception => e + error "Error watching #{feed}: #{e.inspect}" + debug e.backtrace.join("\n") + failures += 1 + end + + status[:failures] = failures + + seconds = calculate_timeout(feed, failures) + debug "watcher for #{feed} going to sleep #{seconds} seconds.." + begin + @bot.timer.reschedule(@watch[feed.handle], seconds) + rescue + warning "watcher for #{feed} failed to reschedule: #{$!.inspect}" + end + } + debug "watcher for #{feed} added" + end + + def calculate_timeout(feed, failures = 0) + seconds = @bot.config['rss.thread_sleep'] + feed.mutex.synchronize do + seconds = feed.refresh_rate if feed.refresh_rate + end + seconds *= failures + 1 + seconds += seconds * (rand(100)-50)/100 + return seconds + end + + def select_nonempty(*ar) + debug ar + ret = ar.map { |i| (i && i.empty?) ? nil : i }.compact.first + (ret && ret.empty?) ? nil : ret + end + + def printFormattedRss(feed, item, opts=nil) + debug item + places = feed.watchers + handle = "::#{feed.handle}:: " + date = String.new + if opts + places = opts[:places] if opts.key?(:places) + handle = opts[:handle].to_s if opts.key?(:handle) + if opts.key?(:date) && opts[:date] + if item.respond_to?(:updated) + if item.updated.content.class <= Time + date = item.updated.content.strftime("%Y/%m/%d %H:%M") + else + date = item.updated.content.to_s + end + elsif item.respond_to?(:source) and item.source.respond_to?(:updated) + if item.source.updated.content.class <= Time + date = item.source.updated.content.strftime("%Y/%m/%d %H:%M") + else + date = item.source.updated.content.to_s + end + elsif item.respond_to?(:pubDate) + if item.pubDate.class <= Time + date = item.pubDate.strftime("%Y/%m/%d %H:%M") + else + date = item.pubDate.to_s + end + elsif item.respond_to?(:date) + if item.date.class <= Time + date = item.date.strftime("%Y/%m/%d %H:%M") + else + date = item.date.to_s + end + else + date = "(no date)" + end + date += " :: " + end + end + + tit_opt = {} + # Twitters don't need a cap on the title length since they have a hard + # limit to 160 characters, and most of them are under 140 characters + tit_opt[:limit] = @bot.config['rss.head_max'] unless feed.type == 'twitter' + + if item.title + base_title = item.title.to_s.dup + # git changesets are SHA1 hashes (40 hex digits), way too long, get rid of them, as they are + # visible in the URL anyway + # TODO make this optional? + base_title.sub!(/^Changeset \[([\da-f]{40})\]:/) { |c| "(git commit)"} if feed.type == 'trac' + title = "#{Bold}#{base_title.ircify_html(tit_opt)}#{Bold}" + end + + desc_opt = {} + desc_opt[:limit] = @bot.config['rss.text_max'] + desc_opt[:a_href] = :link_out if @bot.config['rss.show_links'] + + # We prefer content_encoded here as it tends to provide more html formatting + # for use with ircify_html. + if item.respond_to?(:content_encoded) && item.content_encoded + desc = item.content_encoded.ircify_html(desc_opt) + elsif item.respond_to?(:description) && item.description + desc = item.description.ircify_html(desc_opt) + else + if item.content.type == "html" + desc = item.content.content.ircify_html(desc_opt) + else + desc = item.content.content + if desc.size > desc_opt[:limit] + desc = desc.slice(0, desc_opt[:limit]) + "#{Reverse}...#{Reverse}" + end + end + end + + link = item.link.href rescue item.link.chomp rescue nil + + category = select_nonempty((item.category.content rescue nil), (item.dc_subject rescue nil)) + author = select_nonempty((item.author.name.content rescue nil), (item.dc_creator rescue nil), (item.author rescue nil)) + + line1 = nil + line2 = nil + + at = ((item.title && item.link) ? ' @ ' : '') + + case feed.type + when 'blog' + author += " " if author + abt = category ? "about #{category} " : "" + line1 = "#{handle}#{date}#{author}blogged #{abt}at #{link}" + line2 = "#{handle}#{title} - #{desc}" + when 'photoblog' + author += " " if author + abt = category ? "under #{category} " : "" + line1 = "#{handle}#{date}#{author}added an image #{abt}at #{link}" + line2 = "#{handle}#{title} - #{desc}" + when 'news' + line1 = "#{handle}#{date}#{title} @ #{link}" + line2 = line2 = "#{handle}#{date}#{desc}" + when 'git' + author += " " if author + line1 = "#{handle}#{date}#{author}commited #{title} @ #{link}" + when 'forum' + line1 = "#{handle}#{date}#{title}#{at}#{link}" + when 'wiki' + line1 = "#{handle}#{date}#{title}#{at}#{link} has been edited by #{author}. #{desc}" + when 'gmane' + line1 = "#{handle}#{date}Message #{title} sent by #{author}. #{desc}" + when 'trac' + line1 = "#{handle}#{date}#{title} @ #{link}" + unless item.title =~ /^(?:Changeset \[(?:[\da-f]+)\]|\(git commit\))/ + line2 = "#{handle}#{date}#{desc}" + end + when '/.' + dept = "(from the #{item.slash_department} dept) " rescue nil + sec = " in section #{item.slash_section}" rescue nil + + line1 = "#{handle}#{date}#{dept}#{title}#{at}#{link} (posted by #{author}#{sec})" + else + line1 = "#{handle}#{date}#{title}#{at}#{link}" + line1 << " (by #{author})" if author + end + places.each { |loc| + @bot.say loc, line1, :overlong => :truncate + next unless line2 + @bot.say loc, line2, :overlong => :truncate + } + end + + def fetchRss(feed, m=nil, cache=true) + feed.last_fetched = Time.now + begin + # Use 60 sec timeout, cause the default is too low + xml = @bot.httputil.get(feed.url, + :read_timeout => 60, + :open_timeout => 60, + :cache => cache) + rescue URI::InvalidURIError, URI::BadURIError => e + report_problem("invalid rss feed #{feed.url}", e, m) + return nil + rescue => e + report_problem("error getting #{feed.url}", e, m) + return nil + end + debug "fetched #{feed}" + unless xml + report_problem("reading feed #{feed} failed", nil, m) + return nil + end + # Ok, 0.9 feeds are not supported, maybe because + # Netscape happily removed the DTD. So what we do is just to + # reassign the 0.9 RDFs to 1.0, and hope it goes right. + xml.gsub!("xmlns=\"http://my.netscape.com/rdf/simple/0.9/\"", + "xmlns=\"http://purl.org/rss/1.0/\"") + feed.mutex.synchronize do + feed.xml = xml + end + return true + end + + def parseRss(feed, m=nil) + return nil unless feed.xml + feed.mutex.synchronize do + xml = feed.xml + begin + ## do validate parse + rss = RSS::Parser.parse(xml) + debug "parsed and validated #{feed}" + rescue RSS::InvalidRSSError + ## do non validate parse for invalid RSS 1.0 + begin + rss = RSS::Parser.parse(xml, false) + debug "parsed but not validated #{feed}" + rescue RSS::Error => e + report_problem("parsing rss stream failed, whoops =(", e, m) + return nil + end + rescue RSS::Error => e + report_problem("parsing rss stream failed, oioi", e, m) + return nil + rescue => e + report_problem("processing error occured, sorry =(", e, m) + return nil + end + items = [] + if rss.nil? + report_problem("#{feed} does not include RSS 1.0 or 0.9x/2.0", nil, m) + else + begin + rss.output_encoding = 'UTF-8' + rescue RSS::UnknownConvertMethod => e + report_problem("bah! something went wrong =(", e, m) + return nil + end + if rss.respond_to? :channel + rss.channel.title ||= "Unknown" + title = rss.channel.title + else + title = rss.title.content + end + rss.items.each do |item| + item.title ||= "Unknown" + items << item + end + end + + if items.empty? + report_problem("no items found in the feed, maybe try weed?", e, m) + return nil + end + feed.title = title + feed.items = items + return true + end + end +end + +plugin = RSSFeedsPlugin.new + +plugin.default_auth( 'edit', false ) +plugin.default_auth( 'edit:add', true) + +plugin.map 'rss show :handle :limit', + :action => 'show_rss', + :requirements => {:limit => /^\d+(?:\.\.\d+)?$/}, + :defaults => {:limit => 5} +plugin.map 'rss list :handle', + :action => 'list_rss', + :defaults => {:handle => nil} +plugin.map 'rss watched :handle [in :chan]', + :action => 'watched_rss', + :defaults => {:handle => nil} +plugin.map 'rss who watches :handle', + :action => 'who_watches', + :defaults => {:handle => nil} +plugin.map 'rss add :handle :url :type', + :action => 'add_rss', + :auth_path => 'edit', + :defaults => {:type => nil} +plugin.map 'rss change :what of :handle to :new', + :action => 'change_rss', + :auth_path => 'edit', + :requirements => { :what => /handle|url|format|type|refresh/ } +plugin.map 'rss change :what for :handle to :new', + :action => 'change_rss', + :auth_path => 'edit', + :requirements => { :what => /handle|url|format|type|refesh/ } +plugin.map 'rss del :handle', + :auth_path => 'edit:rm!', + :action => 'del_rss' +plugin.map 'rss delete :handle', + :auth_path => 'edit:rm!', + :action => 'del_rss' +plugin.map 'rss rm :handle', + :auth_path => 'edit:rm!', + :action => 'del_rss' +plugin.map 'rss replace :handle :url :type', + :auth_path => 'edit', + :action => 'replace_rss', + :defaults => {:type => nil} +plugin.map 'rss forcereplace :handle :url :type', + :auth_path => 'edit', + :action => 'forcereplace_rss', + :defaults => {:type => nil} +plugin.map 'rss watch :handle [in :chan]', + :action => 'watch_rss', + :defaults => {:url => nil, :type => nil} +plugin.map 'rss watch :handle :url :type [in :chan]', + :action => 'watch_rss', + :defaults => {:url => nil, :type => nil} +plugin.map 'rss unwatch :handle [in :chan]', + :action => 'unwatch_rss' +plugin.map 'rss rmwatch :handle [in :chan]', + :action => 'unwatch_rss' +plugin.map 'rss rewatch [:handle]', + :action => 'rewatch_rss' diff --git a/data/rbot/plugins/salut.rb b/data/rbot/plugins/salut.rb index abcb1744..569bade4 100644 --- a/data/rbot/plugins/salut.rb +++ b/data/rbot/plugins/salut.rb @@ -1,226 +1,226 @@ -#-- vim:sw=2:et
-#++
-#
-# :title: Salutations plugin for rbot
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006-2007 Giuseppe Bilotta
-# License:: GPL v2
-#
-# Salutations plugin: respond to salutations
-#
-# TODO:: allow online editing of salutations
-#
-# TODO:: *REMEMBER* to set @changed to true after edit or changes won't be saved
-
-class SalutPlugin < Plugin
- Config.register Config::BooleanValue.new('salut.all_languages',
- :default => true,
- :desc => "Check for a salutation in all languages and not just in the one defined by core.language",
- :on_change => Proc.new {|bot, v| bot.plugins['salut'].reload}
- )
- Config.register Config::BooleanValue.new('salut.address_only',
- :default => true,
- :desc => "When set to true, the bot will only reply to salutations directed at him",
- :on_change => Proc.new {|bot, v| bot.plugins['salut'].reload}
- )
-
-
- def initialize
- @salutations = Hash.new
- @match = Hash.new
- @match_langs = Array.new
- @main_lang_str = nil
- @main_lang = nil
- @all_langs = true
- @changed = false
- super
- reload
- end
-
- def set_language(what)
- reload
- end
-
- def create_match
- @match.clear
- ar_dest = Array.new
- ar_in = Array.new
- ar_out = Array.new
- ar_both = Array.new
- @salutations.each { |lang, hash|
- ar_dest.clear
- ar_in.clear
- ar_out.clear
- ar_both.clear
- hash.each { |situation, array|
- case situation.to_s
- when /^generic-dest$/
- ar_dest += array
- when /in$/
- ar_in += array
- when /out$/
- ar_out += array
- else
- ar_both += array
- end
- }
- @match[lang] = Hash.new
- @match[lang][:in] = Regexp.new("\\b(?:" + ar_in.uniq.map { |txt|
- Regexp.escape(txt)
- }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_in.empty?
- @match[lang][:out] = Regexp.new("\\b(?:" + ar_out.uniq.map { |txt|
- Regexp.escape(txt)
- }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_out.empty?
- @match[lang][:both] = Regexp.new("\\b(?:" + ar_both.uniq.map { |txt|
- Regexp.escape(txt)
- }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_both.empty?
- @match[lang][:dest] = Regexp.new("\\b(?:" + ar_dest.uniq.map { |txt|
- Regexp.escape(txt)
- }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_dest.empty?
- }
- @punct = /\s*[.,:!;?]?\s*/ # Punctuation
-
- # Languages to match for, in order
- @match_langs.clear
- @match_langs << @main_lang if @match.key?(@main_lang)
- @match_langs << :english if @match.key?(:english)
- @match.each_key { |key|
- @match_langs << key
- }
- @match_langs.uniq!
- end
-
- def unreplied(m)
- return if @match.empty?
- return unless m.kind_of?(PrivMessage)
- return if m.address? and m.plugin == 'config'
- to_me = m.address? || m.message =~ /#{Regexp.escape(@bot.nick)}/i
- if @bot.config['salut.address_only']
- return unless to_me
- end
- salut = nil
- @match_langs.each { |lang|
- [:both, :in, :out].each { |k|
- next unless @match[lang][k]
- if m.message =~ @match[lang][k]
- salut = [@match[lang][k], lang, k]
- break
- end
- }
- break if salut
- }
- return unless salut
- # If the bot wasn't addressed, we continue only if the match was exact
- # (apart from space and punctuation) or if @match[:dest] matches too
- return unless to_me or m.message =~ /^#{@punct}#{salut.first}#{@punct}$/ or m.message =~ @match[salut[1]][:dest]
- h = Time.new.hour
- case h
- when 4...12
- salut_reply(m, salut, :morning)
- when 12...18
- salut_reply(m, salut, :afternoon)
- else
- salut_reply(m, salut, :evening)
- end
- end
-
- def salut_reply(m, salut, time)
- lang = salut[1]
- k = salut[2]
- debug "Replying to #{salut.first} (#{lang} #{k}) in the #{time}"
- # salut_ar = @salutations[@main_lang].update @salutations[:english].update @salutations[lang]
- salut_ar = @salutations[lang]
- case k
- when :both
- sfx = ""
- else
- sfx = "-#{k}"
- end
- debug "Building array ..."
- rep_ar = Array.new
- rep_ar += salut_ar.fetch("#{time}#{sfx}".to_sym, [])
- rep_ar += salut_ar.fetch("#{time}".to_sym, []) unless sfx.empty?
- rep_ar += salut_ar.fetch("generic#{sfx}".to_sym, [])
- rep_ar += salut_ar.fetch("generic".to_sym, []) unless sfx.empty?
- debug "Choosing reply in #{rep_ar.inspect} ..."
- if rep_ar.empty?
- if m.public? # and (m.address? or m =~ /#{Regexp.escape(@bot.nick)}/)
- choice = @bot.lang.get("hello_X") % m.sourcenick
- else
- choice = @bot.lang.get("hello") % m.sourcenick
- end
- else
- choice = rep_ar.pick_one
- if m.public? and (m.address? or m.message =~ /#{Regexp.escape(@bot.nick)}/)
- choice += "#{[',',''].pick_one} #{m.sourcenick}"
- choice += [" :)", " :D", "!", "", "", ""].pick_one
- end
- end
- debug "Replying #{choice}"
- m.plainreply choice
- end
-
- def reload
- save
- @main_lang_str = @bot.config['core.language']
- @main_lang = @main_lang_str.to_sym
- @all_langs = @bot.config['salut.all_languages']
- if @all_langs
- # Get all available languages
- langs = Dir.new("#{@bot.botclass}/salut").collect {|f|
- f =~ /salut-([^.]+)/ ? $1 : nil
- }.compact
- langs.each { |lang|
- @salutations[lang.to_sym] = load_lang(lang)
- }
- else
- @salutations.clear
- @salutations[@main_lang] = load_lang(@main_lang_str)
- end
- create_match
- @changed = false
- end
-
- def load_lang(lang)
- dir = "#{@bot.botclass}/salut"
- if not File.exist?(dir)
- Dir.mkdir(dir)
- end
- file = "#{@bot.botclass}/salut/salut-#{lang}"
- if File.exist?(file)
- begin
- salutations = Hash.new
- content = YAML::load_file(file)
- content.each { |key, val|
- salutations[key.to_sym] = val
- }
- return salutations
- rescue
- error "failed to read salutations in #{lang}: #{$!}"
- end
- end
- return nil
- end
-
- def save
- return if @salutations.empty?
- return unless @changed
- @salutations.each { |lang, val|
- l = lang.to_s
- save_lang(lang, val)
- }
- @changed = false
- end
-
- def save_lang(lang, val)
- fn = "#{@bot.botclass}/salut/salut-#{lang}"
- Utils.safe_save(fn) { |file|
- file.puts val.to_yaml
- }
- end
-
-end
-
-plugin = SalutPlugin.new
-
+#-- vim:sw=2:et +#++ +# +# :title: Salutations plugin for rbot +# +# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com> +# Copyright:: (C) 2006-2007 Giuseppe Bilotta +# License:: GPL v2 +# +# Salutations plugin: respond to salutations +# +# TODO:: allow online editing of salutations +# +# TODO:: *REMEMBER* to set @changed to true after edit or changes won't be saved + +class SalutPlugin < Plugin + Config.register Config::BooleanValue.new('salut.all_languages', + :default => true, + :desc => "Check for a salutation in all languages and not just in the one defined by core.language", + :on_change => Proc.new {|bot, v| bot.plugins['salut'].reload} + ) + Config.register Config::BooleanValue.new('salut.address_only', + :default => true, + :desc => "When set to true, the bot will only reply to salutations directed at him", + :on_change => Proc.new {|bot, v| bot.plugins['salut'].reload} + ) + + + def initialize + @salutations = Hash.new + @match = Hash.new + @match_langs = Array.new + @main_lang_str = nil + @main_lang = nil + @all_langs = true + @changed = false + super + reload + end + + def set_language(what) + reload + end + + def create_match + @match.clear + ar_dest = Array.new + ar_in = Array.new + ar_out = Array.new + ar_both = Array.new + @salutations.each { |lang, hash| + ar_dest.clear + ar_in.clear + ar_out.clear + ar_both.clear + hash.each { |situation, array| + case situation.to_s + when /^generic-dest$/ + ar_dest += array + when /in$/ + ar_in += array + when /out$/ + ar_out += array + else + ar_both += array + end + } + @match[lang] = Hash.new + @match[lang][:in] = Regexp.new("\\b(?:" + ar_in.uniq.map { |txt| + Regexp.escape(txt) + }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_in.empty? + @match[lang][:out] = Regexp.new("\\b(?:" + ar_out.uniq.map { |txt| + Regexp.escape(txt) + }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_out.empty? + @match[lang][:both] = Regexp.new("\\b(?:" + ar_both.uniq.map { |txt| + Regexp.escape(txt) + }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_both.empty? + @match[lang][:dest] = Regexp.new("\\b(?:" + ar_dest.uniq.map { |txt| + Regexp.escape(txt) + }.join('|') + ")\\b", Regexp::IGNORECASE) unless ar_dest.empty? + } + @punct = /\s*[.,:!;?]?\s*/ # Punctuation + + # Languages to match for, in order + @match_langs.clear + @match_langs << @main_lang if @match.key?(@main_lang) + @match_langs << :english if @match.key?(:english) + @match.each_key { |key| + @match_langs << key + } + @match_langs.uniq! + end + + def unreplied(m) + return if @match.empty? + return unless m.kind_of?(PrivMessage) + return if m.address? and m.plugin == 'config' + to_me = m.address? || m.message =~ /#{Regexp.escape(@bot.nick)}/i + if @bot.config['salut.address_only'] + return unless to_me + end + salut = nil + @match_langs.each { |lang| + [:both, :in, :out].each { |k| + next unless @match[lang][k] + if m.message =~ @match[lang][k] + salut = [@match[lang][k], lang, k] + break + end + } + break if salut + } + return unless salut + # If the bot wasn't addressed, we continue only if the match was exact + # (apart from space and punctuation) or if @match[:dest] matches too + return unless to_me or m.message =~ /^#{@punct}#{salut.first}#{@punct}$/ or m.message =~ @match[salut[1]][:dest] + h = Time.new.hour + case h + when 4...12 + salut_reply(m, salut, :morning) + when 12...18 + salut_reply(m, salut, :afternoon) + else + salut_reply(m, salut, :evening) + end + end + + def salut_reply(m, salut, time) + lang = salut[1] + k = salut[2] + debug "Replying to #{salut.first} (#{lang} #{k}) in the #{time}" + # salut_ar = @salutations[@main_lang].update @salutations[:english].update @salutations[lang] + salut_ar = @salutations[lang] + case k + when :both + sfx = "" + else + sfx = "-#{k}" + end + debug "Building array ..." + rep_ar = Array.new + rep_ar += salut_ar.fetch("#{time}#{sfx}".to_sym, []) + rep_ar += salut_ar.fetch("#{time}".to_sym, []) unless sfx.empty? + rep_ar += salut_ar.fetch("generic#{sfx}".to_sym, []) + rep_ar += salut_ar.fetch("generic".to_sym, []) unless sfx.empty? + debug "Choosing reply in #{rep_ar.inspect} ..." + if rep_ar.empty? + if m.public? # and (m.address? or m =~ /#{Regexp.escape(@bot.nick)}/) + choice = @bot.lang.get("hello_X") % m.sourcenick + else + choice = @bot.lang.get("hello") % m.sourcenick + end + else + choice = rep_ar.pick_one + if m.public? and (m.address? or m.message =~ /#{Regexp.escape(@bot.nick)}/) + choice += "#{[',',''].pick_one} #{m.sourcenick}" + choice += [" :)", " :D", "!", "", "", ""].pick_one + end + end + debug "Replying #{choice}" + m.plainreply choice + end + + def reload + save + @main_lang_str = @bot.config['core.language'] + @main_lang = @main_lang_str.to_sym + @all_langs = @bot.config['salut.all_languages'] + if @all_langs + # Get all available languages + langs = Dir.new("#{@bot.botclass}/salut").collect {|f| + f =~ /salut-([^.]+)/ ? $1 : nil + }.compact + langs.each { |lang| + @salutations[lang.to_sym] = load_lang(lang) + } + else + @salutations.clear + @salutations[@main_lang] = load_lang(@main_lang_str) + end + create_match + @changed = false + end + + def load_lang(lang) + dir = "#{@bot.botclass}/salut" + if not File.exist?(dir) + Dir.mkdir(dir) + end + file = "#{@bot.botclass}/salut/salut-#{lang}" + if File.exist?(file) + begin + salutations = Hash.new + content = YAML::load_file(file) + content.each { |key, val| + salutations[key.to_sym] = val + } + return salutations + rescue + error "failed to read salutations in #{lang}: #{$!}" + end + end + return nil + end + + def save + return if @salutations.empty? + return unless @changed + @salutations.each { |lang, val| + l = lang.to_s + save_lang(lang, val) + } + @changed = false + end + + def save_lang(lang, val) + fn = "#{@bot.botclass}/salut/salut-#{lang}" + Utils.safe_save(fn) { |file| + file.puts val.to_yaml + } + end + +end + +plugin = SalutPlugin.new + |