summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog7
-rw-r--r--data/rbot/plugins/nslookup.rb71
-rw-r--r--data/rbot/plugins/opmeh.rb19
-rw-r--r--data/rbot/plugins/remind.rb214
-rw-r--r--data/rbot/plugins/rot13.rb10
-rw-r--r--data/rbot/plugins/roulette.rb82
-rw-r--r--data/rbot/plugins/seen.rb21
-rw-r--r--data/rbot/plugins/slashdot.rb35
-rw-r--r--data/rbot/plugins/tube.rb16
-rw-r--r--data/rbot/plugins/url.rb72
-rw-r--r--data/rbot/plugins/weather.rb620
-rw-r--r--data/rbot/plugins/wserver.rb12
-rw-r--r--data/rbot/templates/keywords.rbot2
-rw-r--r--data/rbot/templates/levels.rbot2
-rw-r--r--lib/rbot/config.rb5
-rw-r--r--lib/rbot/httputil.rb16
-rw-r--r--lib/rbot/messagemapper.rb3
-rw-r--r--lib/rbot/plugins.rb30
-rw-r--r--lib/rbot/utils.rb713
19 files changed, 930 insertions, 1020 deletions
diff --git a/ChangeLog b/ChangeLog
index fcb65aac..aca85919 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+Fri Jul 29 13:07:56 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
+
+ * Moved some stuff out of util.rb into the plugins that actually need
+ them. Those methods didn't belong in util as they were plugin-specific.
+ * moved a few more plugins to use map() where appropriate
+ * made the url plugin only store unique urls
+
Thu Jul 28 23:45:26 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
* Reworked the Timer module. The Timer now has a smart thread manager to
diff --git a/data/rbot/plugins/nslookup.rb b/data/rbot/plugins/nslookup.rb
index 92da1ba7..160fee85 100644
--- a/data/rbot/plugins/nslookup.rb
+++ b/data/rbot/plugins/nslookup.rb
@@ -1,56 +1,43 @@
class DnsPlugin < Plugin
- begin
- require 'resolv-replace'
- def gethostname(address)
- Resolv.getname(address)
- end
- def getaddresses(name)
- Resolv.getaddresses(name)
- end
- rescue LoadError
- def gethostname(address)
- Socket.gethostbyname(address).first
- end
- def getaddresses(name)
- a = Socket.gethostbyname(name)
- list = Socket.getaddrinfo(a[0], 'http')
- addresses = Array.new
- list.each {|line|
- addresses << line[3]
- }
- addresses
- end
+ require 'resolv'
+ def gethostname(address)
+ Resolv.getname(address)
+ end
+ def getaddresses(name)
+ Resolv.getaddresses(name)
end
def help(plugin, topic="")
- "nslookup|dns <hostname|ip> => show local resolution results for hostname or ip address"
+ "dns <hostname|ip> => show local resolution results for hostname or ip address"
end
- def privmsg(m)
- unless(m.params)
- m.reply "incorrect usage: " + help(m.plugin)
- return
+
+ def name_to_ip(m, params)
+ Thread.new do
+ begin
+ a = getaddresses(params[:host])
+ if a.length > 0
+ m.reply m.params + ": " + a.join(", ")
+ else
+ m.reply "#{params[:host]}: not found"
+ end
+ rescue StandardError => err
+ m.reply "#{params[:host]}: not found"
+ end
end
+ end
+
+ def ip_to_name(m, params)
Thread.new do
- if(m.params =~ /^\d+\.\d+\.\d+\.\d+$/)
begin
- a = gethostname(m.params)
+ a = gethostname(params[:ip])
m.reply m.params + ": " + a if a
rescue StandardError => err
- m.reply "#{m.params}: not found"
- end
- elsif(m.params =~ /^\S+$/)
- begin
- a = getaddresses(m.params)
- m.reply m.params + ": " + a.join(", ")
- rescue StandardError => err
- m.reply "#{m.params}: not found"
+ m.reply "#{params[:ip]}: not found (does not reverse resolve)"
end
- else
- m.reply "incorrect usage: " + help(m.plugin)
- end
- end
+ end
end
end
plugin = DnsPlugin.new
-plugin.register("nslookup")
-plugin.register("dns")
+plugin.map 'dns :ip', :action => 'ip_to_name',
+ :requirements => {:ip => /^\d+\.\d+\.\d+\.\d+$/}
+plugin.map 'dns :host', :action => 'name_to_ip'
diff --git a/data/rbot/plugins/opmeh.rb b/data/rbot/plugins/opmeh.rb
deleted file mode 100644
index aad388a9..00000000
--- a/data/rbot/plugins/opmeh.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class OpMePlugin < Plugin
-
- def help(plugin, topic="")
- return "opme <channel> => grant user ops in <channel>"
- end
-
- def privmsg(m)
- if(m.params)
- channel = m.params
- else
- channel = m.channel
- end
- target = m.sourcenick
- @bot.sendq("MODE #{channel} +o #{target}")
- m.okay
- end
-end
-plugin = OpMePlugin.new
-plugin.register("opme")
diff --git a/data/rbot/plugins/remind.rb b/data/rbot/plugins/remind.rb
index 5ad980ae..f66c4fc8 100644
--- a/data/rbot/plugins/remind.rb
+++ b/data/rbot/plugins/remind.rb
@@ -1,6 +1,108 @@
require 'rbot/utils'
class RemindPlugin < Plugin
+ # 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 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
+
def initialize
super
@reminders = Hash.new
@@ -14,15 +116,11 @@ class RemindPlugin < Plugin
@reminders.clear
end
def help(plugin, topic="")
- if(plugin =~ /^remind\+$/)
- "see remind. remind+ can be used to remind someone else of something, using <nick> instead of 'me'. However this will generally require a higher auth level than remind."
- else
- "remind me [about] <message> in <time>, remind me [about] <message> every <time>, remind me [about] <message> at <time>, remind me no more [about] <message>, remind me no more"
- end
+ "reminder plugin: remind <who> [about] <message> in <time>, remind <who> [about] <message> every <time>, remind <who> [about] <message> at <time>, remind <who> no more [about] <message>, remind <who> no more. Generally <who> should be 'me', but you can remind others (nick or channel) if you have remind_others auth"
end
def add_reminder(who, subject, timestr, repeat=false)
begin
- period = Irc::Utils.timestr_offset(timestr)
+ period = timestr_offset(timestr)
rescue RuntimeError
return "couldn't parse that time string (#{timestr}) :("
end
@@ -58,6 +156,9 @@ class RemindPlugin < Plugin
if(@reminders.has_key?(who) && @reminders[who].has_key?(subject))
@bot.timer.remove(@reminders[who][subject])
@reminders[who].delete(subject)
+ return true
+ else
+ return false
end
else
if(@reminders.has_key?(who))
@@ -65,90 +166,63 @@ class RemindPlugin < Plugin
@bot.timer.remove(v)
}
@reminders.delete(who)
+ return true
+ else
+ return false
end
end
end
- def privmsg(m)
-
- if(m.params =~ /^(\S+)\s+(?:about\s+)?(.*)\s+in\s+(.*)$/)
- who = $1
- subject = $2
- period = $3
- if(who =~ /^me$/)
- who = m.sourcenick
- else
- unless(m.plugin =~ /^remind\+$/)
- m.reply "incorrect usage: use remind+ to remind persons other than yourself"
- return
- end
- end
+ def remind(m, params)
+ who = params.has_key?(:who) ? params[:who] : m.sourcenick
+ string = params[:string].to_s
+ puts "in remind, string is: #{string}"
+ if(string =~ /^(.*)\s+in\s+(.*)$/)
+ subject = $1
+ period = $2
if(err = add_reminder(who, subject, period))
m.reply "incorrect usage: " + err
return
end
- elsif(m.params =~ /^(\S+)\s+(?:about\s+)?(.*)\s+every\s+(.*)$/)
- who = $1
- subject = $2
- period = $3
- if(who =~ /^me$/)
- who = m.sourcenick
- else
- unless(m.plugin =~ /^remind\+$/)
- m.reply "incorrect usage: use remind+ to remind persons other than yourself"
- return
- end
- end
+ elsif(string =~ /^(.*)\s+every\s+(.*)$/)
+ subject = $1
+ period = $2
if(err = add_reminder(who, subject, period, true))
m.reply "incorrect usage: " + err
return
end
- elsif(m.params =~ /^(\S+)\s+(?:about\s+)?(.*)\s+at\s+(.*)$/)
- who = $1
- subject = $2
- time = $3
- if(who =~ /^me$/)
- who = m.sourcenick
- else
- unless(m.plugin =~ /^remind\+$/)
- m.reply "incorrect usage: use remind+ to remind persons other than yourself"
- return
- end
- end
+ elsif(string =~ /^(.*)\s+at\s+(.*)$/)
+ subject = $1
+ time = $2
if(err = add_reminder(who, subject, time))
m.reply "incorrect usage: " + err
return
end
- elsif(m.params =~ /^(\S+)\s+no\s+more\s+(?:about\s+)?(.*)$/)
- who = $1
- subject = $2
- if(who =~ /^me$/)
- who = m.sourcenick
- else
- unless(m.plugin =~ /^remind\+$/)
- m.reply "incorrect usage: use remind+ to remind persons other than yourself"
- return
- end
- end
- del_reminder(who, subject)
- elsif(m.params =~ /^(\S+)\s+no\s+more$/)
- who = $1
- if(who =~ /^me$/)
- who = m.sourcenick
- else
- unless(m.plugin =~ /^remind\+$/)
- m.reply "incorrect usage: use remind+ to remind persons other than yourself"
- return
- end
- end
- del_reminder(who)
else
- m.reply "incorrect usage: " + help(m.plugin)
+ usage(m)
return
end
m.okay
end
+ def no_more(m, params)
+ who = params.has_key?(:who) ? params[:who] : m.sourcenick
+ deleted = params.has_key?(:string) ?
+ del_reminder(who, params[:string].to_s) : del_reminder(who)
+ if deleted
+ m.okay
+ else
+ m.reply "but I wasn't going to :/"
+ end
+ end
end
plugin = RemindPlugin.new
-plugin.register("remind")
-plugin.register("remind+")
+plugin.map 'remind me no more', :action => 'no_more'
+plugin.map 'remind me no more about *string', :action => 'no_more'
+plugin.map 'remind me no more *string', :action => 'no_more'
+plugin.map 'remind me about *string'
+plugin.map 'remind me *string'
+plugin.map 'remind :who no more', :auth => 'remind_other', :action => 'no_more'
+plugin.map 'remind :who no more about *string', :auth => 'remind_other', :action => 'no_more'
+plugin.map 'remind :who no more *string', :auth => 'remind_other', :action => 'no_more'
+plugin.map 'remind :who about *string', :auth => 'remind_other'
+plugin.map 'remind :who *string', :auth => 'remind_other'
diff --git a/data/rbot/plugins/rot13.rb b/data/rbot/plugins/rot13.rb
index 1f367dbd..28e9243f 100644
--- a/data/rbot/plugins/rot13.rb
+++ b/data/rbot/plugins/rot13.rb
@@ -2,13 +2,9 @@ class RotPlugin < Plugin
def help(plugin, topic="")
"rot13 <string> => encode <string> to rot13 or back"
end
- def privmsg(m)
- unless(m.params && m.params =~ /^.+$/)
- m.reply "incorrect usage: " + help(m.plugin)
- return
- end
- m.reply m.params.tr("A-Za-z", "N-ZA-Mn-za-m");
+ def rot13(m, params)
+ m.reply params[:string].tr("A-Za-z", "N-ZA-Mn-za-m");
end
end
plugin = RotPlugin.new
-plugin.register("rot13")
+plugin.map 'rot13 :string'
diff --git a/data/rbot/plugins/roulette.rb b/data/rbot/plugins/roulette.rb
index c9d585ea..d57bc621 100644
--- a/data/rbot/plugins/roulette.rb
+++ b/data/rbot/plugins/roulette.rb
@@ -3,40 +3,18 @@ RouletteHistory = Struct.new("RouletteHistory", :games, :shots, :deaths, :misses
class RoulettePlugin < Plugin
def initialize
super
- reload
+ reset_chambers
+ @players = Array.new
end
def help(plugin, topic="")
"roulette => play russian roulette - starts a new game if one isn't already running. One round in a six chambered gun. Take turns to say roulette to the bot, until somebody dies. roulette reload => force the gun to reload, roulette stats => show stats from all games, roulette stats <player> => show stats for <player>, roulette clearstats => clear stats (config level auth required)"
end
- def privmsg(m)
- if m.params == "reload"
- @bot.action m.replyto, "reloads"
- reload
- # all players win on a reload
- # (allows you to play 3-shot matches etc)
- @players.each {|plyr|
- pdata = @registry[plyr]
- next if pdata == nil
- pdata.wins += 1
- @registry[plyr] = pdata
- }
- return
- elsif m.params == "stats"
- m.reply stats
- return
- elsif m.params =~ /^stats\s+(.+)$/
- m.reply(playerstats($1))
- return
- elsif m.params == "clearstats"
- if @bot.auth.allow?("config", m.source, m.replyto)
- @registry.clear
- m.okay
- end
- return
- elsif m.params
- m.reply "incorrect usage: " + help(m.plugin)
- return
- end
+ def clearstats(m, params)
+ @registry.clear
+ m.okay
+ end
+
+ def roulette(m, params)
if m.private?
m.reply "you gotta play roulette in channel dude"
return
@@ -74,21 +52,36 @@ class RoulettePlugin < Plugin
@registry[m.sourcenick] = playerdata
if shot || @chambers.empty?
- @bot.action m.replyto, "reloads"
- reload
+ reload(m)
end
end
- def reload
+ def reload(m, params = {})
+ @bot.action m.replyto, "reloads"
+ reset_chambers
+ # all players win on a reload
+ # (allows you to play 3-shot matches etc)
+ @players.each {|plyr|
+ pdata = @registry[plyr]
+ next if pdata == nil
+ pdata.wins += 1
+ @registry[plyr] = pdata
+ }
+ @players = Array.new
+ end
+ def reset_chambers
@chambers = [false, false, false, false, false, false]
@chambers[rand(@chambers.length)] = true
- @players = Array.new
end
- def playerstats(player)
+ def playerstats(m, params)
+ player = params[:player]
pstats = @registry[player]
- return "#{player} hasn't played enough games yet" if pstats.nil?
- return "#{player} has played #{pstats.games} games, won #{pstats.wins} and lost #{pstats.deaths}. #{player} pulled the trigger #{pstats.shots} times and found the chamber empty on #{pstats.misses} occasions."
+ if pstats.nil?
+ m.reply "#{player} hasn't played enough games yet"
+ else
+ m.reply "#{player} has played #{pstats.games} games, won #{pstats.wins} and lost #{pstats.deaths}. #{player} pulled the trigger #{pstats.shots} times and found the chamber empty on #{pstats.misses} occasions."
+ end
end
- def stats
+ def stats(m, params)
total_players = 0
total_games = 0
total_shots = 0
@@ -139,9 +132,16 @@ class RoulettePlugin < Plugin
won_most[0] << k
end
}
- return "roulette stats: no games played yet" if total_games < 1
- return "roulette stats: #{total_games} games completed, #{total_shots} shots fired at #{total_players} players. Luckiest: #{h_luck_percent[0].join(',')} (#{sprintf '%.1f', h_luck_percent[1]}% clicks). Unluckiest: #{l_luck_percent[0].join(',')} (#{sprintf '%.1f', l_luck_percent[1]}% clicks). Highest survival rate: #{h_win_percent[0].join(',')} (#{sprintf '%.1f', h_win_percent[1]}%). Lowest survival rate: #{l_win_percent[0].join(',')} (#{sprintf '%.1f', l_win_percent[1]}%). Most wins: #{won_most[0].join(',')} (#{won_most[1]}). Most deaths: #{died_most[0].join(',')} (#{died_most[1]})."
+ if total_games < 1
+ m.reply "roulette stats: no games played yet"
+ else
+ m.reply "roulette stats: #{total_games} games completed, #{total_shots} shots fired at #{total_players} players. Luckiest: #{h_luck_percent[0].join(',')} (#{sprintf '%.1f', h_luck_percent[1]}% clicks). Unluckiest: #{l_luck_percent[0].join(',')} (#{sprintf '%.1f', l_luck_percent[1]}% clicks). Highest survival rate: #{h_win_percent[0].join(',')} (#{sprintf '%.1f', h_win_percent[1]}%). Lowest survival rate: #{l_win_percent[0].join(',')} (#{sprintf '%.1f', l_win_percent[1]}%). Most wins: #{won_most[0].join(',')} (#{won_most[1]}). Most deaths: #{died_most[0].join(',')} (#{died_most[1]})."
+ end
end
end
plugin = RoulettePlugin.new
-plugin.register("roulette")
+plugin.map 'roulette reload', :action => 'reload'
+plugin.map 'roulette stats :player', :action => 'playerstats'
+plugin.map 'roulette stats', :action => 'stats'
+plugin.map 'roulette clearstats', :action => 'clearstats', :auth => 'config'
+plugin.map 'roulette'
diff --git a/data/rbot/plugins/seen.rb b/data/rbot/plugins/seen.rb
index 6bd86a70..80d52f65 100644
--- a/data/rbot/plugins/seen.rb
+++ b/data/rbot/plugins/seen.rb
@@ -1,6 +1,23 @@
Saw = Struct.new("Saw", :nick, :time, :type, :where, :message)
class SeenPlugin < Plugin
+ # turn a number of seconds into a human readable string, e.g
+ # 2 days, 3 hours, 18 minutes, 10 seconds
+ def 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 help(plugin, topic="")
"seen <nick> => have you seen, or when did you last see <nick>"
end
@@ -23,7 +40,7 @@ class SeenPlugin < Plugin
def listen(m)
# keep database up to date with who last said what
if m.kind_of?(PrivMessage)
- return if m.private? || m.address?
+ return if m.private?
if m.action?
@registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "ACTION",
m.target, m.message.dup)
@@ -63,7 +80,7 @@ class SeenPlugin < Plugin
if (ago.to_i == 0)
ret += "just now, "
else
- ret += Utils.secs_to_string(ago) + " ago, "
+ ret += secs_to_string(ago) + " ago, "
end
case saw.type
diff --git a/data/rbot/plugins/slashdot.rb b/data/rbot/plugins/slashdot.rb
index b09ac7a7..1a70de08 100644
--- a/data/rbot/plugins/slashdot.rb
+++ b/data/rbot/plugins/slashdot.rb
@@ -6,23 +6,11 @@ class SlashdotPlugin < Plugin
def help(plugin, topic="")
"slashdot search <string> [<max>=4] => search slashdot for <string>, slashdot [<max>=4] => return up to <max> slashdot headlines (use negative max to return that many headlines, but all on one line.)"
end
- def privmsg(m)
- if m.params && m.params =~ /^search\s+(.*)\s+(\d+)$/
- search = $1
- limit = $2.to_i
- search_slashdot m, search, limit
- elsif m.params && m.params =~ /^search\s+(.*)$/
- search = $1
- search_slashdot m, search
- elsif m.params && m.params =~ /^([-\d]+)$/
- limit = $1.to_i
- slashdot m, limit
- else
- slashdot m
- end
- end
- def search_slashdot(m, search, max=4)
+ def search_slashdot(m, params)
+ max = params[:limit].to_i
+ search = params[:search].to_s
+
begin
xml = @bot.httputil.get(URI.parse("http://slashdot.org/search.pl?content_type=rss&query=#{URI.escape(search)}"))
rescue URI::InvalidURIError, URI::BadURIError => e
@@ -33,6 +21,7 @@ class SlashdotPlugin < Plugin
m.reply "search for #{search} failed"
return
end
+ puts xml.inspect
begin
doc = Document.new xml
rescue REXML::ParseException => e
@@ -44,6 +33,7 @@ class SlashdotPlugin < Plugin
m.reply "search for #{search} failed"
return
end
+ puts doc.inspect
max = 8 if max > 8
done = 0
doc.elements.each("*/item") {|e|
@@ -54,9 +44,15 @@ class SlashdotPlugin < Plugin
done += 1
break if done >= max
}
+ unless done > 0
+ m.reply "search for #{search} failed"
+ end
end
- def slashdot(m, max=4)
+ def slashdot(m, params)
+ puts params.inspect
+ max = params[:limit].to_i
+ puts "max is #{max}"
xml = @bot.httputil.get(URI.parse("http://slashdot.org/slashdot.xml"))
unless xml
m.reply "slashdot news parse failed"
@@ -92,4 +88,7 @@ class SlashdotPlugin < Plugin
end
end
plugin = SlashdotPlugin.new
-plugin.register("slashdot")
+plugin.map 'slashdot search :limit *search', :action => 'search_slashdot',
+ :defaults => {:limit => 4}, :requirements => {:limit => /^-?\d+$/}
+plugin.map 'slashdot :limit', :defaults => {:limit => 4},
+ :requirements => {:limit => /^-?\d+$/}
diff --git a/data/rbot/plugins/tube.rb b/data/rbot/plugins/tube.rb
index 77ca5227..85316718 100644
--- a/data/rbot/plugins/tube.rb
+++ b/data/rbot/plugins/tube.rb
@@ -9,16 +9,9 @@ class TubePlugin < Plugin
def help(plugin, topic="")
"tube [district|circle|metropolitan|central|jubilee|bakerloo|waterloo_city|hammersmith_city|victoria|eastlondon|northern|piccadilly] => display tube service status for the specified line(Docklands Light Railway is not currently supported), tube stations => list tube stations (not lines) with problems"
end
- def privmsg(m)
- if m.params && m.params =~ /^stations$/
- check_stations m
- elsif m.params && m.params =~ /^(.*)$/
- line = $1.downcase.capitalize
- check_tube m, line
- end
- end
- def check_tube(m, line)
+ def tube(m, params)
+ line = params[:line]
begin
tube_page = @bot.httputil.get(URI.parse("http://www.tfl.gov.uk/tfl/service_rt_tube.shtml"), 1, 1)
rescue URI::InvalidURIError, URI::BadURIError => e
@@ -47,7 +40,7 @@ class TubePlugin < Plugin
m.reply "No Problems on the #{line} line."
end
- def check_stations(m)
+ def check_stations(m, params)
begin
tube_page = @bot.httputil.get(URI.parse("http://www.tfl.gov.uk/tfl/service_rt_tube.shtml"))
rescue URI::InvalidURIError, URI::BadURIError => e
@@ -74,4 +67,5 @@ class TubePlugin < Plugin
end
end
plugin = TubePlugin.new
-plugin.register("tube")
+plugin.map 'tube stations', :action => 'check_stations'
+plugin.map 'tube :line'
diff --git a/data/rbot/plugins/url.rb b/data/rbot/plugins/url.rb
index ed82d1c1..ced92133 100644
--- a/data/rbot/plugins/url.rb
+++ b/data/rbot/plugins/url.rb
@@ -6,7 +6,7 @@ class UrlPlugin < Plugin
@registry.set_default(Array.new)
end
def help(plugin, topic="")
- "urls [<max>=4] => list <max> last urls mentioned in current channel, urls <channel> [<max>=4] => list <max> last urls mentioned in <channel>, urls search <regexp> => search for matching urls, urls search <channel> <regexp>, search for matching urls in channel <channel>"
+ "urls [<max>=4] => list <max> last urls mentioned in current channel, urls search [<max>=4] <regexp> => search for matching urls. In a private message, you must specify the channel to query, eg. urls <channel> [max], urls search <channel> [max] <regexp>"
end
def listen(m)
return unless m.kind_of?(PrivMessage)
@@ -14,10 +14,15 @@ class UrlPlugin < Plugin
# TODO support multiple urls in one line
if m.message =~ /(f|ht)tps?:\/\//
if m.message =~ /((f|ht)tps?:\/\/.*?)(?:\s+|$)/
- url = Url.new(m.target, m.sourcenick, Time.new, $1)
+ urlstr = $1
list = @registry[m.target]
+ # check to see if this url is already listed
+ return if list.find {|u|
+ u.url == urlstr
+ }
+ url = Url.new(m.target, m.sourcenick, Time.new, urlstr)
debug "#{list.length} urls so far"
- if list.length > 50
+ if list.length > 50 # TODO make this configurable
list.pop
end
debug "storing url #{url.url}"
@@ -27,45 +32,10 @@ class UrlPlugin < Plugin
end
end
end
- def privmsg(m)
- case m.params
- when nil
- if m.public?
- urls m, m.target
- else
- m.reply "in a private message, you need to specify a channel name for urls"
- end
- when (/^(\d+)$/)
- max = $1.to_i
- if m.public?
- urls m, m.target, max
- else
- m.reply "in a private message, you need to specify a channel name for urls"
- end
- when (/^(#.*?)\s+(\d+)$/)
- channel = $1
- max = $2.to_i
- urls m, channel, max
- when (/^(#.*?)$/)
- channel = $1
- urls m, channel
- when (/^search\s+(#.*?)\s+(.*)$/)
- channel = $1
- string = $2
- search m, channel, string
- when (/^search\s+(.*)$/)
- string = $1
- if m.public?
- search m, m.target, string
- else
- m.reply "in a private message, you need to specify a channel name for urls"
- end
- else
- m.reply "incorrect usage: " + help(m.plugin)
- end
- end
- def urls(m, channel, max=4)
+ def urls(m, params)
+ channel = params[:channel] ? params[:channel] : m.target
+ max = params[:limit].to_i
max = 10 if max > 10
max = 1 if max < 1
list = @registry[channel]
@@ -78,7 +48,10 @@ class UrlPlugin < Plugin
end
end
- def search(m, channel, string, max=4)
+ def search(m, params)
+ channel = params[:channel] ? params[:channel] : m.target
+ max = params[:limit].to_i
+ string = params[:string]
max = 10 if max > 10
max = 1 if max < 1
regex = Regexp.new(string)
@@ -95,4 +68,17 @@ class UrlPlugin < Plugin
end
end
plugin = UrlPlugin.new
-plugin.register("urls")
+plugin.map 'urls search :channel :limit :string', :action => 'search',
+ :defaults => {:limit => 4},
+ :requirements => {:limit => /^\d+$/},
+ :public => false
+plugin.map 'urls search :limit :string', :action => 'search',
+ :defaults => {:limit => 4},
+ :requirements => {:limit => /^\d+$/},
+ :private => false
+plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
+ :requirements => {:limit => /^\d+$/},
+ :public => false
+plugin.map 'urls :limit', :defaults => {:limit => 4},
+ :requirements => {:limit => /^\d+$/},
+ :private => false
diff --git a/data/rbot/plugins/weather.rb b/data/rbot/plugins/weather.rb
index 3e4134e4..f2c3e368 100644
--- a/data/rbot/plugins/weather.rb
+++ b/data/rbot/plugins/weather.rb
@@ -1,8 +1,605 @@
+# 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
+
+
class WeatherPlugin < Plugin
def help(plugin, topic="")
- "weather <ICAO> => display the current weather at the location specified by the ICAO code [Lookup your ICAO code at http://www.nws.noaa.gov/oso/siteloc.shtml] - this will also store the ICAO against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for"
+ "weather <ICAO> => display the current weather at the location specified by the ICAO code [Lookup your ICAO code at http://www.nws.noaa.gov/tg/siteloc.shtml - this will also store the ICAO against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for"
+ end
+
+ def get_metar(station)
+ station.upcase!
+
+ result = @bot.httputil.get(URI.parse("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT"))
+ return nil unless result
+ return Metar.new(result)
end
+
def initialize
super
@@ -23,7 +620,7 @@ class WeatherPlugin < Plugin
Time.now - @metar_cache[where].date < 3600
met = @metar_cache[where]
else
- met = Utils.get_metar(where)
+ met = get_metar(where)
end
if met
@@ -33,23 +630,20 @@ class WeatherPlugin < Plugin
m.reply "couldn't find weather data for #{where}"
end
end
-
- def privmsg(m)
- case m.params
- when nil
+
+ def weather(m, params)
+ if params[:where]
+ @registry[m.sourcenick] = params[:where]
+ describe(m,params[:where])
+ else
if @registry.has_key?(m.sourcenick)
where = @registry[m.sourcenick]
describe(m,where)
else
- m.reply "I don't know where #{m.sourcenick} is!"
+ m.reply "I don't know where you are yet! Lookup your code at http://www.nws.noaa.gov/tg/siteloc.shtml and tell me 'weather <code>', then I'll know."
end
- when (/^(\S{4})$/)
- where = $1
- @registry[m.sourcenick] = where
- describe(m,where)
end
end
-
end
plugin = WeatherPlugin.new
-plugin.register("weather")
+plugin.map 'weather :where', :defaults => {:where => false}
diff --git a/data/rbot/plugins/wserver.rb b/data/rbot/plugins/wserver.rb
index e1fe10bd..fb4738c1 100644
--- a/data/rbot/plugins/wserver.rb
+++ b/data/rbot/plugins/wserver.rb
@@ -6,14 +6,10 @@ class WserverPlugin < Plugin
def help(plugin, topic="")
"wserver <uri> => try and determine what webserver <uri> is using"
end
- def privmsg(m)
- unless(m.params && m.params =~ /^\S+$/)
- m.reply "incorrect usage: " + help(m.plugins)
- return
- end
+ def wserver(m, params)
redirect_count = 0
- hostname = m.params.dup
+ hostname = params[:host].dup
hostname = "http://#{hostname}" unless hostname =~ /:\/\//
begin
if(redirect_count > 3)
@@ -24,7 +20,7 @@ class WserverPlugin < Plugin
begin
uri = URI.parse(hostname)
rescue URI::InvalidURIError => err
- m.reply "#{m.params} is not a valid URI"
+ m.reply "#{hostname} is not a valid URI"
return
end
@@ -72,4 +68,4 @@ class WserverPlugin < Plugin
end
end
plugin = WserverPlugin.new
-plugin.register("wserver")
+plugin.map 'wserver :host'
diff --git a/data/rbot/templates/keywords.rbot b/data/rbot/templates/keywords.rbot
index d985fa35..3648b1e9 100644
--- a/data/rbot/templates/keywords.rbot
+++ b/data/rbot/templates/keywords.rbot
@@ -1,4 +1,4 @@
lb<=is=>http://linuxbrit.co.uk
offended<=is=><reply><who> is offended!
-giblet<=is=>My master!
+giblet<=is=><reply>daddy!
rbot<=is=><reply>That's me! :-))
diff --git a/data/rbot/templates/levels.rbot b/data/rbot/templates/levels.rbot
index ce338e3b..a445d5be 100644
--- a/data/rbot/templates/levels.rbot
+++ b/data/rbot/templates/levels.rbot
@@ -10,7 +10,7 @@
50 join
15 delquote
12 msginsult
-12 remind+
+12 remind_other
5 rmlart
5 rmpraise
5 keycmd
diff --git a/lib/rbot/config.rb b/lib/rbot/config.rb
index e93af811..19506ab2 100644
--- a/lib/rbot/config.rb
+++ b/lib/rbot/config.rb
@@ -34,6 +34,11 @@ module Irc
@config['server.sendq_burst'] = 4
@config['keyword.address'] = true
@config['keyword.listen'] = false
+ @config['http.proxy'] = false
+ @config['http.proxy_include'] = false
+ @config['http.proxy_exclude'] = false
+ @config['http.proxy_user'] = false
+ @config['http.proxy_pass'] = false
# TODO
# have this class persist key/values in hash using yaml as it kinda
diff --git a/lib/rbot/httputil.rb b/lib/rbot/httputil.rb
index ff3216a6..85b56be4 100644
--- a/lib/rbot/httputil.rb
+++ b/lib/rbot/httputil.rb
@@ -25,17 +25,19 @@ class HttpUtil
if (ENV['http_proxy'])
proxy = URI.parse ENV['http_proxy']
end
- if (@bot.config["http_proxy"])
+ 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
+ # the excludes are a list of regexps, and each regexp is checked against
+ # the server name, and its IP addresses
if uri
- if @bot.config["http_proxy_exclude"]
+ if @bot.config["http.proxy_exclude"]
# TODO
end
- if @bot.config["http_proxy_include"]
+ if @bot.config["http.proxy_include"]
end
end
@@ -43,10 +45,10 @@ class HttpUtil
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"]
+ 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
diff --git a/lib/rbot/messagemapper.rb b/lib/rbot/messagemapper.rb
index 42563d23..f83fafb2 100644
--- a/lib/rbot/messagemapper.rb
+++ b/lib/rbot/messagemapper.rb
@@ -106,8 +106,7 @@ module Irc
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
+ options[item] = @defaults[item]
# push the test-failed component back on the stack
components.unshift value
else
diff --git a/lib/rbot/plugins.rb b/lib/rbot/plugins.rb
index 8d9dcfc9..d4e5be9f 100644
--- a/lib/rbot/plugins.rb
+++ b/lib/rbot/plugins.rb
@@ -133,7 +133,7 @@ module Irc
# default usage method provided as a utility for simple plugins. The
# MessageMapper uses 'usage' as its default fallback method.
- def usage(m, params)
+ def usage(m, params = {})
m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
end
@@ -175,12 +175,14 @@ module Irc
dirs.each {|dir|
if(FileTest.directory?(dir))
d = Dir.new(dir)
- d.each {|file|
+ d.sort.each {|file|
next if(file =~ /^\./)
next unless(file =~ /\.rb$/)
@tmpfilename = "#{dir}/#{file}"
# create a new, anonymous module to "house" the plugin
+ # the idea here is to prevent namespace pollution. perhaps there
+ # is another way?
plugin_module = Module.new
begin
@@ -198,28 +200,12 @@ module Irc
# 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
- }
+ delegate 'save'
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
- }
+ delegate 'cleanup'
end
# drop all plugins and rescan plugins on disk
@@ -265,11 +251,11 @@ module Irc
# see if each plugin handles +method+, and if so, call it, passing
# +message+ as a parameter
- def delegate(method, message)
+ def delegate(method, *args)
@@plugins.values.uniq.each {|p|
if(p.respond_to? method)
begin
- p.send method, message
+ p.send method, *args
rescue StandardError, NameError, SyntaxError => err
puts "plugin #{p.name} #{method}() failed: " + err
puts err.backtrace.join("\n")
diff --git a/lib/rbot/utils.rb b/lib/rbot/utils.rb
index b22a417d..4c474ae4 100644
--- a/lib/rbot/utils.rb
+++ b/lib/rbot/utils.rb
@@ -5,124 +5,6 @@ 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|
@@ -179,600 +61,5 @@ module Irc
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