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