]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/utils/utils.rb
864426b09793d7fe5aab85eda9c627986e39b567
[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 # TODO some of these Utils should be rewritten as extensions to the approriate
10 # standard Ruby classes and accordingly be moved to extends.rb
11
12 require 'tempfile'
13 require 'set'
14
15 # Try to load htmlentities, fall back to an HTML escape table.
16 begin
17   require 'htmlentities'
18 rescue LoadError
19   gems = nil
20   begin
21     gems = require 'rubygems'
22   rescue LoadError
23     gems = false
24   end
25   if gems
26     retry
27   else
28     module ::Irc
29       module Utils
30         UNESCAPE_TABLE = {
31     'laquo' => '«',
32     'raquo' => '»',
33     'quot' => '"',
34     'apos' => '\'',
35     'micro' => 'µ',
36     'copy' => '©',
37     'trade' => '™',
38     'reg' => '®',
39     'amp' => '&',
40     'lt' => '<',
41     'gt' => '>',
42     'hellip' => '…',
43     'nbsp' => ' ',
44     'Agrave' => 'À',
45     'Aacute' => 'Á',
46     'Acirc' => 'Â',
47     'Atilde' => 'Ã',
48     'Auml' => 'Ä',
49     'Aring' => 'Å',
50     'AElig' => 'Æ',
51     'OElig' => 'Œ',
52     'Ccedil' => 'Ç',
53     'Egrave' => 'È',
54     'Eacute' => 'É',
55     'Ecirc' => 'Ê',
56     'Euml' => 'Ë',
57     'Igrave' => 'Ì',
58     'Iacute' => 'Í',
59     'Icirc' => 'Î',
60     'Iuml' => 'Ï',
61     'ETH' => 'Ð',
62     'Ntilde' => 'Ñ',
63     'Ograve' => 'Ò',
64     'Oacute' => 'Ó',
65     'Ocirc' => 'Ô',
66     'Otilde' => 'Õ',
67     'Ouml' => 'Ö',
68     'Oslash' => 'Ø',
69     'Ugrave' => 'Ù',
70     'Uacute' => 'Ú',
71     'Ucirc' => 'Û',
72     'Uuml' => 'Ü',
73     'Yacute' => 'Ý',
74     'THORN' => 'Þ',
75     'szlig' => 'ß',
76     'agrave' => 'à',
77     'aacute' => 'á',
78     'acirc' => 'â',
79     'atilde' => 'ã',
80     'auml' => 'ä',
81     'aring' => 'å',
82     'aelig' => 'æ',
83     'oelig' => 'œ',
84     'ccedil' => 'ç',
85     'egrave' => 'è',
86     'eacute' => 'é',
87     'ecirc' => 'ê',
88     'euml' => 'ë',
89     'igrave' => 'ì',
90     'iacute' => 'í',
91     'icirc' => 'î',
92     'iuml' => 'ï',
93     'eth' => 'ð',
94     'ntilde' => 'ñ',
95     'ograve' => 'ò',
96     'oacute' => 'ó',
97     'ocirc' => 'ô',
98     'otilde' => 'õ',
99     'ouml' => 'ö',
100     'oslash' => 'ø',
101     'ugrave' => 'ù',
102     'uacute' => 'ú',
103     'ucirc' => 'û',
104     'uuml' => 'ü',
105     'yacute' => 'ý',
106     'thorn' => 'þ',
107     'yuml' => 'ÿ'
108         }
109       end
110     end
111   end
112 end
113
114 begin
115   require 'hpricot'
116   module ::Irc
117     module Utils
118       AFTER_PAR_PATH = /^(?:div|span)$/
119       AFTER_PAR_EX = /^(?:td|tr|tbody|table)$/
120       AFTER_PAR_CLASS = /body|message|text/i
121     end
122   end
123 rescue LoadError
124   gems = nil
125   begin
126     gems = require 'rubygems'
127   rescue LoadError
128     gems = false
129   end
130   if gems
131     retry
132   else
133     module ::Irc
134       module Utils
135         # Some regular expressions to manage HTML data
136
137         # Title
138         TITLE_REGEX = /<\s*?title\s*?>(.+?)<\s*?\/title\s*?>/im
139
140         # H1, H2, etc
141         HX_REGEX = /<h(\d)(?:\s+[^>]*)?>(.*?)<\/h\1>/im
142         # A paragraph
143         PAR_REGEX = /<p(?:\s+[^>]*)?>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
144
145         # Some blogging and forum platforms use spans or divs with a 'body' or 'message' or 'text' in their class
146         # to mark actual text
147         AFTER_PAR1_REGEX = /<\w+\s+[^>]*(?:body|message|text)[^>]*>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im
148
149         # At worst, we can try stuff which is comprised between two <br>
150         AFTER_PAR2_REGEX = /<br(?:\s+[^>]*)?\/?>.*?<\/?(?:br|p|div|html|body|table|td|tr)(?:\s+[^>]*)?\/?>/im
151       end
152     end
153   end
154 end
155
156 module ::Irc
157
158   # Miscellaneous useful functions
159   module Utils
160     @@bot = nil unless defined? @@bot
161     @@safe_save_dir = nil unless defined?(@@safe_save_dir)
162
163     # The bot instance
164     def Utils.bot
165       @@bot
166     end
167
168     # Set up some Utils routines which depend on the associated bot.
169     def Utils.bot=(b)
170       debug "initializing utils"
171       @@bot = b
172       @@safe_save_dir = "#{@@bot.botclass}/safe_save"
173     end
174
175
176     # Seconds per minute
177     SEC_PER_MIN = 60
178     # Seconds per hour
179     SEC_PER_HR = SEC_PER_MIN * 60
180     # Seconds per day
181     SEC_PER_DAY = SEC_PER_HR * 24
182     # Seconds per (30-day) month
183     SEC_PER_MNTH = SEC_PER_DAY * 30
184     # Second per (30*12 = 360 day) year
185     SEC_PER_YR = SEC_PER_MNTH * 12
186
187     # Auxiliary method needed by Utils.secs_to_string
188     def Utils.secs_to_string_case(array, var, string, plural)
189       case var
190       when 1
191         array << "1 #{string}"
192       else
193         array << "#{var} #{plural}"
194       end
195     end
196
197     # Turn a number of seconds into a human readable string, e.g
198     # 2 days, 3 hours, 18 minutes and 10 seconds
199     def Utils.secs_to_string(secs)
200       ret = []
201       years, secs = secs.divmod SEC_PER_YR
202       secs_to_string_case(ret, years, _("year"), _("years")) if years > 0
203       months, secs = secs.divmod SEC_PER_MNTH
204       secs_to_string_case(ret, months, _("month"), _("months")) if months > 0
205       days, secs = secs.divmod SEC_PER_DAY
206       secs_to_string_case(ret, days, _("day"), _("days")) if days > 0
207       hours, secs = secs.divmod SEC_PER_HR
208       secs_to_string_case(ret, hours, _("hour"), _("hours")) if hours > 0
209       mins, secs = secs.divmod SEC_PER_MIN
210       secs_to_string_case(ret, mins, _("minute"), _("minutes")) if mins > 0
211       secs = secs.to_i
212       secs_to_string_case(ret, secs, _("second"), _("seconds")) if secs > 0 or ret.empty?
213       case ret.length
214       when 0
215         raise "Empty ret array!"
216       when 1
217         return ret.to_s
218       else
219         return [ret[0, ret.length-1].join(", ") , ret[-1]].join(_(" and "))
220       end
221     end
222
223     # Turn a number of seconds into a hours:minutes:seconds e.g.
224     # 3:18:10 or 5'12" or 7s
225     #
226     def Utils.secs_to_short(seconds)
227       secs = seconds.to_i # make sure it's an integer
228       mins, secs = secs.divmod 60
229       hours, mins = mins.divmod 60
230       if hours > 0
231         return ("%s:%s:%s" % [hours, mins, secs])
232       elsif mins > 0
233         return ("%s'%s\"" % [mins, secs])
234       else
235         return ("%ss" % [secs])
236       end
237     end
238
239     # Returns human readable time.
240     # Like: 5 days ago
241     #       about one hour ago
242     # options
243     # :start_date, sets the time to measure against, defaults to now
244     # :date_format, used with <tt>to_formatted_s<tt>, default to :default
245     def Utils.timeago(time, options = {})
246       start_date = options.delete(:start_date) || Time.new
247       date_format = options.delete(:date_format) || :default
248       delta_minutes = (start_date.to_i - time.to_i).floor / 60
249       if delta_minutes.abs <= (8724*60) # eight weeks? I'm lazy to count days for longer than that
250         distance = Utils.distance_of_time_in_words(delta_minutes);
251         if delta_minutes < 0
252           _("%{d} from now") % {:d => distance}
253         else
254           _("%{d} ago") % {:d => distance}
255         end
256       else
257         return _("on %{date}") % {:date => system_date.to_formatted_s(date_format)}
258       end
259     end
260   # Translates a number of minutes into verbal distances.
261   # e.g. 0.5 => less than a minute
262   #      70 => about one hour
263   def Utils.distance_of_time_in_words(minutes)
264     case
265       when minutes < 0
266         Utils.distance_of_time_in_words(-minutes)
267       when minutes < 1
268         _("less than a minute")
269       when minutes < 50
270         _("%{m} minutes") % {:m => minutes}
271       when minutes < 90
272         _("about one hour")
273       when minutes < 1080
274         _("%{m} hours") % {:m => (minutes / 60).round}
275       when minutes < 1440
276         _("one day")
277       when minutes < 2880
278         _("about one day")
279       else
280         _("%{m} days") % {:m => (minutes / 1440).round}
281     end
282   end
283
284
285     # Execute an external program, returning a String obtained by redirecting
286     # the program's standards errors and output 
287     #
288     def Utils.safe_exec(command, *args)
289       IO.popen("-") { |p|
290         if p
291           return p.readlines.join("\n")
292         else
293           begin
294             $stderr.reopen($stdout)
295             exec(command, *args)
296           rescue Exception => e
297             puts "exec of #{command} led to exception: #{e.pretty_inspect}"
298             Kernel::exit! 0
299           end
300           puts "exec of #{command} failed"
301           Kernel::exit! 0
302         end
303       }
304     end
305
306
307     # Safely (atomically) save to _file_, by passing a tempfile to the block
308     # and then moving the tempfile to its final location when done.
309     #
310     # call-seq: Utils.safe_save(file, &block)
311     #
312     def Utils.safe_save(file)
313       raise 'No safe save directory defined!' if @@safe_save_dir.nil?
314       basename = File.basename(file)
315       temp = Tempfile.new(basename,@@safe_save_dir)
316       temp.binmode
317       yield temp if block_given?
318       temp.close
319       File.rename(temp.path, file)
320     end
321
322
323     # Decode HTML entities in the String _str_, using HTMLEntities if the
324     # package was found, or UNESCAPE_TABLE otherwise.
325     #
326     def Utils.decode_html_entities(str)
327       if defined? ::HTMLEntities
328         return HTMLEntities.decode_entities(str)
329       else
330         str.gsub(/(&(.+?);)/) {
331           symbol = $2
332           # remove the 0-paddng from unicode integers
333           if symbol =~ /^#(\d+)$/
334             symbol = $1.to_i.to_s
335           end
336
337           # output the symbol's irc-translated character, or a * if it's unknown
338           UNESCAPE_TABLE[symbol] || (symbol.match(/^\d+$/) ? [symbol.to_i].pack("U") : '*')
339         }
340       end
341     end
342
343     # Try to grab and IRCify the first HTML par (<p> tag) in the given string.
344     # If possible, grab the one after the first heading
345     #
346     # It is possible to pass some options to determine how the stripping
347     # occurs. Currently supported options are
348     # strip:: Regex or String to strip at the beginning of the obtained
349     #         text
350     # min_spaces:: minimum number of spaces a paragraph should have
351     #
352     def Utils.ircify_first_html_par(xml_org, opts={})
353       if defined? ::Hpricot
354         Utils.ircify_first_html_par_wh(xml_org, opts)
355       else
356         Utils.ircify_first_html_par_woh(xml_org, opts)
357       end
358     end
359
360     # HTML first par grabber using hpricot
361     def Utils.ircify_first_html_par_wh(xml_org, opts={})
362       doc = Hpricot(xml_org)
363
364       # Strip styles and scripts
365       (doc/"style|script").remove
366
367       debug doc
368
369       strip = opts[:strip]
370       strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String)
371
372       min_spaces = opts[:min_spaces] || 8
373       min_spaces = 0 if min_spaces < 0
374
375       txt = String.new
376
377       pre_h = pars = by_span = nil
378
379       while true
380         debug "Minimum number of spaces: #{min_spaces}"
381
382         # Initial attempt: <p> that follows <h\d>
383         if pre_h.nil?
384           pre_h = Hpricot::Elements[]
385           found_h = false
386           doc.search("*") { |e|
387             next if e.bogusetag?
388             case e.pathname
389             when /^h\d/
390               found_h = true
391             when 'p'
392               pre_h << e if found_h
393             end
394           }
395           debug "Hx: found: #{pre_h.pretty_inspect}"
396         end
397
398         pre_h.each { |p|
399           debug p
400           txt = p.to_html.ircify_html
401           txt.sub!(strip, '') if strip
402           debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
403           break unless txt.empty? or txt.count(" ") < min_spaces
404         }
405
406         return txt unless txt.empty? or txt.count(" ") < min_spaces
407
408         # Second natural attempt: just get any <p>
409         pars = doc/"p" if pars.nil?
410         debug "par: found: #{pars.pretty_inspect}"
411         pars.each { |p|
412           debug p
413           txt = p.to_html.ircify_html
414           txt.sub!(strip, '') if strip
415           debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
416           break unless txt.empty? or txt.count(" ") < min_spaces
417         }
418
419         return txt unless txt.empty? or txt.count(" ") < min_spaces
420
421         # Nothing yet ... let's get drastic: we look for non-par elements too,
422         # but only for those that match something that we know is likely to
423         # contain text
424
425         # Some blogging and forum platforms use spans or divs with a 'body' or
426         # 'message' or 'text' in their class to mark actual text. Since we want
427         # the class match to be partial and case insensitive, we collect
428         # the common elements that may have this class and then filter out those
429         # we don't need. If no divs or spans are found, we'll accept additional
430         # elements too (td, tr, tbody, table).
431         if by_span.nil?
432           by_span = Hpricot::Elements[]
433           extra = Hpricot::Elements[]
434           doc.search("*") { |el|
435             next if el.bogusetag?
436             case el.pathname
437             when AFTER_PAR_PATH
438               by_span.push el if el[:class] =~ AFTER_PAR_CLASS or el[:id] =~ AFTER_PAR_CLASS
439             when AFTER_PAR_EX
440               extra.push el if el[:class] =~ AFTER_PAR_CLASS or el[:id] =~ AFTER_PAR_CLASS
441             end
442           }
443           if by_span.empty? and not extra.empty?
444             by_span.concat extra
445           end
446           debug "other \#1: found: #{by_span.pretty_inspect}"
447         end
448
449         by_span.each { |p|
450           debug p
451           txt = p.to_html.ircify_html
452           txt.sub!(strip, '') if strip
453           debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces"
454           break unless txt.empty? or txt.count(" ") < min_spaces
455         }
456
457         return txt unless txt.empty? or txt.count(" ") < min_spaces
458
459         # At worst, we can try stuff which is comprised between two <br>
460         # TODO
461
462         debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces"
463         return txt unless txt.count(" ") < min_spaces
464         break if min_spaces == 0
465         min_spaces /= 2
466       end
467     end
468
469     # HTML first par grabber without hpricot
470     def Utils.ircify_first_html_par_woh(xml_org, opts={})
471       xml = xml_org.gsub(/<!--.*?-->/m, '').gsub(/<script(?:\s+[^>]*)?>.*?<\/script>/im, "").gsub(/<style(?:\s+[^>]*)?>.*?<\/style>/im, "")
472
473       strip = opts[:strip]
474       strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String)
475
476       min_spaces = opts[:min_spaces] || 8
477       min_spaces = 0 if min_spaces < 0
478
479       txt = String.new
480
481       while true
482         debug "Minimum number of spaces: #{min_spaces}"
483         header_found = xml.match(HX_REGEX)
484         if header_found
485           header_found = $'
486           while txt.empty? or txt.count(" ") < min_spaces
487             candidate = header_found[PAR_REGEX]
488             break unless candidate
489             txt = candidate.ircify_html
490             header_found = $'
491             txt.sub!(strip, '') if strip
492             debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
493           end
494         end
495
496         return txt unless txt.empty? or txt.count(" ") < min_spaces
497
498         # If we haven't found a first par yet, try to get it from the whole
499         # document
500         header_found = xml
501         while txt.empty? or txt.count(" ") < min_spaces
502           candidate = header_found[PAR_REGEX]
503           break unless candidate
504           txt = candidate.ircify_html
505           header_found = $'
506           txt.sub!(strip, '') if strip
507           debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces"
508         end
509
510         return txt unless txt.empty? or txt.count(" ") < min_spaces
511
512         # Nothing yet ... let's get drastic: we look for non-par elements too,
513         # but only for those that match something that we know is likely to
514         # contain text
515
516         # Attempt #1
517         header_found = xml
518         while txt.empty? or txt.count(" ") < min_spaces
519           candidate = header_found[AFTER_PAR1_REGEX]
520           break unless candidate
521           txt = candidate.ircify_html
522           header_found = $'
523           txt.sub!(strip, '') if strip
524           debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces"
525         end
526
527         return txt unless txt.empty? or txt.count(" ") < min_spaces
528
529         # Attempt #2
530         header_found = xml
531         while txt.empty? or txt.count(" ") < min_spaces
532           candidate = header_found[AFTER_PAR2_REGEX]
533           break unless candidate
534           txt = candidate.ircify_html
535           header_found = $'
536           txt.sub!(strip, '') if strip
537           debug "(other attempt \#2) #{txt.inspect} has #{txt.count(" ")} spaces"
538         end
539
540         debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces"
541         return txt unless txt.count(" ") < min_spaces
542         break if min_spaces == 0
543         min_spaces /= 2
544       end
545     end
546
547     # This method extracts title, content (first par) and extra
548     # information from the given document _doc_.
549     #
550     # _doc_ can be an URI, a Net::HTTPResponse or a String.
551     #
552     # If _doc_ is a String, only title and content information
553     # are retrieved (if possible), using standard methods.
554     #
555     # If _doc_ is an URI or a Net::HTTPResponse, additional
556     # information is retrieved, and special title/summary
557     # extraction routines are used if possible.
558     #
559     def Utils.get_html_info(doc, opts={})
560       case doc
561       when String
562         Utils.get_string_html_info(doc, opts)
563       when Net::HTTPResponse
564         Utils.get_resp_html_info(doc, opts)
565       when URI
566         ret = DataStream.new
567         @@bot.httputil.get_response(doc) { |resp|
568           ret.replace Utils.get_resp_html_info(resp, opts)
569         }
570         return ret
571       else
572         raise
573       end
574     end
575
576     class ::UrlLinkError < RuntimeError
577     end
578
579     # This method extracts title, content (first par) and extra
580     # information from the given Net::HTTPResponse _resp_.
581     #
582     # Currently, the only accepted options (in _opts_) are
583     # uri_fragment:: the URI fragment of the original request
584     # full_body::    get the whole body instead of
585     #                @@bot.config['http.info_bytes'] bytes only
586     #
587     # Returns a DataStream with the following keys:
588     # text:: the (partial) body
589     # title:: the title of the document (if any)
590     # content:: the first paragraph of the document (if any)
591     # headers::
592     #   the headers of the Net::HTTPResponse. The value is
593     #   a Hash whose keys are lowercase forms of the HTTP
594     #   header fields, and whose values are Arrays.
595     #
596     def Utils.get_resp_html_info(resp, opts={})
597       case resp
598       when Net::HTTPSuccess
599         loc = URI.parse(resp['x-rbot-location'] || resp['location']) rescue nil
600         if loc and loc.fragment and not loc.fragment.empty?
601           opts[:uri_fragment] ||= loc.fragment
602         end
603         ret = DataStream.new(opts.dup)
604         ret[:headers] = resp.to_hash
605         ret[:text] = partial = opts[:full_body] ? resp.body : resp.partial_body(@@bot.config['http.info_bytes'])
606
607         filtered = Utils.try_htmlinfo_filters(ret)
608
609         if filtered
610           return filtered
611         elsif resp['content-type'] =~ /^text\/|(?:x|ht)ml/
612           ret.merge!(Utils.get_string_html_info(partial, opts))
613         end
614         return ret
615       else
616         raise UrlLinkError, "getting link (#{resp.code} - #{resp.message})"
617       end
618     end
619
620     # This method runs an appropriately-crafted DataStream _ds_ through the
621     # filters in the :htmlinfo filter group, in order. If one of the filters
622     # returns non-nil, its results are merged in _ds_ and returned. Otherwise
623     # nil is returned.
624     #
625     # The input DataStream shuold have the downloaded HTML as primary key
626     # (:text) and possibly a :headers key holding the resonse headers.
627     #
628     def Utils.try_htmlinfo_filters(ds)
629       filters = @@bot.filter_names(:htmlinfo)
630       return nil if filters.empty?
631       cur = nil
632       # TODO filter priority
633       filters.each { |n|
634         debug "testing filter #{n}"
635         cur = @@bot.filter(@@bot.global_filter_name(n, :htmlinfo), ds)
636         debug "returned #{cur.pretty_inspect}"
637         break if cur
638       }
639       return ds.merge(cur) if cur
640     end
641
642     # HTML info filters often need to check if the webpage location
643     # of a passed DataStream _ds_ matches a given Regexp.
644     def Utils.check_location(ds, rx)
645       debug ds[:headers]
646       if h = ds[:headers]
647         loc = [h['x-rbot-location'],h['location']].flatten.grep(rx)
648       end
649       loc ||= []
650       debug loc
651       return loc.empty? ? nil : loc
652     end
653
654     # This method extracts title and content (first par)
655     # from the given HTML or XML document _text_, using
656     # standard methods (String#ircify_html_title,
657     # Utils.ircify_first_html_par)
658     #
659     # Currently, the only accepted option (in _opts_) is
660     # uri_fragment:: the URI fragment of the original request
661     #
662     def Utils.get_string_html_info(text, opts={})
663       debug "getting string html info"
664       txt = text.dup
665       title = txt.ircify_html_title
666       debug opts
667       if frag = opts[:uri_fragment] and not frag.empty?
668         fragreg = /<a\s+(?:[^>]+\s+)?(?:name|id)=["']?#{frag}["']?[^>]*>/im
669         debug fragreg
670         debug txt
671         if txt.match(fragreg)
672           # grab the post-match
673           txt = $'
674         end
675         debug txt
676       end
677       c_opts = opts.dup
678       c_opts[:strip] ||= title
679       content = Utils.ircify_first_html_par(txt, c_opts)
680       content = nil if content.empty?
681       return {:title => title, :content => content}
682     end
683
684     # Get the first pars of the first _count_ _urls_.
685     # The pages are downloaded using the bot httputil service.
686     # Returns an array of the first paragraphs fetched.
687     # If (optional) _opts_ :message is specified, those paragraphs are
688     # echoed as replies to the IRC message passed as _opts_ :message
689     #
690     def Utils.get_first_pars(urls, count, opts={})
691       idx = 0
692       msg = opts[:message]
693       retval = Array.new
694       while count > 0 and urls.length > 0
695         url = urls.shift
696         idx += 1
697
698         begin
699           info = Utils.get_html_info(URI.parse(url), opts)
700
701           par = info[:content]
702           retval.push(par)
703
704           if par
705             msg.reply "[#{idx}] #{par}", :overlong => :truncate if msg
706             count -=1
707           end
708         rescue
709           debug "Unable to retrieve #{url}: #{$!}"
710           next
711         end
712       end
713       return retval
714     end
715
716   end
717 end
718
719 Irc::Utils.bot = Irc::Bot::Plugins.manager.bot