rbot 0.9.9
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +16 -0
- data/COPYING +21 -0
- data/ChangeLog +418 -0
- data/INSTALL +8 -0
- data/README +44 -0
- data/REQUIREMENTS +34 -0
- data/TODO +5 -0
- data/Usage_en.txt +129 -0
- data/bin/rbot +81 -0
- data/data/rbot/contrib/plugins/figlet.rb +20 -0
- data/data/rbot/contrib/plugins/ri.rb +83 -0
- data/data/rbot/contrib/plugins/stats.rb +232 -0
- data/data/rbot/contrib/plugins/vandale.rb +49 -0
- data/data/rbot/languages/dutch.lang +73 -0
- data/data/rbot/languages/english.lang +75 -0
- data/data/rbot/languages/french.lang +39 -0
- data/data/rbot/languages/german.lang +67 -0
- data/data/rbot/plugins/autoop.rb +68 -0
- data/data/rbot/plugins/autorejoin.rb +16 -0
- data/data/rbot/plugins/cal.rb +15 -0
- data/data/rbot/plugins/dice.rb +81 -0
- data/data/rbot/plugins/eightball.rb +19 -0
- data/data/rbot/plugins/excuse.rb +470 -0
- data/data/rbot/plugins/fish.rb +61 -0
- data/data/rbot/plugins/fortune.rb +22 -0
- data/data/rbot/plugins/freshmeat.rb +98 -0
- data/data/rbot/plugins/google.rb +51 -0
- data/data/rbot/plugins/host.rb +14 -0
- data/data/rbot/plugins/httpd.rb.disabled +35 -0
- data/data/rbot/plugins/insult.rb +258 -0
- data/data/rbot/plugins/karma.rb +85 -0
- data/data/rbot/plugins/lart.rb +181 -0
- data/data/rbot/plugins/math.rb +122 -0
- data/data/rbot/plugins/nickserv.rb +89 -0
- data/data/rbot/plugins/nslookup.rb +43 -0
- data/data/rbot/plugins/opme.rb +19 -0
- data/data/rbot/plugins/quakeauth.rb +51 -0
- data/data/rbot/plugins/quotes.rb +321 -0
- data/data/rbot/plugins/remind.rb +228 -0
- data/data/rbot/plugins/roshambo.rb +54 -0
- data/data/rbot/plugins/rot13.rb +10 -0
- data/data/rbot/plugins/roulette.rb +147 -0
- data/data/rbot/plugins/rss.rb.disabled +414 -0
- data/data/rbot/plugins/seen.rb +89 -0
- data/data/rbot/plugins/slashdot.rb +94 -0
- data/data/rbot/plugins/spell.rb +36 -0
- data/data/rbot/plugins/tube.rb +71 -0
- data/data/rbot/plugins/url.rb +88 -0
- data/data/rbot/plugins/weather.rb +649 -0
- data/data/rbot/plugins/wserver.rb +71 -0
- data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
- data/data/rbot/templates/keywords.rbot +4 -0
- data/data/rbot/templates/lart/larts +98 -0
- data/data/rbot/templates/lart/praises +5 -0
- data/data/rbot/templates/levels.rbot +30 -0
- data/data/rbot/templates/users.rbot +1 -0
- data/lib/rbot/auth.rb +203 -0
- data/lib/rbot/channel.rb +54 -0
- data/lib/rbot/config.rb +363 -0
- data/lib/rbot/dbhash.rb +112 -0
- data/lib/rbot/httputil.rb +141 -0
- data/lib/rbot/ircbot.rb +808 -0
- data/lib/rbot/ircsocket.rb +185 -0
- data/lib/rbot/keywords.rb +433 -0
- data/lib/rbot/language.rb +69 -0
- data/lib/rbot/message.rb +256 -0
- data/lib/rbot/messagemapper.rb +262 -0
- data/lib/rbot/plugins.rb +291 -0
- data/lib/rbot/post-install.rb +8 -0
- data/lib/rbot/rbotconfig.rb +36 -0
- data/lib/rbot/registry.rb +271 -0
- data/lib/rbot/rfc2812.rb +1104 -0
- data/lib/rbot/timer.rb +201 -0
- data/lib/rbot/utils.rb +83 -0
- data/setup.rb +1360 -0
- 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(/<[^>]+>| /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}
|