]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
quotes plugin: fix help
[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 = @bot.filter(:htmlinfo, 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         debug e
144         # we might get a 404 because of trailing punctuation, so we try again
145         # with the last character stripped. this might generate invalid URIs
146         # (e.g. because "some.url" gets chopped to some.url%2, so catch that too
147         if e.message =~ /\(404 - Not Found\)/i or e.kind_of?(URI::InvalidURIError)
148           # chop off last character, and retry if we still have enough string to
149           # look like a minimal URL
150           retry if urlstr.chop! and urlstr =~ /^https?:\/\/./
151         end
152         reply = "Error #{e.message}"
153       end
154
155       if display_info > urls_displayed
156         if reply
157           m.plainreply(reply, :overlong => :truncate)
158           urls_displayed += 1
159         end
160       end
161
162       next unless list
163
164       # check to see if this url is already listed
165       next if list.find {|u| u.url == urlstr }
166
167       url = Url.new(m.target, m.sourcenick, Time.new, urlstr, title)
168       debug "#{list.length} urls so far"
169       list.pop if list.length > @bot.config['url.max_urls']
170       debug "storing url #{url.url}"
171       list.unshift url
172       debug "#{list.length} urls now"
173     end
174     @registry[m.target] = list
175   end
176
177   def info(m, params)
178     escaped = URI.escape(params[:urls].to_s, OUR_UNSAFE)
179     urls = URI.extract(escaped)
180     Thread.new { handle_urls(m, urls, params[:urls].length) }
181   end
182
183   def message(m)
184     return if m.address?
185
186     escaped = URI.escape(m.message, OUR_UNSAFE)
187     urls = URI.extract(escaped, ['http', 'https'])
188     return if urls.empty?
189     Thread.new { handle_urls(m, urls) }
190   end
191
192   def reply_urls(opts={})
193     list = opts[:list]
194     max = opts[:max]
195     channel = opts[:channel]
196     m = opts[:msg]
197     return unless list and max and m
198     list[0..(max-1)].each do |url|
199       disp = "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
200       if @bot.config['url.info_on_list']
201         title = url.info ||
202           get_title_for_url(url.url,
203                             :nick => url.nick, :channel => channel) rescue nil
204         # If the url info was missing and we now have some, try to upgrade it
205         if channel and title and not url.info
206           ll = @registry[channel]
207           debug ll
208           if el = ll.find { |u| u.url == url.url }
209             el.info = title
210             @registry[channel] = ll
211           end
212         end
213         disp << " --> #{title}" if title
214       end
215       m.reply disp, :overlong => :truncate
216     end
217   end
218
219   def urls(m, params)
220     channel = params[:channel] ? params[:channel] : m.target
221     max = params[:limit].to_i
222     max = 10 if max > 10
223     max = 1 if max < 1
224     list = @registry[channel]
225     if list.empty?
226       m.reply "no urls seen yet for channel #{channel}"
227     else
228       reply_urls :msg => m, :channel => channel, :list => list, :max => max
229     end
230   end
231
232   def search(m, params)
233     channel = params[:channel] ? params[:channel] : m.target
234     max = params[:limit].to_i
235     string = params[:string]
236     max = 10 if max > 10
237     max = 1 if max < 1
238     regex = Regexp.new(string, Regexp::IGNORECASE)
239     list = @registry[channel].find_all {|url|
240       regex.match(url.url) || regex.match(url.nick) ||
241         (@bot.config['url.info_on_list'] && regex.match(url.info))
242     }
243     if list.empty?
244       m.reply "no matches for channel #{channel}"
245     else
246       reply_urls :msg => m, :channel => channel, :list => list, :max => max
247     end
248   end
249 end
250
251 plugin = UrlPlugin.new
252 plugin.map 'urls info *urls', :action => 'info'
253 plugin.map 'url info *urls', :action => 'info'
254 plugin.map 'urls search :channel :limit :string', :action => 'search',
255                           :defaults => {:limit => 4},
256                           :requirements => {:limit => /^\d+$/},
257                           :public => false
258 plugin.map 'urls search :limit :string', :action => 'search',
259                           :defaults => {:limit => 4},
260                           :requirements => {:limit => /^\d+$/},
261                           :private => false
262 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
263                           :requirements => {:limit => /^\d+$/},
264                           :public => false
265 plugin.map 'urls :limit', :defaults => {:limit => 4},
266                           :requirements => {:limit => /^\d+$/},
267                           :private => false