rbot 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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}