plugins: remove excess requires and Net::HTTP.version setups
[rbot] / data / rbot / plugins / weather.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Weather plugin for rbot
5 #
6 # Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2006 Ralph M. Churchill
10 # Copyright:: (C) 2006-2007 Giuseppe Bilotta
11 #
12 # License:: GPL v2
13
14 require 'rexml/document'
15
16 # Wraps NOAA National Weather Service information
17 class CurrentConditions
18     def initialize(station)
19         @station = station
20         @url = "http://www.nws.noaa.gov/data/current_obs/#{@station.upcase}.xml"
21         @etag = String.new
22         @mtime = Time.mktime(0)
23         @current_conditions = String.new
24         @iscached = false
25     end
26     def update
27         begin
28             open(@url,"If-Modified-Since" => @mtime.rfc2822) do |feed|
29             # open(@url,"If-None-Match"=>@etag) do |feed|
30                 @etag = feed.meta['etag']
31                 @mtime = feed.last_modified
32                 cc_doc = (REXML::Document.new feed).root
33                 @iscached = false
34                 @current_conditions = parse(cc_doc)
35             end
36         rescue OpenURI::HTTPError => e
37             case e
38             when /304/:
39                 @iscached = true
40             when /404/:
41                 raise "Data for #{@station} not found"
42             else
43                 raise "Error retrieving data: #{e}"
44             end
45         end
46         @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N")
47     end
48     def parse(cc_doc)
49         cc = Hash.new
50         cc_doc.elements.each do |c|
51             cc[c.name.to_sym] = c.text
52         end
53         "At #{cc[:observation_time_rfc822]}, the wind was #{cc[:wind_string]} at #{cc[:location]} (#{cc[:station_id]}). The temperature was #{cc[:temperature_string]}#{heat_index_or_wind_chill(cc)}, and the pressure was #{cc[:pressure_string]}. The relative humidity was #{cc[:relative_humidity]}%. Current conditions are #{cc[:weather]} with #{cc[:visibility_mi]}mi visibility."
54     end
55 private
56     def heat_index_or_wind_chill(cc)
57         hi = cc[:heat_index_string]
58         wc = cc[:windchill_string]
59         if hi != 'NA' then
60             " with a heat index of #{hi}"
61         elsif wc != 'NA' then
62             " with a windchill of #{wc}"
63         else
64             ""
65         end
66     end
67 end
68
69 class WeatherPlugin < Plugin
70   
71   def help(plugin, topic="")
72     case topic
73     when "nws"
74       "weather nws <station> => display the current conditions at the location specified by the NOAA National Weather Service station code <station> ( lookup your station code at http://www.nws.noaa.gov/data/current_obs/ )"
75     when "station", "wu"
76       "weather [<units>] <location> => display the current conditions at the location specified, looking it up on the Weather Underground site; you can use 'station <code>' to look up data by station code ( lookup your station code at http://www.weatherunderground.com/ ); you can optionally set <units>  to 'metric' or 'english' if you only want data with the units; use 'both' for units to go back to having both." 
77     else
78       "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
79     end
80   end
81   
82   def initialize
83     super
84
85     @nws_cache = Hash.new
86
87     @wu_url         = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
88     @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
89   end
90   
91   def weather(m, params)
92     if params[:where].empty?
93       if @registry.has_key?(m.sourcenick)
94         where = @registry[m.sourcenick]
95         debug "Loaded weather info #{where.inspect} for #{m.sourcenick}"
96
97         service = where.first.to_sym
98         loc = where[1].to_s
99         units = params[:units] || where[2] rescue nil
100       else
101         debug "No weather info for #{m.sourcenick}"
102         m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
103         return
104       end
105     else
106       where = params[:where]
107       if ['nws','station'].include?(where.first)
108         service = where.first.to_sym
109         loc = where[1].to_s
110       else
111         service = :wu
112         loc = where.to_s
113       end
114       units = params[:units]
115     end
116
117     if loc.empty?
118       debug "No weather location found for #{m.sourcenick}"
119       m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
120       return
121     end
122
123     wu_units = String.new
124     if units
125       case units.to_sym
126       when :english, :metric
127         wu_units = "_#{units}"
128       when :both
129       else
130         m.reply "Ignoring unknown units #{units}"
131         wu_units = String.new
132       end
133     end
134
135     case service
136     when :nws
137       nws_describe(m, loc)
138     when :station
139       wu_station(m, loc, wu_units)
140     when :wu
141       wu_weather(m, loc, wu_units)
142     end
143
144     @registry[m.sourcenick] = [service, loc, units]
145   end
146
147   def nws_describe(m, where)
148     if @nws_cache.has_key?(where) then
149         met = @nws_cache[where]
150     else
151         met = CurrentConditions.new(where)
152     end
153     if met
154       begin
155         m.reply met.update
156         @nws_cache[where] = met
157       rescue => e
158         m.reply e.message
159       end
160     else
161       m.reply "couldn't find weather data for #{where}"
162     end
163   end
164
165   def wu_station(m, where, units)
166     begin
167       xml = @bot.httputil.get(@wu_station_url % [units, URI.escape(where)])
168       case xml
169       when nil
170         m.reply "couldn't retrieve weather information, sorry"
171         return
172       when /Search not found:/
173         m.reply "no such station found (#{where})"
174         return
175       when /<table border.*?>(.*?)<\/table>/m
176         data = $1
177         m.reply wu_weather_filter(data)
178       else
179         debug xml
180         m.reply "something went wrong with the data for #{where}..."
181       end
182     rescue => e
183       m.reply "retrieving info about '#{where}' failed (#{e})"
184     end
185   end
186
187   def wu_weather(m, where, units)
188     begin
189       xml = @bot.httputil.get(@wu_url % [units, URI.escape(where)])
190       case xml
191       when nil
192         m.reply "couldn't retrieve weather information, sorry"
193         return
194       when /City Not Found/
195         m.reply "no such location found (#{where})"
196         return
197       when /<table border.*?>(.*?)<\/table>/m
198         data = $1
199         m.reply wu_weather_filter(data)
200       when /<a href="\/global\/stations\//
201         stations = xml.scan(/<a href="\/global\/stations\/(.*?)\.html">/)
202         m.reply "multiple stations available, use 'weather station <code>' where code is one of " + stations.join(", ")
203       else
204         debug xml
205         m.reply "something went wrong with the data from #{where}..."
206       end
207     rescue => e
208       m.reply "retrieving info about '#{where}' failed (#{e})"
209     end
210   end
211
212   def wu_weather_filter(stuff)
213     txt = stuff
214     txt.gsub!(/[\n\s]+/,' ')
215     data = Hash.new
216     txt.gsub!(/&nbsp;/, ' ')
217     txt.gsub!(/&#176;/, ' ') # degree sign
218     txt.gsub!(/<\/?b>/,'')
219     txt.gsub!(/<\/?span[^<>]*?>/,'')
220     txt.gsub!(/<img\s*[^<>]*?>/,'')
221     txt.gsub!(/<br\s?\/?>/,'')
222
223     result = Array.new
224     if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
225       result << ("Weather info for %s (updated on %s)" % [$2, $1])
226     end
227     txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
228       next if v.empty?
229       next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
230       next if k == "Raw METAR"
231       result << ("%s: %s" % [k, v])
232     }
233     return result.join('; ')
234   end
235 end
236
237 plugin = WeatherPlugin.new
238 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}