]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
weather: use bot standard html cleanup functions
[user/henk/code/ruby/rbot.git] / 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   Config.register Config::BooleanValue.new('weather.advisory',
72     :default => true,
73     :desc => "Should the bot report special weather advisories when any is present?")
74   Config.register Config::EnumValue.new('weather.units',
75     :values => ['metric', 'english', 'both'],
76     :default => 'both',
77     :desc => "Units to be used by default in Weather Underground reports")
78
79
80   def help(plugin, topic="")
81     case topic
82     when "nws"
83       "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/ )"
84     when "station", "wu"
85       "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."
86     else
87       "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
88     end
89   end
90
91   def initialize
92     super
93
94     @nws_cache = Hash.new
95
96     @wu_url         = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
97     @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
98   end
99
100   def weather(m, params)
101     if params[:where].empty?
102       if @registry.has_key?(m.sourcenick)
103         where = @registry[m.sourcenick]
104         debug "Loaded weather info #{where.inspect} for #{m.sourcenick}"
105
106         service = where.first.to_sym
107         loc = where[1].to_s
108         units = params[:units] || where[2] rescue nil
109       else
110         debug "No weather info for #{m.sourcenick}"
111         m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
112         return
113       end
114     else
115       where = params[:where]
116       if ['nws','station'].include?(where.first)
117         service = where.first.to_sym
118         loc = where[1].to_s
119       else
120         service = :wu
121         loc = where.to_s
122       end
123       units = params[:units]
124     end
125
126     if loc.empty?
127       debug "No weather location found for #{m.sourcenick}"
128       m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
129       return
130     end
131
132     wu_units = String.new
133
134     units ||= @bot.config['weather.units']
135     case units.to_sym
136     when :english, :metric
137       wu_units = "_#{units}"
138     when :both
139     else
140       m.reply "Ignoring unknown units #{units}"
141     end
142
143     case service
144     when :nws
145       nws_describe(m, loc)
146     when :station
147       wu_station(m, loc, wu_units)
148     when :wu
149       wu_weather(m, loc, wu_units)
150     end
151
152     @registry[m.sourcenick] = [service, loc, units]
153   end
154
155   def nws_describe(m, where)
156     if @nws_cache.has_key?(where) then
157         met = @nws_cache[where]
158     else
159         met = CurrentConditions.new(where)
160     end
161     if met
162       begin
163         m.reply met.update
164         @nws_cache[where] = met
165       rescue => e
166         m.reply e.message
167       end
168     else
169       m.reply "couldn't find weather data for #{where}"
170     end
171   end
172
173   def wu_station(m, where, units)
174     begin
175       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
176       case xml
177       when nil
178         m.reply "couldn't retrieve weather information, sorry"
179         return
180       when /Search not found:/
181         m.reply "no such station found (#{where})"
182         return
183       when /<table border.*?>(.*?)<\/table>/m
184         data = $1.dup
185         m.reply wu_weather_filter(data)
186         wu_out_special(m, xml)
187       else
188         debug xml
189         m.reply "something went wrong with the data for #{where}..."
190       end
191     rescue => e
192       m.reply "retrieving info about '#{where}' failed (#{e})"
193     end
194   end
195
196   def wu_weather(m, where, units)
197     begin
198       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
199       case xml
200       when nil
201         m.reply "couldn't retrieve weather information, sorry"
202       when /City Not Found/
203         m.reply "no such location found (#{where})"
204       when /Current<\/a>/
205         data = ""
206         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
207           data += wu_weather_filter(match.first)
208         end
209         if data.length > 0
210           m.reply data
211         else
212           m.reply "couldn't parse weather data from #{where}"
213         end
214         wu_out_special(m, xml)
215       when /<a href="\/(?:global\/stations|US\/\w\w)\//
216         wu_weather_multi(m, xml)
217       else
218         debug xml
219         m.reply "something went wrong with the data from #{where}..."
220       end
221     rescue => e
222       m.reply "retrieving info about '#{where}' failed (#{e})"
223     end
224   end
225
226   def wu_weather_multi(m, xml)
227     # debug xml
228     stations = xml.scan(/<td>\s*(?:<a href="([^?"]+\?feature=[^"]+)"\s*[^>]*><img [^>]+><\/a>\s*)?<a href="\/(?:global\/stations|US\/(\w\w))\/([^"]*?)\.html">(.*?)<\/a>\s*:\s*(.*?)<\/td>/m)
229     # debug stations
230     m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
231     stations.map! { |ar|
232       warning = ar[0]
233       loc = ar[2]
234       state = ar[1]
235       par = ar[3]
236       w = ar[4]
237       if state # US station
238         (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
239       else # non-US station
240         (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
241       end
242     }
243     m.reply stations.join("; ")
244   end
245
246   def wu_check_special(xml)
247     specials = []
248     # We only scan the first half to prevent getting the advisories twice
249     xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
250       special = {
251         :url => "http://mobile.wunderground.com"+$1,
252         :type => $2.dup,
253         :special => $3.dup
254       }
255       spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
256       spec_xml = @bot.httputil.get(special[:url])
257       if spec_xml and spec_td = spec_xml.match(spec_rx)
258         special.merge!(:text => spec_td.captures.first.ircify_html)
259       end
260       specials << special
261     end
262     return specials
263   end
264
265   def wu_out_special(m, xml)
266     return unless @bot.config['weather.advisory']
267     specials = wu_check_special(xml)
268     debug specials
269     specials.each do |special|
270       special.merge!(:underline => Underline)
271       if special[:text]
272         m.reply("%{underline}%{special}%{underline}: %{text}" % special)
273       else
274         m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
275       end
276     end
277   end
278
279   def wu_weather_filter(stuff)
280     result = Array.new
281     if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
282       result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
283     end
284     stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
285       kk = k.riphtml
286       vv = v.riphtml
287       next if vv.empty?
288       next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
289       next if kk == "Raw METAR"
290       result << ("%s: %s" % [kk, vv])
291     }
292     return result.join('; ')
293   end
294 end
295
296 plugin = WeatherPlugin.new
297 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}