]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/utils/utils.rb
HttpUtils: inline rescue doesn't rescue LoadError, rewrite code to require rubygems...
[user/henk/code/ruby/rbot.git] / lib / rbot / core / utils / utils.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: rbot utilities provider
5 #
6 # Author:: Tom Gilbert <tom@linuxbrit.co.uk>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2002-2006 Tom Gilbert
10 # Copyright:: (C) 2007 Giuseppe Bilotta
11 #
12 # TODO some of these Utils should be rewritten as extensions to the approriate
13 # standard Ruby classes and accordingly be moved to extends.rb
14
15 require 'net/http'
16 require 'uri'
17 require 'tempfile'
18
19 begin
20   require 'htmlentities'
21   $we_have_html_entities_decoder = true
22 rescue LoadError
23   gems = nil
24   begin
25     gems = require 'rubygems'
26   rescue LoadError
27     gems = false
28   end
29   if gems
30     retry
31   else
32     $we_have_html_entities_decoder = false
33     module ::Irc
34       module Utils
35         UNESCAPE_TABLE = {
36     'laquo' => '<<',
37     'raquo' => '>>',
38     'quot' => '"',
39     'apos' => '\'',
40     'micro' => 'u',
41     'copy' => '(c)',
42     'trade' => '(tm)',
43     'reg' => '(R)',
44     '#174' => '(R)',
45     '#8220' => '"',
46     '#8221' => '"',
47     '#8212' => '--',
48     '#39' => '\'',
49     'amp' => '&',
50     'lt' => '<',
51     'gt' => '>',
52     'hellip' => '...',
53     'nbsp' => ' ',
54 =begin
55     # extras codes, for future use...
56     'zwnj' => '&#8204;',
57     'aring' => '\xe5',
58     'gt' => '>',
59     'yen' => '\xa5',
60     'ograve' => '\xf2',
61     'Chi' => '&#935;',
62     'bull' => '&#8226;',
63     'Egrave' => '\xc8',
64     'Ntilde' => '\xd1',
65     'upsih' => '&#978;',
66     'Yacute' => '\xdd',
67     'asymp' => '&#8776;',
68     'radic' => '&#8730;',
69     'otimes' => '&#8855;',
70     'nabla' => '&#8711;',
71     'aelig' => '\xe6',
72     'oelig' => '&#339;',
73     'equiv' => '&#8801;',
74     'Psi' => '&#936;',
75     'auml' => '\xe4',
76     'circ' => '&#710;',
77     'Acirc' => '\xc2',
78     'Epsilon' => '&#917;',
79     'Yuml' => '&#376;',
80     'Eta' => '&#919;',
81     'Icirc' => '\xce',
82     'Upsilon' => '&#933;',
83     'ndash' => '&#8211;',
84     'there4' => '&#8756;',
85     'Prime' => '&#8243;',
86     'prime' => '&#8242;',
87     'psi' => '&#968;',
88     'Kappa' => '&#922;',
89     'rsaquo' => '&#8250;',
90     'Tau' => '&#932;',
91     'darr' => '&#8595;',
92     'ocirc' => '\xf4',
93     'lrm' => '&#8206;',
94     'zwj' => '&#8205;',
95     'cedil' => '\xb8',
96     'Ecirc' => '\xca',
97     'not' => '\xac',
98     'AElig' => '\xc6',
99     'oslash' => '\xf8',
100     'acute' => '\xb4',
101     'lceil' => '&#8968;',
102     'shy' => '\xad',
103     'rdquo' => '&#8221;',
104     'ge' => '&#8805;',
105     'Igrave' => '\xcc',
106     'Ograve' => '\xd2',
107     'euro' => '&#8364;',
108     'dArr' => '&#8659;',
109     'sdot' => '&#8901;',
110     'nbsp' => '\xa0',
111     'lfloor' => '&#8970;',
112     'lArr' => '&#8656;',
113     'Auml' => '\xc4',
114     'larr' => '&#8592;',
115     'Atilde' => '\xc3',
116     'Otilde' => '\xd5',
117     'szlig' => '\xdf',
118     'clubs' => '&#9827;',
119     'diams' => '&#9830;',
120     'agrave' => '\xe0',
121     'Ocirc' => '\xd4',
122     'Iota' => '&#921;',
123     'Theta' => '&#920;',
124     'Pi' => '&#928;',
125     'OElig' => '&#338;',
126     'Scaron' => '&#352;',
127     'frac14' => '\xbc',
128     'egrave' => '\xe8',
129     'sub' => '&#8834;',
130     'iexcl' => '\xa1',
131     'frac12' => '\xbd',
132     'sbquo' => '&#8218;',
133     'ordf' => '\xaa',
134     'sum' => '&#8721;',
135     'prop' => '&#8733;',
136     'Uuml' => '\xdc',
137     'ntilde' => '\xf1',
138     'sup' => '&#8835;',
139     'theta' => '&#952;',
140     'prod' => '&#8719;',
141     'nsub' => '&#8836;',
142     'hArr' => '&#8660;',
143     'rlm' => '&#8207;',
144     'THORN' => '\xde',
145     'infin' => '&#8734;',
146     'yuml' => '\xff',
147     'Mu' => '&#924;',
148     'le' => '&#8804;',
149     'Eacute' => '\xc9',
150     'thinsp' => '&#8201;',
151     'ecirc' => '\xea',
152     'bdquo' => '&#8222;',
153     'Sigma' => '&#931;',
154     'fnof' => '&#402;',
155     'Aring' => '\xc5',
156     'tilde' => '&#732;',
157     'frac34' => '\xbe',
158     'emsp' => '&#8195;',
159     'mdash' => '&#8212;',
160     'uarr' => '&#8593;',
161     'permil' => '&#8240;',
162     'Ugrave' => '\xd9',
163     'rarr' => '&#8594;',
164     'Agrave' => '\xc0',
165     'chi' => '&#967;',
166     'forall' => '&#8704;',
167     'eth' => '\xf0',
168     'rceil' => '&#8969;',
169     'iuml' => '\xef',
170     'gamma' => '&#947;',
171     'lambda' => '&#955;',
172     'harr' => '&#8596;',
173     'rang' => '&#9002;',
174     'xi' => '&#958;',
175     'dagger' => '&#8224;',
176     'divide' => '\xf7',
177     'Ouml' => '\xd6',
178     'image' => '&#8465;',
179     'alefsym' => '&#8501;',
180     'igrave' => '\xec',
181     'otilde' => '\xf5',
182     'Oacute' => '\xd3',
183     'sube' => '&#8838;',
184     'alpha' => '&#945;',
185     'frasl' => '&#8260;',
186     'ETH' => '\xd0',
187     'lowast' => '&#8727;',
188     'Nu' => '&#925;',
189     'plusmn' => '\xb1',
190     'Euml' => '\xcb',
191     'real' => '&#8476;',
192     'sup1' => '\xb9',
193     'sup2' => '\xb2',
194     'sup3' => '\xb3',
195     'Oslash' => '\xd8',
196     'Aacute' => '\xc1',
197     'cent' => '\xa2',
198     'oline' => '&#8254;',
199     'Beta' => '&#914;',
200     'perp' => '&#8869;',
201     'Delta' => '&#916;',
202     'loz' => '&#9674;',
203     'pi' => '&#960;',
204     'iota' => '&#953;',
205     'empty' => '&#8709;',
206     'euml' => '\xeb',
207     'brvbar' => '\xa6',
208     'iacute' => '\xed',
209     'para' => '\xb6',
210     'micro' => '\xb5',
211     'cup' => '&#8746;',
212     'weierp' => '&#8472;',
213     'uuml' => '\xfc',
214     'part' => '&#8706;',
215     'icirc' => '\xee',
216     'delta' => '&#948;',
217     'omicron' => '&#959;',
218     'upsilon' => '&#965;',
219     'Iuml' => '\xcf',
220     'Lambda' => '&#923;',
221     'Xi' => '&#926;',
222     'kappa' => '&#954;',
223     'ccedil' => '\xe7',
224     'Ucirc' => '\xdb',
225     'cap' => '&#8745;',
226     'mu' => '&#956;',
227     'scaron' => '&#353;',
228     'lsquo' => '&#8216;',
229     'isin' => '&#8712;',
230     'Zeta' => '&#918;',
231     'supe' => '&#8839;',
232     'deg' => '\xb0',
233     'and' => '&#8743;',
234     'tau' => '&#964;',
235     'pound' => '\xa3',
236     'hellip' => '&#8230;',
237     'curren' => '\xa4',
238     'int' => '&#8747;',
239     'ucirc' => '\xfb',
240     'rfloor' => '&#8971;',
241     'ensp' => '&#8194;',
242     'crarr' => '&#8629;',
243     'ugrave' => '\xf9',
244     'notin' => '&#8713;',
245     'exist' => '&#8707;',
246     'uArr' => '&#8657;',
247     'cong' => '&#8773;',
248     'Dagger' => '&#8225;',
249     'oplus' => '&#8853;',
250     'times' => '\xd7',
251     'atilde' => '\xe3',
252     'piv' => '&#982;',
253     'ni' => '&#8715;',
254     'Phi' => '&#934;',
255     'lsaquo' => '&#8249;',
256     'Uacute' => '\xda',
257     'Omicron' => '&#927;',
258     'ang' => '&#8736;',
259     'ne' => '&#8800;',
260     'iquest' => '\xbf',
261     'eta' => '&#951;',
262     'yacute' => '\xfd',
263     'Rho' => '&#929;',
264     'uacute' => '\xfa',
265     'Alpha' => '&#913;',
266     'zeta' => '&#950;',
267     'Omega' => '&#937;',
268     'nu' => '&#957;',
269     'sim' => '&#8764;',
270     'sect' => '\xa7',
271     'phi' => '&#966;',
272     'sigmaf' => '&#962;',
273     'macr' => '\xaf',
274     'minus' => '&#8722;',
275     'Ccedil' => '\xc7',
276     'ordm' => '\xba',
277     'epsilon' => '&#949;',
278     'beta' => '&#946;',
279     'rArr' => '&#8658;',
280     'rho' => '&#961;',
281     'aacute' => '\xe1',
282     'eacute' => '\xe9',
283     'omega' => '&#969;',
284     'middot' => '\xb7',
285     'Gamma' => '&#915;',
286     'Iacute' => '\xcd',
287     'lang' => '&#9001;',
288     'spades' => '&#9824;',
289     'rsquo' => '&#8217;',
290     'uml' => '\xa8',
291     'thorn' => '\xfe',
292     'ouml' => '\xf6',
293     'thetasym' => '&#977;',
294     'or' => '&#8744;',
295     'raquo' => '\xbb',
296     'acirc' => '\xe2',
297     'ldquo' => '&#8220;',
298     'hearts' => '&#9829;',
299     'sigma' => '&#963;',
300     'oacute' => '\xf3',
301 =end
302         }
303       end
304     end
305   end
306 end
307
308
309 module ::Irc
310
311   # miscellaneous useful functions
312   module Utils
313     SEC_PER_MIN = 60
314     SEC_PER_HR = SEC_PER_MIN * 60
315     SEC_PER_DAY = SEC_PER_HR * 24
316     SEC_PER_MNTH = SEC_PER_DAY * 30
317     SEC_PER_YR = SEC_PER_MNTH * 12
318
319     def Utils.secs_to_string_case(array, var, string, plural)
320       case var
321       when 1
322         array << "1 #{string}"
323       else
324         array << "#{var} #{plural}"
325       end
326     end
327
328     # turn a number of seconds into a human readable string, e.g
329     # 2 days, 3 hours, 18 minutes, 10 seconds
330     def Utils.secs_to_string(secs)
331       ret = []
332       years, secs = secs.divmod SEC_PER_YR
333       secs_to_string_case(ret, years, "year", "years") if years > 0
334       months, secs = secs.divmod SEC_PER_MNTH
335       secs_to_string_case(ret, months, "month", "months") if months > 0
336       days, secs = secs.divmod SEC_PER_DAY
337       secs_to_string_case(ret, days, "day", "days") if days > 0
338       hours, secs = secs.divmod SEC_PER_HR
339       secs_to_string_case(ret, hours, "hour", "hours") if hours > 0
340       mins, secs = secs.divmod SEC_PER_MIN
341       secs_to_string_case(ret, mins, "minute", "minutes") if mins > 0
342       secs = secs.to_i
343       secs_to_string_case(ret, secs, "second", "seconds") if secs > 0 or ret.empty?
344       case ret.length
345       when 0
346         raise "Empty ret array!"
347       when 1
348         return ret.to_s
349       else
350         return [ret[0, ret.length-1].join(", ") , ret[-1]].join(" and ")
351       end
352     end
353
354
355     def Utils.safe_exec(command, *args)
356       IO.popen("-") {|p|
357         if(p)
358           return p.readlines.join("\n")
359         else
360           begin
361             $stderr = $stdout
362             exec(command, *args)
363           rescue Exception => e
364             puts "exec of #{command} led to exception: #{e.inspect}"
365             Kernel::exit! 0
366           end
367           puts "exec of #{command} failed"
368           Kernel::exit! 0
369         end
370       }
371     end
372
373
374     @@safe_save_dir = nil unless defined?(@@safe_save_dir)
375     def Utils.set_safe_save_dir(str)
376       @@safe_save_dir = str.dup
377     end
378
379     def Utils.safe_save(file)
380       raise 'No safe save directory defined!' if @@safe_save_dir.nil?
381       basename = File.basename(file)
382       temp = Tempfile.new(basename,@@safe_save_dir)
383       temp.binmode
384       yield temp if block_given?
385       temp.close
386       File.rename(temp.path, file)
387     end
388
389
390     # returns a string containing the result of an HTTP GET on the uri
391     def Utils.http_get(uristr, readtimeout=8, opentimeout=4)
392
393       # ruby 1.7 or better needed for this (or 1.6 and debian unstable)
394       Net::HTTP.version_1_2
395       # (so we support the 1_1 api anyway, avoids problems)
396
397       uri = URI.parse uristr
398       query = uri.path
399       if uri.query
400         query += "?#{uri.query}"
401       end
402
403       proxy_host = nil
404       proxy_port = nil
405       if(ENV['http_proxy'] && proxy_uri = URI.parse(ENV['http_proxy']))
406         proxy_host = proxy_uri.host
407         proxy_port = proxy_uri.port
408       end
409
410       begin
411         http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
412         http.open_timeout = opentimeout
413         http.read_timeout = readtimeout
414
415         http.start {|http|
416           resp = http.get(query)
417           if resp.code == "200"
418             return resp.body
419           end
420         }
421       rescue => e
422         # cheesy for now
423         error "Utils.http_get exception: #{e.inspect}, while trying to get #{uristr}"
424         return nil
425       end
426     end
427
428     def Utils.decode_html_entities(str)
429       if $we_have_html_entities_decoder
430         return HTMLEntities.decode_entities(str)
431       else
432         str.gsub(/(&(.+?);)/) {
433           symbol = $2
434           # remove the 0-paddng from unicode integers
435           if symbol =~ /#(.+)/
436             symbol = "##{$1.to_i.to_s}"
437           end
438
439           # output the symbol's irc-translated character, or a * if it's unknown
440           UNESCAPE_TABLE[symbol] || [symbol[/\d+/].to_i].pack("U") rescue '*'
441         }
442       end
443     end
444
445     HX_REGEX = /<h(\d)(?:\s+[^>]*)?>(.*?)<\/h\1>/im
446     PAR_REGEX = /<p(?:\s+[^>]*)?>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
447
448     # Some blogging and forum platforms use spans or divs with a 'body' or 'message' or 'text' in their class
449     # to mark actual text
450     AFTER_PAR1_REGEX = /<\w+\s+[^>]*(?:body|message|text)[^>]*>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
451
452     # At worst, we can try stuff which is comprised between two <br>
453     AFTER_PAR2_REGEX = /<br(?:\s+[^>]*)?\/?>.*?<\/?(?:br|p|div|html|body|table|td|tr)(?:\s+[^>]*)?\/?>/im
454
455     # Try to grab and IRCify the first HTML par (<p> tag) in the given string.
456     # If possible, grab the one after the first heading
457     #
458     # It is possible to pass some options to determine how the stripping
459     # occurs. Currently supported options are
460     #   * :strip => Regex or String to strip at the beginning of the obtained
461     #               text
462     #   * :min_spaces => Minimum number of spaces a paragraph should have
463     #
464     def Utils.ircify_first_html_par(xml_org, opts={})
465       xml = xml_org.gsub(/<!--.*?-->/, '')
466
467       strip = opts[:strip]
468       strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String)
469
470       min_spaces = opts[:min_spaces] || 8
471       min_spaces = 0 if min_spaces < 0
472
473       txt = String.new
474
475       while true
476         debug "Minimum number of spaces: #{min_spaces}"
477         header_found = xml.match(HX_REGEX)
478         if header_found
479           header_found = $'
480           while txt.empty? or txt.count(" ") < min_spaces
481             candidate = header_found[PAR_REGEX]
482             break unless candidate
483             txt = candidate.ircify_html
484             header_found = $'
485             txt.sub!(strip, '') if strip
486             debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
487           end
488         end
489
490         return txt unless txt.empty? or txt.count(" ") < min_spaces
491
492         # If we haven't found a first par yet, try to get it from the whole
493         # document
494         header_found = xml
495         while txt.empty? or txt.count(" ") < min_spaces
496           candidate = header_found[PAR_REGEX]
497           break unless candidate
498           txt = candidate.ircify_html
499           header_found = $'
500           txt.sub!(strip, '') if strip
501           debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
502         end
503
504         return txt unless txt.empty? or txt.count(" ") < min_spaces
505
506         # Nothing yet ... let's get drastic: we look for non-par elements too,
507         # but only for those that match something that we know is likely to
508         # contain text
509
510         # Attempt #1
511         header_found = xml
512         while txt.empty? or txt.count(" ") < min_spaces
513           candidate = header_found[AFTER_PAR1_REGEX]
514           break unless candidate
515           txt = candidate.ircify_html
516           header_found = $'
517           txt.sub!(strip, '') if strip
518           debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces"
519         end
520
521         return txt unless txt.empty? or txt.count(" ") < min_spaces
522
523         # Attempt #2
524         header_found = xml
525         while txt.empty? or txt.count(" ") < min_spaces
526           candidate = header_found[AFTER_PAR2_REGEX]
527           break unless candidate
528           txt = candidate.ircify_html
529           header_found = $'
530           txt.sub!(strip, '') if strip
531           debug "(other attempt \#2) #{txt.inspect} has #{txt.count(" ")} spaces"
532         end
533
534         debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces"
535         return txt unless txt.count(" ") < min_spaces
536         min_spaces /= 2
537       end
538     end
539
540     # Get the first pars of the first _count_ _urls_.
541     # The pages are downloaded using an HttpUtil service passed as _opts_ :http_util,
542     # and echoed as replies to the IRC message passed as _opts_ :message.
543     #
544     def Utils.get_first_pars(urls, count, opts={})
545       idx = 0
546       msg = opts[:message]
547       while count > 0 and urls.length > 0
548         url = urls.shift
549         idx += 1
550
551         # FIXME what happens if some big file is returned? We should share
552         # code with the url plugin to only retrieve partial file content!
553         xml = opts[:http_util].get_cached(url)
554         if xml.nil?
555           debug "Unable to retrieve #{url}"
556           next
557         end
558         par = Utils.ircify_first_html_par(xml, opts)
559         if par.empty?
560           debug "No first par found\n#{xml}"
561           # FIXME only do this if the 'url' plugin is loaded
562           # TODO even better, put the code here
563           # par = @bot.plugins['url'].get_title_from_html(xml)
564           next if par.empty?
565         end
566         msg.reply "[#{idx}] #{par}", :overlong => :truncate if msg
567         count -=1
568       end
569     end
570
571
572   end
573 end