diff options
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | data/rbot/plugins/nslookup.rb | 71 | ||||
-rw-r--r-- | data/rbot/plugins/opmeh.rb | 19 | ||||
-rw-r--r-- | data/rbot/plugins/remind.rb | 214 | ||||
-rw-r--r-- | data/rbot/plugins/rot13.rb | 10 | ||||
-rw-r--r-- | data/rbot/plugins/roulette.rb | 82 | ||||
-rw-r--r-- | data/rbot/plugins/seen.rb | 21 | ||||
-rw-r--r-- | data/rbot/plugins/slashdot.rb | 35 | ||||
-rw-r--r-- | data/rbot/plugins/tube.rb | 16 | ||||
-rw-r--r-- | data/rbot/plugins/url.rb | 72 | ||||
-rw-r--r-- | data/rbot/plugins/weather.rb | 620 | ||||
-rw-r--r-- | data/rbot/plugins/wserver.rb | 12 | ||||
-rw-r--r-- | data/rbot/templates/keywords.rbot | 2 | ||||
-rw-r--r-- | data/rbot/templates/levels.rbot | 2 | ||||
-rw-r--r-- | lib/rbot/config.rb | 5 | ||||
-rw-r--r-- | lib/rbot/httputil.rb | 16 | ||||
-rw-r--r-- | lib/rbot/messagemapper.rb | 3 | ||||
-rw-r--r-- | lib/rbot/plugins.rb | 30 | ||||
-rw-r--r-- | lib/rbot/utils.rb | 713 |
19 files changed, 930 insertions, 1020 deletions
@@ -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 |