diff options
Diffstat (limited to 'rbot')
-rw-r--r-- | rbot/auth.rb | 199 | ||||
-rw-r--r-- | rbot/channel.rb | 54 | ||||
-rw-r--r-- | rbot/config.rb | 205 | ||||
-rw-r--r-- | rbot/dbhash.rb | 133 | ||||
-rw-r--r-- | rbot/httputil.rb | 88 | ||||
-rw-r--r-- | rbot/ircbot.rb | 750 | ||||
-rw-r--r-- | rbot/ircsocket.rb | 186 | ||||
-rw-r--r-- | rbot/keywords.rb | 427 | ||||
-rw-r--r-- | rbot/language.rb | 55 | ||||
-rw-r--r-- | rbot/languages/dutch.lang | 73 | ||||
-rw-r--r-- | rbot/languages/english.lang | 75 | ||||
-rw-r--r-- | rbot/languages/german.lang | 67 | ||||
-rw-r--r-- | rbot/message.rb | 256 | ||||
-rw-r--r-- | rbot/messagemapper.rb | 168 | ||||
-rw-r--r-- | rbot/plugins.rb | 300 | ||||
-rw-r--r-- | rbot/registry.rb | 292 | ||||
-rw-r--r-- | rbot/rfc2812.rb | 1027 | ||||
-rw-r--r-- | rbot/timer.rb | 123 | ||||
-rw-r--r-- | rbot/utils.rb | 778 |
19 files changed, 0 insertions, 5256 deletions
diff --git a/rbot/auth.rb b/rbot/auth.rb deleted file mode 100644 index 7811d9e4..00000000 --- a/rbot/auth.rb +++ /dev/null @@ -1,199 +0,0 @@ -module Irc - - # globmask:: glob to test with - # netmask:: netmask to test against - # Compare a netmask with a standard IRC glob, e.g foo!bar@baz.com would - # match *!*@baz.com, foo!*@*, *!bar@*, etc. - def Irc.netmaskmatch(globmask, netmask) - regmask = globmask.gsub(/\*/, ".*?") - return true if(netmask =~ /#{regmask}/) - return false - end - - # check if a string is an actual IRC hostmask - def Irc.ismask(mask) - mask =~ /^.+!.+@.+$/ - end - - - # User-level authentication to allow/disallow access to bot commands based - # on hostmask and userlevel. - class IrcAuth - # create a new IrcAuth instance. - # bot:: associated bot class - def initialize(bot) - @bot = bot - @users = Hash.new(0) - @levels = Hash.new(0) - if(File.exist?("#{@bot.botclass}/users.rbot")) - IO.foreach("#{@bot.botclass}/users.rbot") do |line| - if(line =~ /\s*(\d+)\s*(\S+)/) - level = $1.to_i - mask = $2 - @users[mask] = level - end - end - end - if(File.exist?("#{@bot.botclass}/levels.rbot")) - IO.foreach("#{@bot.botclass}/levels.rbot") do |line| - if(line =~ /\s*(\d+)\s*(\S+)/) - level = $1.to_i - command = $2 - @levels[command] = level - end - end - end - end - - # save current users and levels to files. - # levels are written to #{botclass}/levels.rbot - # users are written to #{botclass}/users.rbot - def save - Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}")) - File.open("#{@bot.botclass}/users.rbot", "w") do |file| - @users.each do |key, value| - file.puts "#{value} #{key}" - end - end - File.open("#{@bot.botclass}/levels.rbot", "w") do |file| - @levels.each do |key, value| - file.puts "#{value} #{key}" - end - end - end - - # command:: command user wishes to perform - # mask:: hostmask of user - # tell:: optional recipient for "insufficient auth" message - # - # returns true if user with hostmask +mask+ is permitted to perform - # +command+ optionally pass tell as the target for the "insufficient auth" - # message, if the user is not authorised - def allow?(command, mask, tell=nil) - auth = userlevel(mask) - if(auth >= @levels[command]) - return true - else - debug "#{mask} is not allowed to perform #{command}" - @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell - return false - end - end - - # add user with hostmask matching +mask+ with initial auth level +level+ - def useradd(mask, level) - if(Irc.ismask(mask)) - @users[mask] = level - end - end - - # mask:: mask of user to remove - # remove user with mask +mask+ - def userdel(mask) - if(Irc.ismask(mask)) - @users.delete(mask) - end - end - - # command:: command to adjust - # level:: new auth level for the command - # set required auth level of +command+ to +level+ - def setlevel(command, level) - @levels[command] = level - end - - # specific users. - # mask:: mask of user - # returns the authlevel of user with mask +mask+ - # finds the matching user which has the highest authlevel (so you can have - # a default level of 5 for *!*@*, and yet still give higher levels to - def userlevel(mask) - # go through hostmask list, find match with _highest_ level (all users - # will match *!*@*) - level = 0 - @users.each {|user,userlevel| - if(Irc.netmaskmatch(user, mask)) - level = userlevel if userlevel > level - end - } - level - end - - # return all currently defined commands (for which auth is required) and - # their required authlevels - def showlevels - reply = "Current levels are:" - @levels.sort.each {|a| - key = a[0] - value = a[1] - reply += " #{key}(#{value})" - } - reply - end - - # return all currently defined users and their authlevels - def showusers - reply = "Current users are:" - @users.sort.each {|a| - key = a[0] - value = a[1] - reply += " #{key}(#{value})" - } - reply - end - - # module help - def help(topic="") - case topic - when "setlevel" - return "setlevel <command> <level> => Sets required level for <command> to <level> (private addressing only)" - when "useradd" - return "useradd <mask> <level> => Add user <mask> at level <level> (private addressing only)" - when "userdel" - return "userdel <mask> => Remove user <mask> (private addressing only)" - when "auth" - return "auth <masterpw> => Recognise your hostmask as bot master (private addressing only)" - when "levels" - return "levels => list commands and their required levels (private addressing only)" - when "users" - return "users => list users and their levels (private addressing only)" - else - return "Auth module (User authentication) topics: setlevel, useradd, userdel, auth, levels, users" - end - end - - # privmsg handler - def privmsg(m) - if(m.address? && m.private?) - case m.message - when (/^setlevel\s+(\S+)\s+(\d+)$/) - if(@bot.auth.allow?("auth", m.source, m.replyto)) - @bot.auth.setlevel($1, $2.to_i) - m.reply "level for #$1 set to #$2" - end - when (/^useradd\s+(\S+)\s+(\d+)/) - if(@bot.auth.allow?("auth", m.source, m.replyto)) - @bot.auth.useradd($1, $2.to_i) - m.reply "added user #$1 at level #$2" - end - when (/^userdel\s+(\S+)/) - if(@bot.auth.allow?("auth", m.source, m.replyto)) - @bot.auth.userdel($1) - m.reply "user #$1 is gone" - end - when (/^auth\s+(\S+)/) - if($1 == @bot.config["auth.password"]) - @bot.auth.useradd(Regexp.escape(m.source), 1000) - m.reply "Identified, security level maxed out" - else - m.reply "incorrect password" - end - when ("levels") - m.reply @bot.auth.showlevels if(@bot.auth.allow?("config", m.source, m.replyto)) - when ("users") - m.reply @bot.auth.showusers if(@bot.auth.allow?("config", m.source, m.replyto)) - end - end - end - end -end diff --git a/rbot/channel.rb b/rbot/channel.rb deleted file mode 100644 index edd206bf..00000000 --- a/rbot/channel.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Irc - - # class to store IRC channel data (users, topic, per-channel configurations) - class IRCChannel - # name of channel - attr_reader :name - - # current channel topic - attr_reader :topic - - # hash containing users currently in the channel - attr_accessor :users - - # if true, bot won't talk in this channel - attr_accessor :quiet - - # name:: channel name - # create a new IRCChannel - def initialize(name) - @name = name - @users = Hash.new - @quiet = false - @topic = Topic.new - end - - # eg @bot.channels[chan].topic = topic - def topic=(name) - @topic.name = name - end - - # class to store IRC channel topic information - class Topic - # topic name - attr_accessor :name - - # timestamp - attr_accessor :timestamp - - # topic set by - attr_accessor :by - - def initialize - @name = "" - end - - # when called like "puts @bots.channels[chan].topic" - def to_s - @name - end - end - - end - -end diff --git a/rbot/config.rb b/rbot/config.rb deleted file mode 100644 index 971a413c..00000000 --- a/rbot/config.rb +++ /dev/null @@ -1,205 +0,0 @@ -module Irc - - require 'yaml' - - # container for bot configuration - class BotConfig - - # currently we store values in a hash but this could be changed in the - # future. We use hash semantics, however. - def method_missing(method, *args, &block) - return @config.send(method, *args, &block) - end - - # bot:: parent bot class - # create a new config hash from #{botclass}/conf.rbot - def initialize(bot) - @bot = bot - # some defaults - @config = Hash.new(false) - - @config['server.name'] = "localhost" - @config['server.port'] = 6667 - @config['server.password'] = false - @config['server.bindhost'] = false - @config['irc.nick'] = "rbot" - @config['irc.user'] = "rbot" - @config['irc.join_channels'] = "" - @config['core.language'] = "english" - @config['core.save_every'] = 60 - @config['keyword.listen'] = false - @config['auth.password'] = "" - @config['server.sendq_delay'] = 2.0 - @config['server.sendq_burst'] = 4 - @config['keyword.address'] = true - @config['keyword.listen'] = false - - # TODO - # have this class persist key/values in hash using yaml as it kinda - # already does. - # have other users of the class describe config to it on init, like: - # @config.add(:key => 'server.name', :type => 'string', - # :default => 'localhost', :restart => true, - # :help => 'irc server to connect to') - # that way the config module doesn't have to know about all the other - # classes but can still provide help and defaults. - # Classes don't have to add keys, they can just use config as a - # persistent hash, but then they won't be presented by the config - # module for runtime display/changes. - # (:restart, if true, makes the bot reply to changes with "this change - # will take effect after the next restart) - # :proc => Proc.new {|newvalue| ...} - # (:proc, proc to run on change of setting) - # or maybe, @config.add_key(...) do |newvalue| .... end - # :validate => /regex/ - # (operates on received string before conversion) - # Special handling for arrays so the config module can be used to - # add/remove elements as well as changing the whole thing - # Allow config options to list possible valid values (if type is enum, - # for example). Then things like the language module can list the - # available languages for choosing. - - if(File.exist?("#{@bot.botclass}/conf.yaml")) - newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml") - @config.update(newconfig) - else - # first-run wizard! - wiz = BotConfigWizard.new(@bot) - newconfig = wiz.run(@config) - @config.update(newconfig) - end - end - - # write current configuration to #{botclass}/conf.rbot - def save - Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}")) - File.open("#{@bot.botclass}/conf.yaml", "w") do |file| - file.puts @config.to_yaml - end - end - end - - # I don't see a nice way to avoid the first start wizard knowing way too - # much about other modules etc, because it runs early and stuff it - # configures is used to initialise the other modules... - # To minimise this we'll do as little as possible and leave the rest to - # online modification - class BotConfigWizard - - # TODO things to configure.. - # config directory (botclass) - people don't realise they should set - # this. The default... isn't good. - # users? - default *!*@* to 10 - # levels? - need a way to specify a default level, methinks, for - # unconfigured items. - # - def initialize(bot) - @bot = bot - @questions = [ - { - :question => "What server should the bot connect to?", - :prompt => "Hostname", - :key => "server.name", - :type => :string, - }, - { - :question => "What port should the bot connect to?", - :prompt => "Port", - :key => "server.port", - :type => :number, - }, - { - :question => "Does this IRC server require a password for access? Leave blank if not.", - :prompt => "Password", - :key => "server.password", - :type => :password, - }, - { - :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.", - :prompt => "Local bind", - :key => "server.bindhost", - :type => :string, - }, - { - :question => "What IRC nickname should the bot attempt to use?", - :prompt => "Nick", - :key => "irc.nick", - :type => :string, - }, - { - :question => "What local user should the bot appear to be?", - :prompt => "User", - :key => "irc.user", - :type => :string, - }, - { - :question => "What channels should the bot always join at startup? List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'", - :prompt => "Channels", - :key => "irc.join_channels", - :type => :string, - }, - { - :question => "Which language file should the bot use?", - :prompt => "Language", - :key => "core.language", - :type => :enum, - :items => Dir.new(File.dirname(__FILE__) + "/languages/").collect {|f| - f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil - }.compact - }, - { - :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)", - :prompt => "Password", - :key => "auth.password", - :type => :password, - }, - ] - end - - def run(defaults) - config = defaults.clone - puts "First time rbot configuration wizard" - puts "====================================" - puts "This is the first time you have run rbot with a config directory of:" - puts @bot.botclass - puts "This wizard will ask you a few questions to get you started." - puts "The rest of rbot's configuration can be manipulated via IRC once" - puts "rbot is connected and you are auth'd." - puts "-----------------------------------" - - @questions.each do |q| - puts q[:question] - begin - key = q[:key] - if q[:type] == :enum - puts "valid values are: " + q[:items].join(", ") - end - if (defaults.has_key?(key)) - print q[:prompt] + " [#{defaults[key]}]: " - else - print q[:prompt] + " []: " - end - response = STDIN.gets - response.chop! - response = defaults[key] if response == "" && defaults.has_key?(key) - case q[:type] - when :string - when :number - raise "value '#{response}' is not a number" unless (response.class == Fixnum || response =~ /^\d+$/) - response = response.to_i - when :password - when :enum - raise "selected value '#{response}' is not one of the valid values" unless q[:items].include?(response) - end - config[key] = response - puts "configured #{key} => #{config[key]}" - puts "-----------------------------------" - rescue RuntimeError => e - puts e.message - retry - end - end - return config - end - end -end diff --git a/rbot/dbhash.rb b/rbot/dbhash.rb deleted file mode 100644 index 5ae2ba87..00000000 --- a/rbot/dbhash.rb +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2002 Tom Gilbert. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies of the Software and its documentation and acknowledgment shall be -# given in the documentation and software packages that this Software was -# used. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -begin - require 'bdb' -rescue Exception => e - puts "Got exception: "+e - puts "rbot couldn't load the bdb module, perhaps you need to install it? try: http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb" - exit 2 -end - -# make BTree lookups case insensitive -module BDB - class CIBtree < Btree - def bdb_bt_compare(a, b) - a.downcase <=> b.downcase - end - end -end - -module Irc - - # DBHash is for tying a hash to disk (using bdb). - # Call it with an identifier, for example "mydata". It'll look for - # mydata.db, if it exists, it will load and reference that db. - # Otherwise it'll create and empty db called mydata.db - class DBHash - - # absfilename:: use +key+ as an actual filename, don't prepend the bot's - # config path and don't append ".db" - def initialize(bot, key, absfilename=false) - @bot = bot - @key = key - if absfilename && File.exist?(key) - # db already exists, use it - @db = DBHash.open_db(key) - elsif File.exist?(@bot.botclass + "/#{key}.db") - # db already exists, use it - @db = DBHash.open_db(@bot.botclass + "/#{key}.db") - elsif absfilename - # create empty db - @db = DBHash.create_db(key) - else - # create empty db - @db = DBHash.create_db(@bot.botclass + "/#{key}.db") - end - end - - def method_missing(method, *args, &block) - return @db.send(method, *args, &block) - end - - def DBHash.create_db(name) - debug "DBHash: creating empty db #{name}" - return BDB::Hash.open(name, nil, - BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, - 0600, "set_pagesize" => 1024, - "set_cachesize" => [(0), (32 * 1024), (0)]) - end - - def DBHash.open_db(name) - debug "DBHash: opening existing db #{name}" - return BDB::Hash.open(name, nil, - "r+", 0600, "set_pagesize" => 1024, - "set_cachesize" => [(0), (32 * 1024), (0)]) - end - - end - - - # DBTree is a BTree equivalent of DBHash, with case insensitive lookups. - class DBTree - - # absfilename:: use +key+ as an actual filename, don't prepend the bot's - # config path and don't append ".db" - def initialize(bot, key, absfilename=false) - @bot = bot - @key = key - if absfilename && File.exist?(key) - # db already exists, use it - @db = DBTree.open_db(key) - elsif absfilename - # create empty db - @db = DBTree.create_db(key) - elsif File.exist?(@bot.botclass + "/#{key}.db") - # db already exists, use it - @db = DBTree.open_db(@bot.botclass + "/#{key}.db") - else - # create empty db - @db = DBTree.create_db(@bot.botclass + "/#{key}.db") - end - end - - def method_missing(method, *args, &block) - return @db.send(method, *args, &block) - end - - def DBTree.create_db(name) - debug "DBTree: creating empty db #{name}" - return BDB::CIBtree.open(name, nil, - BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, - 0600, "set_pagesize" => 1024, - "set_cachesize" => [(0), (32 * 1024), (0)]) - end - - def DBTree.open_db(name) - debug "DBTree: opening existing db #{name}" - return BDB::CIBtree.open(name, nil, - "r+", 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0]) - end - - end - -end diff --git a/rbot/httputil.rb b/rbot/httputil.rb deleted file mode 100644 index ff3216a6..00000000 --- a/rbot/httputil.rb +++ /dev/null @@ -1,88 +0,0 @@ -module Irc - -require 'net/http' -Net::HTTP.version_1_2 - -# class for making http requests easier (mainly for plugins to use) -# this class can check the bot proxy configuration to determine if a proxy -# needs to be used, which includes support for per-url proxy configuration. -class HttpUtil - def initialize(bot) - @bot = bot - @headers = { - 'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)", - } - end - - # uri:: Uri to create a proxy for - # - # return a net/http Proxy object, which is configured correctly for - # proxying based on the bot's proxy configuration. - # This will include per-url proxy configuration based on the bot config - # +http_proxy_include/exclude+ options. - def get_proxy(uri) - proxy = nil - if (ENV['http_proxy']) - proxy = URI.parse ENV['http_proxy'] - end - if (@bot.config["http_proxy"]) - proxy = URI.parse ENV['http_proxy'] - end - - # if http_proxy_include or http_proxy_exclude are set, then examine the - # uri to see if this is a proxied uri - if uri - if @bot.config["http_proxy_exclude"] - # TODO - end - if @bot.config["http_proxy_include"] - end - end - - proxy_host = nil - proxy_port = nil - proxy_user = nil - proxy_pass = nil - if @bot.config["http_proxy_user"] - proxy_user = @bot.config["http_proxy_user"] - if @bot.config["http_proxy_pass"] - proxy_pass = @bot.config["http_proxy_pass"] - end - end - if proxy - proxy_host = proxy.host - proxy_port = proxy.port - end - - return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port) - end - - # uri:: uri to query (Uri object) - # readtimeout:: timeout for reading the response - # opentimeout:: timeout for opening the connection - # - # simple get request, returns response body if the status code is 200 and - # the request doesn't timeout. - def get(uri, readtimeout=10, opentimeout=5) - proxy = get_proxy(uri) - proxy.open_timeout = opentimeout - proxy.read_timeout = readtimeout - - begin - proxy.start() {|http| - resp = http.get(uri.request_uri(), @headers) - if resp.code == "200" - return resp.body - else - puts "HttpUtil.get return code #{resp.code} #{resp.body}" - end - return nil - } - rescue StandardError, Timeout::Error => e - $stderr.puts "HttpUtil.get exception: #{e}, while trying to get #{uri}" - end - return nil - end -end - -end diff --git a/rbot/ircbot.rb b/rbot/ircbot.rb deleted file mode 100644 index 844231dd..00000000 --- a/rbot/ircbot.rb +++ /dev/null @@ -1,750 +0,0 @@ -# Copyright (C) 2002 Tom Gilbert. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies of the Software and its documentation and acknowledgment shall be -# given in the documentation and software packages that this Software was -# used. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -require 'thread' - -require 'rbot/rfc2812' -require 'rbot/keywords' -require 'rbot/config' -require 'rbot/ircsocket' -require 'rbot/auth' -require 'rbot/timer' -require 'rbot/plugins' -require 'rbot/channel' -require 'rbot/utils' -require 'rbot/message' -require 'rbot/language' -require 'rbot/dbhash' -require 'rbot/registry' -require 'rbot/httputil' - -module Irc - -# Main bot class, which receives messages, handles them or passes them to -# plugins, and stores runtime data -class IrcBot - # the bot's current nickname - attr_reader :nick - - # the bot's IrcAuth data - attr_reader :auth - - # the bot's BotConfig data - attr_reader :config - - # the botclass for this bot (determines configdir among other things) - attr_reader :botclass - - # used to perform actions periodically (saves configuration once per minute - # by default) - attr_reader :timer - - # bot's Language data - attr_reader :lang - - # bot's configured addressing prefixes - attr_reader :addressing_prefixes - - # channel info for channels the bot is in - attr_reader :channels - - # bot's object registry, plugins get an interface to this for persistant - # storage (hash interface tied to a bdb file, plugins use Accessors to store - # and restore objects in their own namespaces.) - attr_reader :registry - - # bot's httputil help object, for fetching resources via http. Sets up - # proxies etc as defined by the bot configuration/environment - attr_reader :httputil - - # create a new IrcBot with botclass +botclass+ - def initialize(botclass) - @botclass = botclass.gsub(/\/$/, "") - @startup_time = Time.new - - Dir.mkdir("#{botclass}") if(!File.exist?("#{botclass}")) - Dir.mkdir("#{botclass}/logs") if(!File.exist?("#{botclass}/logs")) - - @config = Irc::BotConfig.new(self) - @timer = Timer::Timer.new - @registry = BotRegistry.new self - @timer.add(@config['core.save_every']) { save } if @config['core.save_every'] - @channels = Hash.new - @logs = Hash.new - - @httputil = Irc::HttpUtil.new(self) - @lang = Irc::Language.new(@config['core.language']) - @keywords = Irc::Keywords.new(self) - @auth = Irc::IrcAuth.new(self) - @plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"]) - - @socket = Irc::IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst']) - @nick = @config['irc.nick'] - if @config['core.address_prefix'] - @addressing_prefixes = @config['core.address_prefix'].split(" ") - else - @addressing_prefixes = Array.new - end - - @client = Irc::IrcClient.new - @client["PRIVMSG"] = proc { |data| - message = PrivMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"]) - onprivmsg(message) - } - @client["NOTICE"] = proc { |data| - message = NoticeMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"]) - # pass it off to plugins that want to hear everything - @plugins.delegate "listen", message - } - @client["MOTD"] = proc { |data| - data['MOTD'].each_line { |line| - log "MOTD: #{line}", "server" - } - } - @client["NICKTAKEN"] = proc { |data| - nickchg "#{@nick}_" - } - @client["BADNICK"] = proc {|data| - puts "WARNING, bad nick (#{data['NICK']})" - } - @client["PING"] = proc {|data| - # (jump the queue for pongs) - @socket.puts "PONG #{data['PINGID']}" - } - @client["NICK"] = proc {|data| - sourcenick = data["SOURCENICK"] - nick = data["NICK"] - m = NickMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["NICK"]) - if(sourcenick == @nick) - @nick = nick - end - @channels.each {|k,v| - if(v.users.has_key?(sourcenick)) - log "@ #{sourcenick} is now known as #{nick}", k - v.users[nick] = v.users[sourcenick] - v.users.delete(sourcenick) - end - } - @plugins.delegate("listen", m) - @plugins.delegate("nick", m) - } - @client["QUIT"] = proc {|data| - source = data["SOURCE"] - sourcenick = data["SOURCENICK"] - sourceurl = data["SOURCEADDRESS"] - message = data["MESSAGE"] - m = QuitMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["MESSAGE"]) - if(data["SOURCENICK"] =~ /#{@nick}/i) - else - @channels.each {|k,v| - if(v.users.has_key?(sourcenick)) - log "@ Quit: #{sourcenick}: #{message}", k - v.users.delete(sourcenick) - end - } - end - @plugins.delegate("listen", m) - @plugins.delegate("quit", m) - } - @client["MODE"] = proc {|data| - source = data["SOURCE"] - sourcenick = data["SOURCENICK"] - sourceurl = data["SOURCEADDRESS"] - channel = data["CHANNEL"] - targets = data["TARGETS"] - modestring = data["MODESTRING"] - log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel - } - @client["WELCOME"] = proc {|data| - log "joined server #{data['SOURCE']} as #{data['NICK']}", "server" - debug "I think my nick is #{@nick}, server thinks #{data['NICK']}" - if data['NICK'] && data['NICK'].length > 0 - @nick = data['NICK'] - end - if(@config['irc.quser']) - puts "authing with Q using #{@config['quakenet.user']} #{@config['quakenet.auth']}" - @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config['quakenet.user']} #{@config['quakenet.auth']}" - end - - if(@config['irc.join_channels']) - @config['irc.join_channels'].split(", ").each {|c| - puts "autojoining channel #{c}" - if(c =~ /^(\S+)\s+(\S+)$/i) - join $1, $2 - else - join c if(c) - end - } - end - } - @client["JOIN"] = proc {|data| - m = JoinMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"]) - onjoin(m) - } - @client["PART"] = proc {|data| - m = PartMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"]) - onpart(m) - } - @client["KICK"] = proc {|data| - m = KickMessage.new(self, data["SOURCE"], data["TARGET"],data["CHANNEL"],data["MESSAGE"]) - onkick(m) - } - @client["INVITE"] = proc {|data| - if(data["TARGET"] =~ /^#{@nick}$/i) - join data["CHANNEL"] if (@auth.allow?("join", data["SOURCE"], data["SOURCENICK"])) - end - } - @client["CHANGETOPIC"] = proc {|data| - channel = data["CHANNEL"] - sourcenick = data["SOURCENICK"] - topic = data["TOPIC"] - timestamp = data["UNIXTIME"] || Time.now.to_i - if(sourcenick == @nick) - log "@ I set topic \"#{topic}\"", channel - else - log "@ #{sourcenick} set topic \"#{topic}\"", channel - end - m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"]) - - ontopic(m) - @plugins.delegate("listen", m) - @plugins.delegate("topic", m) - } - @client["TOPIC"] = @client["TOPICINFO"] = proc {|data| - channel = data["CHANNEL"] - m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], data["UNIXTIME"], data["TOPIC"]) - ontopic(m) - } - @client["NAMES"] = proc {|data| - channel = data["CHANNEL"] - users = data["USERS"] - unless(@channels[channel]) - puts "bug: got names for channel '#{channel}' I didn't think I was in\n" - exit 2 - end - @channels[channel].users.clear - users.each {|u| - @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]] - } - } - @client["UNKNOWN"] = proc {|data| - debug "UNKNOWN: #{data['SERVERSTRING']}" - } - end - - # connect the bot to IRC - def connect - trap("SIGTERM") { quit } - trap("SIGHUP") { quit } - trap("SIGINT") { quit } - begin - @socket.connect - rescue => e - raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e - end - @socket.puts "PASS " + @config['server.password'] if @config['server.password'] - @socket.puts "NICK #{@nick}\nUSER #{@config['server.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert" - end - - # begin event handling loop - def mainloop - socket_timeout = 0.2 - reconnect_wait = 5 - - while true - connect - - begin - while true - if @socket.select socket_timeout - break unless reply = @socket.gets - @client.process reply - end - @timer.tick - end - rescue => e - puts "connection closed: #{e}" - puts e.backtrace.join("\n") - end - - puts "disconnected" - @channels.clear - @socket.clearq - - puts "waiting to reconnect" - sleep reconnect_wait - end - end - - # type:: message type - # where:: message target - # message:: message text - # send message +message+ of type +type+ to target +where+ - # Type can be PRIVMSG, NOTICE, etc, but those you should really use the - # relevant say() or notice() methods. This one should be used for IRCd - # extensions you want to use in modules. - def sendmsg(type, where, message) - # limit it 440 chars + CRLF.. so we have to split long lines - left = 440 - type.length - where.length - 3 - begin - if(left >= message.length) - sendq("#{type} #{where} :#{message}") - log_sent(type, where, message) - return - end - line = message.slice!(0, left) - lastspace = line.rindex(/\s+/) - if(lastspace) - message = line.slice!(lastspace, line.length) + message - message.gsub!(/^\s+/, "") - end - sendq("#{type} #{where} :#{line}") - log_sent(type, where, line) - end while(message.length > 0) - end - - def sendq(message="") - # temporary - @socket.queue(message) - end - - # send a notice message to channel/nick +where+ - def notice(where, message) - message.each_line { |line| - line.chomp! - next unless(line.length > 0) - sendmsg("NOTICE", where, line) - } - end - - # say something (PRIVMSG) to channel/nick +where+ - def say(where, message) - message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line| - line.chomp! - next unless(line.length > 0) - unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet)) - sendmsg("PRIVMSG", where, line) - end - } - end - - # perform a CTCP action with message +message+ to channel/nick +where+ - def action(where, message) - sendq("PRIVMSG #{where} :\001ACTION #{message}\001") - if(where =~ /^#/) - log "* #{@nick} #{message}", where - elsif (where =~ /^(\S*)!.*$/) - log "* #{@nick}[#{where}] #{message}", $1 - else - log "* #{@nick}[#{where}] #{message}", where - end - end - - # quick way to say "okay" (or equivalent) to +where+ - def okay(where) - say where, @lang.get("okay") - end - - # log message +message+ to a file determined by +where+. +where+ can be a - # channel name, or a nick for private message logging - def log(message, where="server") - message.chomp! - stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S") - unless(@logs.has_key?(where)) - @logs[where] = File.new("#{@botclass}/logs/#{where}", "a") - @logs[where].sync = true - end - @logs[where].puts "[#{stamp}] #{message}" - #debug "[#{stamp}] <#{where}> #{message}" - end - - # set topic of channel +where+ to +topic+ - def topic(where, topic) - sendq "TOPIC #{where} :#{topic}" - end - - # message:: optional IRC quit message - # quit IRC, shutdown the bot - def quit(message=nil) - trap("SIGTERM", "DEFAULT") - trap("SIGHUP", "DEFAULT") - trap("SIGINT", "DEFAULT") - message = @lang.get("quit") if (!message || message.length < 1) - @socket.clearq - save - @plugins.cleanup - @channels.each_value {|v| - log "@ quit (#{message})", v.name - } - @socket.puts "QUIT :#{message}" - @socket.flush - @socket.shutdown - @registry.close - puts "rbot quit (#{message})" - exit 0 - end - - # call the save method for bot's config, keywords, auth and all plugins - def save - @registry.flush - @config.save - @keywords.save - @auth.save - @plugins.save - end - - # call the rescan method for the bot's lang, keywords and all plugins - def rescan - @lang.rescan - @plugins.rescan - @keywords.rescan - end - - # channel:: channel to join - # key:: optional channel key if channel is +s - # join a channel - def join(channel, key=nil) - if(key) - sendq "JOIN #{channel} :#{key}" - else - sendq "JOIN #{channel}" - end - end - - # part a channel - def part(channel, message="") - sendq "PART #{channel} :#{message}" - end - - # attempt to change bot's nick to +name+ - def nickchg(name) - sendq "NICK #{name}" - end - - # changing mode - def mode(channel, mode, target) - sendq "MODE #{channel} #{mode} #{target}" - end - - # m:: message asking for help - # topic:: optional topic help is requested for - # respond to online help requests - def help(topic=nil) - topic = nil if topic == "" - case topic - when nil - helpstr = "help topics: core, auth, keywords" - helpstr += @plugins.helptopics - helpstr += " (help <topic> for more info)" - when /^core$/i - helpstr = corehelp - when /^core\s+(.+)$/i - helpstr = corehelp $1 - when /^auth$/i - helpstr = @auth.help - when /^auth\s+(.+)$/i - helpstr = @auth.help $1 - when /^keywords$/i - helpstr = @keywords.help - when /^keywords\s+(.+)$/i - helpstr = @keywords.help $1 - else - unless(helpstr = @plugins.help(topic)) - helpstr = "no help for topic #{topic}" - end - end - return helpstr - end - - def status - secs_up = Time.new - @startup_time - uptime = Utils.secs_to_string secs_up - return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received." - end - - - private - - # handle help requests for "core" topics - def corehelp(topic="") - case topic - when "quit" - return "quit [<message>] => quit IRC with message <message>" - when "join" - return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level" - when "part" - return "part <channel> => part channel <channel>" - when "hide" - return "hide => part all channels" - when "save" - return "save => save current dynamic data and configuration" - when "rescan" - return "rescan => reload modules and static facts" - when "nick" - return "nick <nick> => attempt to change nick to <nick>" - when "say" - return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>" - when "action" - return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>" - when "topic" - return "topic <channel> <message> => set topic of <channel> to <message>" - when "quiet" - return "quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>" - when "talk" - return "talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>" - when "version" - return "version => describes software version" - when "botsnack" - return "botsnack => reward #{@nick} for being good" - when "hello" - return "hello|hi|hey|yo [#{@nick}] => greet the bot" - else - return "Core help topics: quit, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello" - end - end - - # handle incoming IRC PRIVMSG +m+ - def onprivmsg(m) - # log it first - if(m.action?) - if(m.private?) - log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick - else - log "* #{m.sourcenick} #{m.message}", m.target - end - else - if(m.public?) - log "<#{m.sourcenick}> #{m.message}", m.target - else - log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick - end - end - - # pass it off to plugins that want to hear everything - @plugins.delegate "listen", m - - if(m.private? && m.message =~ /^\001PING\s+(.+)\001/) - notice m.sourcenick, "\001PING #$1\001" - log "@ #{m.sourcenick} pinged me" - return - end - - if(m.address?) - case m.message - when (/^join\s+(\S+)\s+(\S+)$/i) - join $1, $2 if(@auth.allow?("join", m.source, m.replyto)) - when (/^join\s+(\S+)$/i) - join $1 if(@auth.allow?("join", m.source, m.replyto)) - when (/^part$/i) - part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto)) - when (/^part\s+(\S+)$/i) - part $1 if(@auth.allow?("join", m.source, m.replyto)) - when (/^quit(?:\s+(.*))?$/i) - quit $1 if(@auth.allow?("quit", m.source, m.replyto)) - when (/^hide$/i) - join 0 if(@auth.allow?("join", m.source, m.replyto)) - when (/^save$/i) - if(@auth.allow?("config", m.source, m.replyto)) - save - m.okay - end - when (/^nick\s+(\S+)$/i) - nickchg($1) if(@auth.allow?("nick", m.source, m.replyto)) - when (/^say\s+(\S+)\s+(.*)$/i) - say $1, $2 if(@auth.allow?("say", m.source, m.replyto)) - when (/^action\s+(\S+)\s+(.*)$/i) - action $1, $2 if(@auth.allow?("say", m.source, m.replyto)) - when (/^topic\s+(\S+)\s+(.*)$/i) - topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto)) - when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i) - mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto)) - when (/^ping$/i) - say m.replyto, "pong" - when (/^rescan$/i) - if(@auth.allow?("config", m.source, m.replyto)) - m.okay - rescan - end - when (/^quiet$/i) - if(auth.allow?("talk", m.source, m.replyto)) - m.okay - @channels.each_value {|c| c.quiet = true } - end - when (/^quiet in (\S+)$/i) - where = $1 - if(auth.allow?("talk", m.source, m.replyto)) - m.okay - where.gsub!(/^here$/, m.target) if m.public? - @channels[where].quiet = true if(@channels.has_key?(where)) - end - when (/^talk$/i) - if(auth.allow?("talk", m.source, m.replyto)) - @channels.each_value {|c| c.quiet = false } - m.okay - end - when (/^talk in (\S+)$/i) - where = $1 - if(auth.allow?("talk", m.source, m.replyto)) - where.gsub!(/^here$/, m.target) if m.public? - @channels[where].quiet = false if(@channels.has_key?(where)) - m.okay - end - # TODO break this out into a config module - when (/^options get sendq_delay$/i) - if auth.allow?("config", m.source, m.replyto) - m.reply "options->sendq_delay = #{@socket.sendq_delay}" - end - when (/^options get sendq_burst$/i) - if auth.allow?("config", m.source, m.replyto) - m.reply "options->sendq_burst = #{@socket.sendq_burst}" - end - when (/^options set sendq_burst (.*)$/i) - num = $1.to_i - if auth.allow?("config", m.source, m.replyto) - @socket.sendq_burst = num - @config['irc.sendq_burst'] = num - m.okay - end - when (/^options set sendq_delay (.*)$/i) - freq = $1.to_f - if auth.allow?("config", m.source, m.replyto) - @socket.sendq_delay = freq - @config['irc.sendq_delay'] = freq - m.okay - end - when (/^status$/i) - m.reply status if auth.allow?("status", m.source, m.replyto) - when (/^registry stats$/i) - if auth.allow?("config", m.source, m.replyto) - m.reply @registry.stat.inspect - end - when (/^(version)|(introduce yourself)$/i) - say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/" - when (/^help(?:\s+(.*))?$/i) - say m.replyto, help($1) - when (/^(botsnack|ciggie)$/i) - say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?) - say m.replyto, @lang.get("thanks") if(m.private?) - when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i) - say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?) - say m.replyto, @lang.get("hello") if(m.private?) - else - delegate_privmsg(m) - end - else - # stuff to handle when not addressed - case m.message - when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))\s+#{@nick}$/i) - say m.replyto, @lang.get("hello_X") % m.sourcenick - when (/^#{@nick}!*$/) - say m.replyto, @lang.get("hello_X") % m.sourcenick - else - @keywords.privmsg(m) - end - end - end - - # log a message. Internal use only. - def log_sent(type, where, message) - case type - when "NOTICE" - if(where =~ /^#/) - log "-=#{@nick}=- #{message}", where - elsif (where =~ /(\S*)!.*/) - log "[-=#{where}=-] #{message}", $1 - else - log "[-=#{where}=-] #{message}" - end - when "PRIVMSG" - if(where =~ /^#/) - log "<#{@nick}> #{message}", where - elsif (where =~ /^(\S*)!.*$/) - log "[msg(#{where})] #{message}", $1 - else - log "[msg(#{where})] #{message}", where - end - end - end - - def onjoin(m) - @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) - if(m.address?) - log "@ Joined channel #{m.channel}", m.channel - puts "joined channel #{m.channel}" - else - log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel - @channels[m.channel].users[m.sourcenick] = Hash.new - @channels[m.channel].users[m.sourcenick]["mode"] = "" - end - - @plugins.delegate("listen", m) - @plugins.delegate("join", m) - end - - def onpart(m) - if(m.address?) - log "@ Left channel #{m.channel} (#{m.message})", m.channel - @channels.delete(m.channel) - puts "left channel #{m.channel}" - else - log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel - @channels[m.channel].users.delete(m.sourcenick) - end - - # delegate to plugins - @plugins.delegate("listen", m) - @plugins.delegate("part", m) - end - - # respond to being kicked from a channel - def onkick(m) - if(m.address?) - @channels.delete(m.channel) - log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel - puts "kicked from channel #{m.channel}" - else - @channels[m.channel].users.delete(m.sourcenick) - log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel - end - - @plugins.delegate("listen", m) - @plugins.delegate("kick", m) - end - - def ontopic(m) - @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) - @channels[m.channel].topic = m.topic if !m.topic.nil? - @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil? - @channels[m.channel].topic.by = m.source if !m.source.nil? - - debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}" - end - - # delegate a privmsg to auth, keyword or plugin handlers - def delegate_privmsg(message) - [@auth, @plugins, @keywords].each {|m| - break if m.privmsg(message) - } - end - -end - -end diff --git a/rbot/ircsocket.rb b/rbot/ircsocket.rb deleted file mode 100644 index 35857736..00000000 --- a/rbot/ircsocket.rb +++ /dev/null @@ -1,186 +0,0 @@ -module Irc - - require 'socket' - require 'thread' - - # wrapped TCPSocket for communication with the server. - # emulates a subset of TCPSocket functionality - class IrcSocket - # total number of lines sent to the irc server - attr_reader :lines_sent - - # total number of lines received from the irc server - attr_reader :lines_received - - # delay between lines sent - attr_reader :sendq_delay - - # max lines to burst - attr_reader :sendq_burst - - # server:: server to connect to - # port:: IRCd port - # host:: optional local host to bind to (ruby 1.7+ required) - # create a new IrcSocket - def initialize(server, port, host, sendq_delay=2, sendq_burst=4) - @server = server.dup - @port = port.to_i - @host = host - @lines_sent = 0 - @lines_received = 0 - if sendq_delay - @sendq_delay = sendq_delay.to_f - else - @sendq_delay = 2 - end - @last_send = Time.new - @sendq_delay - @burst = 0 - if sendq_burst - @sendq_burst = sendq_burst.to_i - else - @sendq_burst = 4 - end - end - - # open a TCP connection to the server - def connect - if(@host) - begin - @sock=TCPSocket.new(@server, @port, @host) - rescue ArgumentError => e - $stderr.puts "Your version of ruby does not support binding to a " - $stderr.puts "specific local address, please upgrade if you wish " - $stderr.puts "to use HOST = foo" - $stderr.puts "(this option has been disabled in order to continue)" - @sock=TCPSocket.new(@server, @port) - end - else - @sock=TCPSocket.new(@server, @port) - end - @qthread = false - @qmutex = Mutex.new - @sendq = Array.new - if (@sendq_delay > 0) - @qthread = Thread.new { spooler } - end - end - - def sendq_delay=(newfreq) - debug "changing sendq frequency to #{newfreq}" - @qmutex.synchronize do - @sendq_delay = newfreq - if newfreq == 0 && @qthread - clearq - Thread.kill(@qthread) - @qthread = false - elsif(newfreq != 0 && !@qthread) - @qthread = Thread.new { spooler } - end - end - end - - def sendq_burst=(newburst) - @qmutex.synchronize do - @sendq_burst = newburst - end - end - - # used to send lines to the remote IRCd - # message: IRC message to send - def puts(message) - @qmutex.synchronize do - # debug "In puts - got mutex" - puts_critical(message) - end - end - - # get the next line from the server (blocks) - def gets - reply = @sock.gets - @lines_received += 1 - if(reply) - reply.strip! - end - debug "RECV: #{reply.inspect}" - reply - end - - def queue(msg) - if @sendq_delay > 0 - @qmutex.synchronize do - # debug "QUEUEING: #{msg}" - @sendq.push msg - end - else - # just send it if queueing is disabled - self.puts(msg) - end - end - - def spooler - while true - spool - sleep 0.2 - end - end - - # pop a message off the queue, send it - def spool - unless @sendq.empty? - now = Time.new - if (now >= (@last_send + @sendq_delay)) - # reset burst counter after @sendq_delay has passed - @burst = 0 - debug "in spool, resetting @burst" - elsif (@burst >= @sendq_burst) - # nope. can't send anything - return - end - @qmutex.synchronize do - debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)" - (@sendq_burst - @burst).times do - break if @sendq.empty? - puts_critical(@sendq.shift) - end - end - end - end - - def clearq - unless @sendq.empty? - @qmutex.synchronize do - @sendq.clear - end - end - end - - # flush the TCPSocket - def flush - @sock.flush - end - - # Wraps Kernel.select on the socket - def select(timeout) - Kernel.select([@sock], nil, nil, timeout) - end - - # shutdown the connection to the server - def shutdown(how=2) - @sock.shutdown(how) - end - - private - - # same as puts, but expects to be called with a mutex held on @qmutex - def puts_critical(message) - # debug "in puts_critical" - debug "SEND: #{message.inspect}" - @sock.send(message + "\n",0) - @last_send = Time.new - @lines_sent += 1 - @burst += 1 - end - - end - -end diff --git a/rbot/keywords.rb b/rbot/keywords.rb deleted file mode 100644 index 3305af29..00000000 --- a/rbot/keywords.rb +++ /dev/null @@ -1,427 +0,0 @@ -require 'pp' - -module Irc - - # Keyword class - # - # Encapsulates a keyword ("foo is bar" is a keyword called foo, with type - # is, and has a single value of bar). - # Keywords can have multiple values, to_s() will choose one at random - class Keyword - - # type of keyword (e.g. "is" or "are") - attr_reader :type - - # type:: type of keyword (e.g "is" or "are") - # values:: array of values - # - # create a keyword of type +type+ with values +values+ - def initialize(type, values) - @type = type.downcase - @values = values - end - - # pick a random value for this keyword and return it - def to_s - if(@values.length > 1) - Keyword.unescape(@values[rand(@values.length)]) - else - Keyword.unescape(@values[0]) - end - end - - # describe the keyword (show all values without interpolation) - def desc - @values.join(" | ") - end - - # return the keyword in a stringified form ready for storage - def dump - @type + "/" + Keyword.unescape(@values.join("<=or=>")) - end - - # deserialize the stringified form to an object - def Keyword.restore(str) - if str =~ /^(\S+?)\/(.*)$/ - type = $1 - vals = $2.split("<=or=>") - return Keyword.new(type, vals) - end - return nil - end - - # values:: array of values to add - # add values to a keyword - def <<(values) - if(@values.length > 1 || values.length > 1) - values.each {|v| - @values << v - } - else - @values[0] += " or " + values[0] - end - end - - # unescape special words/characters in a keyword - def Keyword.unescape(str) - str.gsub(/\\\|/, "|").gsub(/ \\is /, " is ").gsub(/ \\are /, " are ").gsub(/\\\?(\s*)$/, "?\1") - end - - # escape special words/characters in a keyword - def Keyword.escape(str) - str.gsub(/\|/, "\\|").gsub(/ is /, " \\is ").gsub(/ are /, " \\are ").gsub(/\?(\s*)$/, "\\?\1") - end - end - - # keywords class. - # - # Handles all that stuff like "bot: foo is bar", "bot: foo?" - # - # Fallback after core and auth have had a look at a message and refused to - # handle it, checks for a keyword command or lookup, otherwise the message - # is delegated to plugins - class Keywords - - # create a new Keywords instance, associated to bot +bot+ - def initialize(bot) - @bot = bot - @statickeywords = Hash.new - upgrade_data - @keywords = DBTree.new bot, "keyword" - - scan - - # import old format keywords into DBHash - if(File.exist?("#{@bot.botclass}/keywords.rbot")) - puts "auto importing old keywords.rbot" - IO.foreach("#{@bot.botclass}/keywords.rbot") do |line| - if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/) - lhs = $1 - mhs = $2 - rhs = $3 - mhs = "is" unless mhs - rhs = Keyword.escape rhs - values = rhs.split("<=or=>") - @keywords[lhs] = Keyword.new(mhs, values).dump - end - end - File.delete("#{@bot.botclass}/keywords.rbot") - end - end - - # drop static keywords and reload them from files, picking up any new - # keyword files that have been added - def rescan - @statickeywords = Hash.new - scan - end - - # load static keywords from files, picking up any new keyword files that - # have been added - def scan - # first scan for old DBHash files, and convert them - Dir["#{@bot.botclass}/keywords/*"].each {|f| - next unless f =~ /\.db$/ - puts "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format" - newname = f.gsub(/\.db$/, ".kdb") - old = BDB::Hash.open f, nil, - "r+", 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - new = BDB::CIBtree.open newname, nil, - BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, - 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - old.each {|k,v| - new[k] = v - } - old.close - new.close - File.delete(f) - } - - # then scan for current DBTree files, and load them - Dir["#{@bot.botclass}/keywords/*"].each {|f| - next unless f =~ /\.kdb$/ - hsh = DBTree.new @bot, f, true - key = File.basename(f).gsub(/\.kdb$/, "") - debug "keywords module: loading DBTree file #{f}, key #{key}" - @statickeywords[key] = hsh - } - - # then scan for non DB files, and convert/import them and delete - Dir["#{@bot.botclass}/keywords/*"].each {|f| - next if f =~ /\.kdb$/ - next if f =~ /CVS$/ - puts "auto converting keywords from #{f}" - key = File.basename(f) - unless @statickeywords.has_key?(key) - @statickeywords[key] = DBHash.new @bot, "#{f}.db", true - end - IO.foreach(f) {|line| - if(line =~ /^(.*?)\s*<?=(is|are)?=?>\s*(.*)$/) - lhs = $1 - mhs = $2 - rhs = $3 - # support infobot style factfiles, by fixing them up here - rhs.gsub!(/\$who/, "<who>") - mhs = "is" unless mhs - rhs = Keyword.escape rhs - values = rhs.split("<=or=>") - @statickeywords[key][lhs] = Keyword.new(mhs, values).dump - end - } - File.delete(f) - @statickeywords[key].flush - } - end - - # upgrade data files found in old rbot formats to current - def upgrade_data - if File.exist?("#{@bot.botclass}/keywords.db") - puts "upgrading old keywords (rbot 0.9.5 or prior) database format" - old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil, - "r+", 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - new = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil, - BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, - 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - old.each {|k,v| - new[k] = v - } - old.close - new.close - File.delete("#{@bot.botclass}/keywords.db") - end - end - - # save dynamic keywords to file - def save - @keywords.flush - end - def oldsave - File.open("#{@bot.botclass}/keywords.rbot", "w") do |file| - @keywords.each do |key, value| - file.puts "#{key}<=#{value.type}=>#{value.dump}" - end - end - end - - # lookup keyword +key+, return it or nil - def [](key) - debug "keywords module: looking up key #{key}" - if(@keywords.has_key?(key)) - return Keyword.restore(@keywords[key]) - else - # key name order for the lookup through these - @statickeywords.keys.sort.each {|k| - v = @statickeywords[k] - if v.has_key?(key) - return Keyword.restore(v[key]) - end - } - end - return nil - end - - # does +key+ exist as a keyword? - def has_key?(key) - if @keywords.has_key?(key) && Keyword.restore(@keywords[key]) != nil - return true - end - @statickeywords.each {|k,v| - if v.has_key?(key) && Keyword.restore(v[key]) != nil - return true - end - } - return false - end - - # m:: PrivMessage containing message info - # key:: key being queried - # dunno:: optional, if true, reply "dunno" if +key+ not found - # - # handle a message asking about a keyword - def keyword(m, key, dunno=true) - unless(kw = self[key]) - m.reply @bot.lang.get("dunno") if (dunno) - return - end - response = kw.to_s - response.gsub!(/<who>/, m.sourcenick) - if(response =~ /^<reply>\s*(.*)/) - m.reply "#$1" - elsif(response =~ /^<action>\s*(.*)/) - @bot.action m.replyto, "#$1" - elsif(m.public? && response =~ /^<topic>\s*(.*)/) - topic = $1 - @bot.topic m.target, topic - else - m.reply "#{key} #{kw.type} #{response}" - end - end - - - # m:: PrivMessage containing message info - # target:: channel/nick to tell about the keyword - # key:: key being queried - # - # handle a message asking the bot to tell someone about a keyword - def keyword_tell(m, target, key) - unless(kw = self[key]) - @bot.say m.sourcenick, @bot.lang.get("dunno_about_X") % key - return - end - response = kw.to_s - response.gsub!(/<who>/, m.sourcenick) - if(response =~ /^<reply>\s*(.*)/) - @bot.say target, "#{m.sourcenick} wanted me to tell you: (#{key}) #$1" - m.reply "okay, I told #{target}: (#{key}) #$1" - elsif(response =~ /^<action>\s*(.*)/) - @bot.action target, "#$1 (#{m.sourcenick} wanted me to tell you)" - m.reply "okay, I told #{target}: * #$1" - else - @bot.say target, "#{m.sourcenick} wanted me to tell you that #{key} #{kw.type} #{response}" - m.reply "okay, I told #{target} that #{key} #{kw.type} #{response}" - end - end - - # handle a message which alters a keyword - # like "foo is bar", or "no, foo is baz", or "foo is also qux" - def keyword_command(sourcenick, target, lhs, mhs, rhs, quiet=false) - debug "got keyword command #{lhs}, #{mhs}, #{rhs}" - overwrite = false - overwrite = true if(lhs.gsub!(/^no,\s*/, "")) - also = true if(rhs.gsub!(/^also\s+/, "")) - values = rhs.split(/\s+\|\s+/) - lhs = Keyword.unescape lhs - if(overwrite || also || !has_key?(lhs)) - if(also && has_key?(lhs)) - kw = self[lhs] - kw << values - @keywords[lhs] = kw.dump - else - @keywords[lhs] = Keyword.new(mhs, values).dump - end - @bot.okay target if !quiet - elsif(has_key?(lhs)) - kw = self[lhs] - @bot.say target, "but #{lhs} #{kw.type} #{kw.desc}" if kw && !quiet - end - end - - # return help string for Keywords with option topic +topic+ - def help(topic="") - case topic - when "overview" - return "set: <keyword> is <definition>, overide: no, <keyword> is <definition>, add to definition: <keyword> is also <definition>, random responses: <keyword> is <definition> | <definition> [| ...], plurals: <keyword> are <definition>, escaping: \\is, \\are, \\|, specials: <reply>, <action>, <who>" - when "set" - return "set => <keyword> is <definition>" - when "plurals" - return "plurals => <keywords> are <definition>" - when "override" - return "overide => no, <keyword> is <definition>" - when "also" - return "also => <keyword> is also <definition>" - when "random" - return "random responses => <keyword> is <definition> | <definition> [| ...]" - when "get" - return "asking for keywords => (with addressing) \"<keyword>?\", (without addressing) \"'<keyword>\"" - when "tell" - return "tell <nick> about <keyword> => if <keyword> is known, tell <nick>, via /msg, its definition" - when "forget" - return "forget <keyword> => forget fact <keyword>" - when "keywords" - return "keywords => show current keyword counts" - when "<reply>" - return "<reply> => normal response is \"<keyword> is <definition>\", but if <definition> begins with <reply>, the response will be \"<definition>\"" - when "<action>" - return "<action> => makes keyword respnse \"/me <definition>\"" - when "<who>" - return "<who> => replaced with questioner in reply" - when "<topic>" - return "<topic> => respond by setting the topic to the rest of the definition" - when "search" - return "keywords search [--all] [--full] <regexp> => search keywords for <regexp>. If --all is set, search static keywords too, if --full is set, search definitions too." - else - return "Keyword module (Fact learning and regurgitation) topics: overview, set, plurals, override, also, random, get, tell, forget, keywords, keywords search, <reply>, <action>, <who>, <topic>" - end - end - - # privmsg handler - def privmsg(m) - return if m.replied? - if(m.address?) - if(!(m.message =~ /\\\?\s*$/) && m.message =~ /^(.*\S)\s*\?\s*$/) - keyword m, $1 if(@bot.auth.allow?("keyword", m.source, m.replyto)) - elsif(m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/) - keyword_command(m.sourcenick, m.replyto, $1, $2, $3) if(@bot.auth.allow?("keycmd", m.source, m.replyto)) - elsif (m.message =~ /^tell\s+(\S+)\s+about\s+(.+)$/) - keyword_tell(m, $1, $2) if(@bot.auth.allow?("keyword", m.source, m.replyto)) - elsif (m.message =~ /^forget\s+(.*)$/) - key = $1 - if((@bot.auth.allow?("keycmd", m.source, m.replyto)) && @keywords.has_key?(key)) - @keywords.delete(key) - @bot.okay m.replyto - end - elsif (m.message =~ /^keywords$/) - if(@bot.auth.allow?("keyword", m.source, m.replyto)) - length = 0 - @statickeywords.each {|k,v| - length += v.length - } - m.reply "There are currently #{@keywords.length} keywords, #{length} static facts defined." - end - elsif (m.message =~ /^keywords search\s+(.*)$/) - str = $1 - all = false - all = true if str.gsub!(/--all\s+/, "") - full = false - full = true if str.gsub!(/--full\s+/, "") - - re = Regexp.new(str, Regexp::IGNORECASE) - if(@bot.auth.allow?("keyword", m.source, m.replyto)) - matches = Array.new - @keywords.each {|k,v| - kw = Keyword.restore(v) - if re.match(k) || (full && re.match(kw.desc)) - matches << [k,kw] - end - } - if all - @statickeywords.each {|k,v| - v.each {|kk,vv| - kw = Keyword.restore(vv) - if re.match(kk) || (full && re.match(kw.desc)) - matches << [kk,kw] - end - } - } - end - if matches.length == 1 - rkw = matches[0] - m.reply "#{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" - elsif matches.length > 0 - i = 0 - matches.each {|rkw| - m.reply "[#{i+1}/#{matches.length}] #{rkw[0]} #{rkw[1].type} #{rkw[1].desc}" - i += 1 - break if i == 3 - } - else - m.reply "no keywords match #{str}" - end - end - end - else - # in channel message, not to me - if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.noaddress"] && m.message =~ /^(.*\S)\s*\?\s*$/)) - keyword m, $1, false if(@bot.auth.allow?("keyword", m.source)) - elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)) - # TODO MUCH more selective on what's allowed here - keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source)) - end - end - end - end -end diff --git a/rbot/language.rb b/rbot/language.rb deleted file mode 100644 index 9788b2bb..00000000 --- a/rbot/language.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Irc - - class Language - def initialize(language, file="") - @language = language - if file.empty? - file = File.dirname(__FILE__) + "/languages/#{@language}.lang" - end - unless(FileTest.exist?(file)) - raise "no such language: #{@language} (no such file #{file})" - end - @file = file - scan - end - - def scan - @strings = Hash.new - current_key = nil - IO.foreach(@file) {|l| - next if l =~ /^$/ - next if l =~ /^\s*#/ - if(l =~ /^(\S+):$/) - @strings[$1] = Array.new - current_key = $1 - elsif(l =~ /^\s*(.*)$/) - @strings[current_key] << $1 - end - } - end - - def rescan - scan - end - - def get(key) - if(@strings.has_key?(key)) - return @strings[key][rand(@strings[key].length)] - else - raise "undefined language key" - end - end - - def save - File.open(@file, "w") {|file| - @strings.each {|key,val| - file.puts "#{key}:" - val.each_value {|v| - file.puts " #{v}" - } - } - } - end - end - -end diff --git a/rbot/languages/dutch.lang b/rbot/languages/dutch.lang deleted file mode 100644 index db116264..00000000 --- a/rbot/languages/dutch.lang +++ /dev/null @@ -1,73 +0,0 @@ -okay: - ok - ok dan :) - goed - mooi - voila - in orde - 't is gebeurd - zeker - dat kan ik! - komt in orde - k - ik zal het eens doen -dunno: - geen idee - dat weet ik niet - dat gaat m'n petje te boven - *haal schouder op* - vraag dat aan iemand anders - dat moet je niet aan mij vragen - wie weet dat? - dat kan ik niet - laat je eens nakijken... - wat vraag je nu? -dunno_about_X: - maar ik weet niks over %s - Ik heb nog nooit van %s gehoord :( - maar wat is %s? -insult: - %s: idioot! - %s: :( - %s: Ik haat je:( - %s: val dood! - %s: Ik ben beledigd! -hello: - hallo :) - hey! - hi - yo - yow - joe - jowjowjow -hello_X: - hallo %s :) - %s: hallo - hey %s :) - %s: hi! - yo %s! - joe %s! - alles ok %s? - %s: alles goed? -welcome: - geen probleem - 't is niks - altijd welkom - graag gedaan - np :) -thanks: - danku :) - bedankt! - thx :) - =D - je bent een schatje :) -thanks_X: - %s: danku :) - %s: bedankt! - %s: =D - %s: thx :) - %s: je bent een schatje :) -quit: - ok ik ben weg - yo - ciao diff --git a/rbot/languages/english.lang b/rbot/languages/english.lang deleted file mode 100644 index f275d82f..00000000 --- a/rbot/languages/english.lang +++ /dev/null @@ -1,75 +0,0 @@ -okay: - okay - okay then :) - okies! - fine - done - can do! - alright - sure - aight - lemme take care of that for you -dunno: - dunno - beats me - no idea - no clue - ...eh? - *shrug* - don't ask me - who knows? - I can't do that Dave. - you best check yo'self! -dunno_about_X: - but I dunno anything about %s - I never heard of %s :( - %s? what's that then? - but what's %s? -insult: - %s: wanker! - %s: :( - %s: I hate you :( - %s: die! - %s: I'm offended! - %s: you hurt my feelings -hello: - hello :) - hola :) - salut - hey! - word. - hi - yo - 'sup? -hello_X: - hello %s :) - %s: hey there - %s: hola :) - %s: salut - hey %s :) - %s: word - %s: hi! - yo %s! - %s: 'sup? - 'sup %s? -welcome: - no probbie - you're welcome - de nada - any time - np :) -thanks: - thanks :) - schweet! - ta :) - =D - cheers! -thanks_X: - %s: thanks :) - %s: schweet! - %s: =D - %s: ta :) - %s: cheers -quit: - okay bye - seeya diff --git a/rbot/languages/german.lang b/rbot/languages/german.lang deleted file mode 100644 index 3e23268d..00000000 --- a/rbot/languages/german.lang +++ /dev/null @@ -1,67 +0,0 @@ -okay: - okay - okay na dann :) - gut - gemacht - wird gemacht! - also los - sicher - klar - lass mich sorge tragen :-) -dunno: - kann nicht - schlag mich - habe keine Idee - habe keine Ahnung - *achselzuck* - frag mich nicht - wer weiss? - ich kann es nicht tun - am besten du schaust selber nach -dunno_about_X: - ich weiss nichts uber %s - was zum Teufel ist %s? -insult: - %s: Arsch! - %s: :( - %s: Ich hasse dich :( - %s: Stirb! - %s: Ich bin beleidigt! -hello: - hallo :) - hola :) - salut - hey! - sag nichts. - hi - yo - Was geht? -hello_X: - hallo %s :) - %s: guggus - %s: hola :) - %s: salut - hey %s :) - %s: sag nichts - %s: hi! - yo %s! - %s: Was geht? - Was geht %s? -welcome: - no probbie - you're welcome - de nada - any time - np :) -thanks: - Danke :) - juhu :) - :-D - Prost! -thanks_X: - %s: danke :) - %s: :-D - %s: juhu :) - %s: Prost -quit: - forkbomb rockt diff --git a/rbot/message.rb b/rbot/message.rb deleted file mode 100644 index d7f614ab..00000000 --- a/rbot/message.rb +++ /dev/null @@ -1,256 +0,0 @@ -module Irc - - # base user message class, all user messages derive from this - # (a user message is defined as having a source hostmask, a target - # nick/channel and a message part) - class BasicUserMessage - - # associated bot - attr_reader :bot - - # when the message was received - attr_reader :time - - # hostmask of message source - attr_reader :source - - # nick of message source - attr_reader :sourcenick - - # url part of message source - attr_reader :sourceaddress - - # nick/channel message was sent to - attr_reader :target - - # contents of the message - attr_accessor :message - - # has the message been replied to/handled by a plugin? - attr_accessor :replied - - # instantiate a new Message - # bot:: associated bot class - # source:: hostmask of the message source - # target:: nick/channel message is destined for - # message:: message part - def initialize(bot, source, target, message) - @time = Time.now - @bot = bot - @source = source - @address = false - @target = target - @message = BasicUserMessage.stripcolour message - @replied = false - - # split source into consituent parts - if source =~ /^((\S+)!(\S+))$/ - @sourcenick = $2 - @sourceaddress = $3 - end - - if target && target.downcase == @bot.nick.downcase - @address = true - end - - end - - # returns true if the message was addressed to the bot. - # This includes any private message to the bot, or any public message - # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo", - # a kick message when bot was kicked etc. - def address? - return @address - end - - # has this message been replied to by a plugin? - def replied? - return @replied - end - - # strip mIRC colour escapes from a string - def BasicUserMessage.stripcolour(string) - return "" unless string - ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "") - #ret.tr!("\x00-\x1f", "") - ret - end - - end - - # class for handling IRC user messages. Includes some utilities for handling - # the message, for example in plugins. - # The +message+ member will have any bot addressing "^bot: " removed - # (address? will return true in this case) - class UserMessage < BasicUserMessage - - # for plugin messages, the name of the plugin invoked by the message - attr_reader :plugin - - # for plugin messages, the rest of the message, with the plugin name - # removed - attr_reader :params - - # convenience member. Who to reply to (i.e. would be sourcenick for a - # privately addressed message, or target (the channel) for a publicly - # addressed message - attr_reader :replyto - - # channel the message was in, nil for privately addressed messages - attr_reader :channel - - # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff - # will be stripped from the message) - attr_reader :action - - # instantiate a new UserMessage - # bot:: associated bot class - # source:: hostmask of the message source - # target:: nick/channel message is destined for - # message:: message part - def initialize(bot, source, target, message) - super(bot, source, target, message) - @target = target - @private = false - @plugin = nil - @action = false - - if target.downcase == @bot.nick.downcase - @private = true - @address = true - @channel = nil - @replyto = @sourcenick - else - @replyto = @target - @channel = @target - end - - # check for option extra addressing prefixes, e.g "|search foo", or - # "!version" - first match wins - bot.addressing_prefixes.each {|mprefix| - if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "") - @address = true - break - end - } - - # even if they used above prefixes, we allow for silly people who - # combine all possible types, e.g. "|rbot: hello", or - # "/msg rbot rbot: hello", etc - if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s*/, "") - @address = true - end - - if(@message =~ /^\001ACTION\s(.+)\001/) - @message = $1 - @action = true - end - - # free splitting for plugins - @params = @message.dup - if @params.gsub!(/^\s*(\S+)[\s$]*/, "") - @plugin = $1.downcase - @params = nil unless @params.length > 0 - end - end - - # returns true for private messages, e.g. "/msg bot hello" - def private? - return @private - end - - # returns true if the message was in a channel - def public? - return !@private - end - - def action? - return @action - end - - # convenience method to reply to a message, useful in plugins. It's the - # same as doing: - # <tt>@bot.say m.replyto, string</tt> - # So if the message is private, it will reply to the user. If it was - # in a channel, it will reply in the channel. - def reply(string) - @bot.say @replyto, string - @replied = true - end - - # convenience method to reply "okay" in the current language to the - # message - def okay - @bot.say @replyto, @bot.lang.get("okay") - end - - end - - # class to manage IRC PRIVMSGs - class PrivMessage < UserMessage - end - - # class to manage IRC NOTICEs - class NoticeMessage < UserMessage - end - - # class to manage IRC KICKs - # +address?+ can be used as a shortcut to see if the bot was kicked, - # basically, +target+ was kicked from +channel+ by +source+ with +message+ - class KickMessage < BasicUserMessage - # channel user was kicked from - attr_reader :channel - - def initialize(bot, source, target, channel, message="") - super(bot, source, target, message) - @channel = channel - end - end - - # class to pass IRC Nick changes in. @message contains the old nickame, - # @sourcenick contains the new one. - class NickMessage < BasicUserMessage - def initialize(bot, source, oldnick, newnick) - super(bot, source, oldnick, newnick) - end - end - - class QuitMessage < BasicUserMessage - def initialize(bot, source, target, message="") - super(bot, source, target, message) - end - end - - class TopicMessage < BasicUserMessage - # channel topic - attr_reader :topic - # topic set at (unixtime) - attr_reader :timestamp - # topic set on channel - attr_reader :channel - - def initialize(bot, source, channel, timestamp, topic="") - super(bot, source, channel, topic) - @topic = topic - @timestamp = timestamp - @channel = channel - end - end - - # class to manage channel joins - class JoinMessage < BasicUserMessage - # channel joined - attr_reader :channel - def initialize(bot, source, channel, message="") - super(bot, source, channel, message) - @channel = channel - # in this case sourcenick is the nick that could be the bot - @address = (sourcenick.downcase == @bot.nick.downcase) - end - end - - # class to manage channel parts - # same as a join, but can have a message too - class PartMessage < JoinMessage - end -end diff --git a/rbot/messagemapper.rb b/rbot/messagemapper.rb deleted file mode 100644 index 42563d23..00000000 --- a/rbot/messagemapper.rb +++ /dev/null @@ -1,168 +0,0 @@ -module Irc - class MessageMapper - attr_writer :fallback - - def initialize(parent) - @parent = parent - @routes = Array.new - @fallback = 'usage' - end - - def map(*args) - @routes << Route.new(*args) - end - - def each - @routes.each {|route| yield route} - end - def last - @routes.last - end - - def handle(m) - return false if @routes.empty? - failures = [] - @routes.each do |route| - options, failure = route.recognize(m) - if options.nil? - failures << [route, failure] - else - action = route.options[:action] ? route.options[:action] : route.items[0] - next unless @parent.respond_to?(action) - auth = route.options[:auth] ? route.options[:auth] : action - if m.bot.auth.allow?(auth, m.source, m.replyto) - debug "route found and auth'd: #{action.inspect} #{options.inspect}" - @parent.send(action, m, options) - return true - end - # if it's just an auth failure but otherwise the match is good, - # don't try any more handlers - break - end - end - debug failures.inspect - debug "no handler found, trying fallback" - if @fallback != nil && @parent.respond_to?(@fallback) - if m.bot.auth.allow?(@fallback, m.source, m.replyto) - @parent.send(@fallback, m, {}) - return true - end - end - return false - end - - end - - class Route - attr_reader :defaults # The defaults hash - attr_reader :options # The options hash - attr_reader :items - def initialize(template, hash={}) - raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash) - @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {} - @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {} - self.items = template - @options = hash - end - def items=(str) - items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols - items.shift if items.first == "" - items.pop if items.last == "" - @items = items - - if @items.first.kind_of? Symbol - raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}" - end - - # Verify uniqueness of each component. - @items.inject({}) do |seen, item| - if item.kind_of? Symbol - raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item - seen[item] = true - end - seen - end - end - - # Recognize the provided string components, returning a hash of - # recognized values, or [nil, reason] if the string isn't recognized. - def recognize(m) - components = m.message.split(/\s+/) - options = {} - - @items.each do |item| - if /^\*/ =~ item.to_s - if components.empty? - value = @defaults.has_key?(item) ? @defaults[item].clone : [] - else - value = components.clone - end - components = [] - def value.to_s() self.join(' ') end - options[item.to_s.sub(/^\*/,"").intern] = value - elsif item.kind_of? Symbol - value = components.shift || @defaults[item] - if passes_requirements?(item, value) - options[item] = value - else - if @defaults.has_key?(item) - debug "item #{item} doesn't pass reqs but has a default of #{@defaults[item]}" - options[item] = @defaults[item].clone - # push the test-failed component back on the stack - components.unshift value - else - return nil, requirements_for(item) - end - end - else - return nil, "No value available for component #{item.inspect}" if components.empty? - component = components.shift - return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item - end - end - - return nil, "Unused components were left: #{components.join '/'}" unless components.empty? - - return nil, "route is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private? - return nil, "route is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private? - - options.delete_if {|k, v| v.nil?} # Remove nil values. - return options, nil - end - - def inspect - when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}" - default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}" - "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join(' ').inspect}#{default_str}#{when_str}>" - end - - # Verify that the given value passes this route's requirements - def passes_requirements?(name, value) - return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be - - case @requirements[name] - when nil then true - when Regexp then - value = value.to_s - match = @requirements[name].match(value) - match && match[0].length == value.length - else - @requirements[name] == value.to_s - end - end - - def requirements_for(name) - name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect) - presence = (@defaults.key?(name) && @defaults[name].nil?) - requirement = case @requirements[name] - when nil then nil - when Regexp then "match #{@requirements[name].inspect}" - else "be equal to #{@requirements[name].inspect}" - end - if presence && requirement then "#{name} must be present and #{requirement}" - elsif presence || requirement then "#{name} must #{requirement || 'be present'}" - else "#{name} has no requirements" - end - end - end -end diff --git a/rbot/plugins.rb b/rbot/plugins.rb deleted file mode 100644 index 5db047fb..00000000 --- a/rbot/plugins.rb +++ /dev/null @@ -1,300 +0,0 @@ -module Irc - require 'rbot/messagemapper' - - # base class for all rbot plugins - # certain methods will be called if they are provided, if you define one of - # the following methods, it will be called as appropriate: - # - # map(template, options):: - # map is the new, cleaner way to respond to specific message formats - # without littering your plugin code with regexps - # examples: - # plugin.map 'karmastats', :action => 'karma_stats' - # - # # while in the plugin... - # def karma_stats(m, params) - # m.reply "..." - # end - # - # # the default action is the first component - # plugin.map 'karma' - # - # # attributes can be pulled out of the match string - # plugin.map 'karma for :key' - # plugin.map 'karma :key' - # - # # while in the plugin... - # def karma(m, params) - # item = params[:key] - # m.reply 'karma for #{item}' - # end - # - # # you can setup defaults, to make parameters optional - # plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'} - # - # # the default auth check is also against the first component - # # but that can be changed - # plugin.map 'karmastats', :auth => 'karma' - # - # # maps can be restricted to public or private message: - # plugin.map 'karmastats', :private false, - # plugin.map 'karmastats', :public false, - # end - # - # To activate your maps, you simply register them - # plugin.register_maps - # This also sets the privmsg handler to use the map lookups for - # handling messages. You can still use listen(), kick() etc methods - # - # listen(UserMessage):: - # Called for all messages of any type. To - # differentiate them, use message.kind_of? It'll be - # either a PrivMessage, NoticeMessage, KickMessage, - # QuitMessage, PartMessage, JoinMessage, NickMessage, - # etc. - # - # privmsg(PrivMessage):: - # called for a PRIVMSG if the first word matches one - # the plugin register()d for. Use m.plugin to get - # that word and m.params for the rest of the message, - # if applicable. - # - # kick(KickMessage):: - # Called when a user (or the bot) is kicked from a - # channel the bot is in. - # - # join(JoinMessage):: - # Called when a user (or the bot) joins a channel - # - # part(PartMessage):: - # Called when a user (or the bot) parts a channel - # - # quit(QuitMessage):: - # Called when a user (or the bot) quits IRC - # - # nick(NickMessage):: - # Called when a user (or the bot) changes Nick - # topic(TopicMessage):: - # Called when a user (or the bot) changes a channel - # topic - # - # save:: Called when you are required to save your plugin's - # state, if you maintain data between sessions - # - # cleanup:: called before your plugin is "unloaded", prior to a - # plugin reload or bot quit - close any open - # files/connections or flush caches here - class Plugin - attr_reader :bot # the associated bot - # initialise your plugin. Always call super if you override this method, - # as important variables are set up for you - def initialize - @bot = Plugins.bot - @names = Array.new - @handler = MessageMapper.new(self) - @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) - end - - def map(*args) - @handler.map(*args) - # register this map - name = @handler.last.items[0] - self.register name - unless self.respond_to?('privmsg') - def self.privmsg(m) - @handler.handle(m) - end - end - end - - # return an identifier for this plugin, defaults to a list of the message - # prefixes handled (used for error messages etc) - def name - @names.join("|") - end - - # return a help string for your module. for complex modules, you may wish - # to break your help into topics, and return a list of available topics if - # +topic+ is nil. +plugin+ is passed containing the matching prefix for - # this message - if your plugin handles multiple prefixes, make sure your - # return the correct help for the prefix requested - def help(plugin, topic) - "no help" - end - - # register the plugin as a handler for messages prefixed +name+ - # this can be called multiple times for a plugin to handle multiple - # message prefixes - def register(name) - return if Plugins.plugins.has_key?(name) - Plugins.plugins[name] = self - @names << name - end - - # default usage method provided as a utility for simple plugins. The - # MessageMapper uses 'usage' as its default fallback method. - def usage(m, params) - m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'" - end - - end - - # class to manage multiple plugins and delegate messages to them for - # handling - class Plugins - # hash of registered message prefixes and associated plugins - @@plugins = Hash.new - # associated IrcBot class - @@bot = nil - - # bot:: associated IrcBot class - # dirlist:: array of directories to scan (in order) for plugins - # - # create a new plugin handler, scanning for plugins in +dirlist+ - def initialize(bot, dirlist) - @@bot = bot - @dirs = dirlist - scan - end - - # access to associated bot - def Plugins.bot - @@bot - end - - # access to list of plugins - def Plugins.plugins - @@plugins - end - - # load plugins from pre-assigned list of directories - def scan - dirs = Array.new - dirs << File.dirname(__FILE__) + "/plugins" - dirs += @dirs - dirs.each {|dir| - if(FileTest.directory?(dir)) - d = Dir.new(dir) - d.each {|file| - next if(file =~ /^\./) - next unless(file =~ /\.rb$/) - @tmpfilename = "#{dir}/#{file}" - - # create a new, anonymous module to "house" the plugin - plugin_module = Module.new - - begin - plugin_string = IO.readlines(@tmpfilename).join("") - puts "loading module: #{@tmpfilename}" - plugin_module.module_eval(plugin_string) - rescue StandardError, NameError, LoadError, SyntaxError => err - puts "plugin #{@tmpfilename} load failed: " + err - puts err.backtrace.join("\n") - end - } - end - } - end - - # call the save method for each active plugin - def save - @@plugins.values.uniq.each {|p| - next unless(p.respond_to?("save")) - begin - p.save - rescue StandardError, NameError, SyntaxError => err - puts "plugin #{p.name} save() failed: " + err - puts err.backtrace.join("\n") - end - } - end - - # call the cleanup method for each active plugin - def cleanup - @@plugins.values.uniq.each {|p| - next unless(p.respond_to?("cleanup")) - begin - p.cleanup - rescue StandardError, NameError, SyntaxError => err - puts "plugin #{p.name} cleanup() failed: " + err - puts err.backtrace.join("\n") - end - } - end - - # drop all plugins and rescan plugins on disk - # calls save and cleanup for each plugin before dropping them - def rescan - save - cleanup - @@plugins = Hash.new - scan - end - - # return list of help topics (plugin names) - def helptopics - if(@@plugins.length > 0) - # return " [plugins: " + @@plugins.keys.sort.join(", ") + "]" - return " [#{length} plugins: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ") + "]" - else - return " [no plugins active]" - end - end - - def length - @@plugins.values.uniq.length - end - - # return help for +topic+ (call associated plugin's help method) - def help(topic="") - if(topic =~ /^(\S+)\s*(.*)$/) - key = $1 - params = $2 - if(@@plugins.has_key?(key)) - begin - return @@plugins[key].help(key, params) - rescue StandardError, NameError, SyntaxError => err - puts "plugin #{@@plugins[key].name} help() failed: " + err - puts err.backtrace.join("\n") - end - else - return false - end - end - end - - # see if each plugin handles +method+, and if so, call it, passing - # +message+ as a parameter - def delegate(method, message) - @@plugins.values.uniq.each {|p| - if(p.respond_to? method) - begin - p.send method, message - rescue StandardError, NameError, SyntaxError => err - puts "plugin #{p.name} #{method}() failed: " + err - puts err.backtrace.join("\n") - end - end - } - end - - # see if we have a plugin that wants to handle this message, if so, pass - # it to the plugin and return true, otherwise false - def privmsg(m) - return unless(m.plugin) - if (@@plugins.has_key?(m.plugin) && - @@plugins[m.plugin].respond_to?("privmsg") && - @@bot.auth.allow?(m.plugin, m.source, m.replyto)) - begin - @@plugins[m.plugin].privmsg(m) - rescue StandardError, NameError, SyntaxError => err - puts "plugin #{@@plugins[m.plugin].name} privmsg() failed: " + err - puts err.backtrace.join("\n") - end - return true - end - return false - end - end - -end diff --git a/rbot/registry.rb b/rbot/registry.rb deleted file mode 100644 index cd78dcbf..00000000 --- a/rbot/registry.rb +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright (C) 2002 Tom Gilbert. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies of the Software and its documentation and acknowledgment shall be -# given in the documentation and software packages that this Software was -# used. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -require 'rbot/dbhash' - -module Irc - - # this is the backend of the RegistryAccessor class, which ties it to a - # DBHash object called plugin_registry(.db). All methods are delegated to - # the DBHash. - class BotRegistry - def initialize(bot) - @bot = bot - upgrade_data - @db = DBTree.new @bot, "plugin_registry" - end - - # delegation hack - def method_missing(method, *args, &block) - @db.send(method, *args, &block) - end - - # check for older versions of rbot with data formats that require updating - # NB this function is called _early_ in init(), pretty much all you have to - # work with is @bot.botclass. - def upgrade_data - if File.exist?("#{@bot.botclass}/registry.db") - puts "upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format" - old = BDB::Hash.open "#{@bot.botclass}/registry.db", nil, - "r+", 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - new = BDB::CIBtree.open "#{@bot.botclass}/plugin_registry.db", nil, - BDB::CREATE | BDB::EXCL | BDB::TRUNCATE, - 0600, "set_pagesize" => 1024, - "set_cachesize" => [0, 32 * 1024, 0] - old.each {|k,v| - new[k] = v - } - old.close - new.close - File.delete("#{@bot.botclass}/registry.db") - end - end - end - - # This class provides persistent storage for plugins via a hash interface. - # The default mode is an object store, so you can store ruby objects and - # reference them with hash keys. This is because the default store/restore - # methods of the plugins' RegistryAccessor are calls to Marshal.dump and - # Marshal.restore, - # for example: - # blah = Hash.new - # blah[:foo] = "fum" - # @registry[:blah] = blah - # then, even after the bot is shut down and disconnected, on the next run you - # can access the blah object as it was, with: - # blah = @registry[:blah] - # The registry can of course be used to store simple strings, fixnums, etc as - # well, and should be useful to store or cache plugin data or dynamic plugin - # configuration. - # - # WARNING: - # in object store mode, don't make the mistake of treating it like a live - # object, e.g. (using the example above) - # @registry[:blah][:foo] = "flump" - # will NOT modify the object in the registry - remember that BotRegistry#[] - # returns a Marshal.restore'd object, the object you just modified in place - # will disappear. You would need to: - # blah = @registry[:blah] - # blah[:foo] = "flump" - # @registry[:blah] = blah - - # If you don't need to store objects, and strictly want a persistant hash of - # strings, you can override the store/restore methods to suit your needs, for - # example (in your plugin): - # def initialize - # class << @registry - # def store(val) - # val - # end - # def restore(val) - # val - # end - # end - # end - # Your plugins section of the registry is private, it has its own namespace - # (derived from the plugin's class name, so change it and lose your data). - # Calls to registry.each etc, will only iterate over your namespace. - class BotRegistryAccessor - # plugins don't call this - a BotRegistryAccessor is created for them and - # is accessible via @registry. - def initialize(bot, prefix) - @bot = bot - @registry = @bot.registry - @orig_prefix = prefix - @prefix = prefix + "/" - @default = nil - # debug "initializing registry accessor with prefix #{@prefix}" - end - - # use this to chop up your namespace into bits, so you can keep and - # reference separate object stores under the same registry - def sub_registry(prefix) - return BotRegistryAccessor.new(@bot, @orig_prefix + "+" + prefix) - end - - # convert value to string form for storing in the registry - # defaults to Marshal.dump(val) but you can override this in your module's - # registry object to use any method you like. - # For example, if you always just handle strings use: - # def store(val) - # val - # end - def store(val) - Marshal.dump(val) - end - - # restores object from string form, restore(store(val)) must return val. - # If you override store, you should override restore to reverse the - # action. - # For example, if you always just handle strings use: - # def restore(val) - # val - # end - def restore(val) - Marshal.restore(val) - end - - # lookup a key in the registry - def [](key) - if @registry.has_key?(@prefix + key) - return restore(@registry[@prefix + key]) - elsif @default != nil - return restore(@default) - else - return nil - end - end - - # set a key in the registry - def []=(key,value) - @registry[@prefix + key] = store(value) - end - - # set the default value for registry lookups, if the key sought is not - # found, the default will be returned. The default default (har) is nil. - def set_default (default) - @default = store(default) - end - - # just like Hash#each - def each(&block) - @registry.each {|key,value| - if key.gsub!(/^#{Regexp.escape(@prefix)}/, "") - block.call(key, restore(value)) - end - } - end - - # just like Hash#each_key - def each_key(&block) - @registry.each {|key, value| - if key.gsub!(/^#{Regexp.escape(@prefix)}/, "") - block.call(key) - end - } - end - - # just like Hash#each_value - def each_value(&block) - @registry.each {|key, value| - if key =~ /^#{Regexp.escape(@prefix)}/ - block.call(restore(value)) - end - } - end - - # just like Hash#has_key? - def has_key?(key) - return @registry.has_key?(@prefix + key) - end - alias include? has_key? - alias member? has_key? - - # just like Hash#has_both? - def has_both?(key, value) - return @registry.has_both?(@prefix + key, store(value)) - end - - # just like Hash#has_value? - def has_value?(value) - return @registry.has_value?(store(value)) - end - - # just like Hash#index? - def index(value) - ind = @registry.index(store(value)) - if ind && ind.gsub!(/^#{Regexp.escape(@prefix)}/, "") - return ind - else - return nil - end - end - - # delete a key from the registry - def delete(key) - return @registry.delete(@prefix + key) - end - - # returns a list of your keys - def keys - return @registry.keys.collect {|key| - if key.gsub!(/^#{Regexp.escape(@prefix)}/, "") - key - else - nil - end - }.compact - end - - # Return an array of all associations [key, value] in your namespace - def to_a - ret = Array.new - @registry.each {|key, value| - if key.gsub!(/^#{Regexp.escape(@prefix)}/, "") - ret << [key, restore(value)] - end - } - return ret - end - - # Return an hash of all associations {key => value} in your namespace - def to_hash - ret = Hash.new - @registry.each {|key, value| - if key.gsub!(/^#{Regexp.escape(@prefix)}/, "") - ret[key] = restore(value) - end - } - return ret - end - - # empties the registry (restricted to your namespace) - def clear - @registry.each_key {|key| - if key =~ /^#{Regexp.escape(@prefix)}/ - @registry.delete(key) - end - } - end - alias truncate clear - - # returns an array of the values in your namespace of the registry - def values - ret = Array.new - self.each {|k,v| - ret << restore(v) - } - return ret - end - - # returns the number of keys in your registry namespace - def length - self.keys.length - end - alias size length - - def flush - @registry.flush - end - - end - -end diff --git a/rbot/rfc2812.rb b/rbot/rfc2812.rb deleted file mode 100644 index 6f459c80..00000000 --- a/rbot/rfc2812.rb +++ /dev/null @@ -1,1027 +0,0 @@ -module Irc - # RFC 2812 Internet Relay Chat: Client Protocol - # - RPL_WELCOME=001 - # "Welcome to the Internet Relay Network - # <nick>!<user>@<host>" - RPL_YOURHOST=002 - # "Your host is <servername>, running version <ver>" - RPL_CREATED=003 - # "This server was created <date>" - RPL_MYINFO=004 - # "<servername> <version> <available user modes> - # <available channel modes>" - # - # - The server sends Replies 001 to 004 to a user upon - # successful registration. - # - RPL_BOUNCE=005 - # "Try server <server name>, port <port number>" - # - # - Sent by the server to a user to suggest an alternative - # server. This is often used when the connection is - # refused because the server is already full. - # - RPL_USERHOST=302 - # ":*1<reply> *( " " <reply> )" - # - # - Reply format used by USERHOST to list replies to - # the query list. The reply string is composed as - # follows: - # - # reply = nickname [ "*" ] "=" ( "+" / "-" ) hostname - # - # The '*' indicates whether the client has registered - # as an Operator. The '-' or '+' characters represent - # whether the client has set an AWAY message or not - # respectively. - # - RPL_ISON=303 - # ":*1<nick> *( " " <nick> )" - # - # - Reply format used by ISON to list replies to the - # query list. - # - RPL_AWAY=301 - # "<nick> :<away message>" - RPL_UNAWAY=305 - # ":You are no longer marked as being away" - RPL_NOWAWAY=306 - # ":You have been marked as being away" - # - # - These replies are used with the AWAY command (if - # allowed). RPL_AWAY is sent to any client sending a - # PRIVMSG to a client which is away. RPL_AWAY is only - # sent by the server to which the client is connected. - # Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the - # client removes and sets an AWAY message. - # - RPL_WHOISUSER=311 - # "<nick> <user> <host> * :<real name>" - RPL_WHOISSERVER=312 - # "<nick> <server> :<server info>" - RPL_WHOISOPERATOR=313 - # "<nick> :is an IRC operator" - RPL_WHOISIDLE=317 - # "<nick> <integer> :seconds idle" - RPL_ENDOFWHOIS=318 - # "<nick> :End of WHOIS list" - RPL_WHOISCHANNELS=319 - # "<nick> :*( ( "@" / "+" ) <channel> " " )" - # - # - Replies 311 - 313, 317 - 319 are all replies - # generated in response to a WHOIS message. Given that - # there are enough parameters present, the answering - # server MUST either formulate a reply out of the above - # numerics (if the query nick is found) or return an - # error reply. The '*' in RPL_WHOISUSER is there as - # the literal character and not as a wild card. For - # each reply set, only RPL_WHOISCHANNELS may appear - # more than once (for long lists of channel names). - # The '@' and '+' characters next to the channel name - # indicate whether a client is a channel operator or - # has been granted permission to speak on a moderated - # channel. The RPL_ENDOFWHOIS reply is used to mark - # the end of processing a WHOIS message. - # - RPL_WHOWASUSER=314 - # "<nick> <user> <host> * :<real name>" - RPL_ENDOFWHOWAS=369 - # "<nick> :End of WHOWAS" - # - # - When replying to a WHOWAS message, a server MUST use - # the replies RPL_WHOWASUSER, RPL_WHOISSERVER or - # ERR_WASNOSUCHNICK for each nickname in the presented - # list. At the end of all reply batches, there MUST - # be RPL_ENDOFWHOWAS (even if there was only one reply - # and it was an error). - # - RPL_LISTSTART=321 - # Obsolete. Not used. - # - RPL_LIST=322 - # "<channel> <# visible> :<topic>" - RPL_LISTEND=323 - # ":End of LIST" - # - # - Replies RPL_LIST, RPL_LISTEND mark the actual replies - # with data and end of the server's response to a LIST - # command. If there are no channels available to return, - # only the end reply MUST be sent. - # - RPL_UNIQOPIS=325 - # "<channel> <nickname>" - # - RPL_CHANNELMODEIS=324 - # "<channel> <mode> <mode params>" - # - RPL_NOTOPIC=331 - # "<channel> :No topic is set" - RPL_TOPIC=332 - # "<channel> :<topic>" - # - # - When sending a TOPIC message to determine the - # channel topic, one of two replies is sent. If - # the topic is set, RPL_TOPIC is sent back else - # RPL_NOTOPIC. - # - RPL_TOPIC_INFO=333 - # <channel> <set by> <unixtime> - RPL_INVITING=341 - # "<channel> <nick>" - # - # - Returned by the server to indicate that the - # attempted INVITE message was successful and is - # being passed onto the end client. - # - RPL_SUMMONING=342 - # "<user> :Summoning user to IRC" - # - # - Returned by a server answering a SUMMON message to - # indicate that it is summoning that user. - # - RPL_INVITELIST=346 - # "<channel> <invitemask>" - RPL_ENDOFINVITELIST=347 - # "<channel> :End of channel invite list" - # - # - When listing the 'invitations masks' for a given channel, - # a server is required to send the list back using the - # RPL_INVITELIST and RPL_ENDOFINVITELIST messages. A - # separate RPL_INVITELIST is sent for each active mask. - # After the masks have been listed (or if none present) a - # RPL_ENDOFINVITELIST MUST be sent. - # - RPL_EXCEPTLIST=348 - # "<channel> <exceptionmask>" - RPL_ENDOFEXCEPTLIST=349 - # "<channel> :End of channel exception list" - # - # - When listing the 'exception masks' for a given channel, - # a server is required to send the list back using the - # RPL_EXCEPTLIST and RPL_ENDOFEXCEPTLIST messages. A - # separate RPL_EXCEPTLIST is sent for each active mask. - # After the masks have been listed (or if none present) - # a RPL_ENDOFEXCEPTLIST MUST be sent. - # - RPL_VERSION=351 - # "<version>.<debuglevel> <server> :<comments>" - # - # - Reply by the server showing its version details. - # The <version> is the version of the software being - # used (including any patchlevel revisions) and the - # <debuglevel> is used to indicate if the server is - # running in "debug mode". - # - # The "comments" field may contain any comments about - # the version or further version details. - # - RPL_WHOREPLY=352 - # "<channel> <user> <host> <server> <nick> - # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] - # :<hopcount> <real name>" - # - RPL_ENDOFWHO=315 - # "<name> :End of WHO list" - # - # - The RPL_WHOREPLY and RPL_ENDOFWHO pair are used - # to answer a WHO message. The RPL_WHOREPLY is only - # sent if there is an appropriate match to the WHO - # query. If there is a list of parameters supplied - # with a WHO message, a RPL_ENDOFWHO MUST be sent - # after processing each list item with <name> being - # the item. - # - RPL_NAMREPLY=353 - # "( "=" / "*" / "@" ) <channel> - # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> ) - # - "@" is used for secret channels, "*" for private - # channels, and "=" for others (public channels). - # - RPL_ENDOFNAMES=366 - # "<channel> :End of NAMES list" - # - # - To reply to a NAMES message, a reply pair consisting - # of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the - # server back to the client. If there is no channel - # found as in the query, then only RPL_ENDOFNAMES is - # returned. The exception to this is when a NAMES - # message is sent with no parameters and all visible - # channels and contents are sent back in a series of - # RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark - # the end. - # - RPL_LINKS=364 - # "<mask> <server> :<hopcount> <server info>" - RPL_ENDOFLINKS=365 - # "<mask> :End of LINKS list" - # - # - In replying to the LINKS message, a server MUST send - # replies back using the RPL_LINKS numeric and mark the - # end of the list using an RPL_ENDOFLINKS reply. - # - RPL_BANLIST=367 - # "<channel> <banmask>" - RPL_ENDOFBANLIST=368 - # "<channel> :End of channel ban list" - # - # - When listing the active 'bans' for a given channel, - # a server is required to send the list back using the - # RPL_BANLIST and RPL_ENDOFBANLIST messages. A separate - # RPL_BANLIST is sent for each active banmask. After the - # banmasks have been listed (or if none present) a - # RPL_ENDOFBANLIST MUST be sent. - # - RPL_INFO=371 - # ":<string>" - RPL_ENDOFINFO=374 - # ":End of INFO list" - # - # - A server responding to an INFO message is required to - # send all its 'info' in a series of RPL_INFO messages - # with a RPL_ENDOFINFO reply to indicate the end of the - # replies. - # - RPL_MOTDSTART=375 - # ":- <server> Message of the day - " - RPL_MOTD=372 - # ":- <text>" - RPL_ENDOFMOTD=376 - # ":End of MOTD command" - # - # - When responding to the MOTD message and the MOTD file - # is found, the file is displayed line by line, with - # each line no longer than 80 characters, using - # RPL_MOTD format replies. These MUST be surrounded - # by a RPL_MOTDSTART (before the RPL_MOTDs) and an - # RPL_ENDOFMOTD (after). - # - RPL_YOUREOPER=381 - # ":You are now an IRC operator" - # - # - RPL_YOUREOPER is sent back to a client which has - # just successfully issued an OPER message and gained - # operator status. - # - RPL_REHASHING=382 - # "<config file> :Rehashing" - # - # - If the REHASH option is used and an operator sends - # a REHASH message, an RPL_REHASHING is sent back to - # the operator. - # - RPL_YOURESERVICE=383 - # "You are service <servicename>" - # - # - Sent by the server to a service upon successful - # registration. - # - RPL_TIME=391 - # "<server> :<string showing server's local time>" - # - # - When replying to the TIME message, a server MUST send - # the reply using the RPL_TIME format above. The string - # showing the time need only contain the correct day and - # time there. There is no further requirement for the - # time string. - # - RPL_USERSSTART=392 - # ":UserID Terminal Host" - RPL_USERS=393 - # ":<username> <ttyline> <hostname>" - RPL_ENDOFUSERS=394 - # ":End of users" - RPL_NOUSERS=395 - # ":Nobody logged in" - # - # - If the USERS message is handled by a server, the - # replies RPL_USERSTART, RPL_USERS, RPL_ENDOFUSERS and - # RPL_NOUSERS are used. RPL_USERSSTART MUST be sent - # first, following by either a sequence of RPL_USERS - # or a single RPL_NOUSER. Following this is - # RPL_ENDOFUSERS. - # - RPL_TRACELINK=200 - # "Link <version & debug level> <destination> - # <next server> V<protocol version> - # <link uptime in seconds> <backstream sendq> - # <upstream sendq>" - RPL_TRACECONNECTING=201 - # "Try. <class> <server>" - RPL_TRACEHANDSHAKE=202 - # "H.S. <class> <server>" - RPL_TRACEUNKNOWN=203 - # "???? <class> [<client IP address in dot form>]" - RPL_TRACEOPERATOR=204 - # "Oper <class> <nick>" - RPL_TRACEUSER=205 - # "User <class> <nick>" - RPL_TRACESERVER=206 - # "Serv <class> <int>S <int>C <server> - # <nick!user|*!*>@<host|server> V<protocol version>" - RPL_TRACESERVICE=207 - # "Service <class> <name> <type> <active type>" - RPL_TRACENEWTYPE=208 - # "<newtype> 0 <client name>" - RPL_TRACECLASS=209 - # "Class <class> <count>" - RPL_TRACERECONNECT=210 - # Unused. - RPL_TRACELOG=261 - # "File <logfile> <debug level>" - RPL_TRACEEND=262 - # "<server name> <version & debug level> :End of TRACE" - # - # - The RPL_TRACE* are all returned by the server in - # response to the TRACE message. How many are - # returned is dependent on the TRACE message and - # whether it was sent by an operator or not. There - # is no predefined order for which occurs first. - # Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and - # RPL_TRACEHANDSHAKE are all used for connections - # which have not been fully established and are either - # unknown, still attempting to connect or in the - # process of completing the 'server handshake'. - # RPL_TRACELINK is sent by any server which handles - # a TRACE message and has to pass it on to another - # server. The list of RPL_TRACELINKs sent in - # response to a TRACE command traversing the IRC - # network should reflect the actual connectivity of - # the servers themselves along that path. - # - # RPL_TRACENEWTYPE is to be used for any connection - # which does not fit in the other categories but is - # being displayed anyway. - # RPL_TRACEEND is sent to indicate the end of the list. - # - RPL_STATSLINKINFO=211 - # "<linkname> <sendq> <sent messages> - # <sent Kbytes> <received messages> - # <received Kbytes> <time open>" - # - # - reports statistics on a connection. <linkname> - # identifies the particular connection, <sendq> is - # the amount of data that is queued and waiting to be - # sent <sent messages> the number of messages sent, - # and <sent Kbytes> the amount of data sent, in - # Kbytes. <received messages> and <received Kbytes> - # are the equivalent of <sent messages> and <sent - # Kbytes> for received data, respectively. <time - # open> indicates how long ago the connection was - # opened, in seconds. - # - RPL_STATSCOMMANDS=212 - # "<command> <count> <byte count> <remote count>" - # - # - reports statistics on commands usage. - # - RPL_ENDOFSTATS=219 - # "<stats letter> :End of STATS report" - # - RPL_STATSUPTIME=242 - # ":Server Up %d days %d:%02d:%02d" - # - # - reports the server uptime. - # - RPL_STATSOLINE=243 - # "O <hostmask> * <name>" - # - # - reports the allowed hosts from where user may become IRC - # operators. - # - RPL_UMODEIS=221 - # "<user mode string>" - # - # - To answer a query about a client's own mode, - # RPL_UMODEIS is sent back. - # - RPL_SERVLIST=234 - # "<name> <server> <mask> <type> <hopcount> <info>" - # - RPL_SERVLISTEND=235 - # "<mask> <type> :End of service listing" - # - # - When listing services in reply to a SERVLIST message, - # a server is required to send the list back using the - # RPL_SERVLIST and RPL_SERVLISTEND messages. A separate - # RPL_SERVLIST is sent for each service. After the - # services have been listed (or if none present) a - # RPL_SERVLISTEND MUST be sent. - # - RPL_LUSERCLIENT=251 - # ":There are <integer> users and <integer> - # services on <integer> servers" - RPL_LUSEROP=252 - # "<integer> :operator(s) online" - RPL_LUSERUNKNOWN=253 - # "<integer> :unknown connection(s)" - RPL_LUSERCHANNELS=254 - # "<integer> :channels formed" - RPL_LUSERME=255 - # ":I have <integer> clients and <integer> - # servers" - # - # - In processing an LUSERS message, the server - # sends a set of replies from RPL_LUSERCLIENT, - # RPL_LUSEROP, RPL_USERUNKNOWN, - # RPL_LUSERCHANNELS and RPL_LUSERME. When - # replying, a server MUST send back - # RPL_LUSERCLIENT and RPL_LUSERME. The other - # replies are only sent back if a non-zero count - # is found for them. - # - RPL_ADMINME=256 - # "<server> :Administrative info" - RPL_ADMINLOC1=257 - # ":<admin info>" - RPL_ADMINLOC2=258 - # ":<admin info>" - RPL_ADMINEMAIL=259 - # ":<admin info>" - # - # - When replying to an ADMIN message, a server - # is expected to use replies RPL_ADMINME - # through to RPL_ADMINEMAIL and provide a text - # message with each. For RPL_ADMINLOC1 a - # description of what city, state and country - # the server is in is expected, followed by - # details of the institution (RPL_ADMINLOC2) - # and finally the administrative contact for the - # server (an email address here is REQUIRED) - # in RPL_ADMINEMAIL. - # - RPL_TRYAGAIN=263 - # "<command> :Please wait a while and try again." - # - # - When a server drops a command without processing it, - # it MUST use the reply RPL_TRYAGAIN to inform the - # originating client. - # - # 5.2 Error Replies - # - # Error replies are found in the range from 400 to 599. - # - ERR_NOSUCHNICK=401 - # "<nickname> :No such nick/channel" - # - # - Used to indicate the nickname parameter supplied to a - # command is currently unused. - # - ERR_NOSUCHSERVER=402 - # "<server name> :No such server" - # - # - Used to indicate the server name given currently - # does not exist. - # - ERR_NOSUCHCHANNEL=403 - # "<channel name> :No such channel" - # - # - Used to indicate the given channel name is invalid. - # - ERR_CANNOTSENDTOCHAN=404 - # "<channel name> :Cannot send to channel" - # - # - Sent to a user who is either (a) not on a channel - # which is mode +n or (b) not a chanop (or mode +v) on - # a channel which has mode +m set or where the user is - # banned and is trying to send a PRIVMSG message to - # that channel. - # - ERR_TOOMANYCHANNELS=405 - # "<channel name> :You have joined too many channels" - # - # - Sent to a user when they have joined the maximum - # number of allowed channels and they try to join - # another channel. - # - ERR_WASNOSUCHNICK=406 - # "<nickname> :There was no such nickname" - # - # - Returned by WHOWAS to indicate there is no history - # information for that nickname. - # - ERR_TOOMANYTARGETS=407 - # "<target> :<error code> recipients. <abort message>" - # - # - Returned to a client which is attempting to send a - # PRIVMSG/NOTICE using the user@host destination format - # and for a user@host which has several occurrences. - # - # - Returned to a client which trying to send a - # PRIVMSG/NOTICE to too many recipients. - # - # - Returned to a client which is attempting to JOIN a safe - # channel using the shortname when there are more than one - # such channel. - # - ERR_NOSUCHSERVICE=408 - # "<service name> :No such service" - # - # - Returned to a client which is attempting to send a SQUERY - # to a service which does not exist. - # - ERR_NOORIGIN=409 - # ":No origin specified" - # - # - PING or PONG message missing the originator parameter. - # - ERR_NORECIPIENT=411 - # ":No recipient given (<command>)" - ERR_NOTEXTTOSEND=412 - # ":No text to send" - ERR_NOTOPLEVEL=413 - # "<mask> :No toplevel domain specified" - ERR_WILDTOPLEVEL=414 - # "<mask> :Wildcard in toplevel domain" - ERR_BADMASK=415 - # "<mask> :Bad Server/host mask" - # - # - 412 - 415 are returned by PRIVMSG to indicate that - # the message wasn't delivered for some reason. - # ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that - # are returned when an invalid use of - # "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted. - # - ERR_UNKNOWNCOMMAND=421 - # "<command> :Unknown command" - # - # - Returned to a registered client to indicate that the - # command sent is unknown by the server. - # - ERR_NOMOTD=422 - # ":MOTD File is missing" - # - # - Server's MOTD file could not be opened by the server. - # - ERR_NOADMININFO=423 - # "<server> :No administrative info available" - # - # - Returned by a server in response to an ADMIN message - # when there is an error in finding the appropriate - # information. - # - ERR_FILEERROR=424 - # ":File error doing <file op> on <file>" - # - # - Generic error message used to report a failed file - # operation during the processing of a message. - # - ERR_NONICKNAMEGIVEN=431 - # ":No nickname given" - # - # - Returned when a nickname parameter expected for a - # command and isn't found. - # - ERR_ERRONEUSNICKNAME=432 - # "<nick> :Erroneous nickname" - # - # - Returned after receiving a NICK message which contains - # characters which do not fall in the defined set. See - # section 2.3.1 for details on valid nicknames. - # - ERR_NICKNAMEINUSE=433 - # "<nick> :Nickname is already in use" - # - # - Returned when a NICK message is processed that results - # in an attempt to change to a currently existing - # nickname. - # - ERR_NICKCOLLISION=436 - # "<nick> :Nickname collision KILL from <user>@<host>" - # - # - Returned by a server to a client when it detects a - # nickname collision (registered of a NICK that - # already exists by another server). - # - ERR_UNAVAILRESOURCE=437 - # "<nick/channel> :Nick/channel is temporarily unavailable" - # - # - Returned by a server to a user trying to join a channel - # currently blocked by the channel delay mechanism. - # - # - Returned by a server to a user trying to change nickname - # when the desired nickname is blocked by the nick delay - # mechanism. - # - ERR_USERNOTINCHANNEL=441 - # "<nick> <channel> :They aren't on that channel" - # - # - Returned by the server to indicate that the target - # user of the command is not on the given channel. - # - ERR_NOTONCHANNEL=442 - # "<channel> :You're not on that channel" - # - # - Returned by the server whenever a client tries to - # perform a channel affecting command for which the - # client isn't a member. - # - ERR_USERONCHANNEL=443 - # "<user> <channel> :is already on channel" - # - # - Returned when a client tries to invite a user to a - # channel they are already on. - # - ERR_NOLOGIN=444 - # "<user> :User not logged in" - # - # - Returned by the summon after a SUMMON command for a - # user was unable to be performed since they were not - # logged in. - # - # - ERR_SUMMONDISABLED=445 - # ":SUMMON has been disabled" - # - # - Returned as a response to the SUMMON command. MUST be - # returned by any server which doesn't implement it. - # - ERR_USERSDISABLED=446 - # ":USERS has been disabled" - # - # - Returned as a response to the USERS command. MUST be - # returned by any server which does not implement it. - # - ERR_NOTREGISTERED=451 - # ":You have not registered" - # - # - Returned by the server to indicate that the client - # MUST be registered before the server will allow it - # to be parsed in detail. - # - ERR_NEEDMOREPARAMS=461 - # "<command> :Not enough parameters" - # - # - Returned by the server by numerous commands to - # indicate to the client that it didn't supply enough - # parameters. - # - ERR_ALREADYREGISTRED=462 - # ":Unauthorized command (already registered)" - # - # - Returned by the server to any link which tries to - # change part of the registered details (such as - # password or user details from second USER message). - # - ERR_NOPERMFORHOST=463 - # ":Your host isn't among the privileged" - # - # - Returned to a client which attempts to register with - # a server which does not been setup to allow - # connections from the host the attempted connection - # is tried. - # - ERR_PASSWDMISMATCH=464 - # ":Password incorrect" - # - # - Returned to indicate a failed attempt at registering - # a connection for which a password was required and - # was either not given or incorrect. - # - ERR_YOUREBANNEDCREEP=465 - # ":You are banned from this server" - # - # - Returned after an attempt to connect and register - # yourself with a server which has been setup to - # explicitly deny connections to you. - # - ERR_YOUWILLBEBANNED=466 - # - # - Sent by a server to a user to inform that access to the - # server will soon be denied. - # - ERR_KEYSET=467 - # "<channel> :Channel key already set" - ERR_CHANNELISFULL=471 - # "<channel> :Cannot join channel (+l)" - ERR_UNKNOWNMODE=472 - # "<char> :is unknown mode char to me for <channel>" - ERR_INVITEONLYCHAN=473 - # "<channel> :Cannot join channel (+i)" - ERR_BANNEDFROMCHAN=474 - # "<channel> :Cannot join channel (+b)" - ERR_BADCHANNELKEY=475 - # "<channel> :Cannot join channel (+k)" - ERR_BADCHANMASK=476 - # "<channel> :Bad Channel Mask" - ERR_NOCHANMODES=477 - # "<channel> :Channel doesn't support modes" - ERR_BANLISTFULL=478 - # "<channel> <char> :Channel list is full" - # - ERR_NOPRIVILEGES=481 - # ":Permission Denied- You're not an IRC operator" - # - # - Any command requiring operator privileges to operate - # MUST return this error to indicate the attempt was - # unsuccessful. - # - ERR_CHANOPRIVSNEEDED=482 - # "<channel> :You're not channel operator" - # - # - Any command requiring 'chanop' privileges (such as - # MODE messages) MUST return this error if the client - # making the attempt is not a chanop on the specified - # channel. - # - # - ERR_CANTKILLSERVER=483 - # ":You can't kill a server!" - # - # - Any attempts to use the KILL command on a server - # are to be refused and this error returned directly - # to the client. - # - ERR_RESTRICTED=484 - # ":Your connection is restricted!" - # - # - Sent by the server to a user upon connection to indicate - # the restricted nature of the connection (user mode "+r"). - # - ERR_UNIQOPPRIVSNEEDED=485 - # ":You're not the original channel operator" - # - # - Any MODE requiring "channel creator" privileges MUST - # return this error if the client making the attempt is not - # a chanop on the specified channel. - # - ERR_NOOPERHOST=491 - # ":No O-lines for your host" - # - # - If a client sends an OPER message and the server has - # not been configured to allow connections from the - # client's host as an operator, this error MUST be - # returned. - # - ERR_UMODEUNKNOWNFLAG=501 - # ":Unknown MODE flag" - # - # - Returned by the server to indicate that a MODE - # message was sent with a nickname parameter and that - # the a mode flag sent was not recognized. - # - ERR_USERSDONTMATCH=502 - # ":Cannot change mode for other users" - # - # - Error sent to any user trying to view or change the - # user mode for a user other than themselves. - # - # 5.3 Reserved numerics - # - # These numerics are not described above since they fall into one of - # the following categories: - # - # 1. no longer in use; - # - # 2. reserved for future planned use; - # - # 3. in current use but are part of a non-generic 'feature' of - # the current IRC server. - RPL_SERVICEINFO=231 - RPL_ENDOFSERVICES=232 - RPL_SERVICE=233 - RPL_NONE=300 - RPL_WHOISCHANOP=316 - RPL_KILLDONE=361 - RPL_CLOSING=362 - RPL_CLOSEEND=363 - RPL_INFOSTART=373 - RPL_MYPORTIS=384 - RPL_STATSCLINE=213 - RPL_STATSNLINE=214 - RPL_STATSILINE=215 - RPL_STATSKLINE=216 - RPL_STATSQLINE=217 - RPL_STATSYLINE=218 - RPL_STATSVLINE=240 - RPL_STATSLLINE=241 - RPL_STATSHLINE=244 - RPL_STATSSLINE=244 - RPL_STATSPING=246 - RPL_STATSBLINE=247 - RPL_STATSDLINE=250 - ERR_NOSERVICEHOST=492 - - # implements RFC 2812 and prior IRC RFCs. - # clients register handler proc{}s for different server events and IrcClient - # handles dispatch - class IrcClient - # create a new IrcClient instance - def initialize - @handlers = Hash.new - @users = Array.new - end - - # key:: server event to handle - # value:: proc object called when event occurs - # set a handler for a server event - # - # ==server events currently supported: - # - # PING:: server pings you (default handler returns a pong) - # NICKTAKEN:: you tried to change nick to one that's in use - # BADNICK:: you tried to change nick to one that's invalid - # TOPIC:: someone changed the topic of a channel - # TOPICINFO:: on joining a channel or asking for the topic, tells you - # who set it and when - # NAMES:: server sends list of channel members when you join - # WELCOME:: server welcome message on connect - # MOTD:: server message of the day - # PRIVMSG:: privmsg, the core of IRC, a message to you from someone - # PUBLIC:: optionally instead of getting privmsg you can hook to only - # the public ones... - # MSG:: or only the private ones, or both - # KICK:: someone got kicked from a channel - # PART:: someone left a channel - # QUIT:: someone quit IRC - # JOIN:: someone joined a channel - # CHANGETOPIC:: the topic of a channel changed - # INVITE:: you are invited to a channel - # NICK:: someone changed their nick - # MODE:: a mode change - # NOTICE:: someone sends you a notice - # UNKNOWN:: any other message not handled by the above - def []=(key, value) - @handlers[key] = value - end - - # key:: event name - # remove a handler for a server event - def deletehandler(key) - @handlers.delete(key) - end - - # takes a server string, checks for PING, PRIVMSG, NOTIFY, etc, and parses - # numeric server replies, calling the appropriate handler for each, and - # sending it a hash containing the data from the server - def process(serverstring) - data = Hash.new - data["SERVERSTRING"] = serverstring - - unless serverstring =~ /^(:(\S+)\s)?(\S+)(\s(.*))?/ - raise "Unparseable Server Message!!!: #{serverstring}" - end - - prefix, command, params = $2, $3, $5 - - if prefix != nil - data['SOURCE'] = prefix - if prefix =~ /^(\S+)!(\S+)$/ - data['SOURCENICK'] = $1 - data['SOURCEADDRESS'] = $2 - end - end - - # split parameters in an array - argv = [] - params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params - - case command - when 'PING' - data['PINGID'] = argv[0] - handle('PING', data) - when /^(\d+)$/ # numeric server message - num=command.to_i - case num - when ERR_NICKNAMEINUSE - # "* <nick> :Nickname is already in use" - data['NICK'] = argv[1] - data['MESSAGE'] = argv[2] - handle('NICKTAKEN', data) - when ERR_ERRONEUSNICKNAME - # "* <nick> :Erroneous nickname" - data['NICK'] = argv[1] - data['MESSAGE'] = argv[2] - handle('BADNICK', data) - when RPL_TOPIC - data['CHANNEL'] = argv[1] - data['TOPIC'] = argv[2] - handle('TOPIC', data) - when RPL_TOPIC_INFO - data['NICK'] = argv[0] - data['CHANNEL'] = argv[1] - data['SOURCE'] = argv[2] - data['UNIXTIME'] = argv[3] - handle('TOPICINFO', data) - when RPL_NAMREPLY - # "( "=" / "*" / "@" ) <channel> - # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> ) - # - "@" is used for secret channels, "*" for private - # channels, and "=" for others (public channels). - argv[3].scan(/\S+/).each { |u| - if(u =~ /^([@+])?(.*)$/) - umode = $1 || "" - user = $2 - @users << [user, umode] - end - } - when RPL_ENDOFNAMES - data['CHANNEL'] = argv[1] - data['USERS'] = @users - handle('NAMES', data) - @users = Array.new - when RPL_WELCOME - # "Welcome to the Internet Relay Network - # <nick>!<user>@<host>" - case argv[1] - when /((\S+)!(\S+))/ - data['NETMASK'] = $1 - data['NICK'] = $2 - data['ADDRESS'] = $3 - when /Welcome to the Internet Relay Network\s(\S+)/ - data['NICK'] = $1 - when /Welcome.*\s+(\S+)$/ - data['NICK'] = $1 - when /^(\S+)$/ - data['NICK'] = $1 - end - handle('WELCOME', data) - when RPL_MOTDSTART - # "<nick> :- <server> Message of the Day -" - if argv[1] =~ /^-\s+(\S+)\s/ - server = $1 - @motd = "" - end - when RPL_MOTD - if(argv[1] =~ /^-\s+(.*)$/) - @motd << $1 - @motd << "\n" - end - when RPL_ENDOFMOTD - data['MOTD'] = @motd - handle('MOTD', data) - else - handle('UNKNOWN', data) - end - # end of numeric replies - when 'PRIVMSG' - # you can either bind to 'PRIVMSG', to get every one and - # parse it yourself, or you can bind to 'MSG', 'PUBLIC', - # etc and get it all nicely split up for you. - data['TARGET'] = argv[0] - data['MESSAGE'] = argv[1] - handle('PRIVMSG', data) - - # Now we split it - if(data['TARGET'] =~ /^(#|&).*/) - handle('PUBLIC', data) - else - handle('MSG', data) - end - when 'KICK' - data['CHANNEL'] = argv[0] - data['TARGET'] = argv[1] - data['MESSAGE'] = argv[2] - handle('KICK', data) - when 'PART' - data['CHANNEL'] = argv[0] - data['MESSAGE'] = argv[1] - handle('PART', data) - when 'QUIT' - data['MESSAGE'] = argv[0] - handle('QUIT', data) - when 'JOIN' - data['CHANNEL'] = argv[0] - handle('JOIN', data) - when 'TOPIC' - data['CHANNEL'] = argv[0] - data['TOPIC'] = argv[1] - handle('CHANGETOPIC', data) - when 'INVITE' - data['TARGET'] = argv[0] - data['CHANNEL'] = argv[1] - handle('INVITE', data) - when 'NICK' - data['NICK'] = argv[0] - handle('NICK', data) - when 'MODE' - data['CHANNEL'] = argv[0] - data['MODESTRING'] = argv[1] - data['TARGETS'] = argv[2] - handle('MODE', data) - when 'NOTICE' - data['TARGET'] = argv[0] - data['MESSAGE'] = argv[1] - if data['SOURCENICK'] - handle('NOTICE', data) - else - # "server notice" (not from user, noone to reply to - handle('SNOTICE', data) - end - else - handle('UNKNOWN', data) - end - end - - private - - # key:: server event name - # data:: hash containing data about the event, passed to the proc - # call client's proc for an event, if they set one as a handler - def handle(key, data) - if(@handlers.has_key?(key)) - @handlers[key].call(data) - end - end - end -end diff --git a/rbot/timer.rb b/rbot/timer.rb deleted file mode 100644 index 64b060ba..00000000 --- a/rbot/timer.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Timer - - # timer event, something to do and when/how often to do it - class Action - - # when this action is due next (updated by tick()) - attr_accessor :in - - # is this action blocked? if so it won't be run - attr_accessor :blocked - - # period:: how often (seconds) to run the action - # data:: optional data to pass to the proc - # once:: optional, if true, this action will be run once then removed - # func:: associate a block to be called to perform the action - # - # create a new action - def initialize(period, data=nil, once=false, &func) - @blocked = false - @period = period - @in = period - @func = func - @data = data - @once = once - end - - # run the action by calling its proc - def run - @in += @period - if(@data) - @func.call(@data) - else - @func.call - end - return @once - end - end - - # timer handler, manage multiple Action objects, calling them when required. - # The timer must be ticked by whatever controls it, i.e. regular calls to - # tick() at whatever granularity suits your application's needs. - # Alternatively you can call run(), and the timer will tick itself, but this - # blocks so you gotta do it in a thread (remember ruby's threads block on - # syscalls so that can suck). - class Timer - def initialize - @timers = Array.new - @handle = 0 - @lasttime = 0 - end - - # period:: how often (seconds) to run the action - # data:: optional data to pass to the action's proc - # func:: associate a block with add() to perform the action - # - # add an action to the timer - def add(period, data=nil, &func) - @handle += 1 - @timers[@handle] = Action.new(period, data, &func) - return @handle - end - - # period:: how often (seconds) to run the action - # data:: optional data to pass to the action's proc - # func:: associate a block with add() to perform the action - # - # add an action to the timer which will be run just once, after +period+ - def add_once(period, data=nil, &func) - @handle += 1 - @timers[@handle] = Action.new(period, data, true, &func) - return @handle - end - - # remove action with handle +handle+ from the timer - def remove(handle) - @timers.delete_at(handle) - end - - # block action with handle +handle+ - def block(handle) - @timers[handle].blocked = true - end - - # unblock action with handle +handle+ - def unblock(handle) - @timers[handle].blocked = false - end - - # you can call this when you know you're idle, or you can split off a - # thread and call the run() method to do it for you. - def tick - if(@lasttime != 0) - diff = (Time.now - @lasttime).to_f - @lasttime = Time.now - @timers.compact.each { |timer| - timer.in = timer.in - diff - } - @timers.compact.each { |timer| - if (!timer.blocked) - if(timer.in <= 0) - if(timer.run) - # run once - @timers.delete(timer) - end - end - end - } - else - # don't do anything on the first tick - @lasttime = Time.now - end - end - - # the timer will tick() itself. this blocks, so run it in a thread, and - # watch out for blocking syscalls - def run(granularity=0.1) - while(true) - sleep(granularity) - tick - end - end - end -end diff --git a/rbot/utils.rb b/rbot/utils.rb deleted file mode 100644 index b22a417d..00000000 --- a/rbot/utils.rb +++ /dev/null @@ -1,778 +0,0 @@ -require 'net/http' -require 'uri' - -module Irc - - # miscellaneous useful functions - module Utils - # read a time in string format, turn it into "seconds from now". - # example formats handled are "5 minutes", "2 days", "five hours", - # "11:30", "15:45:11", "one day", etc. - # - # Throws:: RunTimeError "invalid time string" on parse failure - def Utils.timestr_offset(timestr) - case timestr - when (/^(\S+)\s+(\S+)$/) - mult = $1 - unit = $2 - if(mult =~ /^([\d.]+)$/) - num = $1.to_f - raise "invalid time string" unless num - else - case mult - when(/^(one|an|a)$/) - num = 1 - when(/^two$/) - num = 2 - when(/^three$/) - num = 3 - when(/^four$/) - num = 4 - when(/^five$/) - num = 5 - when(/^six$/) - num = 6 - when(/^seven$/) - num = 7 - when(/^eight$/) - num = 8 - when(/^nine$/) - num = 9 - when(/^ten$/) - num = 10 - when(/^fifteen$/) - num = 15 - when(/^twenty$/) - num = 20 - when(/^thirty$/) - num = 30 - when(/^sixty$/) - num = 60 - else - raise "invalid time string" - end - end - case unit - when (/^(s|sec(ond)?s?)$/) - return num - when (/^(m|min(ute)?s?)$/) - return num * 60 - when (/^(h|h(ou)?rs?)$/) - return num * 60 * 60 - when (/^(d|days?)$/) - return num * 60 * 60 * 24 - else - raise "invalid time string" - end - when (/^(\d+):(\d+):(\d+)$/) - hour = $1.to_i - min = $2.to_i - sec = $3.to_i - now = Time.now - later = Time.mktime(now.year, now.month, now.day, hour, min, sec) - return later - now - when (/^(\d+):(\d+)$/) - hour = $1.to_i - min = $2.to_i - now = Time.now - later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec) - return later - now - when (/^(\d+):(\d+)(am|pm)$/) - hour = $1.to_i - min = $2.to_i - ampm = $3 - if ampm == "pm" - hour += 12 - end - now = Time.now - later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec) - return later - now - when (/^(\S+)$/) - num = 1 - unit = $1 - case unit - when (/^(s|sec(ond)?s?)$/) - return num - when (/^(m|min(ute)?s?)$/) - return num * 60 - when (/^(h|h(ou)?rs?)$/) - return num * 60 * 60 - when (/^(d|days?)$/) - return num * 60 * 60 * 24 - else - raise "invalid time string" - end - else - raise "invalid time string" - end - end - - # turn a number of seconds into a human readable string, e.g - # 2 days, 3 hours, 18 minutes, 10 seconds - def Utils.secs_to_string(secs) - ret = "" - days = (secs / (60 * 60 * 24)).to_i - secs = secs % (60 * 60 * 24) - hours = (secs / (60 * 60)).to_i - secs = (secs % (60 * 60)) - mins = (secs / 60).to_i - secs = (secs % 60).to_i - ret += "#{days} days, " if days > 0 - ret += "#{hours} hours, " if hours > 0 || days > 0 - ret += "#{mins} minutes and " if mins > 0 || hours > 0 || days > 0 - ret += "#{secs} seconds" - return ret - end - - def Utils.safe_exec(command, *args) - IO.popen("-") {|p| - if(p) - return p.readlines.join("\n") - else - begin - $stderr = $stdout - exec(command, *args) - rescue Exception => e - puts "exec of #{command} led to exception: #{e}" - Kernel::exit! 0 - end - puts "exec of #{command} failed" - Kernel::exit! 0 - end - } - end - - # returns a string containing the result of an HTTP GET on the uri - def Utils.http_get(uristr, readtimeout=8, opentimeout=4) - - # ruby 1.7 or better needed for this (or 1.6 and debian unstable) - Net::HTTP.version_1_2 - # (so we support the 1_1 api anyway, avoids problems) - - uri = URI.parse uristr - query = uri.path - if uri.query - query += "?#{uri.query}" - end - - proxy_host = nil - proxy_port = nil - if(ENV['http_proxy'] && proxy_uri = URI.parse(ENV['http_proxy'])) - proxy_host = proxy_uri.host - proxy_port = proxy_uri.port - end - - begin - http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port) - http.open_timeout = opentimeout - http.read_timeout = readtimeout - - http.start {|http| - resp = http.get(query) - if resp.code == "200" - return resp.body - end - } - rescue => e - # cheesy for now - $stderr.puts "Utils.http_get exception: #{e}, while trying to get #{uristr}" - return nil - end - end - - # This is nasty-ass. I hate writing parsers. - class Metar - attr_reader :decoded - attr_reader :input - attr_reader :date - attr_reader :nodata - def initialize(string) - str = nil - @nodata = false - string.each_line {|l| - if str == nil - # grab first line (date) - @date = l.chomp.strip - str = "" - else - if(str == "") - str = l.chomp.strip - else - str += " " + l.chomp.strip - end - end - } - if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/ - # 2002/02/26 05:00 - @date = Time.gm($1, $2, $3, $4, $5, 0) - else - @date = Time.now - end - @input = str.chomp - @cloud_layers = 0 - @cloud_coverage = { - 'SKC' => '0', - 'CLR' => '0', - 'VV' => '8/8', - 'FEW' => '1/8 - 2/8', - 'SCT' => '3/8 - 4/8', - 'BKN' => '5/8 - 7/8', - 'OVC' => '8/8' - } - @wind_dir_texts = [ - 'North', - 'North/Northeast', - 'Northeast', - 'East/Northeast', - 'East', - 'East/Southeast', - 'Southeast', - 'South/Southeast', - 'South', - 'South/Southwest', - 'Southwest', - 'West/Southwest', - 'West', - 'West/Northwest', - 'Northwest', - 'North/Northwest', - 'North' - ] - @wind_dir_texts_short = [ - 'N', - 'N/NE', - 'NE', - 'E/NE', - 'E', - 'E/SE', - 'SE', - 'S/SE', - 'S', - 'S/SW', - 'SW', - 'W/SW', - 'W', - 'W/NW', - 'NW', - 'N/NW', - 'N' - ] - @weather_array = { - 'MI' => 'Mild ', - 'PR' => 'Partial ', - 'BC' => 'Patches ', - 'DR' => 'Low Drifting ', - 'BL' => 'Blowing ', - 'SH' => 'Shower(s) ', - 'TS' => 'Thunderstorm ', - 'FZ' => 'Freezing', - 'DZ' => 'Drizzle ', - 'RA' => 'Rain ', - 'SN' => 'Snow ', - 'SG' => 'Snow Grains ', - 'IC' => 'Ice Crystals ', - 'PE' => 'Ice Pellets ', - 'GR' => 'Hail ', - 'GS' => 'Small Hail and/or Snow Pellets ', - 'UP' => 'Unknown ', - 'BR' => 'Mist ', - 'FG' => 'Fog ', - 'FU' => 'Smoke ', - 'VA' => 'Volcanic Ash ', - 'DU' => 'Widespread Dust ', - 'SA' => 'Sand ', - 'HZ' => 'Haze ', - 'PY' => 'Spray', - 'PO' => 'Well-Developed Dust/Sand Whirls ', - 'SQ' => 'Squalls ', - 'FC' => 'Funnel Cloud Tornado Waterspout ', - 'SS' => 'Sandstorm/Duststorm ' - } - @cloud_condition_array = { - 'SKC' => 'clear', - 'CLR' => 'clear', - 'VV' => 'vertical visibility', - 'FEW' => 'a few', - 'SCT' => 'scattered', - 'BKN' => 'broken', - 'OVC' => 'overcast' - } - @strings = { - 'mm_inches' => '%s mm (%s inches)', - 'precip_a_trace' => 'a trace', - 'precip_there_was' => 'There was %s of precipitation ', - 'sky_str_format1' => 'There were %s at a height of %s meters (%s feet)', - 'sky_str_clear' => 'The sky was clear', - 'sky_str_format2' => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)', - 'sky_str_format3' => ' and %s at a height of %s meters (%s feet)', - 'clouds' => ' clouds', - 'clouds_cb' => ' cumulonimbus clouds', - 'clouds_tcu' => ' towering cumulus clouds', - 'visibility_format' => 'The visibility was %s kilometers (%s miles).', - 'wind_str_format1' => 'blowing at a speed of %s meters per second (%s miles per hour)', - 'wind_str_format2' => ', with gusts to %s meters per second (%s miles per hour),', - 'wind_str_format3' => ' from the %s', - 'wind_str_calm' => 'calm', - 'precip_last_hour' => 'in the last hour. ', - 'precip_last_6_hours' => 'in the last 3 to 6 hours. ', - 'precip_last_24_hours' => 'in the last 24 hours. ', - 'precip_snow' => 'There is %s mm (%s inches) of snow on the ground. ', - 'temp_min_max_6_hours' => 'The maximum and minimum temperatures over the last 6 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit).', - 'temp_max_6_hours' => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ', - 'temp_min_6_hours' => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ', - 'temp_min_max_24_hours' => 'The maximum and minimum temperatures over the last 24 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit). ', - 'light' => 'Light ', - 'moderate' => 'Moderate ', - 'heavy' => 'Heavy ', - 'mild' => 'Mild ', - 'nearby' => 'Nearby ', - 'current_weather' => 'Current weather is %s. ', - 'pretty_print_metar' => '%s on %s, the wind was %s at %s. The temperature was %s degrees Celsius (%s degrees Fahrenheit), and the pressure was %s hPa (%s inHg). The relative humidity was %s%%. %s %s %s %s %s' - } - - parse - end - - def store_speed(value, windunit, meterspersec, knots, milesperhour) - # Helper function to convert and store speed based on unit. - # &$meterspersec, &$knots and &$milesperhour are passed on - # reference - if (windunit == 'KT') - # The windspeed measured in knots: - @decoded[knots] = sprintf("%.2f", value) - # The windspeed measured in meters per second, rounded to one decimal place: - @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444) - # The windspeed measured in miles per hour, rounded to one decimal place: */ - @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667) - elsif (windunit == 'MPS') - # The windspeed measured in meters per second: - @decoded[meterspersec] = sprintf("%.2f", value) - # The windspeed measured in knots, rounded to one decimal place: - @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444) - #The windspeed measured in miles per hour, rounded to one decimal place: - @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667) - elsif (windunit == 'KMH') - # The windspeed measured in kilometers per hour: - @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600) - @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444) - # The windspeed measured in miles per hour, rounded to one decimal place: - @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667) - end - end - - def parse - @decoded = Hash.new - puts @input - @input.split(" ").each {|part| - if (part == 'METAR') - # Type of Report: METAR - @decoded['type'] = 'METAR' - elsif (part == 'SPECI') - # Type of Report: SPECI - @decoded['type'] = 'SPECI' - elsif (part == 'AUTO') - # Report Modifier: AUTO - @decoded['report_mod'] = 'AUTO' - elsif (part == 'NIL') - @nodata = true - elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station'))) - # Station Identifier - @decoded['station'] = part - elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/) - # ignore this bit, it's useless without month/year. some of these - # things are hideously out of date. - # now = Time.new - # time = Time.gm(now.year, now.month, $1, $2, $3, 0) - # Date and Time of Report - # @decoded['time'] = time - elsif (part == 'COR') - # Report Modifier: COR - @decoded['report_mod'] = 'COR' - elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/) - # Wind Group - windunit = $3 - # now do ereg to get the actual values - part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/ - if ($1 == 'VRB') - @decoded['wind_deg'] = 'variable directions' - @decoded['wind_dir_text'] = 'variable directions' - @decoded['wind_dir_text_short'] = 'VAR' - else - @decoded['wind_deg'] = $1 - @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round] - @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round] - end - store_speed($2, windunit, - 'wind_meters_per_second', - 'wind_knots', - 'wind_miles_per_hour') - - if ($4 != nil) - # We have a report with information about the gust. - # First we have the gust measured in knots - if ($4 =~ /G([0-9]{2,3})/) - store_speed($1,windunit, - 'wind_gust_meters_per_second', - 'wind_gust_knots', - 'wind_gust_miles_per_hour') - end - end - elsif (part =~ /([0-9]{3})V([0-9]{3})/) - # Variable wind-direction - @decoded['wind_var_beg'] = $1 - @decoded['wind_var_end'] = $2 - elsif (part == "9999") - # A strange value. When you look at other pages you see it - # interpreted like this (where I use > to signify 'Greater - # than'): - @decoded['visibility_miles'] = '>7'; - @decoded['visibility_km'] = '>11.3'; - elsif (part =~ /^([0-9]{4})$/) - # Visibility in meters (4 digits only) - # The visibility measured in kilometers, rounded to one decimal place. - @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000) - # The visibility measured in miles, rounded to one decimal place. - @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344) - elsif (part =~ /^[0-9]$/) - # Temp Visibility Group, single digit followed by space - @decoded['temp_visibility_miles'] = part - elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/) - # Visibility Group - if ($4 == '/') - vis_miles = $2.to_i + $3.to_i/$5.to_i - else - vis_miles = $1.to_i; - end - if (@decoded['temp_visibility_miles'][0] == 'M') - # The visibility measured in miles, prefixed with < to indicate 'Less than' - @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles) - # The visibility measured in kilometers. The value is rounded - # to one decimal place, prefixed with < to indicate 'Less than' */ - @decoded['visibility_km'] = '<' . sprintf("%.1f", vis_miles * 1.609344) - else - # The visibility measured in mile.s */ - @decoded['visibility_miles'] = sprintf("%.1f", vis_miles) - # The visibility measured in kilometers, rounded to one decimal place. - @decoded['visibility_km'] = sprintf("%.1f", vis_miles * 1.609344) - end - elsif (part =~ /^(-|\+|VC|MI)?(TS|SH|FZ|BL|DR|BC|PR|RA|DZ|SN|SG|GR|GS|PE|IC|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+$/) - # Current weather-group - @decoded['weather'] = '' unless @decoded.has_key?('weather') - if (part[0].chr == '-') - # A light phenomenon - @decoded['weather'] += @strings['light'] - part = part[1,part.length] - elsif (part[0].chr == '+') - # A heavy phenomenon - @decoded['weather'] += @strings['heavy'] - part = part[1,part.length] - elsif (part[0,2] == 'VC') - # Proximity Qualifier - @decoded['weather'] += @strings['nearby'] - part = part[2,part.length] - elsif (part[0,2] == 'MI') - @decoded['weather'] += @strings['mild'] - part = part[2,part.length] - else - # no intensity code => moderate phenomenon - @decoded['weather'] += @strings['moderate'] - end - - while (part && bite = part[0,2]) do - # Now we take the first two letters and determine what they - # mean. We append this to the variable so that we gradually - # build up a phrase. - - @decoded['weather'] += @weather_array[bite] - # Here we chop off the two first letters, so that we can take - # a new bite at top of the while-loop. - part = part[2,-1] - end - elsif (part =~ /(SKC|CLR)/) - # Cloud-layer-group. - # There can be up to three of these groups, so we store them as - # cloud_layer1, cloud_layer2 and cloud_layer3. - - @cloud_layers += 1; - # Again we have to translate the code-characters to a - # meaningful string. - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = @cloud_condition_array[$1] - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1] - elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/) - # We have found (another) a cloud-layer-group. There can be up - # to three of these groups, so we store them as cloud_layer1, - # cloud_layer2 and cloud_layer3. - @cloud_layers += 1; - # Again we have to translate the code-characters to a meaningful string. - if ($3 == 'CB') - # cumulonimbus (CB) clouds were observed. */ - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds_cb'] - elsif ($3 == 'TCU') - # towering cumulus (TCU) clouds were observed. - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds_tcu'] - else - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds'] - end - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1] - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100 - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m'] = ($2.to_f * 30.48).round - elsif (part =~ /^T([0-9]{4})$/) - store_temp($1,'temp_c','temp_f') - elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/) - # Temperature/Dew Point Group - # The temperature and dew-point measured in Celsius. - @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-')) - if $2 == "//" || !$2 - @decoded['dew_c'] = 0 - else - @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-')) - end - # The temperature and dew-point measured in Fahrenheit, rounded to - # the nearest degree. - @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round - @decoded['dew_f'] = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round - elsif(part =~ /A([0-9]{4})/) - # Altimeter - # The pressure measured in inHg - @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100) - # The pressure measured in mmHg, hPa and atm - @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254) - @decoded['altimeter_hpa'] = sprintf("%d", ($1.to_f * 0.33863881578947).to_i) - @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 3.3421052631579e-4) - elsif(part =~ /Q([0-9]{4})/) - # Altimeter - # This is strange, the specification doesnt say anything about - # the Qxxxx-form, but it's in the METARs. - # The pressure measured in hPa - @decoded['altimeter_hpa'] = sprintf("%d", $1.to_i) - # The pressure measured in mmHg, inHg and atm - @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827) - @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875) - @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 9.869232667e-4) - elsif (part =~ /^T([0-9]{4})([0-9]{4})/) - # Temperature/Dew Point Group, coded to tenth of degree. - # The temperature and dew-point measured in Celsius. - store_temp($1,'temp_c','temp_f') - store_temp($2,'dew_c','dew_f') - elsif (part =~ /^1([0-9]{4}$)/) - # 6 hour maximum temperature Celsius, coded to tenth of degree - store_temp($1,'temp_max6h_c','temp_max6h_f') - elsif (part =~ /^2([0-9]{4}$)/) - # 6 hour minimum temperature Celsius, coded to tenth of degree - store_temp($1,'temp_min6h_c','temp_min6h_f') - elsif (part =~ /^4([0-9]{4})([0-9]{4})$/) - # 24 hour maximum and minimum temperature Celsius, coded to - # tenth of degree - store_temp($1,'temp_max24h_c','temp_max24h_f') - store_temp($2,'temp_min24h_c','temp_min24h_f') - elsif (part =~ /^P([0-9]{4})/) - # Precipitation during last hour in hundredths of an inch - # (store as inches) - @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif (part =~ /^6([0-9]{4})/) - # Precipitation during last 3 or 6 hours in hundredths of an - # inch (store as inches) - @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif (part =~ /^7([0-9]{4})/) - # Precipitation during last 24 hours in hundredths of an inch - # (store as inches) - @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif(part =~ /^4\/([0-9]{3})/) - # Snow depth in inches - @decoded['snow_in'] = sprintf("%.2f", $1); - @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4) - else - # If we couldn't match the group, we assume that it was a - # remark. - @decoded['remarks'] = '' unless @decoded.has_key?("remarks") - @decoded['remarks'] += ' ' + part; - end - } - - # Relative humidity - # p @decoded['dew_c'] # 11.0 - # p @decoded['temp_c'] # 21.0 - # => 56.1 - @decoded['rel_humidity'] = sprintf("%.1f",100 * - (6.11 * (10.0**(7.5 * @decoded['dew_c'].to_f / (237.7 + @decoded['dew_c'].to_f)))) / (6.11 * (10.0 ** (7.5 * @decoded['temp_c'].to_f / (237.7 + @decoded['temp_c'].to_f))))) if @decoded.has_key?('dew_c') - end - - def store_temp(temp,temp_cname,temp_fname) - # Given a numerical temperature temp in Celsius, coded to tenth of - # degree, store in @decoded[temp_cname], convert to Fahrenheit - # and store in @decoded[temp_fname] - # Note: temp is converted to negative if temp > 100.0 (See - # Federal Meteorological Handbook for groups T, 1, 2 and 4) - - # Temperature measured in Celsius, coded to tenth of degree - temp = temp.to_f/10 - if (temp >100.0) - # first digit = 1 means minus temperature - temp = -(temp - 100.0) - end - @decoded[temp_cname] = sprintf("%.1f", temp) - # The temperature in Fahrenheit. - @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32) - end - - def pretty_print_precip(precip_mm, precip_in) - # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal - # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in - # several places, so standardized in one function. - if (precip_mm.to_i > 0) - amount = sprintf(@strings['mm_inches'], precip_mm, precip_in) - else - amount = @strings['a_trace'] - end - return sprintf(@strings['precip_there_was'], amount) - end - - def pretty_print - if @nodata - return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :(" - end - - ["temp_c", "altimeter_hpa"].each {|key| - if !@decoded.has_key?(key) - return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})" - end - } - - mins_old = ((Time.now - @date.to_i).to_f/60).round - if (mins_old <= 60) - weather_age = mins_old.to_s + " minutes ago," - elsif (mins_old <= 60 * 25) - weather_age = (mins_old / 60).to_s + " hours, " - weather_age += (mins_old % 60).to_s + " minutes ago," - else - # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})" - weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:" - end - - if(@decoded.has_key?("cloud_layer1_altitude_ft")) - sky_str = sprintf(@strings['sky_str_format1'], - @decoded["cloud_layer1_condition"], - @decoded["cloud_layer1_altitude_m"], - @decoded["cloud_layer1_altitude_ft"]) - else - sky_str = @strings['sky_str_clear'] - end - - if(@decoded.has_key?("cloud_layer2_altitude_ft")) - if(@decoded.has_key?("cloud_layer3_altitude_ft")) - sky_str += sprintf(@strings['sky_str_format2'], - @decoded["cloud_layer2_condition"], - @decoded["cloud_layer2_altitude_m"], - @decoded["cloud_layer2_altitude_ft"], - @decoded["cloud_layer3_condition"], - @decoded["cloud_layer3_altitude_m"], - @decoded["cloud_layer3_altitude_ft"]) - else - sky_str += sprintf(@strings['sky_str_format3'], - @decoded["cloud_layer2_condition"], - @decoded["cloud_layer2_altitude_m"], - @decoded["cloud_layer2_altitude_ft"]) - end - end - sky_str += "." - - if(@decoded.has_key?("visibility_miles")) - visibility = sprintf(@strings['visibility_format'], - @decoded["visibility_km"], - @decoded["visibility_miles"]) - else - visibility = "" - end - - if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0) - wind_str = sprintf(@strings['wind_str_format1'], - @decoded["wind_meters_per_second"], - @decoded["wind_miles_per_hour"]) - if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0) - wind_str += sprintf(@strings['wind_str_format2'], - @decoded["wind_gust_meters_per_second"], - @decoded["wind_gust_miles_per_hour"]) - end - wind_str += sprintf(@strings['wind_str_format3'], - @decoded["wind_dir_text"]) - else - wind_str = @strings['wind_str_calm'] - end - - prec_str = "" - if (@decoded.has_key?("precip_in")) - prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour'] - end - if (@decoded.has_key?("precip_6h_in")) - prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours'] - end - if (@decoded.has_key?("precip_24h_in")) - prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours'] - end - if (@decoded.has_key?("snow_in")) - prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"]) - end - - temp_str = "" - if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c")) - temp_str += sprintf(@strings['temp_min_max_6_hours'], - @decoded["temp_max6h_c"], - @decoded["temp_min6h_c"], - @decoded["temp_max6h_f"], - @decoded["temp_min6h_f"]) - else - if (@decoded.has_key?("temp_max6h_c")) - temp_str += sprintf(@strings['temp_max_6_hours'], - @decoded["temp_max6h_c"], - @decoded["temp_max6h_f"]) - end - if (@decoded.has_key?("temp_min6h_c")) - temp_str += sprintf(@strings['temp_max_6_hours'], - @decoded["temp_min6h_c"], - @decoded["temp_min6h_f"]) - end - end - if (@decoded.has_key?("temp_max24h_c")) - temp_str += sprintf(@strings['temp_min_max_24_hours'], - @decoded["temp_max24h_c"], - @decoded["temp_min24h_c"], - @decoded["temp_max24h_f"], - @decoded["temp_min24h_f"]) - end - - if (@decoded.has_key?("weather")) - weather_str = sprintf(@strings['current_weather'], @decoded["weather"]) - else - weather_str = '' - end - - return sprintf(@strings['pretty_print_metar'], - weather_age, - @date, - wind_str, @decoded["station"], @decoded["temp_c"], - @decoded["temp_f"], @decoded["altimeter_hpa"], - @decoded["altimeter_inhg"], - @decoded["rel_humidity"], sky_str, - visibility, weather_str, prec_str, temp_str).strip - end - - def to_s - @input - end - end - - def Utils.get_metar(station) - station.upcase! - - result = Utils.http_get("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT") - return nil unless result - return Metar.new(result) - end - end -end |