]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
httputil and url plugin improvements, see ChangeLog
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / url.rb
1 require 'uri'
2
3 Url = Struct.new("Url", :channel, :nick, :time, :url)
4 TITLE_RE = /<\s*?title\s*?>(.+?)<\s*?\/title\s*?>/im
5
6 class UrlPlugin < Plugin
7   BotConfig.register BotConfigIntegerValue.new('url.max_urls',
8     :default => 100, :validate => Proc.new{|v| v > 0},
9     :desc => "Maximum number of urls to store. New urls replace oldest ones.")
10   BotConfig.register BotConfigBooleanValue.new('url.display_link_info',
11     :default => false,
12     :desc => "Get the title of any links pasted to the channel and display it (also tells if the link is broken or the site is down)")
13   BotConfig.register BotConfigBooleanValue.new('url.titles_only',
14     :default => false,
15     :desc => "Only show info for links that have <title> tags (in other words, don't display info for jpegs, mpegs, etc.)")
16
17   def initialize
18     super
19     @registry.set_default(Array.new)
20   end
21
22   def help(plugin, topic="")
23     "urls [<max>=4] => list <max> last urls mentioned in current channel, urls search [<max>=4] <regexp> => search for matching urls. In a private message, you must specify the channel to query, eg. urls <channel> [max], urls search <channel> [max] <regexp>"
24   end
25
26   def get_title_from_html(pagedata)
27     return unless TITLE_RE.match(pagedata)
28     title = $1.strip.gsub(/\s*\n+\s*/, " ")
29     title = Utils.decode_html_entities title
30     "title: #{title}"
31   end
32
33   def get_title_for_url(uri_str)
34
35     url = uri_str.kind_of?(URI) ? uri_str : URI.parse(uri_str)
36     return if url.scheme !~ /https?/
37
38     title = nil
39
40     begin
41       @bot.httputil.get_response(url) { |response|
42         case response
43         when Net::HTTPSuccess
44           if response['content-type'] =~ /^text\//
45             # since the content is 'text/*' and is small enough to
46             # be a webpage, retrieve the title from the page
47             debug "+ getting #{url.request_uri}"
48
49             # we look for the title in the first 4k bytes
50             # TODO make the amount of data configurable
51             response.partial_body(4096) { |part|
52               title = get_title_from_html(part)
53               return title if title
54             }
55             # if nothing was found, return nothing
56             return
57           else
58             unless @bot.config['url.titles_only']
59               # content doesn't have title, just display info.
60               size = response['content-length'].gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
61               size = size ? ", size: #{size} bytes" : ""
62               return "type: #{response['content-type']}#{size}"
63             end
64           end
65         when Net::HTTPResponse
66           return "Error getting link (#{response.code} - #{response.message})"
67         else
68           raise response
69         end
70       }
71     rescue Object => e
72       if e.class <= StandardError
73         error e.inspect
74         debug e.backtrace.join("\n")
75       end
76
77       msg = e.respond_to?(:message) ? e.message : e.to_s
78       return "Error connecting to site (#{e.message})"
79     end
80   end
81
82   def listen(m)
83     return unless m.kind_of?(PrivMessage)
84     return if m.address?
85     # TODO support multiple urls in one line
86     if m.message =~ /(f|ht)tps?:\/\//
87       if m.message =~ /((f|ht)tps?:\/\/.*?)(?:\s+|$)/
88         urlstr = $1
89         list = @registry[m.target]
90
91         if @bot.config['url.display_link_info']
92           Thread.start do
93             debug "Getting title for #{urlstr}..."
94             begin
95               title = get_title_for_url urlstr
96               if title
97                 m.reply "[Link Info] #{title}"
98                 debug "Title found!"
99               else
100                 debug "Title not found!"
101               end
102             rescue => e
103               debug "Failed: #{e}"
104             end
105           end
106         end
107
108         # check to see if this url is already listed
109         return if list.find {|u| u.url == urlstr }
110
111         url = Url.new(m.target, m.sourcenick, Time.new, urlstr)
112         debug "#{list.length} urls so far"
113         if list.length > @bot.config['url.max_urls']
114           list.pop
115         end
116         debug "storing url #{url.url}"
117         list.unshift url
118         debug "#{list.length} urls now"
119         @registry[m.target] = list
120       end
121     end
122   end
123
124   def urls(m, params)
125     channel = params[:channel] ? params[:channel] : m.target
126     max = params[:limit].to_i
127     max = 10 if max > 10
128     max = 1 if max < 1
129     list = @registry[channel]
130     if list.empty?
131       m.reply "no urls seen yet for channel #{channel}"
132     else
133       list[0..(max-1)].each do |url|
134         m.reply "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
135       end
136     end
137   end
138
139   def search(m, params)
140     channel = params[:channel] ? params[:channel] : m.target
141     max = params[:limit].to_i
142     string = params[:string]
143     max = 10 if max > 10
144     max = 1 if max < 1
145     regex = Regexp.new(string, Regexp::IGNORECASE)
146     list = @registry[channel].find_all {|url|
147       regex.match(url.url) || regex.match(url.nick)
148     }
149     if list.empty?
150       m.reply "no matches for channel #{channel}"
151     else
152       list[0..(max-1)].each do |url|
153         m.reply "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
154       end
155     end
156   end
157 end
158 plugin = UrlPlugin.new
159 plugin.map 'urls search :channel :limit :string', :action => 'search',
160                           :defaults => {:limit => 4},
161                           :requirements => {:limit => /^\d+$/},
162                           :public => false
163 plugin.map 'urls search :limit :string', :action => 'search',
164                           :defaults => {:limit => 4},
165                           :requirements => {:limit => /^\d+$/},
166                           :private => false
167 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
168                           :requirements => {:limit => /^\d+$/},
169                           :public => false
170 plugin.map 'urls :limit', :defaults => {:limit => 4},
171                           :requirements => {:limit => /^\d+$/},
172                           :private => false