4 # :title: rbot utilities provider
6 # Author:: Tom Gilbert <tom@linuxbrit.co.uk>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
9 # Copyright:: (C) 2002-2006 Tom Gilbert
10 # Copyright:: (C) 2007 Giuseppe Bilotta
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
19 require 'htmlentities'
20 $we_have_html_entities_decoder = true
24 gems = require 'rubygems'
31 $we_have_html_entities_decoder = false
54 # extras codes, for future use...
68 'otimes' => '⊗',
77 'Epsilon' => 'Ε',
81 'Upsilon' => 'Υ',
83 'there4' => '∴',
88 'rsaquo' => '›',
100 'lceil' => '⌈',
102 'rdquo' => '”',
110 'lfloor' => '⌊',
117 'clubs' => '♣',
118 'diams' => '♦',
125 'Scaron' => 'Š',
131 'sbquo' => '‚',
144 'infin' => '∞',
149 'thinsp' => ' ',
151 'bdquo' => '„',
158 'mdash' => '—',
160 'permil' => '‰',
165 'forall' => '∀',
167 'rceil' => '⌉',
170 'lambda' => 'λ',
174 'dagger' => '†',
177 'image' => 'ℑ',
178 'alefsym' => 'ℵ',
184 'frasl' => '⁄',
186 'lowast' => '∗',
197 'oline' => '‾',
204 'empty' => '∅',
211 'weierp' => '℘',
216 'omicron' => 'ο',
217 'upsilon' => 'υ',
219 'Lambda' => 'Λ',
226 'scaron' => 'š',
227 'lsquo' => '‘',
235 'hellip' => '…',
239 'rfloor' => '⌋',
241 'crarr' => '↵',
243 'notin' => '∉',
244 'exist' => '∃',
247 'Dagger' => '‡',
248 'oplus' => '⊕',
254 'lsaquo' => '‹',
256 'Omicron' => 'Ο',
271 'sigmaf' => 'ς',
273 'minus' => '−',
276 'epsilon' => 'ε',
287 'spades' => '♠',
288 'rsquo' => '’',
292 'thetasym' => 'ϑ',
296 'ldquo' => '“',
297 'hearts' => '♥',
310 # miscellaneous useful functions
312 @@bot = nil unless defined? @@bot
313 @@safe_save_dir = nil unless defined?(@@safe_save_dir)
320 debug "initializing utils"
322 @@safe_save_dir = "#{@@bot.botclass}/safe_save"
327 SEC_PER_HR = SEC_PER_MIN * 60
328 SEC_PER_DAY = SEC_PER_HR * 24
329 SEC_PER_MNTH = SEC_PER_DAY * 30
330 SEC_PER_YR = SEC_PER_MNTH * 12
332 def Utils.secs_to_string_case(array, var, string, plural)
335 array << "1 #{string}"
337 array << "#{var} #{plural}"
341 # turn a number of seconds into a human readable string, e.g
342 # 2 days, 3 hours, 18 minutes, 10 seconds
343 def Utils.secs_to_string(secs)
345 years, secs = secs.divmod SEC_PER_YR
346 secs_to_string_case(ret, years, "year", "years") if years > 0
347 months, secs = secs.divmod SEC_PER_MNTH
348 secs_to_string_case(ret, months, "month", "months") if months > 0
349 days, secs = secs.divmod SEC_PER_DAY
350 secs_to_string_case(ret, days, "day", "days") if days > 0
351 hours, secs = secs.divmod SEC_PER_HR
352 secs_to_string_case(ret, hours, "hour", "hours") if hours > 0
353 mins, secs = secs.divmod SEC_PER_MIN
354 secs_to_string_case(ret, mins, "minute", "minutes") if mins > 0
356 secs_to_string_case(ret, secs, "second", "seconds") if secs > 0 or ret.empty?
359 raise "Empty ret array!"
363 return [ret[0, ret.length-1].join(", ") , ret[-1]].join(" and ")
368 def Utils.safe_exec(command, *args)
371 return p.readlines.join("\n")
374 $stderr.reopen($stdout)
376 rescue Exception => e
377 puts "exec of #{command} led to exception: #{e.pretty_inspect}"
380 puts "exec of #{command} failed"
387 def Utils.safe_save(file)
388 raise 'No safe save directory defined!' if @@safe_save_dir.nil?
389 basename = File.basename(file)
390 temp = Tempfile.new(basename,@@safe_save_dir)
392 yield temp if block_given?
394 File.rename(temp.path, file)
398 def Utils.decode_html_entities(str)
399 if $we_have_html_entities_decoder
400 return HTMLEntities.decode_entities(str)
402 str.gsub(/(&(.+?);)/) {
404 # remove the 0-paddng from unicode integers
406 symbol = "##{$1.to_i.to_s}"
409 # output the symbol's irc-translated character, or a * if it's unknown
410 UNESCAPE_TABLE[symbol] || [symbol[/\d+/].to_i].pack("U") rescue '*'
415 HX_REGEX = /<h(\d)(?:\s+[^>]*)?>(.*?)<\/h\1>/im
416 PAR_REGEX = /<p(?:\s+[^>]*)?>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
418 # Some blogging and forum platforms use spans or divs with a 'body' or 'message' or 'text' in their class
419 # to mark actual text
420 AFTER_PAR1_REGEX = /<\w+\s+[^>]*(?:body|message|text)[^>]*>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
422 # At worst, we can try stuff which is comprised between two <br>
423 AFTER_PAR2_REGEX = /<br(?:\s+[^>]*)?\/?>.*?<\/?(?:br|p|div|html|body|table|td|tr)(?:\s+[^>]*)?\/?>/im
425 # Try to grab and IRCify the first HTML par (<p> tag) in the given string.
426 # If possible, grab the one after the first heading
428 # It is possible to pass some options to determine how the stripping
429 # occurs. Currently supported options are
430 # * :strip => Regex or String to strip at the beginning of the obtained
432 # * :min_spaces => Minimum number of spaces a paragraph should have
434 def Utils.ircify_first_html_par(xml_org, opts={})
435 xml = xml_org.gsub(/<!--.*?-->/m, '').gsub(/<script(?:\s+[^>]*)?>.*?<\/script>/im, "").gsub(/<style(?:\s+[^>]*)?>.*?<\/style>/im, "")
438 strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String)
440 min_spaces = opts[:min_spaces] || 8
441 min_spaces = 0 if min_spaces < 0
446 debug "Minimum number of spaces: #{min_spaces}"
447 header_found = xml.match(HX_REGEX)
450 while txt.empty? or txt.count(" ") < min_spaces
451 candidate = header_found[PAR_REGEX]
452 break unless candidate
453 txt = candidate.ircify_html
455 txt.sub!(strip, '') if strip
456 debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
460 return txt unless txt.empty? or txt.count(" ") < min_spaces
462 # If we haven't found a first par yet, try to get it from the whole
465 while txt.empty? or txt.count(" ") < min_spaces
466 candidate = header_found[PAR_REGEX]
467 break unless candidate
468 txt = candidate.ircify_html
470 txt.sub!(strip, '') if strip
471 debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
474 return txt unless txt.empty? or txt.count(" ") < min_spaces
476 # Nothing yet ... let's get drastic: we look for non-par elements too,
477 # but only for those that match something that we know is likely to
482 while txt.empty? or txt.count(" ") < min_spaces
483 candidate = header_found[AFTER_PAR1_REGEX]
484 break unless candidate
485 txt = candidate.ircify_html
487 txt.sub!(strip, '') if strip
488 debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces"
491 return txt unless txt.empty? or txt.count(" ") < min_spaces
495 while txt.empty? or txt.count(" ") < min_spaces
496 candidate = header_found[AFTER_PAR2_REGEX]
497 break unless candidate
498 txt = candidate.ircify_html
500 txt.sub!(strip, '') if strip
501 debug "(other attempt \#2) #{txt.inspect} has #{txt.count(" ")} spaces"
504 debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces"
505 return txt unless txt.count(" ") < min_spaces
510 # Get the first pars of the first _count_ _urls_.
511 # The pages are downloaded using the bot httputil service.
512 # Returns an array of the first paragraphs fetched.
513 # If (optional) _opts_ :message is specified, those paragraphs are
514 # echoed as replies to the IRC message passed as _opts_ :message
516 def Utils.get_first_pars(urls, count, opts={})
520 while count > 0 and urls.length > 0
524 # FIXME what happens if some big file is returned? We should share
525 # code with the url plugin to only retrieve partial file content!
526 xml = self.bot.httputil.get(url)
528 debug "Unable to retrieve #{url}"
531 par = Utils.ircify_first_html_par(xml, opts)
533 debug "No first par found\n#{xml}"
534 # FIXME only do this if the 'url' plugin is loaded
535 # TODO even better, put the code here
536 # par = @bot.plugins['url'].get_title_from_html(xml)
542 msg.reply "[#{idx}] #{par}", :overlong => :truncate if msg
552 Irc::Utils.bot = Irc::Plugins.manager.bot