]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
c4fce2ae6a7f3d03d36385e51c08accb2ac89422
[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     if url.host =~ @no_info_hosts
61       return "Sorry, info retrieval for #{url.host} is disabled"
62     end
63
64     logopts = opts.dup
65
66     title = nil
67     extra = []
68
69     begin
70       debug "+ getting info for #{url.request_uri}"
71       info = Utils.get_html_info(url)
72       debug info
73       resp = info[:headers]
74
75       logopts[:title] = title = info[:title]
76
77       if info[:content]
78         logopts[:extra] = info[:content]
79         extra << "#{Bold}text#{Bold}: #{info[:content]}" if @bot.config['url.first_par']
80       else
81         logopts[:extra] = String.new
82         logopts[:extra] << "Content Type: #{resp['content-type']}"
83         extra << "#{Bold}type#{Bold}: #{resp['content-type']}" unless title
84         if enc = resp['content-encoding']
85           logopts[:extra] << ", encoding: #{enc}"
86           extra << "#{Bold}encoding#{Bold}: #{enc}" if @bot.config['url.first_par'] or not title
87         end
88
89         size = resp['content-length'].first.gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2') rescue nil
90         if size
91           logopts[:extra] << ", size: #{size} bytes"
92           extra << "#{Bold}size#{Bold}: #{size} bytes" if @bot.config['url.first_par'] or not title
93         end
94       end
95     rescue Exception => e
96       case e
97       when UrlLinkError
98         raise e
99       else
100         error e
101         raise "connecting to site/processing information (#{e.message})"
102       end
103     end
104
105     call_event(:url_added, url.to_s, logopts)
106     if title
107       extra.unshift("#{Bold}title#{Bold}: #{title}")
108     end
109     return extra.join(", ") if title or not @bot.config['url.titles_only']
110   end
111
112   def handle_urls(m, urls, display_info=@bot.config['url.display_link_info'])
113     return if urls.empty?
114     debug "found urls #{urls.inspect}"
115     list = m.public? ? @registry[m.target] : nil
116     debug "display link info: #{display_info}"
117     urls_displayed = 0
118     urls.each do |urlstr|
119       debug "working on #{urlstr}"
120       next unless urlstr =~ /^https?:/
121       title = nil
122       debug "Getting title for #{urlstr}..."
123       begin
124         title = get_title_for_url(urlstr,
125                                   :nick => m.source.nick,
126                                   :channel => m.channel,
127                                   :ircline => m.message)
128         debug "Title #{title ? '' : 'not '} found"
129       rescue => e
130         m.reply "Error #{e.message}"
131       end
132
133       if display_info > urls_displayed
134         if title
135           m.reply("#{LINK_INFO} #{title}", :overlong => :truncate)
136           urls_displayed += 1
137         end
138       end
139
140       next unless list
141
142       # check to see if this url is already listed
143       next if list.find {|u| u.url == urlstr }
144
145       url = Url.new(m.target, m.sourcenick, Time.new, urlstr, title)
146       debug "#{list.length} urls so far"
147       list.pop if list.length > @bot.config['url.max_urls']
148       debug "storing url #{url.url}"
149       list.unshift url
150       debug "#{list.length} urls now"
151     end
152     @registry[m.target] = list
153   end
154
155   def info(m, params)
156     escaped = URI.escape(params[:urls].to_s, OUR_UNSAFE)
157     urls = URI.extract(escaped)
158     Thread.new { handle_urls(m, urls, params[:urls].length) }
159   end
160
161   def listen(m)
162     return unless m.kind_of?(PrivMessage)
163     return if m.address?
164
165     escaped = URI.escape(m.message, OUR_UNSAFE)
166     urls = URI.extract(escaped)
167     Thread.new { handle_urls(m, urls) }
168   end
169
170   def reply_urls(opts={})
171     list = opts[:list]
172     max = opts[:max]
173     channel = opts[:channel]
174     m = opts[:msg]
175     return unless list and max and m
176     list[0..(max-1)].each do |url|
177       disp = "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
178       if @bot.config['url.info_on_list']
179         title = url.info ||
180           get_title_for_url(url.url,
181                             :nick => url.nick, :channel => channel) rescue nil
182         # If the url info was missing and we now have some, try to upgrade it
183         if channel and title and not url.info
184           ll = @registry[channel]
185           debug ll
186           if el = ll.find { |u| u.url == url.url }
187             el.info = title
188             @registry[channel] = ll
189           end
190         end
191         disp << " --> #{title}" if title
192       end
193       m.reply disp, :overlong => :truncate
194     end
195   end
196
197   def urls(m, params)
198     channel = params[:channel] ? params[:channel] : m.target
199     max = params[:limit].to_i
200     max = 10 if max > 10
201     max = 1 if max < 1
202     list = @registry[channel]
203     if list.empty?
204       m.reply "no urls seen yet for channel #{channel}"
205     else
206       reply_urls :msg => m, :channel => channel, :list => list, :max => max
207     end
208   end
209
210   def search(m, params)
211     channel = params[:channel] ? params[:channel] : m.target
212     max = params[:limit].to_i
213     string = params[:string]
214     max = 10 if max > 10
215     max = 1 if max < 1
216     regex = Regexp.new(string, Regexp::IGNORECASE)
217     list = @registry[channel].find_all {|url|
218       regex.match(url.url) || regex.match(url.nick) ||
219         (@bot.config['url.info_on_list'] && regex.match(url.info))
220     }
221     if list.empty?
222       m.reply "no matches for channel #{channel}"
223     else
224       reply_urls :msg => m, :channel => channel, :list => list, :max => max
225     end
226   end
227 end
228
229 plugin = UrlPlugin.new
230 plugin.map 'urls info *urls', :action => 'info'
231 plugin.map 'url info *urls', :action => 'info'
232 plugin.map 'urls search :channel :limit :string', :action => 'search',
233                           :defaults => {:limit => 4},
234                           :requirements => {:limit => /^\d+$/},
235                           :public => false
236 plugin.map 'urls search :limit :string', :action => 'search',
237                           :defaults => {:limit => 4},
238                           :requirements => {:limit => /^\d+$/},
239                           :private => false
240 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
241                           :requirements => {:limit => /^\d+$/},
242                           :public => false
243 plugin.map 'urls :limit', :defaults => {:limit => 4},
244                           :requirements => {:limit => /^\d+$/},
245                           :private => false