]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/url.rb
lastfm: allow bolding in translations in nowplaying
[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   Config.register Config::ArrayValue.new('url.only_on_channels',
32     :desc => "Show link info only on these channels",
33     :default => [])
34   Config.register Config::ArrayValue.new('url.ignore',
35     :desc => "Don't show link info for urls from users represented as hostmasks on this list. Useful for ignoring other bots, for example.",
36     :default => [])
37
38   def initialize
39     super
40     @registry.set_default(Array.new)
41     unless @bot.config['url.display_link_info'].kind_of?(Integer)
42       @bot.config.items[:'url.display_link_info'].set_string(@bot.config['url.display_link_info'].to_s)
43     end
44     reset_no_info_hosts
45   end
46
47   def reset_no_info_hosts
48     @no_info_hosts = Regexp.new(@bot.config['url.no_info_hosts'].join('|'), true)
49     debug "no info hosts regexp set to #{@no_info_hosts}"
50   end
51
52   def help(plugin, topic="")
53     "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>"
54   end
55
56   def get_title_from_html(pagedata)
57     return pagedata.ircify_html_title
58   end
59
60   def get_title_for_url(uri_str, opts = {})
61
62     url = uri_str.kind_of?(URI) ? uri_str : URI.parse(uri_str)
63     return if url.scheme !~ /https?/
64
65     # also check the ip, the canonical name and the aliases
66     begin
67       checks = TCPSocket.gethostbyname(url.host)
68       checks.delete_at(-2)
69     rescue => e
70       return "Unable to retrieve info for #{url.host}: #{e.message}"
71     end
72
73     checks << url.host
74     checks.flatten!
75
76     unless checks.grep(@no_info_hosts).empty?
77       return ( opts[:always_reply] ? "Sorry, info retrieval for #{url.host} (#{checks.first}) is disabled" : false )
78     end
79
80     logopts = opts.dup
81
82     title = nil
83     extra = []
84
85     begin
86       debug "+ getting info for #{url.request_uri}"
87       info = @bot.filter(:htmlinfo, url)
88       debug info
89       resp = info[:headers]
90
91       logopts[:title] = title = info[:title]
92
93       if info[:content]
94         logopts[:extra] = info[:content]
95         extra << "#{Bold}text#{Bold}: #{info[:content]}" if @bot.config['url.first_par']
96       else
97         logopts[:extra] = String.new
98         logopts[:extra] << "Content Type: #{resp['content-type']}"
99         extra << "#{Bold}type#{Bold}: #{resp['content-type']}" unless title
100         if enc = resp['content-encoding']
101           logopts[:extra] << ", encoding: #{enc}"
102           extra << "#{Bold}encoding#{Bold}: #{enc}" if @bot.config['url.first_par'] or not title
103         end
104
105         size = resp['content-length'].first.gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2') rescue nil
106         if size
107           logopts[:extra] << ", size: #{size} bytes"
108           extra << "#{Bold}size#{Bold}: #{size} bytes" if @bot.config['url.first_par'] or not title
109         end
110       end
111     rescue Exception => e
112       case e
113       when UrlLinkError
114         raise e
115       else
116         error e
117         raise "connecting to site/processing information (#{e.message})"
118       end
119     end
120
121     call_event(:url_added, url.to_s, logopts)
122     if title
123       extra.unshift("#{Bold}title#{Bold}: #{title}")
124     end
125     return extra.join(", ") if title or not @bot.config['url.titles_only']
126   end
127
128   def handle_urls(m, params={})
129     opts = {
130       :display_info => @bot.config['url.display_link_info'],
131       :channels => @bot.config['url.only_on_channels'],
132       :ignore => @bot.config['url.ignore']
133     }.merge params
134     urls = opts[:urls]
135     display_info= opts[:display_info]
136     channels = opts[:channels]
137     ignore = opts[:ignore]
138
139     unless channels.empty?
140       return unless channels.map { |c| c.downcase }.include?(m.channel.downcase)
141     end
142
143     ignore.each { |u| return if m.source.matches?(u) }
144
145     return if urls.empty?
146     debug "found urls #{urls.inspect}"
147     list = m.public? ? @registry[m.target] : nil
148     debug "display link info: #{display_info}"
149     urls_displayed = 0
150     urls.each do |urlstr|
151       debug "working on #{urlstr}"
152       next unless urlstr =~ /^https?:\/\/./
153       title = nil
154       debug "Getting title for #{urlstr}..."
155       reply = nil
156       begin
157         title = get_title_for_url(urlstr,
158                                   :always_reply => m.address?,
159                                   :nick => m.source.nick,
160                                   :channel => m.channel,
161                                   :ircline => m.message)
162         debug "Title #{title ? '' : 'not '} found"
163         reply = "#{LINK_INFO} #{title}" if title
164       rescue => e
165         debug e
166         # we might get a 404 because of trailing punctuation, so we try again
167         # with the last character stripped. this might generate invalid URIs
168         # (e.g. because "some.url" gets chopped to some.url%2, so catch that too
169         if e.message =~ /\(404 - Not Found\)/i or e.kind_of?(URI::InvalidURIError)
170           # chop off last character, and retry if we still have enough string to
171           # look like a minimal URL
172           retry if urlstr.chop! and urlstr =~ /^https?:\/\/./
173         end
174         reply = "Error #{e.message}"
175       end
176
177       if display_info > urls_displayed
178         if reply
179           m.reply reply, :overlong => :truncate, :to => :public,
180             :nick => (m.address? ? :auto : false)
181           urls_displayed += 1
182         end
183       end
184
185       next unless list
186
187       # check to see if this url is already listed
188       next if list.find {|u| u.url == urlstr }
189
190       url = Url.new(m.target, m.sourcenick, Time.new, urlstr, title)
191       debug "#{list.length} urls so far"
192       list.pop if list.length > @bot.config['url.max_urls']
193       debug "storing url #{url.url}"
194       list.unshift url
195       debug "#{list.length} urls now"
196     end
197     @registry[m.target] = list
198   end
199
200   def info(m, params)
201     escaped = URI.escape(params[:urls].to_s, OUR_UNSAFE)
202     urls = URI.extract(escaped)
203     Thread.new do
204       handle_urls(m,
205                   :urls => urls,
206                   :display_info => params[:urls].length,
207                   :channels => [])
208     end
209   end
210
211   def message(m)
212     return if m.address?
213
214     escaped = URI.escape(m.message, OUR_UNSAFE)
215     urls = URI.extract(escaped, ['http', 'https'])
216     return if urls.empty?
217     Thread.new { handle_urls(m, :urls => urls) }
218   end
219
220   def reply_urls(opts={})
221     list = opts[:list]
222     max = opts[:max]
223     channel = opts[:channel]
224     m = opts[:msg]
225     return unless list and max and m
226     list[0..(max-1)].each do |url|
227       disp = "[#{url.time.strftime('%Y/%m/%d %H:%M:%S')}] <#{url.nick}> #{url.url}"
228       if @bot.config['url.info_on_list']
229         title = url.info ||
230           get_title_for_url(url.url,
231                             :nick => url.nick, :channel => channel) rescue nil
232         # If the url info was missing and we now have some, try to upgrade it
233         if channel and title and not url.info
234           ll = @registry[channel]
235           debug ll
236           if el = ll.find { |u| u.url == url.url }
237             el.info = title
238             @registry[channel] = ll
239           end
240         end
241         disp << " --> #{title}" if title
242       end
243       m.reply disp, :overlong => :truncate
244     end
245   end
246
247   def urls(m, params)
248     channel = params[:channel] ? params[:channel] : m.target
249     max = params[:limit].to_i
250     max = 10 if max > 10
251     max = 1 if max < 1
252     list = @registry[channel]
253     if list.empty?
254       m.reply "no urls seen yet for channel #{channel}"
255     else
256       reply_urls :msg => m, :channel => channel, :list => list, :max => max
257     end
258   end
259
260   def search(m, params)
261     channel = params[:channel] ? params[:channel] : m.target
262     max = params[:limit].to_i
263     string = params[:string]
264     max = 10 if max > 10
265     max = 1 if max < 1
266     regex = Regexp.new(string, Regexp::IGNORECASE)
267     list = @registry[channel].find_all {|url|
268       regex.match(url.url) || regex.match(url.nick) ||
269         (@bot.config['url.info_on_list'] && regex.match(url.info))
270     }
271     if list.empty?
272       m.reply "no matches for channel #{channel}"
273     else
274       reply_urls :msg => m, :channel => channel, :list => list, :max => max
275     end
276   end
277 end
278
279 plugin = UrlPlugin.new
280 plugin.map 'urls info *urls', :action => 'info'
281 plugin.map 'url info *urls', :action => 'info'
282 plugin.map 'urls search :channel :limit :string', :action => 'search',
283                           :defaults => {:limit => 4},
284                           :requirements => {:limit => /^\d+$/},
285                           :public => false
286 plugin.map 'urls search :limit :string', :action => 'search',
287                           :defaults => {:limit => 4},
288                           :requirements => {:limit => /^\d+$/},
289                           :private => false
290 plugin.map 'urls :channel :limit', :defaults => {:limit => 4},
291                           :requirements => {:limit => /^\d+$/},
292                           :public => false
293 plugin.map 'urls :limit', :defaults => {:limit => 4},
294                           :requirements => {:limit => /^\d+$/},
295                           :private => false