1 #!/usr/bin/env ruby -wKU
13 def initialize(uri_string, etag = nil)
14 @uri_string = uri_string
18 def request(uri_string, etag = nil)
19 uri = URI.parse(uri_string)
20 path = uri.path.empty? ? '/' : uri.path
21 re = Net::HTTP.start(uri.host, uri.port) { |http| http.get(path, etag.nil? ? nil : { 'If-None-Match' => etag }) }
23 if re.kind_of? Net::HTTPSuccess
26 elsif re.kind_of? Net::HTTPFound
27 $log.puts "#{uri.host}: Found (redirect) → " + re['Location']
28 request(re['Location'], etag)
29 elsif re.kind_of? Net::HTTPMovedPermanently
30 $log.puts "#{uri.host}: Permanent redirect → " + re['Location']
31 request(re['Location'], etag)
32 elsif re.kind_of? Net::HTTPTemporaryRedirect
33 $log.puts "#{uri.host}: Temporary redirect → " + re['Location']
34 request(re['Location'], etag)
35 elsif re.kind_of? Net::HTTPNotModified
36 $log.puts "#{uri.host}: Not modified."
39 raise "Unknown response (#{re.code}) from #{uri_string}"
45 if re = request(@uri_string, @etag)
49 $log.puts "*** network error"
50 $log.puts "#{e.message}\n#{e.backtrace.join("\n")}"
56 res = { 'uri' => @uri_string }
57 res['etag'] = @etag unless @etag.nil?
64 def initialize(channel, item, master)
71 @master.did_read(self) if flag
75 if @item.respond_to? :guid
86 def summary(limit = 420)
87 prefix = "[#{@channel.title}] #{@item.title}: "
88 suffix = @item.link ? " — #{@item.link}" : ''
89 length = limit - prefix.length - suffix.length
91 body = @item.description.gsub(/<.*?>/, ' ')
92 body = CGI::unescapeHTML(body)
93 body = body.gsub(/\s+/, ' ').gsub(/\A\s+|\s+\z/, '')
94 body = body.sub(/(.{0,#{length}})(\s.+)?$/) { $1 + ($2.nil? ? '' : '…')}
96 prefix + body + suffix
100 def initialize(uri_string, etag = nil, last_check = nil, seen = [])
101 @ressource = Resource.new(uri_string, etag)
102 @last_check = last_check
108 if body = @ressource.get
109 if rss = RSS::Parser.parse(body, false)
110 new_items = rss.items.map { |e| Item.new(rss.channel, e, self) }
111 new_items.reject! { |item| @seen.include? item.guid }
112 @seen.concat(new_items.map { |item| item.guid })
113 @unread.concat(new_items)
115 $log.puts "Error parsing feed at " + @ressource.save['uri']
118 @last_check = Time.now
123 @unread.reject! { |e| e.guid == item.guid }
127 res = { 'seen' => @seen }
128 res['last_check'] = @last_check unless @last_check.nil?
129 res.merge(@ressource.save)
139 http://henrik.nyh.se/feed/
140 http://macromates.com/blog/feed/
141 http://macromates.com/blog/comments/feed/
142 http://macromates.com/svnlog/bundles.rss
143 http://macromates.com/textmate/screencast.rss
144 http://macromates.com/textmate/changelog.rss
145 http://alterslash.org/rss_full.xml
146 http://blog.grayproductions.net/xml/rss20/feed.xml
147 http://theocacao.com/index.rss
148 http://blog.circlesixdesign.com/feed/
149 http://kevin.sb.org/xml/rss/feed.xml
152 defaults = uris.map { |uri| Feed.new(uri) }
153 @feeds = open(filename) { |io| YAML.load(io).map { |e| Feed.new(e['uri'], e['etag'], e['last_check'], e['seen']) } } rescue defaults
157 open(filename, 'w') do |io|
158 io << "# Feeds we follow and their status.\n"
159 io << "# Last update: #{Time.now.strftime('%F %T')}.\n"
160 YAML.dump(@feeds.map { |e| e.save }, io)
165 def run(out = STDOUT, filename = '/tmp/feeds.yaml', period = 30*60)
169 @feeds.each do |feed|
170 feed.unread.each do |item|
171 $log.puts "Skip new item: #{item.title}"
177 $log.puts Time.now.strftime('%H:%M:%S: Checking feeds…')
179 @feeds.each do |feed|
180 feed.unread.each do |item|
181 out.puts item.summary
188 $log.puts Time.now.strftime('%H:%M:%S: Done checking feeds!')
191 rescue Exception => e
192 $log.puts "*** thread error"
193 $log.puts "#{e.message}\n#{e.backtrace.join("\n")}"
200 @feeds << Feed.new(uri)
207 # ===================
208 # = Cybot Interface =
209 # ===================
211 class Feed < PluginBase
212 def initialize(*args)
213 @brief_help = 'Feeds the channel with hot news from various RSS channels'
219 # Checks whether the user is allowed to use this plugin
220 # Informs them and returns false if not
222 if !$user.caps(irc, 'phrases', 'op', 'owner').any?
223 irc.reply "You aren't allowed to use this command"
229 # Called for all incoming channel messages
230 # We can’t start the thread before we have the irc object
231 # this method seems to be the quickest way to grab it
232 def hook_privmsg_chan(irc, msg)
234 FeedStuff.run(irc, @filename)
238 def cmd_subscribe(irc, line)
239 return unless authed?(irc)
240 irc.reply("Not yet implemented.")
241 # FeedStuff.add(line)
243 help :subscribe, "Subscribe to an RSS feed."
245 # We want to load in the thread, so we only store the filename
247 @filename = File.expand_path(file_name('feeds.yml'))