]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
lart plugin: fix listlart/praise logic
[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       reply = nil
124       begin
125         title = get_title_for_url(urlstr,
126                                   :nick => m.source.nick,
127                                   :channel => m.channel,
128                                   :ircline => m.message)
129         debug "Title #{title ? '' : 'not '} found"
130         reply = "#{LINK_INFO} #{title}" if title
131       rescue => e
132         reply = "Error #{e.message}"
133       end
134
135       if display_info > urls_displayed
136         if reply
137           m.reply(reply, :overlong => :truncate)
138           urls_displayed += 1
139         end
140       end
141
142       next unless list
143
144       # check to see if this url is already listed
145       next if list.find {|u| u.url == urlstr }
146
147       url = Url.new(m.target, m.sourcenick, Time.new, urlstr, title)
148       debug "#{list.length} urls so far"
149       list.pop if list.length > @bot.config['url.max_urls']
150       debug "storing url #{url.url}"
151       list.unshift url
152       debug "#{list.length} urls now"
153     end
154     @registry[m.target] = list
155   end
156
157   def info(m, params)
158     escaped = URI.escape(params[:urls].to_s, OUR_UNSAFE)
159     urls = URI.extract(escaped)
160     Thread.new { handle_urls(m, urls, params[:urls].length) }
161   end
162
163   def listen(m)
164     return unless m.kind_of?(PrivMessage)
165     return if m.address?
166
167     escaped = URI.escape(m.message, OUR_UNSAFE)
168     urls = URI.extract(escaped, ['http', 'https'])
169     return if urls.empty?
170     Thread.new { handle_urls(m, urls) }
171   end
172
173   def reply_urls(opts={})
174     list = opts[:list]
175     max = opts[:max]
176     channel = opts[:channel]
177     m = opts[:msg]
178     return unless list and max and m
179     list[0..(max-1)].each do |url|
180       disp = "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
181       if @bot.config['url.info_on_list']
182         title = url.info ||
183           get_title_for_url(url.url,
184                             :nick => url.nick, :channel => channel) rescue nil
185         # If the url info was missing and we now have some, try to upgrade it
186         if channel and title and not url.info
187           ll = @registry[channel]
188           debug ll
189           if el = ll.find { |u| u.url == url.url }
190             el.info = title
191             @registry[channel] = ll
192           end
193         end
194         disp << " --> #{title}" if title
195       end
196       m.reply disp, :overlong => :truncate
197     end
198   end
199
200   def urls(m, params)
201     channel = params[:channel] ? params[:channel] : m.target
202     max = params[:limit].to_i
203     max = 10 if max > 10
204     max = 1 if max < 1
205     list = @registry[channel]
206     if list.empty?
207       m.reply "no urls seen yet for channel #{channel}"
208     else
209       reply_urls :msg => m, :channel => channel, :list => list, :max => max
210     end
211   end
212
213   def search(m, params)
214     channel = params[:channel] ? params[:channel] : m.target
215     max = params[:limit].to_i
216     string = params[:string]
217     max = 10 if max > 10
218     max = 1 if max < 1
219     regex = Regexp.new(string, Regexp::IGNORECASE)
220     list = @registry[channel].find_all {|url|
221       regex.match(url.url) || regex.match(url.nick) ||
222         (@bot.config['url.info_on_list'] && regex.match(url.info))
223     }
224     if list.empty?
225       m.reply "no matches for channel #{channel}"
226     else
227       reply_urls :msg => m, :channel => channel, :list => list, :max => max
228     end
229   end
230 end
231
232 plugin = UrlPlugin.new
233 plugin.map 'urls info *urls', :action => 'info'
234 plugin.map 'url info *urls', :action => 'info'
235 plugin.map 'urls search :channel :limit :string', :action => 'search',
236                           :defaults => {:limit => 4},
237                           :requirements => {:limit => /^\d+$/},
238                           :public => false
239 plugin.map 'urls search :limit :string', :action => 'search',
240                           :defaults => {:limit => 4},
241                           :requirements => {:limit => /^\d+$/},
242                           :private => false
243 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
244                           :requirements => {:limit => /^\d+$/},
245                           :public => false
246 plugin.map 'urls :limit', :defaults => {:limit => 4},
247                           :requirements => {:limit => /^\d+$/},
248                           :private => false