]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
url plugin: check the canonical name, the IP address, and any known aliases against...
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / url.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Url plugin
5
6 define_structure :Url, :channel, :nick, :time, :url, :info
7
8 class UrlPlugin < Plugin
9   LINK_INFO = "[Link Info]"
10   OUR_UNSAFE = Regexp.new("[^#{URI::PATTERN::UNRESERVED}#{URI::PATTERN::RESERVED}%# ]", false, 'N')
11
12   Config.register Config::IntegerValue.new('url.max_urls',
13     :default => 100, :validate => Proc.new{|v| v > 0},
14     :desc => "Maximum number of urls to store. New urls replace oldest ones.")
15   Config.register Config::IntegerValue.new('url.display_link_info',
16     :default => 0,
17     :desc => "Get the title of links pasted to the channel and display it (also tells if the link is broken or the site is down). Do it for at most this many links per line (set to 0 to disable)")
18   Config.register Config::BooleanValue.new('url.titles_only',
19     :default => false,
20     :desc => "Only show info for links that have <title> tags (in other words, don't display info for jpegs, mpegs, etc.)")
21   Config.register Config::BooleanValue.new('url.first_par',
22     :default => false,
23     :desc => "Also try to get the first paragraph of a web page")
24   Config.register Config::BooleanValue.new('url.info_on_list',
25     :default => false,
26     :desc => "Show link info when listing/searching for urls")
27   Config.register Config::ArrayValue.new('url.no_info_hosts',
28     :default => ['localhost', '^192\.168\.', '^10\.', '^127\.', '^172\.(1[6-9]|2\d|31)\.'],
29     :on_change => Proc.new { |bot, v| bot.plugins['url'].reset_no_info_hosts },
30     :desc => "A list of regular expressions matching hosts for which no info should be provided")
31
32
33   def initialize
34     super
35     @registry.set_default(Array.new)
36     unless @bot.config['url.display_link_info'].kind_of?(Integer)
37       @bot.config.items[:'url.display_link_info'].set_string(@bot.config['url.display_link_info'].to_s)
38     end
39     reset_no_info_hosts
40   end
41
42   def reset_no_info_hosts
43     @no_info_hosts = Regexp.new(@bot.config['url.no_info_hosts'].join('|'), true)
44     debug "no info hosts regexp set to #{@no_info_hosts}"
45   end
46
47   def help(plugin, topic="")
48     "url info <url> => display link info for <url> (set url.display_link_info > 0 if you want the bot to do it automatically when someone writes an url), 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>"
49   end
50
51   def get_title_from_html(pagedata)
52     return pagedata.ircify_html_title
53   end
54
55   def get_title_for_url(uri_str, opts = {})
56
57     url = uri_str.kind_of?(URI) ? uri_str : URI.parse(uri_str)
58     return if url.scheme !~ /https?/
59
60     # also check the ip, the canonical name and the aliases
61     begin
62       checks = TCPSocket.gethostbyname(url.host)
63       checks.delete_at(-2)
64     rescue => e
65       return "Unable to retrieve info for #{url.host}: #{e.message}"
66     end
67
68     checks << url.host
69     checks.flatten!
70
71     unless checks.grep(@no_info_hosts).empty?
72       return "Sorry, info retrieval for #{url.host} (#{checks.first}) is disabled"
73     end
74
75     logopts = opts.dup
76
77     title = nil
78     extra = []
79
80     begin
81       debug "+ getting info for #{url.request_uri}"
82       info = Utils.get_html_info(url)
83       debug info
84       resp = info[:headers]
85
86       logopts[:title] = title = info[:title]
87
88       if info[:content]
89         logopts[:extra] = info[:content]
90         extra << "#{Bold}text#{Bold}: #{info[:content]}" if @bot.config['url.first_par']
91       else
92         logopts[:extra] = String.new
93         logopts[:extra] << "Content Type: #{resp['content-type']}"
94         extra << "#{Bold}type#{Bold}: #{resp['content-type']}" unless title
95         if enc = resp['content-encoding']
96           logopts[:extra] << ", encoding: #{enc}"
97           extra << "#{Bold}encoding#{Bold}: #{enc}" if @bot.config['url.first_par'] or not title
98         end
99
100         size = resp['content-length'].first.gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2') rescue nil
101         if size
102           logopts[:extra] << ", size: #{size} bytes"
103           extra << "#{Bold}size#{Bold}: #{size} bytes" if @bot.config['url.first_par'] or not title
104         end
105       end
106     rescue Exception => e
107       case e
108       when UrlLinkError
109         raise e
110       else
111         error e
112         raise "connecting to site/processing information (#{e.message})"
113       end
114     end
115
116     call_event(:url_added, url.to_s, logopts)
117     if title
118       extra.unshift("#{Bold}title#{Bold}: #{title}")
119     end
120     return extra.join(", ") if title or not @bot.config['url.titles_only']
121   end
122
123   def handle_urls(m, urls, display_info=@bot.config['url.display_link_info'])
124     return if urls.empty?
125     debug "found urls #{urls.inspect}"
126     list = m.public? ? @registry[m.target] : nil
127     debug "display link info: #{display_info}"
128     urls_displayed = 0
129     urls.each do |urlstr|
130       debug "working on #{urlstr}"
131       next unless urlstr =~ /^https?:/
132       title = nil
133       debug "Getting title for #{urlstr}..."
134       reply = nil
135       begin
136         title = get_title_for_url(urlstr,
137                                   :nick => m.source.nick,
138                                   :channel => m.channel,
139                                   :ircline => m.message)
140         debug "Title #{title ? '' : 'not '} found"
141         reply = "#{LINK_INFO} #{title}" if title
142       rescue => e
143         reply = "Error #{e.message}"
144       end
145
146       if display_info > urls_displayed
147         if reply
148           m.reply(reply, :overlong => :truncate)
149           urls_displayed += 1
150         end
151       end
152
153       next unless list
154
155       # check to see if this url is already listed
156       next if list.find {|u| u.url == urlstr }
157
158       url = Url.new(m.target, m.sourcenick, Time.new, urlstr, title)
159       debug "#{list.length} urls so far"
160       list.pop if list.length > @bot.config['url.max_urls']
161       debug "storing url #{url.url}"
162       list.unshift url
163       debug "#{list.length} urls now"
164     end
165     @registry[m.target] = list
166   end
167
168   def info(m, params)
169     escaped = URI.escape(params[:urls].to_s, OUR_UNSAFE)
170     urls = URI.extract(escaped)
171     Thread.new { handle_urls(m, urls, params[:urls].length) }
172   end
173
174   def listen(m)
175     return unless m.kind_of?(PrivMessage)
176     return if m.address?
177
178     escaped = URI.escape(m.message, OUR_UNSAFE)
179     urls = URI.extract(escaped, ['http', 'https'])
180     return if urls.empty?
181     Thread.new { handle_urls(m, urls) }
182   end
183
184   def reply_urls(opts={})
185     list = opts[:list]
186     max = opts[:max]
187     channel = opts[:channel]
188     m = opts[:msg]
189     return unless list and max and m
190     list[0..(max-1)].each do |url|
191       disp = "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
192       if @bot.config['url.info_on_list']
193         title = url.info ||
194           get_title_for_url(url.url,
195                             :nick => url.nick, :channel => channel) rescue nil
196         # If the url info was missing and we now have some, try to upgrade it
197         if channel and title and not url.info
198           ll = @registry[channel]
199           debug ll
200           if el = ll.find { |u| u.url == url.url }
201             el.info = title
202             @registry[channel] = ll
203           end
204         end
205         disp << " --> #{title}" if title
206       end
207       m.reply disp, :overlong => :truncate
208     end
209   end
210
211   def urls(m, params)
212     channel = params[:channel] ? params[:channel] : m.target
213     max = params[:limit].to_i
214     max = 10 if max > 10
215     max = 1 if max < 1
216     list = @registry[channel]
217     if list.empty?
218       m.reply "no urls seen yet for channel #{channel}"
219     else
220       reply_urls :msg => m, :channel => channel, :list => list, :max => max
221     end
222   end
223
224   def search(m, params)
225     channel = params[:channel] ? params[:channel] : m.target
226     max = params[:limit].to_i
227     string = params[:string]
228     max = 10 if max > 10
229     max = 1 if max < 1
230     regex = Regexp.new(string, Regexp::IGNORECASE)
231     list = @registry[channel].find_all {|url|
232       regex.match(url.url) || regex.match(url.nick) ||
233         (@bot.config['url.info_on_list'] && regex.match(url.info))
234     }
235     if list.empty?
236       m.reply "no matches for channel #{channel}"
237     else
238       reply_urls :msg => m, :channel => channel, :list => list, :max => max
239     end
240   end
241 end
242
243 plugin = UrlPlugin.new
244 plugin.map 'urls info *urls', :action => 'info'
245 plugin.map 'url info *urls', :action => 'info'
246 plugin.map 'urls search :channel :limit :string', :action => 'search',
247                           :defaults => {:limit => 4},
248                           :requirements => {:limit => /^\d+$/},
249                           :public => false
250 plugin.map 'urls search :limit :string', :action => 'search',
251                           :defaults => {:limit => 4},
252                           :requirements => {:limit => /^\d+$/},
253                           :private => false
254 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
255                           :requirements => {:limit => /^\d+$/},
256                           :public => false
257 plugin.map 'urls :limit', :defaults => {:limit => 4},
258                           :requirements => {:limit => /^\d+$/},
259                           :private => false