]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/search.rb
search: be more rbot-ish in ddg
[user/henk/code/ruby/rbot.git] / 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 = /"r">(?:<div[^>]*>)?<a href="([^"]+)"[^>]*>(.*?)<\/a>/im
21 GOOGLE_CALC_RESULT = %r{<h[1-6] class="r" [^>]*>(.+?)</h}
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 DDG_API_SEARCH = "http://api.duckduckgo.com/?format=xml&no_html=1&no_redirect=0&q="
27
28 class SearchPlugin < Plugin
29   Config.register Config::IntegerValue.new('duckduckgo.hits',
30     :default => 3, :validate => Proc.new{|v| v > 0},
31     :desc => "Number of hits to return from searches")
32   Config.register Config::IntegerValue.new('google.hits',
33     :default => 3,
34     :desc => "Number of hits to return from Google searches")
35   Config.register Config::IntegerValue.new('google.first_par',
36     :default => 0,
37     :desc => "When set to n > 0, the bot will return the first paragraph from the first n search hits")
38   Config.register Config::IntegerValue.new('wikipedia.hits',
39     :default => 3,
40     :desc => "Number of hits to return from Wikipedia searches")
41   Config.register Config::IntegerValue.new('wikipedia.first_par',
42     :default => 1,
43     :desc => "When set to n > 0, the bot will return the first paragraph from the first n wikipedia search hits")
44
45   def help(plugin, topic="")
46     case topic
47     when "ddg"
48       "Use '#{topic} <string>' to return a search or calculation from " +
49       "DuckDuckGo. Use #{topic} define <string> to return a definition."
50     when "search", "google"
51       "#{topic} <string> => search google for <string>"
52     when "gcalc"
53       "gcalc <equation> => use the google calculator to find the answer to <equation>"
54     when "gdef"
55       "gdef <term(s)> => use the google define mechanism to find a definition of <term(s)>"
56     when "gtime"
57       "gtime <location> => use the google clock to find the current time at <location>"
58     when "wp"
59       "wp [<code>] <string> => search for <string> on Wikipedia. You can select a national <code> to only search the national Wikipedia"
60     when "unpedia"
61       "unpedia <string> => search for <string> on Uncyclopedia"
62     else
63       "search <string> (or: google <string>) => search google for <string> | ddg <string> to search DuckDuckGo | wp <string> => search for <string> on Wikipedia | unpedia <string> => search for <string> on Uncyclopedia"
64     end
65   end
66
67   def duckduckgo(m, params)
68     what = params[:words].to_s
69     terms = CGI.escape what
70     url = DDG_API_SEARCH + terms
71     begin
72       feed = @bot.httputil.get(url)
73       raise unless feed
74     rescue => e
75       m.reply "error duckduckgoing for #{what}"
76       return
77     end
78     debug feed
79
80     xml = REXML::Document.new feed
81     heading = xml.elements['//Heading/text()'].to_s
82     # answer is returned for calculations
83     answer = xml.elements['//Answer/text()'].to_s
84     if heading.empty? and answer.empty?
85       m.reply "no results"
86       return
87     end
88     if terms =~ /^define/
89       if heading.empty?
90         m.reply "no definition found"
91         return
92       end
93       # Format and return a different string if it is a definition search.
94       definition = xml.elements['//AbstractText/text()'].to_s
95       source = " -- #{xml.elements['//AbstractURL/text()']}"
96       m.reply Bold + heading + ": " + Bold + definition + source
97     elsif heading.empty?
98       # return a calculation
99       m.reply answer
100     else
101       # else, return a zeroclick search
102       links, text = [], []
103       hits = @bot.config['duckduckgo.hits']
104       xml.elements.each("//RelatedTopics/RelatedTopic/FirstURL") { |element|
105         links << element.text
106       }
107       xml.elements.each("//RelatedTopics/RelatedTopic/Text") { |element|
108         text << " #{element.text}"
109       }
110       num = 0
111       m.reply Bold + heading + ": " + Bold
112       until num >= hits
113         m.reply links[num] + text[num]
114         num += 1
115       end
116     end
117   end
118
119   def google(m, params)
120     what = params[:words].to_s
121     if what.match(/^define:/)
122       return google_define(m, what, params)
123     end
124
125     searchfor = CGI.escape what
126     # This method is also called by other methods to restrict searching to some sites
127     if params[:site]
128       site = "site:#{params[:site]}+"
129     else
130       site = ""
131     end
132     # It is also possible to choose a filter to remove constant parts from the titles
133     # e.g.: "Wikipedia, the free encyclopedia" when doing Wikipedia searches
134     filter = params[:filter] || ""
135
136     url = GOOGLE_WAP_SEARCH + site + searchfor
137
138     hits = params[:hits] || @bot.config['google.hits']
139     hits = 1 if params[:lucky]
140
141     first_pars = params[:firstpar] || @bot.config['google.first_par']
142
143     single = params[:lucky] || (hits == 1 and first_pars == 1)
144
145     begin
146       wml = @bot.httputil.get(url)
147       raise unless wml
148     rescue => e
149       m.reply "error googling for #{what}"
150       return
151     end
152     results = wml.scan(GOOGLE_WAP_LINK)
153
154     if results.length == 0
155       m.reply "no results found for #{what}"
156       return
157     end
158
159     single ||= (results.length==1)
160     pretty = []
161
162     begin
163       urls = Array.new
164
165       debug results
166       results.each do |res|
167         t = res[1].ircify_html(:img => "[%{src} %{alt} %{dimensions}]").strip
168         u = res[0]
169         if u.sub!(%r{^http://www.google.com/aclk\?},'')
170           u = CGI::parse(u)['adurl'].first
171           debug "skipping ad for #{u}"
172           next
173         elsif u.sub!(%r{^http://www.google.com/gwt/x\?},'')
174           u = CGI::parse(u)['u'].first
175         elsif u.sub!(%r{^/url\?},'')
176           u = CGI::parse(u)['q'].first
177         end
178         urls.push(u)
179         pretty.push("%{n}%{b}%{t}%{b}%{sep}%{u}" % {
180           :n => (single ? "" : "#{urls.length}. "),
181           :sep => (single ? " -- " : ": "),
182           :b => Bold, :t => t, :u => u
183         })
184         break if urls.length == hits
185       end
186     rescue => e
187       m.reply "failed to understand what google found for #{what}"
188       error e
189       debug wml
190       debug results
191       return
192     end
193
194     if params[:lucky]
195       m.reply pretty.first
196       return
197     end
198
199     result_string = pretty.join(" | ")
200
201     # If we return a single, full result, change the output to a more compact representation
202     if single
203       m.reply "Result for %s: %s -- %s" % [what, result_string, Utils.get_first_pars(urls, first_pars)], :overlong => :truncate
204       return
205     end
206
207     m.reply "Results for #{what}: #{result_string}", :split_at => /\s+\|\s+/
208
209     return unless first_pars > 0
210
211     Utils.get_first_pars urls, first_pars, :message => m
212
213   end
214
215   def google_define(m, what, params)
216     begin
217       wml = @bot.httputil.get(GOOGLE_SEARCH + CGI.escape(what))
218       raise unless wml
219     rescue => e
220       m.reply "error googling for #{what}"
221       return
222     end
223
224     begin
225       related_index = wml.index(/Related phrases:/, 0)
226       raise unless related_index
227       defs_index = wml.index(/Definitions of <b>/, related_index)
228       raise unless defs_index
229       defs_end = wml.index(/<input/, defs_index)
230       raise unless defs_end
231     rescue => e
232       m.reply "no results found for #{what}"
233       return
234     end
235
236     related = wml[related_index...defs_index]
237     defs = wml[defs_index...defs_end]
238
239     m.reply defs.ircify_html(:a_href => Underline), :split_at => (Underline + ' ')
240
241   end
242
243   def lucky(m, params)
244     params.merge!(:lucky => true)
245     google(m, params)
246   end
247
248   def gcalc(m, params)
249     what = params[:words].to_s
250     searchfor = CGI.escape(what)
251
252     debug "Getting gcalc 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 googlecalcing #{what}"
259       return
260     end
261
262     debug "#{html.size} bytes of html recieved"
263     debug html
264
265     candidates = html.match(GOOGLE_CALC_RESULT)
266     debug "candidates: #{candidates.inspect}"
267
268     if candidates.nil?
269       m.reply "couldn't calculate #{what}"
270       return
271     end
272     result = candidates[1]
273
274     debug "replying with: #{result.inspect}"
275     m.reply result.ircify_html
276   end
277
278   def gcount(m, params)
279     what = params[:words].to_s
280     searchfor = CGI.escape(what)
281
282     debug "Getting gcount thing: #{searchfor.inspect}"
283     url = GOOGLE_SEARCH + searchfor
284
285     begin
286       html = @bot.httputil.get(url)
287     rescue => e
288       m.reply "error googlecounting #{what}"
289       return
290     end
291
292     debug "#{html.size} bytes of html recieved"
293
294     results = html.scan(GOOGLE_COUNT_RESULT)
295     debug "results: #{results.inspect}"
296
297     if results.length != 1
298       m.reply "couldn't count #{what}"
299       return
300     end
301
302     result = results[0][0].ircify_html
303     debug "replying with: #{result.inspect}"
304     m.reply "total results: #{result}"
305
306   end
307
308   def gdef(m, params)
309     what = params[:words].to_s
310     searchfor = CGI.escape("define " + what)
311
312     debug "Getting gdef thing: #{searchfor.inspect}"
313     url = GOOGLE_WAP_SEARCH + searchfor
314
315     begin
316       html = @bot.httputil.get(url)
317     rescue => e
318       m.reply "error googledefining #{what}"
319       return
320     end
321
322     debug html
323     results = html.scan(GOOGLE_DEF_RESULT)
324     debug "results: #{results.inspect}"
325
326     if results.length != 1
327       m.reply "couldn't find a definition for #{what} on Google"
328       return
329     end
330
331     head = results[0][0].ircify_html
332     text = results[0][1].ircify_html
333     m.reply "#{head} -- #{text}"
334   end
335
336   def wikipedia(m, params)
337     lang = params[:lang]
338     site = "#{lang.nil? ? '' : lang + '.'}wikipedia.org"
339     debug "Looking up things on #{site}"
340     params[:site] = site
341     params[:filter] = / - Wikipedia.*$/
342     params[:hits] = @bot.config['wikipedia.hits']
343     params[:firstpar] = @bot.config['wikipedia.first_par']
344     return google(m, params)
345   end
346
347   def unpedia(m, params)
348     site = "uncyclopedia.org"
349     debug "Looking up things on #{site}"
350     params[:site] = site
351     params[:filter] = / - Uncyclopedia.*$/
352     params[:hits] = @bot.config['wikipedia.hits']
353     params[:firstpar] = @bot.config['wikipedia.first_par']
354     return google(m, params)
355   end
356
357   def gtime(m, params)
358     where = params[:words].to_s
359     where.sub!(/^\s*in\s*/, '')
360     searchfor = CGI.escape("time in " + where)
361     url = GOOGLE_SEARCH + searchfor
362
363     begin
364       html = @bot.httputil.get(url)
365     rescue => e
366       m.reply "Error googletiming #{where}"
367       return
368     end
369
370     debug html
371     results = html.scan(GOOGLE_TIME_RESULT)
372     debug "results: #{results.inspect}"
373
374     if results.length != 1
375       m.reply "Couldn't find the time for #{where} on Google"
376       return
377     end
378
379     time = results[0][0].ircify_html
380     m.reply "#{time}"
381   end
382 end
383
384 plugin = SearchPlugin.new
385
386 plugin.map "ddg *words", :action => 'duckduckgo'
387 plugin.map "search *words", :action => 'google', :threaded => true
388 plugin.map "google *words", :action => 'google', :threaded => true
389 plugin.map "lucky *words", :action => 'lucky', :threaded => true
390 plugin.map "gcount *words", :action => 'gcount', :threaded => true
391 plugin.map "gcalc *words", :action => 'gcalc', :threaded => true
392 plugin.map "gdef *words", :action => 'gdef', :threaded => true
393 plugin.map "gtime *words", :action => 'gtime', :threaded => true
394 plugin.map "wp :lang *words", :action => 'wikipedia', :requirements => { :lang => /^\w\w\w?$/ }, :threaded => true
395 plugin.map "wp *words", :action => 'wikipedia', :threaded => true
396 plugin.map "unpedia *words", :action => 'unpedia', :threaded => true