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.
- 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}
|