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