rbot 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/AUTHORS +16 -0
  2. data/COPYING +21 -0
  3. data/ChangeLog +418 -0
  4. data/INSTALL +8 -0
  5. data/README +44 -0
  6. data/REQUIREMENTS +34 -0
  7. data/TODO +5 -0
  8. data/Usage_en.txt +129 -0
  9. data/bin/rbot +81 -0
  10. data/data/rbot/contrib/plugins/figlet.rb +20 -0
  11. data/data/rbot/contrib/plugins/ri.rb +83 -0
  12. data/data/rbot/contrib/plugins/stats.rb +232 -0
  13. data/data/rbot/contrib/plugins/vandale.rb +49 -0
  14. data/data/rbot/languages/dutch.lang +73 -0
  15. data/data/rbot/languages/english.lang +75 -0
  16. data/data/rbot/languages/french.lang +39 -0
  17. data/data/rbot/languages/german.lang +67 -0
  18. data/data/rbot/plugins/autoop.rb +68 -0
  19. data/data/rbot/plugins/autorejoin.rb +16 -0
  20. data/data/rbot/plugins/cal.rb +15 -0
  21. data/data/rbot/plugins/dice.rb +81 -0
  22. data/data/rbot/plugins/eightball.rb +19 -0
  23. data/data/rbot/plugins/excuse.rb +470 -0
  24. data/data/rbot/plugins/fish.rb +61 -0
  25. data/data/rbot/plugins/fortune.rb +22 -0
  26. data/data/rbot/plugins/freshmeat.rb +98 -0
  27. data/data/rbot/plugins/google.rb +51 -0
  28. data/data/rbot/plugins/host.rb +14 -0
  29. data/data/rbot/plugins/httpd.rb.disabled +35 -0
  30. data/data/rbot/plugins/insult.rb +258 -0
  31. data/data/rbot/plugins/karma.rb +85 -0
  32. data/data/rbot/plugins/lart.rb +181 -0
  33. data/data/rbot/plugins/math.rb +122 -0
  34. data/data/rbot/plugins/nickserv.rb +89 -0
  35. data/data/rbot/plugins/nslookup.rb +43 -0
  36. data/data/rbot/plugins/opme.rb +19 -0
  37. data/data/rbot/plugins/quakeauth.rb +51 -0
  38. data/data/rbot/plugins/quotes.rb +321 -0
  39. data/data/rbot/plugins/remind.rb +228 -0
  40. data/data/rbot/plugins/roshambo.rb +54 -0
  41. data/data/rbot/plugins/rot13.rb +10 -0
  42. data/data/rbot/plugins/roulette.rb +147 -0
  43. data/data/rbot/plugins/rss.rb.disabled +414 -0
  44. data/data/rbot/plugins/seen.rb +89 -0
  45. data/data/rbot/plugins/slashdot.rb +94 -0
  46. data/data/rbot/plugins/spell.rb +36 -0
  47. data/data/rbot/plugins/tube.rb +71 -0
  48. data/data/rbot/plugins/url.rb +88 -0
  49. data/data/rbot/plugins/weather.rb +649 -0
  50. data/data/rbot/plugins/wserver.rb +71 -0
  51. data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
  52. data/data/rbot/templates/keywords.rbot +4 -0
  53. data/data/rbot/templates/lart/larts +98 -0
  54. data/data/rbot/templates/lart/praises +5 -0
  55. data/data/rbot/templates/levels.rbot +30 -0
  56. data/data/rbot/templates/users.rbot +1 -0
  57. data/lib/rbot/auth.rb +203 -0
  58. data/lib/rbot/channel.rb +54 -0
  59. data/lib/rbot/config.rb +363 -0
  60. data/lib/rbot/dbhash.rb +112 -0
  61. data/lib/rbot/httputil.rb +141 -0
  62. data/lib/rbot/ircbot.rb +808 -0
  63. data/lib/rbot/ircsocket.rb +185 -0
  64. data/lib/rbot/keywords.rb +433 -0
  65. data/lib/rbot/language.rb +69 -0
  66. data/lib/rbot/message.rb +256 -0
  67. data/lib/rbot/messagemapper.rb +262 -0
  68. data/lib/rbot/plugins.rb +291 -0
  69. data/lib/rbot/post-install.rb +8 -0
  70. data/lib/rbot/rbotconfig.rb +36 -0
  71. data/lib/rbot/registry.rb +271 -0
  72. data/lib/rbot/rfc2812.rb +1104 -0
  73. data/lib/rbot/timer.rb +201 -0
  74. data/lib/rbot/utils.rb +83 -0
  75. data/setup.rb +1360 -0
  76. metadata +129 -0
@@ -0,0 +1,89 @@
1
+ Saw = Struct.new("Saw", :nick, :time, :type, :where, :message)
2
+
3
+ class SeenPlugin < Plugin
4
+ def help(plugin, topic="")
5
+ "seen <nick> => have you seen, or when did you last see <nick>"
6
+ end
7
+
8
+ def privmsg(m)
9
+ unless(m.params =~ /^(\S)+$/)
10
+ m.reply "incorrect usage: " + help(m.plugin)
11
+ return
12
+ end
13
+
14
+ m.params.gsub!(/\?$/, "")
15
+
16
+ if @registry.has_key?(m.params)
17
+ m.reply seen(@registry[m.params])
18
+ else
19
+ m.reply "nope!"
20
+ end
21
+ end
22
+
23
+ def listen(m)
24
+ # keep database up to date with who last said what
25
+ if m.kind_of?(PrivMessage)
26
+ return if m.private?
27
+ if m.action?
28
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "ACTION",
29
+ m.target, m.message.dup)
30
+ else
31
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "PUBLIC",
32
+ m.target, m.message.dup)
33
+ end
34
+ elsif m.kind_of?(QuitMessage)
35
+ return if m.address?
36
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "QUIT",
37
+ nil, m.message.dup)
38
+ elsif m.kind_of?(NickMessage)
39
+ return if m.address?
40
+ @registry[m.message] = Saw.new(m.sourcenick.dup, Time.new, "NICK",
41
+ nil, m.message.dup)
42
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "NICK",
43
+ nil, m.message.dup)
44
+ elsif m.kind_of?(PartMessage)
45
+ return if m.address?
46
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "PART",
47
+ m.target, m.message.dup)
48
+ elsif m.kind_of?(JoinMessage)
49
+ return if m.address?
50
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "JOIN",
51
+ m.target, m.message.dup)
52
+ elsif m.kind_of?(TopicMessage)
53
+ return if m.address?
54
+ @registry[m.sourcenick] = Saw.new(m.sourcenick.dup, Time.new, "TOPIC",
55
+ m.target, m.message.dup)
56
+ end
57
+ end
58
+
59
+ def seen(saw)
60
+ ret = "#{saw.nick} was last seen "
61
+ ago = Time.new - saw.time
62
+
63
+ if (ago.to_i == 0)
64
+ ret += "just now, "
65
+ else
66
+ ret += Utils.secs_to_string(ago) + " ago, "
67
+ end
68
+
69
+ case saw.type
70
+ when "PUBLIC"
71
+ ret += "saying #{saw.message}"
72
+ when "ACTION"
73
+ ret += "doing #{saw.nick} #{saw.message}"
74
+ when "NICK"
75
+ ret += "changing nick from #{saw.nick} to #{saw.message}"
76
+ when "PART"
77
+ ret += "leaving #{saw.where}"
78
+ when "JOIN"
79
+ ret += "joining #{saw.where}"
80
+ when "QUIT"
81
+ ret += "quiting IRC (#{saw.message})"
82
+ when "TOPIC"
83
+ ret += "changing the topic of #{saw.where} to #{saw.message}"
84
+ end
85
+ end
86
+
87
+ end
88
+ plugin = SeenPlugin.new
89
+ plugin.register("seen")
@@ -0,0 +1,94 @@
1
+ require 'rexml/document'
2
+ require 'uri/common'
3
+
4
+ class SlashdotPlugin < Plugin
5
+ include REXML
6
+ def help(plugin, topic="")
7
+ "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.)"
8
+ end
9
+
10
+ def search_slashdot(m, params)
11
+ max = params[:limit].to_i
12
+ search = params[:search].to_s
13
+
14
+ begin
15
+ xml = @bot.httputil.get(URI.parse("http://slashdot.org/search.pl?content_type=rss&query=#{URI.escape(search)}"))
16
+ rescue URI::InvalidURIError, URI::BadURIError => e
17
+ m.reply "illegal search string #{search}"
18
+ return
19
+ end
20
+ unless xml
21
+ m.reply "search for #{search} failed"
22
+ return
23
+ end
24
+ puts xml.inspect
25
+ begin
26
+ doc = Document.new xml
27
+ rescue REXML::ParseException => e
28
+ puts e
29
+ m.reply "couldn't parse output XML: #{e.class}"
30
+ return
31
+ end
32
+ unless doc
33
+ m.reply "search for #{search} failed"
34
+ return
35
+ end
36
+ puts doc.inspect
37
+ max = 8 if max > 8
38
+ done = 0
39
+ doc.elements.each("*/item") {|e|
40
+ desc = e.elements["title"].text
41
+ desc.gsub!(/(.{150}).*/, '\1..')
42
+ reply = sprintf("%s | %s", e.elements["link"].text, desc)
43
+ m.reply reply
44
+ done += 1
45
+ break if done >= max
46
+ }
47
+ unless done > 0
48
+ m.reply "search for #{search} failed"
49
+ end
50
+ end
51
+
52
+ def slashdot(m, params)
53
+ puts params.inspect
54
+ max = params[:limit].to_i
55
+ puts "max is #{max}"
56
+ xml = @bot.httputil.get(URI.parse("http://slashdot.org/slashdot.xml"))
57
+ unless xml
58
+ m.reply "slashdot news parse failed"
59
+ return
60
+ end
61
+ doc = Document.new xml
62
+ unless doc
63
+ m.reply "slashdot news parse failed (invalid xml)"
64
+ return
65
+ end
66
+ done = 0
67
+ oneline = false
68
+ if max < 0
69
+ max = (0 - max)
70
+ oneline = true
71
+ end
72
+ max = 8 if max > 8
73
+ matches = Array.new
74
+ doc.elements.each("*/story") {|e|
75
+ matches << [ e.elements["title"].text,
76
+ e.elements["author"].text,
77
+ e.elements["time"].text.gsub(/\d{4}-(\d{2})-(\d{2})/, "\\2/\\1").gsub(/:\d\d$/, "") ]
78
+ done += 1
79
+ break if done >= max
80
+ }
81
+ if oneline
82
+ m.reply matches.collect{|mat| mat[0]}.join(" | ")
83
+ else
84
+ matches.each {|mat|
85
+ m.reply sprintf("%36s | %8s | %8s", mat[0][0,36], mat[1][0,8], mat[2])
86
+ }
87
+ end
88
+ end
89
+ end
90
+ plugin = SlashdotPlugin.new
91
+ plugin.map 'slashdot search :limit *search', :action => 'search_slashdot',
92
+ :defaults => {:limit => 4}, :requirements => {:limit => /^-?\d+$/}
93
+ plugin.map 'slashdot :limit', :defaults => {:limit => 4},
94
+ :requirements => {:limit => /^-?\d+$/}
@@ -0,0 +1,36 @@
1
+ class SpellPlugin < Plugin
2
+ def help(plugin, topic="")
3
+ "spell <word> => check spelling of <word>, suggest alternatives"
4
+ end
5
+ def privmsg(m)
6
+ unless(m.params && m.params =~ /^\S+$/)
7
+ m.reply "incorrect usage: " + help(m.plugin)
8
+ return
9
+ end
10
+ p = IO.popen("ispell -a -S", "w+")
11
+ if(p)
12
+ p.puts m.params
13
+ p.close_write
14
+ p.each_line {|l|
15
+ if(l =~ /^\*/)
16
+ m.reply "#{m.params} may be spelled correctly"
17
+ return
18
+ elsif(l =~ /^\s*&.*: (.*)$/)
19
+ m.reply "#{m.params}: #$1"
20
+ return
21
+ elsif(l =~ /^\s*\+ (.*)$/)
22
+ m.reply "#{m.params} is presumably derived from " + $1.downcase
23
+ return
24
+ elsif(l =~ /^\s*#/)
25
+ m.reply "#{m.params}: no suggestions"
26
+ return
27
+ end
28
+ }
29
+ else
30
+ m.reply "couldn't exec ispell :("
31
+ return
32
+ end
33
+ end
34
+ end
35
+ plugin = SpellPlugin.new
36
+ plugin.register("spell")
@@ -0,0 +1,71 @@
1
+ #Tube Status Enquiry plugin for rbot
2
+ #Plugin by Colm Linehan
3
+
4
+ require 'rexml/document'
5
+ require 'uri/common'
6
+
7
+ class TubePlugin < Plugin
8
+ include REXML
9
+ def help(plugin, topic="")
10
+ "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"
11
+ end
12
+
13
+ def tube(m, params)
14
+ line = params[:line]
15
+ begin
16
+ tube_page = @bot.httputil.get(URI.parse("http://www.tfl.gov.uk/tfl/service_rt_tube.shtml"), 1, 1)
17
+ rescue URI::InvalidURIError, URI::BadURIError => e
18
+ m.reply "Cannot contact Tube Service Status page"
19
+ return
20
+ end
21
+ unless tube_page
22
+ m.reply "Cannot contact Tube Service Status page"
23
+ return
24
+ end
25
+ next_line = false
26
+ tube_page.each_line {|l|
27
+ next if l == "\r\n"
28
+ next if l == "\n"
29
+ if (next_line)
30
+ if (l =~ /^<tr valign=top> <td>\s*(.*)<\/td><\/tr>/i)
31
+ m.reply $1.split(/<[^>]+>|&nbsp;/i).join(" ")
32
+ return
33
+ else
34
+ m.reply "There are problems on the #{line} line, but I didn't understand the page format. You should check out http://www.tfl.gov.uk/tfl/service_rt_tube.shtml for more details."
35
+ return
36
+ end
37
+ end
38
+ next_line = true if (l =~ /class="#{line}"/i)
39
+ }
40
+ m.reply "No Problems on the #{line} line."
41
+ end
42
+
43
+ def check_stations(m, params)
44
+ begin
45
+ tube_page = @bot.httputil.get(URI.parse("http://www.tfl.gov.uk/tfl/service_rt_tube.shtml"))
46
+ rescue URI::InvalidURIError, URI::BadURIError => e
47
+ m.reply "Cannot contact Tube Service Status page"
48
+ return
49
+ end
50
+ unless tube_page
51
+ m.reply "Cannot contact Tube Service Status page"
52
+ return
53
+ end
54
+ stations_array = Array.new
55
+ tube_page.each_line {|l|
56
+ if (l =~ /<tr valign=top> <td valign="middle" class="Station"><b>(.*)<\/b><\/td><\/tr>\s*/i)
57
+ stations_array.push $1
58
+ end
59
+ }
60
+ if stations_array.empty?
61
+ m.reply "There are no station-specific announcements"
62
+ return
63
+ else
64
+ m.reply stations_array.join(", ")
65
+ return
66
+ end
67
+ end
68
+ end
69
+ plugin = TubePlugin.new
70
+ plugin.map 'tube stations', :action => 'check_stations'
71
+ plugin.map 'tube :line'
@@ -0,0 +1,88 @@
1
+ Url = Struct.new("Url", :channel, :nick, :time, :url)
2
+
3
+ class UrlPlugin < Plugin
4
+ BotConfig.register BotConfigIntegerValue.new('url.max_urls',
5
+ :default => 100, :validate => Proc.new{|v| v > 0},
6
+ :desc => "Maximum number of urls to store. New urls replace oldest ones.")
7
+
8
+ def initialize
9
+ super
10
+ @registry.set_default(Array.new)
11
+ end
12
+ def help(plugin, topic="")
13
+ "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>"
14
+ end
15
+ def listen(m)
16
+ return unless m.kind_of?(PrivMessage)
17
+ return if m.address?
18
+ # TODO support multiple urls in one line
19
+ if m.message =~ /(f|ht)tps?:\/\//
20
+ if m.message =~ /((f|ht)tps?:\/\/.*?)(?:\s+|$)/
21
+ urlstr = $1
22
+ list = @registry[m.target]
23
+ # check to see if this url is already listed
24
+ return if list.find {|u|
25
+ u.url == urlstr
26
+ }
27
+ url = Url.new(m.target, m.sourcenick, Time.new, urlstr)
28
+ debug "#{list.length} urls so far"
29
+ if list.length > @bot.config['url.max_urls']
30
+ list.pop
31
+ end
32
+ debug "storing url #{url.url}"
33
+ list.unshift url
34
+ debug "#{list.length} urls now"
35
+ @registry[m.target] = list
36
+ end
37
+ end
38
+ end
39
+
40
+ def urls(m, params)
41
+ channel = params[:channel] ? params[:channel] : m.target
42
+ max = params[:limit].to_i
43
+ max = 10 if max > 10
44
+ max = 1 if max < 1
45
+ list = @registry[channel]
46
+ if list.empty?
47
+ m.reply "no urls seen yet for channel #{channel}"
48
+ else
49
+ list[0..(max-1)].each do |url|
50
+ m.reply "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def search(m, params)
56
+ channel = params[:channel] ? params[:channel] : m.target
57
+ max = params[:limit].to_i
58
+ string = params[:string]
59
+ max = 10 if max > 10
60
+ max = 1 if max < 1
61
+ regex = Regexp.new(string)
62
+ list = @registry[channel].find_all {|url|
63
+ regex.match(url.url) || regex.match(url.nick)
64
+ }
65
+ if list.empty?
66
+ m.reply "no matches for channel #{channel}"
67
+ else
68
+ list[0..(max-1)].each do |url|
69
+ m.reply "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ plugin = UrlPlugin.new
75
+ plugin.map 'urls search :channel :limit :string', :action => 'search',
76
+ :defaults => {:limit => 4},
77
+ :requirements => {:limit => /^\d+$/},
78
+ :public => false
79
+ plugin.map 'urls search :limit :string', :action => 'search',
80
+ :defaults => {:limit => 4},
81
+ :requirements => {:limit => /^\d+$/},
82
+ :private => false
83
+ plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
84
+ :requirements => {:limit => /^\d+$/},
85
+ :public => false
86
+ plugin.map 'urls :limit', :defaults => {:limit => 4},
87
+ :requirements => {:limit => /^\d+$/},
88
+ :private => false
@@ -0,0 +1,649 @@
1
+ # This is nasty-ass. I hate writing parsers.
2
+ class Metar
3
+ attr_reader :decoded
4
+ attr_reader :input
5
+ attr_reader :date
6
+ attr_reader :nodata
7
+ def initialize(string)
8
+ str = nil
9
+ @nodata = false
10
+ string.each_line {|l|
11
+ if str == nil
12
+ # grab first line (date)
13
+ @date = l.chomp.strip
14
+ str = ""
15
+ else
16
+ if(str == "")
17
+ str = l.chomp.strip
18
+ else
19
+ str += " " + l.chomp.strip
20
+ end
21
+ end
22
+ }
23
+ if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/
24
+ # 2002/02/26 05:00
25
+ @date = Time.gm($1, $2, $3, $4, $5, 0)
26
+ else
27
+ @date = Time.now
28
+ end
29
+ @input = str.chomp
30
+ @cloud_layers = 0
31
+ @cloud_coverage = {
32
+ 'SKC' => '0',
33
+ 'CLR' => '0',
34
+ 'VV' => '8/8',
35
+ 'FEW' => '1/8 - 2/8',
36
+ 'SCT' => '3/8 - 4/8',
37
+ 'BKN' => '5/8 - 7/8',
38
+ 'OVC' => '8/8'
39
+ }
40
+ @wind_dir_texts = [
41
+ 'North',
42
+ 'North/Northeast',
43
+ 'Northeast',
44
+ 'East/Northeast',
45
+ 'East',
46
+ 'East/Southeast',
47
+ 'Southeast',
48
+ 'South/Southeast',
49
+ 'South',
50
+ 'South/Southwest',
51
+ 'Southwest',
52
+ 'West/Southwest',
53
+ 'West',
54
+ 'West/Northwest',
55
+ 'Northwest',
56
+ 'North/Northwest',
57
+ 'North'
58
+ ]
59
+ @wind_dir_texts_short = [
60
+ 'N',
61
+ 'N/NE',
62
+ 'NE',
63
+ 'E/NE',
64
+ 'E',
65
+ 'E/SE',
66
+ 'SE',
67
+ 'S/SE',
68
+ 'S',
69
+ 'S/SW',
70
+ 'SW',
71
+ 'W/SW',
72
+ 'W',
73
+ 'W/NW',
74
+ 'NW',
75
+ 'N/NW',
76
+ 'N'
77
+ ]
78
+ @weather_array = {
79
+ 'MI' => 'Mild ',
80
+ 'PR' => 'Partial ',
81
+ 'BC' => 'Patches ',
82
+ 'DR' => 'Low Drifting ',
83
+ 'BL' => 'Blowing ',
84
+ 'SH' => 'Shower(s) ',
85
+ 'TS' => 'Thunderstorm ',
86
+ 'FZ' => 'Freezing',
87
+ 'DZ' => 'Drizzle ',
88
+ 'RA' => 'Rain ',
89
+ 'SN' => 'Snow ',
90
+ 'SG' => 'Snow Grains ',
91
+ 'IC' => 'Ice Crystals ',
92
+ 'PE' => 'Ice Pellets ',
93
+ 'GR' => 'Hail ',
94
+ 'GS' => 'Small Hail and/or Snow Pellets ',
95
+ 'UP' => 'Unknown ',
96
+ 'BR' => 'Mist ',
97
+ 'FG' => 'Fog ',
98
+ 'FU' => 'Smoke ',
99
+ 'VA' => 'Volcanic Ash ',
100
+ 'DU' => 'Widespread Dust ',
101
+ 'SA' => 'Sand ',
102
+ 'HZ' => 'Haze ',
103
+ 'PY' => 'Spray',
104
+ 'PO' => 'Well-Developed Dust/Sand Whirls ',
105
+ 'SQ' => 'Squalls ',
106
+ 'FC' => 'Funnel Cloud Tornado Waterspout ',
107
+ 'SS' => 'Sandstorm/Duststorm '
108
+ }
109
+ @cloud_condition_array = {
110
+ 'SKC' => 'clear',
111
+ 'CLR' => 'clear',
112
+ 'VV' => 'vertical visibility',
113
+ 'FEW' => 'a few',
114
+ 'SCT' => 'scattered',
115
+ 'BKN' => 'broken',
116
+ 'OVC' => 'overcast'
117
+ }
118
+ @strings = {
119
+ 'mm_inches' => '%s mm (%s inches)',
120
+ 'precip_a_trace' => 'a trace',
121
+ 'precip_there_was' => 'There was %s of precipitation ',
122
+ 'sky_str_format1' => 'There were %s at a height of %s meters (%s feet)',
123
+ 'sky_str_clear' => 'The sky was clear',
124
+ 'sky_str_format2' => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)',
125
+ 'sky_str_format3' => ' and %s at a height of %s meters (%s feet)',
126
+ 'clouds' => ' clouds',
127
+ 'clouds_cb' => ' cumulonimbus clouds',
128
+ 'clouds_tcu' => ' towering cumulus clouds',
129
+ 'visibility_format' => 'The visibility was %s kilometers (%s miles).',
130
+ 'wind_str_format1' => 'blowing at a speed of %s meters per second (%s miles per hour)',
131
+ 'wind_str_format2' => ', with gusts to %s meters per second (%s miles per hour),',
132
+ 'wind_str_format3' => ' from the %s',
133
+ 'wind_str_calm' => 'calm',
134
+ 'precip_last_hour' => 'in the last hour. ',
135
+ 'precip_last_6_hours' => 'in the last 3 to 6 hours. ',
136
+ 'precip_last_24_hours' => 'in the last 24 hours. ',
137
+ 'precip_snow' => 'There is %s mm (%s inches) of snow on the ground. ',
138
+ '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).',
139
+ 'temp_max_6_hours' => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
140
+ 'temp_min_6_hours' => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ',
141
+ '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). ',
142
+ 'light' => 'Light ',
143
+ 'moderate' => 'Moderate ',
144
+ 'heavy' => 'Heavy ',
145
+ 'mild' => 'Mild ',
146
+ 'nearby' => 'Nearby ',
147
+ 'current_weather' => 'Current weather is %s. ',
148
+ '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'
149
+ }
150
+
151
+ parse
152
+ end
153
+
154
+ def store_speed(value, windunit, meterspersec, knots, milesperhour)
155
+ # Helper function to convert and store speed based on unit.
156
+ # &$meterspersec, &$knots and &$milesperhour are passed on
157
+ # reference
158
+ if (windunit == 'KT')
159
+ # The windspeed measured in knots:
160
+ @decoded[knots] = sprintf("%.2f", value)
161
+ # The windspeed measured in meters per second, rounded to one decimal place:
162
+ @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444)
163
+ # The windspeed measured in miles per hour, rounded to one decimal place: */
164
+ @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667)
165
+ elsif (windunit == 'MPS')
166
+ # The windspeed measured in meters per second:
167
+ @decoded[meterspersec] = sprintf("%.2f", value)
168
+ # The windspeed measured in knots, rounded to one decimal place:
169
+ @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444)
170
+ #The windspeed measured in miles per hour, rounded to one decimal place:
171
+ @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667)
172
+ elsif (windunit == 'KMH')
173
+ # The windspeed measured in kilometers per hour:
174
+ @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600)
175
+ @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444)
176
+ # The windspeed measured in miles per hour, rounded to one decimal place:
177
+ @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667)
178
+ end
179
+ end
180
+
181
+ def parse
182
+ @decoded = Hash.new
183
+ puts @input
184
+ @input.split(" ").each {|part|
185
+ if (part == 'METAR')
186
+ # Type of Report: METAR
187
+ @decoded['type'] = 'METAR'
188
+ elsif (part == 'SPECI')
189
+ # Type of Report: SPECI
190
+ @decoded['type'] = 'SPECI'
191
+ elsif (part == 'AUTO')
192
+ # Report Modifier: AUTO
193
+ @decoded['report_mod'] = 'AUTO'
194
+ elsif (part == 'NIL')
195
+ @nodata = true
196
+ elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station')))
197
+ # Station Identifier
198
+ @decoded['station'] = part
199
+ elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/)
200
+ # ignore this bit, it's useless without month/year. some of these
201
+ # things are hideously out of date.
202
+ # now = Time.new
203
+ # time = Time.gm(now.year, now.month, $1, $2, $3, 0)
204
+ # Date and Time of Report
205
+ # @decoded['time'] = time
206
+ elsif (part == 'COR')
207
+ # Report Modifier: COR
208
+ @decoded['report_mod'] = 'COR'
209
+ elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/)
210
+ # Wind Group
211
+ windunit = $3
212
+ # now do ereg to get the actual values
213
+ part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/
214
+ if ($1 == 'VRB')
215
+ @decoded['wind_deg'] = 'variable directions'
216
+ @decoded['wind_dir_text'] = 'variable directions'
217
+ @decoded['wind_dir_text_short'] = 'VAR'
218
+ else
219
+ @decoded['wind_deg'] = $1
220
+ @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round]
221
+ @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round]
222
+ end
223
+ store_speed($2, windunit,
224
+ 'wind_meters_per_second',
225
+ 'wind_knots',
226
+ 'wind_miles_per_hour')
227
+
228
+ if ($4 != nil)
229
+ # We have a report with information about the gust.
230
+ # First we have the gust measured in knots
231
+ if ($4 =~ /G([0-9]{2,3})/)
232
+ store_speed($1,windunit,
233
+ 'wind_gust_meters_per_second',
234
+ 'wind_gust_knots',
235
+ 'wind_gust_miles_per_hour')
236
+ end
237
+ end
238
+ elsif (part =~ /([0-9]{3})V([0-9]{3})/)
239
+ # Variable wind-direction
240
+ @decoded['wind_var_beg'] = $1
241
+ @decoded['wind_var_end'] = $2
242
+ elsif (part == "9999")
243
+ # A strange value. When you look at other pages you see it
244
+ # interpreted like this (where I use > to signify 'Greater
245
+ # than'):
246
+ @decoded['visibility_miles'] = '>7';
247
+ @decoded['visibility_km'] = '>11.3';
248
+ elsif (part =~ /^([0-9]{4})$/)
249
+ # Visibility in meters (4 digits only)
250
+ # The visibility measured in kilometers, rounded to one decimal place.
251
+ @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000)
252
+ # The visibility measured in miles, rounded to one decimal place.
253
+ @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344)
254
+ elsif (part =~ /^[0-9]$/)
255
+ # Temp Visibility Group, single digit followed by space
256
+ @decoded['temp_visibility_miles'] = part
257
+ elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/)
258
+ # Visibility Group
259
+ if ($4 == '/')
260
+ vis_miles = $2.to_i + $3.to_i/$5.to_i
261
+ else
262
+ vis_miles = $1.to_i;
263
+ end
264
+ if (@decoded['temp_visibility_miles'][0] == 'M')
265
+ # The visibility measured in miles, prefixed with < to indicate 'Less than'
266
+ @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles)
267
+ # The visibility measured in kilometers. The value is rounded
268
+ # to one decimal place, prefixed with < to indicate 'Less than' */
269
+ @decoded['visibility_km'] = '<' . sprintf("%.1f", vis_miles * 1.609344)
270
+ else
271
+ # The visibility measured in mile.s */
272
+ @decoded['visibility_miles'] = sprintf("%.1f", vis_miles)
273
+ # The visibility measured in kilometers, rounded to one decimal place.
274
+ @decoded['visibility_km'] = sprintf("%.1f", vis_miles * 1.609344)
275
+ end
276
+ 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)+$/)
277
+ # Current weather-group
278
+ @decoded['weather'] = '' unless @decoded.has_key?('weather')
279
+ if (part[0].chr == '-')
280
+ # A light phenomenon
281
+ @decoded['weather'] += @strings['light']
282
+ part = part[1,part.length]
283
+ elsif (part[0].chr == '+')
284
+ # A heavy phenomenon
285
+ @decoded['weather'] += @strings['heavy']
286
+ part = part[1,part.length]
287
+ elsif (part[0,2] == 'VC')
288
+ # Proximity Qualifier
289
+ @decoded['weather'] += @strings['nearby']
290
+ part = part[2,part.length]
291
+ elsif (part[0,2] == 'MI')
292
+ @decoded['weather'] += @strings['mild']
293
+ part = part[2,part.length]
294
+ else
295
+ # no intensity code => moderate phenomenon
296
+ @decoded['weather'] += @strings['moderate']
297
+ end
298
+
299
+ while (part && bite = part[0,2]) do
300
+ # Now we take the first two letters and determine what they
301
+ # mean. We append this to the variable so that we gradually
302
+ # build up a phrase.
303
+
304
+ @decoded['weather'] += @weather_array[bite]
305
+ # Here we chop off the two first letters, so that we can take
306
+ # a new bite at top of the while-loop.
307
+ part = part[2,-1]
308
+ end
309
+ elsif (part =~ /(SKC|CLR)/)
310
+ # Cloud-layer-group.
311
+ # There can be up to three of these groups, so we store them as
312
+ # cloud_layer1, cloud_layer2 and cloud_layer3.
313
+
314
+ @cloud_layers += 1;
315
+ # Again we have to translate the code-characters to a
316
+ # meaningful string.
317
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = @cloud_condition_array[$1]
318
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
319
+ elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/)
320
+ # We have found (another) a cloud-layer-group. There can be up
321
+ # to three of these groups, so we store them as cloud_layer1,
322
+ # cloud_layer2 and cloud_layer3.
323
+ @cloud_layers += 1;
324
+ # Again we have to translate the code-characters to a meaningful string.
325
+ if ($3 == 'CB')
326
+ # cumulonimbus (CB) clouds were observed. */
327
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
328
+ @cloud_condition_array[$1] + @strings['clouds_cb']
329
+ elsif ($3 == 'TCU')
330
+ # towering cumulus (TCU) clouds were observed.
331
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
332
+ @cloud_condition_array[$1] + @strings['clouds_tcu']
333
+ else
334
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] =
335
+ @cloud_condition_array[$1] + @strings['clouds']
336
+ end
337
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1]
338
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100
339
+ @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m'] = ($2.to_f * 30.48).round
340
+ elsif (part =~ /^T([0-9]{4})$/)
341
+ store_temp($1,'temp_c','temp_f')
342
+ elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/)
343
+ # Temperature/Dew Point Group
344
+ # The temperature and dew-point measured in Celsius.
345
+ @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-'))
346
+ if $2 == "//" || !$2
347
+ @decoded['dew_c'] = 0
348
+ else
349
+ @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-'))
350
+ end
351
+ # The temperature and dew-point measured in Fahrenheit, rounded to
352
+ # the nearest degree.
353
+ @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round
354
+ @decoded['dew_f'] = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round
355
+ elsif(part =~ /A([0-9]{4})/)
356
+ # Altimeter
357
+ # The pressure measured in inHg
358
+ @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100)
359
+ # The pressure measured in mmHg, hPa and atm
360
+ @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254)
361
+ @decoded['altimeter_hpa'] = sprintf("%d", ($1.to_f * 0.33863881578947).to_i)
362
+ @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 3.3421052631579e-4)
363
+ elsif(part =~ /Q([0-9]{4})/)
364
+ # Altimeter
365
+ # This is strange, the specification doesnt say anything about
366
+ # the Qxxxx-form, but it's in the METARs.
367
+ # The pressure measured in hPa
368
+ @decoded['altimeter_hpa'] = sprintf("%d", $1.to_i)
369
+ # The pressure measured in mmHg, inHg and atm
370
+ @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827)
371
+ @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875)
372
+ @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 9.869232667e-4)
373
+ elsif (part =~ /^T([0-9]{4})([0-9]{4})/)
374
+ # Temperature/Dew Point Group, coded to tenth of degree.
375
+ # The temperature and dew-point measured in Celsius.
376
+ store_temp($1,'temp_c','temp_f')
377
+ store_temp($2,'dew_c','dew_f')
378
+ elsif (part =~ /^1([0-9]{4}$)/)
379
+ # 6 hour maximum temperature Celsius, coded to tenth of degree
380
+ store_temp($1,'temp_max6h_c','temp_max6h_f')
381
+ elsif (part =~ /^2([0-9]{4}$)/)
382
+ # 6 hour minimum temperature Celsius, coded to tenth of degree
383
+ store_temp($1,'temp_min6h_c','temp_min6h_f')
384
+ elsif (part =~ /^4([0-9]{4})([0-9]{4})$/)
385
+ # 24 hour maximum and minimum temperature Celsius, coded to
386
+ # tenth of degree
387
+ store_temp($1,'temp_max24h_c','temp_max24h_f')
388
+ store_temp($2,'temp_min24h_c','temp_min24h_f')
389
+ elsif (part =~ /^P([0-9]{4})/)
390
+ # Precipitation during last hour in hundredths of an inch
391
+ # (store as inches)
392
+ @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100)
393
+ @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254)
394
+ elsif (part =~ /^6([0-9]{4})/)
395
+ # Precipitation during last 3 or 6 hours in hundredths of an
396
+ # inch (store as inches)
397
+ @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100)
398
+ @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
399
+ elsif (part =~ /^7([0-9]{4})/)
400
+ # Precipitation during last 24 hours in hundredths of an inch
401
+ # (store as inches)
402
+ @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100)
403
+ @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254)
404
+ elsif(part =~ /^4\/([0-9]{3})/)
405
+ # Snow depth in inches
406
+ @decoded['snow_in'] = sprintf("%.2f", $1);
407
+ @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4)
408
+ else
409
+ # If we couldn't match the group, we assume that it was a
410
+ # remark.
411
+ @decoded['remarks'] = '' unless @decoded.has_key?("remarks")
412
+ @decoded['remarks'] += ' ' + part;
413
+ end
414
+ }
415
+
416
+ # Relative humidity
417
+ # p @decoded['dew_c'] # 11.0
418
+ # p @decoded['temp_c'] # 21.0
419
+ # => 56.1
420
+ @decoded['rel_humidity'] = sprintf("%.1f",100 *
421
+ (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')
422
+ end
423
+
424
+ def store_temp(temp,temp_cname,temp_fname)
425
+ # Given a numerical temperature temp in Celsius, coded to tenth of
426
+ # degree, store in @decoded[temp_cname], convert to Fahrenheit
427
+ # and store in @decoded[temp_fname]
428
+ # Note: temp is converted to negative if temp > 100.0 (See
429
+ # Federal Meteorological Handbook for groups T, 1, 2 and 4)
430
+
431
+ # Temperature measured in Celsius, coded to tenth of degree
432
+ temp = temp.to_f/10
433
+ if (temp >100.0)
434
+ # first digit = 1 means minus temperature
435
+ temp = -(temp - 100.0)
436
+ end
437
+ @decoded[temp_cname] = sprintf("%.1f", temp)
438
+ # The temperature in Fahrenheit.
439
+ @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32)
440
+ end
441
+
442
+ def pretty_print_precip(precip_mm, precip_in)
443
+ # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal
444
+ # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in
445
+ # several places, so standardized in one function.
446
+ if (precip_mm.to_i > 0)
447
+ amount = sprintf(@strings['mm_inches'], precip_mm, precip_in)
448
+ else
449
+ amount = @strings['a_trace']
450
+ end
451
+ return sprintf(@strings['precip_there_was'], amount)
452
+ end
453
+
454
+ def pretty_print
455
+ if @nodata
456
+ return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :("
457
+ end
458
+
459
+ ["temp_c", "altimeter_hpa"].each {|key|
460
+ if !@decoded.has_key?(key)
461
+ return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})"
462
+ end
463
+ }
464
+
465
+ mins_old = ((Time.now - @date.to_i).to_f/60).round
466
+ if (mins_old <= 60)
467
+ weather_age = mins_old.to_s + " minutes ago,"
468
+ elsif (mins_old <= 60 * 25)
469
+ weather_age = (mins_old / 60).to_s + " hours, "
470
+ weather_age += (mins_old % 60).to_s + " minutes ago,"
471
+ else
472
+ # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})"
473
+ weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:"
474
+ end
475
+
476
+ if(@decoded.has_key?("cloud_layer1_altitude_ft"))
477
+ sky_str = sprintf(@strings['sky_str_format1'],
478
+ @decoded["cloud_layer1_condition"],
479
+ @decoded["cloud_layer1_altitude_m"],
480
+ @decoded["cloud_layer1_altitude_ft"])
481
+ else
482
+ sky_str = @strings['sky_str_clear']
483
+ end
484
+
485
+ if(@decoded.has_key?("cloud_layer2_altitude_ft"))
486
+ if(@decoded.has_key?("cloud_layer3_altitude_ft"))
487
+ sky_str += sprintf(@strings['sky_str_format2'],
488
+ @decoded["cloud_layer2_condition"],
489
+ @decoded["cloud_layer2_altitude_m"],
490
+ @decoded["cloud_layer2_altitude_ft"],
491
+ @decoded["cloud_layer3_condition"],
492
+ @decoded["cloud_layer3_altitude_m"],
493
+ @decoded["cloud_layer3_altitude_ft"])
494
+ else
495
+ sky_str += sprintf(@strings['sky_str_format3'],
496
+ @decoded["cloud_layer2_condition"],
497
+ @decoded["cloud_layer2_altitude_m"],
498
+ @decoded["cloud_layer2_altitude_ft"])
499
+ end
500
+ end
501
+ sky_str += "."
502
+
503
+ if(@decoded.has_key?("visibility_miles"))
504
+ visibility = sprintf(@strings['visibility_format'],
505
+ @decoded["visibility_km"],
506
+ @decoded["visibility_miles"])
507
+ else
508
+ visibility = ""
509
+ end
510
+
511
+ if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0)
512
+ wind_str = sprintf(@strings['wind_str_format1'],
513
+ @decoded["wind_meters_per_second"],
514
+ @decoded["wind_miles_per_hour"])
515
+ if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0)
516
+ wind_str += sprintf(@strings['wind_str_format2'],
517
+ @decoded["wind_gust_meters_per_second"],
518
+ @decoded["wind_gust_miles_per_hour"])
519
+ end
520
+ wind_str += sprintf(@strings['wind_str_format3'],
521
+ @decoded["wind_dir_text"])
522
+ else
523
+ wind_str = @strings['wind_str_calm']
524
+ end
525
+
526
+ prec_str = ""
527
+ if (@decoded.has_key?("precip_in"))
528
+ prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour']
529
+ end
530
+ if (@decoded.has_key?("precip_6h_in"))
531
+ prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours']
532
+ end
533
+ if (@decoded.has_key?("precip_24h_in"))
534
+ prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours']
535
+ end
536
+ if (@decoded.has_key?("snow_in"))
537
+ prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"])
538
+ end
539
+
540
+ temp_str = ""
541
+ if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c"))
542
+ temp_str += sprintf(@strings['temp_min_max_6_hours'],
543
+ @decoded["temp_max6h_c"],
544
+ @decoded["temp_min6h_c"],
545
+ @decoded["temp_max6h_f"],
546
+ @decoded["temp_min6h_f"])
547
+ else
548
+ if (@decoded.has_key?("temp_max6h_c"))
549
+ temp_str += sprintf(@strings['temp_max_6_hours'],
550
+ @decoded["temp_max6h_c"],
551
+ @decoded["temp_max6h_f"])
552
+ end
553
+ if (@decoded.has_key?("temp_min6h_c"))
554
+ temp_str += sprintf(@strings['temp_max_6_hours'],
555
+ @decoded["temp_min6h_c"],
556
+ @decoded["temp_min6h_f"])
557
+ end
558
+ end
559
+ if (@decoded.has_key?("temp_max24h_c"))
560
+ temp_str += sprintf(@strings['temp_min_max_24_hours'],
561
+ @decoded["temp_max24h_c"],
562
+ @decoded["temp_min24h_c"],
563
+ @decoded["temp_max24h_f"],
564
+ @decoded["temp_min24h_f"])
565
+ end
566
+
567
+ if (@decoded.has_key?("weather"))
568
+ weather_str = sprintf(@strings['current_weather'], @decoded["weather"])
569
+ else
570
+ weather_str = ''
571
+ end
572
+
573
+ return sprintf(@strings['pretty_print_metar'],
574
+ weather_age,
575
+ @date,
576
+ wind_str, @decoded["station"], @decoded["temp_c"],
577
+ @decoded["temp_f"], @decoded["altimeter_hpa"],
578
+ @decoded["altimeter_inhg"],
579
+ @decoded["rel_humidity"], sky_str,
580
+ visibility, weather_str, prec_str, temp_str).strip
581
+ end
582
+
583
+ def to_s
584
+ @input
585
+ end
586
+ end
587
+
588
+
589
+ class WeatherPlugin < Plugin
590
+
591
+ def help(plugin, topic="")
592
+ "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"
593
+ end
594
+
595
+ def get_metar(station)
596
+ station.upcase!
597
+
598
+ result = @bot.httputil.get(URI.parse("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT"))
599
+ return nil unless result
600
+ return Metar.new(result)
601
+ end
602
+
603
+
604
+ def initialize
605
+ super
606
+ # this plugin only wants to store strings
607
+ class << @registry
608
+ def store(val)
609
+ val
610
+ end
611
+ def restore(val)
612
+ val
613
+ end
614
+ end
615
+ @metar_cache = Hash.new
616
+ end
617
+
618
+ def describe(m, where)
619
+ if @metar_cache.has_key?(where) &&
620
+ Time.now - @metar_cache[where].date < 3600
621
+ met = @metar_cache[where]
622
+ else
623
+ met = get_metar(where)
624
+ end
625
+
626
+ if met
627
+ m.reply met.pretty_print
628
+ @metar_cache[where] = met
629
+ else
630
+ m.reply "couldn't find weather data for #{where}"
631
+ end
632
+ end
633
+
634
+ def weather(m, params)
635
+ if params[:where]
636
+ @registry[m.sourcenick] = params[:where]
637
+ describe(m,params[:where])
638
+ else
639
+ if @registry.has_key?(m.sourcenick)
640
+ where = @registry[m.sourcenick]
641
+ describe(m,where)
642
+ else
643
+ 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."
644
+ end
645
+ end
646
+ end
647
+ end
648
+ plugin = WeatherPlugin.new
649
+ plugin.map 'weather :where', :defaults => {:where => false}