summaryrefslogtreecommitdiff
path: root/rbot
diff options
context:
space:
mode:
Diffstat (limited to 'rbot')
-rw-r--r--rbot/auth.rb199
-rw-r--r--rbot/channel.rb54
-rw-r--r--rbot/config.rb205
-rw-r--r--rbot/dbhash.rb133
-rw-r--r--rbot/httputil.rb88
-rw-r--r--rbot/ircbot.rb750
-rw-r--r--rbot/ircsocket.rb186
-rw-r--r--rbot/keywords.rb427
-rw-r--r--rbot/language.rb55
-rw-r--r--rbot/languages/dutch.lang73
-rw-r--r--rbot/languages/english.lang75
-rw-r--r--rbot/languages/german.lang67
-rw-r--r--rbot/message.rb256
-rw-r--r--rbot/messagemapper.rb168
-rw-r--r--rbot/plugins.rb300
-rw-r--r--rbot/registry.rb292
-rw-r--r--rbot/rfc2812.rb1027
-rw-r--r--rbot/timer.rb123
-rw-r--r--rbot/utils.rb778
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