search: fix google
[rbot] / data / rbot / plugins / search.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Google and Wikipedia search plugin for rbot
5 #
6 # Author:: Tom Gilbert (giblet) <tom@linuxbrit.co.uk>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2002-2005 Tom Gilbert
10 # Copyright:: (C) 2006 Tom Gilbert, Giuseppe Bilotta
11 # Copyright:: (C) 2006-2007 Giuseppe Bilotta
12
13 # TODO:: use lr=lang_<code> or whatever is most appropriate to let google know
14 #        it shouldn't use the bot's location to find the preferred language
15 # TODO:: support localized uncyclopedias -- not easy because they have different names
16 #        for most languages
17
18 GOOGLE_SEARCH = "http://www.google.com/search?oe=UTF-8&q="
19 GOOGLE_WAP_SEARCH = "http://www.google.com/m/search?hl=en&q="
20 GOOGLE_WAP_LINK = /result">(?:<div[^>]*>)?<a href="([^"]+)"[^>]*>(.*?)<\/a>/im
21 GOOGLE_CALC_RESULT = %r{<img src=/images/calc_img\.gif(?: width=40 height=30 alt="")?>.*?<h[1-6] class=r[^>]*><b>(.+?)</b>}
22 GOOGLE_COUNT_RESULT = %r{<font size=-1>Results <b>1<\/b> - <b>10<\/b> of about <b>(.*)<\/b> for}
23 GOOGLE_DEF_RESULT = %r{onebox_result">\s*(.*?)\s*<br/>\s*(.*?)<table}
24 GOOGLE_TIME_RESULT = %r{alt="Clock"></td><td valign=[^>]+>(.+?)<(br|/td)>}
25
26 class SearchPlugin < Plugin
27   Config.register Config::IntegerValue.new('google.hits',
28     :default => 3,
29     :desc => "Number of hits to return from Google searches")
30   Config.register Config::IntegerValue.new('google.first_par',
31     :default => 0,
32     :desc => "When set to n > 0, the bot will return the first paragraph from the first n search hits")
33   Config.register Config::IntegerValue.new('wikipedia.hits',
34     :default => 3,
35     :desc => "Number of hits to return from Wikipedia searches")
36   Config.register Config::IntegerValue.new('wikipedia.first_par',
37     :default => 1,
38     :desc => "When set to n > 0, the bot will return the first paragraph from the first n wikipedia search hits")
39
40   def help(plugin, topic="")
41     case topic
42     when "search", "google"
43       "#{topic} <string> => search google for <string>"
44     when "gcalc"
45       "gcalc <equation> => use the google calculator to find the answer to <equation>"
46     when "gdef"
47       "gdef <term(s)> => use the google define mechanism to find a definition of <term(s)>"
48     when "gtime"
49       "gtime <location> => use the google clock to find the current time at <location>"
50     when "wp"
51       "wp [<code>] <string> => search for <string> on Wikipedia. You can select a national <code> to only search the national Wikipedia"
52     when "unpedia"
53       "unpedia <string> => search for <string> on Uncyclopedia"
54     else
55       "search <string> (or: google <string>) => search google for <string> | wp <string> => search for <string> on Wikipedia | unpedia <string> => search for <string> on Uncyclopedia"
56     end
57   end
58
59   def google(m, params)
60     what = params[:words].to_s
61     if what.match(/^define:/)
62       return google_define(m, what, params)
63     end
64
65     searchfor = CGI.escape what
66     # This method is also called by other methods to restrict searching to some sites
67     if params[:site]
68       site = "site:#{params[:site]}+"
69     else
70       site = ""
71     end
72     # It is also possible to choose a filter to remove constant parts from the titles
73     # e.g.: "Wikipedia, the free encyclopedia" when doing Wikipedia searches
74     filter = params[:filter] || ""
75
76     url = GOOGLE_WAP_SEARCH + site + searchfor
77
78     hits = params[:hits] || @bot.config['google.hits']
79     hits = 1 if params[:lucky]
80
81     first_pars = params[:firstpar] || @bot.config['google.first_par']
82
83     single = params[:lucky] || (hits == 1 and first_pars == 1)
84
85     begin
86       wml = @bot.httputil.get(url)
87       raise unless wml
88     rescue => e
89       m.reply "error googling for #{what}"
90       return
91     end
92     results = wml.scan(GOOGLE_WAP_LINK)
93
94     if results.length == 0
95       m.reply "no results found for #{what}"
96       return
97     end
98
99     single ||= (results.length==1)
100     pretty = []
101
102     begin
103       urls = Array.new
104
105       debug results
106       results.each do |res|
107         t = res[1].ircify_html(:img => "[%{src} %{alt} %{dimensions}]").strip
108         u = res[0]
109         if u.sub!(%r{^http://www.google.com/aclk\?},'')
110           u = CGI::parse(u)['adurl'].first
111           debug "skipping ad for #{u}"
112           next
113         elsif u.sub!(%r{^http://www.google.com/gwt/x\?},'')
114           u = CGI::parse(u)['u'].first
115         elsif u.sub!(%r{^/url\?},'')
116           u = CGI::parse(u)['q'].first
117         end
118         urls.push(u)
119         pretty.push("%{n}%{b}%{t}%{b}%{sep}%{u}" % {
120           :n => (single ? "" : "#{urls.length}. "),
121           :sep => (single ? " -- " : ": "),
122           :b => Bold, :t => t, :u => u
123         })
124         break if urls.length == hits
125       end
126     rescue => e
127       m.reply "failed to understand what google found for #{what}"
128       error e
129       debug wml
130       debug results
131       return
132     end
133
134     if params[:lucky]
135       m.reply pretty.first
136       return
137     end
138
139     result_string = pretty.join(" | ")
140
141     # If we return a single, full result, change the output to a more compact representation
142     if single
143       m.reply "Result for %s: %s -- %s" % [what, result_string, Utils.get_first_pars(urls, first_pars)], :overlong => :truncate
144       return
145     end
146
147     m.reply "Results for #{what}: #{result_string}", :split_at => /\s+\|\s+/
148
149     return unless first_pars > 0
150
151     Utils.get_first_pars urls, first_pars, :message => m
152
153   end
154
155   def google_define(m, what, params)
156     begin
157       wml = @bot.httputil.get(GOOGLE_SEARCH + CGI.escape(what))
158       raise unless wml
159     rescue => e
160       m.reply "error googling for #{what}"
161       return
162     end
163
164     begin
165       related_index = wml.index(/Related phrases:/, 0)
166       raise unless related_index
167       defs_index = wml.index(/Definitions of <b>/, related_index)
168       raise unless defs_index
169       defs_end = wml.index(/<input/, defs_index)
170       raise unless defs_end
171     rescue => e
172       m.reply "no results found for #{what}"
173       return
174     end
175
176     related = wml[related_index...defs_index]
177     defs = wml[defs_index...defs_end]
178
179     m.reply defs.ircify_html(:a_href => Underline), :split_at => (Underline + ' ')
180
181   end
182
183   def lucky(m, params)
184     params.merge!(:lucky => true)
185     google(m, params)
186   end
187
188   def gcalc(m, params)
189     what = params[:words].to_s
190     searchfor = CGI.escape(what)
191
192     debug "Getting gcalc thing: #{searchfor.inspect}"
193     url = GOOGLE_WAP_SEARCH + searchfor
194
195     begin
196       html = @bot.httputil.get(url)
197     rescue => e
198       m.reply "error googlecalcing #{what}"
199       return
200     end
201
202     debug "#{html.size} bytes of html recieved"
203     debug html
204
205     candidates = html.match(/font-weight:bold">(.*?)<\/(?:span|div)>/)
206     debug "candidates: #{candidates.inspect}"
207
208     if candidates.nil?
209       m.reply "couldn't calculate #{what}"
210       return
211     end
212     result = candidates[1]
213
214     debug "replying with: #{result.inspect}"
215     m.reply result.ircify_html
216   end
217
218   def gcount(m, params)
219     what = params[:words].to_s
220     searchfor = CGI.escape(what)
221
222     debug "Getting gcount thing: #{searchfor.inspect}"
223     url = GOOGLE_SEARCH + searchfor
224
225     begin
226       html = @bot.httputil.get(url)
227     rescue => e
228       m.reply "error googlecounting #{what}"
229       return
230     end
231
232     debug "#{html.size} bytes of html recieved"
233
234     results = html.scan(GOOGLE_COUNT_RESULT)
235     debug "results: #{results.inspect}"
236
237     if results.length != 1
238       m.reply "couldn't count #{what}"
239       return
240     end
241
242     result = results[0][0].ircify_html
243     debug "replying with: #{result.inspect}"
244     m.reply "total results: #{result}"
245
246   end
247
248   def gdef(m, params)
249     what = params[:words].to_s
250     searchfor = CGI.escape("define " + what)
251
252     debug "Getting gdef thing: #{searchfor.inspect}"
253     url = GOOGLE_WAP_SEARCH + searchfor
254
255     begin
256       html = @bot.httputil.get(url)
257     rescue => e
258       m.reply "error googledefining #{what}"
259       return
260     end
261
262     debug html
263     results = html.scan(GOOGLE_DEF_RESULT)
264     debug "results: #{results.inspect}"
265
266     if results.length != 1
267       m.reply "couldn't find a definition for #{what} on Google"
268       return
269     end
270
271     head = results[0][0].ircify_html
272     text = results[0][1].ircify_html
273     m.reply "#{head} -- #{text}"
274   end
275
276   def wikipedia(m, params)
277     lang = params[:lang]
278     site = "#{lang.nil? ? '' : lang + '.'}wikipedia.org"
279     debug "Looking up things on #{site}"
280     params[:site] = site
281     params[:filter] = / - Wikipedia.*$/
282     params[:hits] = @bot.config['wikipedia.hits']
283     params[:firstpar] = @bot.config['wikipedia.first_par']
284     return google(m, params)
285   end
286
287   def unpedia(m, params)
288     site = "uncyclopedia.org"
289     debug "Looking up things on #{site}"
290     params[:site] = site
291     params[:filter] = / - Uncyclopedia.*$/
292     params[:hits] = @bot.config['wikipedia.hits']
293     params[:firstpar] = @bot.config['wikipedia.first_par']
294     return google(m, params)
295   end
296
297   def gtime(m, params)
298     where = params[:words].to_s
299     where.sub!(/^\s*in\s*/, '')
300     searchfor = CGI.escape("time in " + where)
301     url = GOOGLE_SEARCH + searchfor
302
303     begin
304       html = @bot.httputil.get(url)
305     rescue => e
306       m.reply "Error googletiming #{where}"
307       return
308     end
309
310     debug html
311     results = html.scan(GOOGLE_TIME_RESULT)
312     debug "results: #{results.inspect}"
313
314     if results.length != 1
315       m.reply "Couldn't find the time for #{where} on Google"
316       return
317     end
318
319     time = results[0][0].ircify_html
320     m.reply "#{time}"
321   end
322 end
323
324 plugin = SearchPlugin.new
325
326 plugin.map "search *words", :action => 'google', :threaded => true
327 plugin.map "google *words", :action => 'google', :threaded => true
328 plugin.map "lucky *words", :action => 'lucky', :threaded => true
329 plugin.map "gcount *words", :action => 'gcount', :threaded => true
330 plugin.map "gcalc *words", :action => 'gcalc', :threaded => true
331 plugin.map "gdef *words", :action => 'gdef', :threaded => true
332 plugin.map "gtime *words", :action => 'gtime', :threaded => true
333 plugin.map "wp :lang *words", :action => 'wikipedia', :requirements => { :lang => /^\w\w\w?$/ }, :threaded => true
334 plugin.map "wp *words", :action => 'wikipedia', :threaded => true
335 plugin.map "unpedia *words", :action => 'unpedia', :threaded => true
336