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