]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/rss.rb
rss: protect against nil field
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / rss.rb
index 8bf59dfc5af74f0744cb8f8c45a38637f947669b..f7e559f7601ed86a10a1e17c26da681e25e812cf 100644 (file)
@@ -16,7 +16,7 @@
 
 require 'rss'
 
-# Try to load rss/content/2.0 so we can access the data in <content:encoded> 
+# Try to load rss/content/2.0 so we can access the data in <content:encoded>
 # tags.
 begin
   require 'rss/content/2.0'
@@ -25,21 +25,6 @@ end
 
 module ::RSS
 
-  # Make an  'unique' ID for a given item, based on appropriate bot options
-  # Currently only suppored is bot.config['rss.show_updated']: when true, the
-  # description is included in the uid hashing, otherwise it's not
-  #
-  def RSS.item_uid_for_bot(item, opts={})
-    options = { :show_updated => true}.merge(opts)
-    desc = nil
-    if options[:show_updated]
-      desc = item.content.content rescue item.description rescue nil
-    end
-    [(item.title.content rescue item.title rescue nil),
-     (item.link.href rescue item.link),
-     desc].hash
-  end
-
   # Add support for Slashdot namespace in RDF. The code is just an adaptation
   # of the DublinCore code.
   unless defined?(SLASH_PREFIX)
@@ -165,12 +150,73 @@ module ::RSS
 
     SlashModel::ELEMENTS.collect! {|name| "#{SLASH_PREFIX}_#{name}"}
   end
+
+  if self.const_defined? :Atom
+    # There are improper Atom feeds around that use the non-standard
+    # 'modified' element instead of the correct 'updated' one. Let's
+    # support it too.
+    module Atom
+      class Feed
+        class Modified < RSS::Element
+          include CommonModel
+          include DateConstruct
+        end
+        __send__("install_have_child_element",
+                 "modified", URI, nil, "modified", :content)
+
+        class Entry
+          Modified = Feed::Modified
+          __send__("install_have_child_element",
+                   "modified", URI, nil, "modified", :content)
+        end
+      end
+    end
+  end
+
+  class Element
+    class << self
+      def def_bang(name, chain)
+        class_eval %<
+          def #{name}!
+            blank2nil { #{chain.join(' rescue ')} rescue nil }
+          end
+        >, *get_file_and_line_from_caller(0)
+      end
+    end
+
+    # Atom categories are squashed to their label only
+    {
+      :link => %w{link.href link},
+      :guid => %w{guid.content guid},
+      :content => %w{content.content content},
+      :description => %w{description.content description},
+      :title => %w{title.content title},
+      :category => %w{category.content category.label category},
+      :dc_subject => %w{dc_subject},
+      :author => %w{author.name.content author.name author},
+      :dc_creator => %w{dc_creator}
+    }.each { |name, chain| def_bang name, chain }
+
+    def categories!
+      return nil unless self.respond_to? :categories
+      cats = categories.map do |c|
+        blank2nil { c.content rescue c.label rescue c rescue nil }
+      end.compact
+      cats.empty? ? nil : cats
+    end
+
+    protected
+    def blank2nil(&block)
+      x = yield
+      (x && !x.empty?) ? x : nil
+    end
+  end
 end
 
 
 class ::RssBlob
   attr_accessor :url, :handle, :type, :refresh_rate, :xml, :title, :items,
-    :mutex, :watchers, :last_fetched
+    :mutex, :watchers, :last_fetched, :http_cache, :last_success
 
   def initialize(url,handle=nil,type=nil,watchers=[], xml=nil, lf = nil)
     @url = url
@@ -182,11 +228,13 @@ class ::RssBlob
     @type = type
     @watchers=[]
     @refresh_rate = nil
+    @http_cache = false
     @xml = xml
     @title = nil
     @items = nil
     @mutex = Mutex.new
     @last_fetched = lf
+    @last_success = nil
     sanitize_watchers(watchers)
   end
 
@@ -261,6 +309,14 @@ class RSSFeedsPlugin < Plugin
     :default => 300, :validate => Proc.new{|v| v > 30},
     :desc => "How many seconds to sleep before checking RSS feeds again")
 
+  Config.register Config::IntegerValue.new('rss.announce_timeout',
+    :default => 0,
+    :desc => "Don't announce watched feed if these many seconds elapsed since the last successful update")
+
+  Config.register Config::IntegerValue.new('rss.announce_max',
+    :default => 3,
+    :desc => "Maximum number of new items to announce when a watched feed is updated")
+
   Config.register Config::BooleanValue.new('rss.show_updated',
     :default => true,
     :desc => "Whether feed items for which the description was changed should be shown as new")
@@ -269,8 +325,28 @@ class RSSFeedsPlugin < Plugin
     :default => true,
     :desc => "Whether to display links from the text of a feed item.")
 
+  Config.register Config::EnumValue.new('rss.announce_method',
+    :values => ['say', 'notice'],
+    :default => 'say',
+    :desc => "Whether to display links from the text of a feed item.")
+
+  # Make an  'unique' ID for a given item, based on appropriate bot options
+  # Currently only supported is bot.config['rss.show_updated']: when false,
+  # only the guid/link is accounted for.
+
+  def make_uid(item)
+    uid = [item.guid! || item.link!]
+    if @bot.config['rss.show_updated']
+      uid.push(item.content! || item.description!)
+      uid.unshift item.title!
+    end
+    # debug "taking hash of #{uid.inspect}"
+    uid.hash
+  end
+
+
   # We used to save the Mutex with the RssBlob, which was idiotic. And
-  # since Mutexes dumped in one version might not be resotrable in another,
+  # since Mutexes dumped in one version might not be restorable in another,
   # we need a few tricks to be able to restore data from other versions of Ruby
   #
   # When migrating 1.8.6 => 1.8.5, all we need to do is define an empty
@@ -286,91 +362,106 @@ class RSSFeedsPlugin < Plugin
 
   # Auxiliary method used to collect two lines for rss output filters,
   # running substitutions against DataStream _s_ optionally joined
-  # with hash _h_
+  # with hash _h_.
+  #
+  # For substitutions, *_wrap keys can be used to alter the content of
+  # other nonempty keys. If the value of *_wrap is a String, it will be
+  # put before and after the corresponding key; if it's an Array, the first
+  # and second elements will be used for wrapping; if it's nil, no wrapping
+  # will be done (useful to override a default wrapping).
+  #
+  # For example:
+  # :handle_wrap => '::'::
+  #   will wrap s[:handle] by prefixing and postfixing it with '::'
+  # :date_wrap => [nil, ' :: ']::
+  #   will put ' :: ' after s[:date]
   def make_stream(line1, line2, s, h={})
     ss = s.merge(h)
-    DataStream.new([line1, line2].compact.join("\n") % ss, ss)
+    subs = {}
+    wraps = {}
+    ss.each do |k, v|
+      kk = k.to_s.chomp!('_wrap')
+      if kk
+        nk = kk.intern
+        case v
+        when String
+          wraps[nk] = ss[nk].wrap_nonempty(v, v)
+        when Array
+          wraps[nk] = ss[nk].wrap_nonempty(*v)
+        when nil
+          # do nothing
+        else
+          warning "ignoring #{v.inspect} wrapping of unknown class"
+        end unless ss[nk].nil?
+      else
+        subs[k] = v
+      end
+    end
+    subs.merge! wraps
+    DataStream.new([line1, line2].compact.join("\n") % subs, ss)
+  end
+
+  # Auxiliary method used to define rss output filters
+  def rss_type(key, &block)
+    @bot.register_filter(key, @outkey, &block)
   end
 
-  # Define default RSS filters
+  # Define default output filters (rss types), and load custom ones.
+  # Custom filters are looked for in the plugin's default filter locations
+  # and in rss/types.rb under botclass.
+  # Preferably, the rss_type method should be used in these files, e.g.:
+  #   rss_type :my_type do |s|
+  #     line1 = "%{handle} and some %{author} info"
+  #     make_stream(line1, nil, s)
+  #   end
+  # to define the new type 'my_type'. The keys available in the DataStream
+  # are:
+  # item::
+  #   the actual rss item
+  # handle::
+  #   the item handle
+  # date::
+  #   the item date
+  # title::
+  #   the item title
+  # desc, link, category, author::
+  #   the item description, link, category, author
+  # at::
+  #   the string ' @ ' if the item has both an title and a link
+  # handle_wrap, date_wrap, title_wrap, ...::
+  #   these keys can be defined to wrap the corresponding elements if they
+  #   are nonempty. By default handle is wrapped with '::', date has a ' ::'
+  #   appended and title is enbolden
   #
-  # TODO: load personal ones
   def define_filters
-    @outkey = :"rss.out"
-    @bot.register_filter(:blog, @outkey) { |s|
-      author = s[:author] ? (s[:author] + " ") : ""
-      abt = s[:category] ? "about #{s[:category]} " : ""
-      line1 = "%{handle}%{date}%{author}blogged %{abt}at %{link}"
-      line2 = "%{handle}%{title} - %{desc}"
-      make_stream(line1, line2, s, :author => author, :abt => abt)
-    }
-    @bot.register_filter(:photoblog, @outkey) { |s|
-      author = s[:author] ? (s[:author] + " ") : ""
-      abt = s[:category] ? "under #{s[:category]} " : ""
-      line1 = "%{handle}%{date}%{author}added an image %{abt}at %{link}"
-      line2 = "%{handle}%{title} - %{desc}"
-      make_stream(line1, line2, s, :author => author, :abt => abt)
-    }
-    @bot.register_filter(:news, @outkey) { |s|
-      line1 = "%{handle}%{date}%{title} @ %{link}" % s
-      line2 = "%{handle}%{date}%{desc}" % s
-      make_stream(line1, line2, s)
-    }
-    @bot.register_filter(:git, @outkey) { |s|
-      author = s[:author] ? (s[:author] + " ") : ""
-      line1 = "%{handle}%{date}%{author}committed %{title} @ %{link}"
-      make_stream(line1, nil, s, :author => author)
-    }
-    @bot.register_filter(:forum, @outkey) { |s|
-      line1 = "%{handle}%{date}%{title}%{at}%{link}"
-      make_stream(line1, nil, s)
-    }
-    @bot.register_filter(:wiki, @outkey) { |s|
-      line1 = "%{handle}%{date}%{title}%{at}%{link}"
-      line1 << "has been edited by %{author}. %{desc}"
-      make_stream(line1, nil, s)
-    }
-    @bot.register_filter(:gmane, @outkey) { |s|
-      line1 = "%{handle}%{date}Message %{title} sent by %{author}. %{desc}"
-      make_stream(line1, nil, s)
-    }
-    @bot.register_filter(:trac, @outkey) { |s|
-      author = s[:author].sub(/@\S+?\s*>/, "@...>") + ": " if s[:author]
-      line1 = "%{handle}%{date}%{author}%{title} @ %{link}"
-      line2 = nil
-      unless s[:item].title =~ /^(?:Changeset \[(?:[\da-f]+)\]|\(git commit\))/
-        line2 = "%{handle}%{date}%{desc}"
-      end
-      make_stream(line1, line2, s, :author => author)
-    }
-    @bot.register_filter(:"/.", @outkey) { |s|
-      dept = "(from the #{s[:item].slash_department} dept) " rescue nil
-      sec = " in section #{s[:item].slash_section}" rescue nil
-      line1 = "%{handle}%{date}%{dept}%{title}%{at}%{link} "
-      line1 << "(posted by %{author}%{sec})"
-      make_stream(line1, nil, s, :dept => dept, :sec => sec)
-    }
-    @bot.register_filter(:default, @outkey) { |s|
-      line1 = "%{handle}%{date}%{title}%{at}%{link}"
-      line1 << " (by %{author})" if s[:author]
-      make_stream(line1, nil, s)
-    }
+    @outkey ||= :"rss.out"
 
-    # Define an HTML info filter too
+    # Define an HTML info filter
     @bot.register_filter(:rss, :htmlinfo) { |s| htmlinfo_filter(s) }
-
     # This is the output format used by the input filter
-    @bot.register_filter(:htmlinfo, @outkey) { |s|
+    rss_type :htmlinfo do |s|
       line1 = "%{title}%{at}%{link}"
       make_stream(line1, nil, s)
-    }
+    end
+
+    # the default filter
+    rss_type :default do |s|
+      line1 = "%{handle}%{date}%{title}%{at}%{link}"
+      line1 << " (by %{author})" if s[:author]
+      make_stream(line1, nil, s)
+    end
+
+    @user_types ||= datafile 'types.rb'
+    load_filters
+    load_filters :path => @user_types
   end
 
   FEED_NS = %r{xmlns.*http://(purl\.org/rss|www.w3c.org/1999/02/22-rdf)}
   def htmlinfo_filter(s)
     return nil unless s[:headers] and s[:headers]['x-rbot-location']
     return nil unless s[:headers]['content-type'].first.match(/xml|rss|atom|rdf/i) or
-      s[:text].include?("<rdf:RDF") or s[:text].include?("<rss") or s[:text].include?("<feed") or
+      (s[:text].include?("<rdf:RDF") and s[:text].include?("<channel")) or
+      s[:text].include?("<rss") or s[:text].include?("<feed") or
       s[:text].match(FEED_NS)
     blob = RssBlob.new(s[:headers]['x-rbot-location'],"", :htmlinfo)
     unless (fetchRss(blob, nil) and parseRss(blob, nil) rescue nil)
@@ -423,7 +514,7 @@ class RSSFeedsPlugin < Plugin
       }
 
       @feeds = @registry[:feeds]
-      raise unless @feeds
+      raise LoadError, "corrupted feed database" unless @feeds
 
       @registry.recovery = nil
 
@@ -513,7 +604,7 @@ class RSSFeedsPlugin < Plugin
     when "rewatch"
       "rss rewatch : restart threads that watch for changes in watched rss"
     when "types"
-      "rss types : show the rss types for which an output format existi (all other types will use the default one)"
+      "rss types : show the rss types for which an output format exist (all other types will use the default one)"
     else
       "manage RSS feeds: rss types|show|list|watched|add|change|del(ete)|rm|(force)replace|watch|unwatch|rmwatch|rewatch|who watches"
     end
@@ -573,13 +664,12 @@ class RSSFeedsPlugin < Plugin
       fetched = fetchRss(feed, m, false)
     end
     return unless fetched or feed.xml
-    if not fetched and feed.items
-      m.reply "using old data"
-    else
+    if fetched or not feed.items
       parsed = parseRss(feed, m)
-      m.reply "using old data" unless parsed
     end
     return unless feed.items
+    m.reply "using old data" unless fetched and parsed and parsed > 0
+
     title = feed.title
     items = feed.items
 
@@ -590,7 +680,12 @@ class RSSFeedsPlugin < Plugin
 
     m.reply "Channel : #{title}"
     disp.each do |item|
-      printFormattedRss(feed, item, {:places=>[m.replyto],:handle=>nil,:date=>true})
+      printFormattedRss(feed, item, {
+        :places => [m.replyto],
+        :handle => nil,
+        :date => true,
+        :announce_method => :say
+      })
     end
   end
 
@@ -609,19 +704,34 @@ class RSSFeedsPlugin < Plugin
 
   def list_rss(m, params)
     wanted = params[:handle]
-    reply = String.new
-    @feeds.each { |handle, feed|
-      next if wanted and !handle.match(/#{wanted}/i)
-      reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
-      (reply << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate
-      (reply << " (watched)") if feed.watched_by?(m.replyto)
-      reply << "\n"
-    }
-    if reply.empty?
+    listed = @feeds.keys
+    if wanted
+      wanted_rx = Regexp.new(wanted, true)
+      listed.reject! { |handle| !handle.match(wanted_rx) }
+    end
+    listed.sort!
+    debug listed
+    if @bot.config['send.max_lines'] > 0 and listed.size > @bot.config['send.max_lines']
+      reply = listed.inject([]) do |ar, handle|
+        feed = @feeds[handle]
+        string = handle.dup
+        (string << " (#{feed.type})") if feed.type
+        (string << " (watched)") if feed.watched_by?(m.replyto)
+        ar << string
+      end.join(', ')
+    elsif listed.size > 0
+      reply = listed.inject([]) do |ar, handle|
+        feed = @feeds[handle]
+        string = "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
+        (string << " refreshing every #{Utils.secs_to_string(feed.refresh_rate)}") if feed.refresh_rate
+        (string << " (watched)") if feed.watched_by?(m.replyto)
+        ar << string
+      end.join("\n")
+    else
       reply = "no feeds found"
       reply << " matching #{wanted}" if wanted
     end
-    m.reply reply, :max_lines => reply.length
+    m.reply reply, :max_lines => 0
   end
 
   def watched_rss(m, params)
@@ -693,15 +803,28 @@ class RSSFeedsPlugin < Plugin
     end
     case params[:what].intern
     when :handle
-      new = params[:new].downcase
-      if @feeds.key?(new) and @feeds[new]
+      # preserve rename case, but beware of key
+      realnew = params[:new]
+      new = realnew.downcase
+      if feed.handle.downcase == new
+        if feed.handle == realnew
+          m.reply _("You want me to rename %{handle} to itself?") % {
+            :handle => feed.handle
+          }
+          return false
+        else
+          feed.mutex.synchronize do
+            feed.handle = realnew
+          end
+        end
+      elsif @feeds.key?(new) and @feeds[new]
         m.reply "There already is a feed with handle #{new}"
         return
       else
         feed.mutex.synchronize do
           @feeds[new] = feed
           @feeds.delete(handle)
-          feed.handle = new
+          feed.handle = realnew
         end
         handle = new
       end
@@ -802,7 +925,8 @@ class RSSFeedsPlugin < Plugin
     if params and handle = params[:handle]
       feed = @feeds.fetch(handle.downcase, nil)
       if feed
-        @bot.timer.reschedule(@watch[feed.handle], 0)
+        feed.http_cache = false
+        @bot.timer.reschedule(@watch[feed.handle], (params[:delay] || 0).to_f)
         m.okay if m
       else
         m.reply _("no such feed %{handle}") % { :handle => handle } if m
@@ -811,8 +935,8 @@ class RSSFeedsPlugin < Plugin
       stop_watches
 
       # Read watches from list.
-      watchlist.each{ |handle, feed|
-        watchRss(feed, m)
+      watchlist.each{ |hndl, fd|
+        watchRss(fd, m)
       }
       m.okay if m
     end
@@ -821,7 +945,7 @@ class RSSFeedsPlugin < Plugin
   private
   def watchRss(feed, m=nil)
     if @watch.has_key?(feed.handle)
-      report_problem("watcher thread for #{feed.handle} is already running", nil, m)
+      report_problem("watcher thread for #{feed.handle} is already running", nil, m)
       return
     end
     status = Hash.new
@@ -837,11 +961,18 @@ class RSSFeedsPlugin < Plugin
       failures = status[:failures]
       begin
         debug "fetching #{feed}"
-        first_run = !feed.last_fetched
+
+        first_run = !feed.last_success
+        if (!first_run && @bot.config['rss.announce_timeout'] > 0 &&
+           (Time.now - feed.last_success > @bot.config['rss.announce_timeout']))
+          debug "#{feed} wasn't polled for too long, supressing output"
+          first_run = true
+        end
         oldxml = feed.xml ? feed.xml.dup : nil
-        unless fetchRss(feed)
+        unless fetchRss(feed, nil, feed.http_cache)
           failures += 1
         else
+          feed.http_cache = true
           if first_run
             debug "first run for #{feed}, getting items"
             parseRss(feed)
@@ -849,27 +980,27 @@ class RSSFeedsPlugin < Plugin
             debug "xml for #{feed} didn't change"
             failures -= 1 if failures > 0
           else
-            if not feed.items
-              debug "no previous items in feed #{feed}"
-              parseRss(feed)
-              failures -= 1 if failures > 0
-            else
-              # This one is used for debugging
-              otxt = []
+            # This one is used for debugging
+            otxt = []
 
+            if feed.items.nil?
+              oids = []
+            else
               # These are used for checking new items vs old ones
-              uid_opts = { :show_updated => @bot.config['rss.show_updated'] }
               oids = Set.new feed.items.map { |item|
-                uid = RSS.item_uid_for_bot(item, uid_opts)
+                uid = make_uid item
                 otxt << item.to_s
                 debug [uid, item].inspect
                 debug [uid, otxt.last].inspect
                 uid
               }
+            end
 
-              unless parseRss(feed)
-                debug "no items in feed #{feed}"
+              nitems = parseRss(feed)
+              if nitems.nil?
                 failures += 1
+              elsif nitems == 0
+                debug "no items in feed #{feed}"
               else
                 debug "Checking if new items are available for #{feed}"
                 failures -= 1 if failures > 0
@@ -879,7 +1010,7 @@ class RSSFeedsPlugin < Plugin
                 # debug feed.xml
 
                 dispItems = feed.items.reject { |item|
-                  uid = RSS.item_uid_for_bot(item, uid_opts)
+                  uid = make_uid item
                   txt = item.to_s
                   if oids.include?(uid)
                     debug "rejecting old #{uid} #{item.inspect}"
@@ -894,7 +1025,19 @@ class RSSFeedsPlugin < Plugin
                 }
 
                 if dispItems.length > 0
+                  max = @bot.config['rss.announce_max']
                   debug "Found #{dispItems.length} new items in #{feed}"
+                  if max > 0 and dispItems.length > max
+                    debug "showing only the latest #{dispItems.length}"
+                    feed.watchers.each do |loc|
+                      @bot.say loc, (_("feed %{feed} had %{num} updates, showing the latest %{max}") % {
+                        :feed => feed.handle,
+                        :num => dispItems.length,
+                        :max => max
+                      })
+                    end
+                    dispItems.slice!(max..-1)
+                  end
                   # When displaying watched feeds, publish them from older to newer
                   dispItems.reverse.each { |item|
                     printFormattedRss(feed, item)
@@ -903,7 +1046,6 @@ class RSSFeedsPlugin < Plugin
                   debug "No new items found in #{feed}"
                 end
               end
-            end
           end
         end
       rescue Exception => e
@@ -935,50 +1077,45 @@ class RSSFeedsPlugin < Plugin
       return seconds
   end
 
-  def select_nonempty(*ar)
-    debug ar
-    ret = ar.map { |i| (i && i.empty?) ? nil : i }.compact.first
-    (ret && ret.empty?) ? nil : ret
+  def make_date(obj)
+    if obj.kind_of? Time
+      obj.strftime("%Y/%m/%d %H:%M")
+    else
+      obj.to_s
+    end
   end
 
-  def printFormattedRss(feed, item, opts=nil)
-    debug item
-    places = feed.watchers
-    handle = feed.handle.empty? ? "" : "::#{feed.handle}:: "
-    date = String.new
-    if opts
-      places = opts[:places] if opts.key?(:places)
-      handle = opts[:handle].to_s if opts.key?(:handle)
-      if opts.key?(:date) && opts[:date]
-        if item.respond_to?(:updated)
-          if item.updated.content.class <= Time
-            date = item.updated.content.strftime("%Y/%m/%d %H:%M")
-          else
-            date = item.updated.content.to_s
-          end
-        elsif item.respond_to?(:source) and item.source.respond_to?(:updated)
-          if item.source.updated.content.class <= Time
-            date = item.source.updated.content.strftime("%Y/%m/%d %H:%M")
-          else
-            date = item.source.updated.content.to_s
-          end
-        elsif item.respond_to?(:pubDate) 
-          if item.pubDate.class <= Time
-            date = item.pubDate.strftime("%Y/%m/%d %H:%M")
-          else
-            date = item.pubDate.to_s
-          end
-        elsif item.respond_to?(:date)
-          if item.date.class <= Time
-            date = item.date.strftime("%Y/%m/%d %H:%M")
-          else
-            date = item.date.to_s
-          end
-        else
-          date = "(no date)"
-        end
-        date += " :: "
+  def printFormattedRss(feed, item, options={})
+    # debug item
+    opts = {
+      :places => feed.watchers,
+      :handle => feed.handle,
+      :date => false,
+      :announce_method => @bot.config['rss.announce_method']
+    }.merge options
+
+    places = opts[:places]
+    announce_method = opts[:announce_method]
+
+    handle = opts[:handle].to_s
+
+    date = \
+    if opts[:date]
+      if item.respond_to?(:updated) and item.updated
+        make_date(item.updated.content)
+      elsif item.respond_to?(:modified) and item.modified
+        make_date(item.modified.content)
+      elsif item.respond_to?(:source) and item.source.respond_to?(:updated)
+        make_date(item.source.updated.content)
+      elsif item.respond_to?(:pubDate)
+        make_date(item.pubDate)
+      elsif item.respond_to?(:date)
+        make_date(item.date)
+      else
+        "(no date)"
       end
+    else
+      String.new
     end
 
     tit_opt = {}
@@ -992,14 +1129,14 @@ class RSSFeedsPlugin < Plugin
       # visible in the URL anyway
       # TODO make this optional?
       base_title.sub!(/^Changeset \[([\da-f]{40})\]:/) { |c| "(git commit)"} if feed.type == 'trac'
-      title = "#{Bold}#{base_title.ircify_html(tit_opt)}#{Bold}"
+      title = base_title.ircify_html(tit_opt)
     end
 
     desc_opt = {}
     desc_opt[:limit] = @bot.config['rss.text_max']
     desc_opt[:a_href] = :link_out if @bot.config['rss.show_links']
 
-    # We prefer content_encoded here as it tends to provide more html formatting 
+    # We prefer content_encoded here as it tends to provide more html formatting
     # for use with ircify_html.
     if item.respond_to?(:content_encoded) && item.content_encoded
       desc = item.content_encoded.ircify_html(desc_opt)
@@ -1018,10 +1155,14 @@ class RSSFeedsPlugin < Plugin
       desc = "(?)"
     end
 
-    link = item.link.href rescue item.link.chomp rescue nil
+    link = item.link!
+    link.strip! if link
 
-    category = select_nonempty((item.category.content rescue nil), (item.dc_subject rescue nil))
-    author = select_nonempty((item.author.name.content rescue nil), (item.dc_creator rescue nil), (item.author rescue nil))
+    categories = item.categories!
+    category = item.category! || item.dc_subject!
+    category.strip! if category
+    author = item.dc_creator! || item.author!
+    author.strip! if author
 
     line1 = nil
     line2 = nil
@@ -1031,15 +1172,25 @@ class RSSFeedsPlugin < Plugin
     key = @bot.global_filter_name(feed.type, @outkey)
     key = @bot.global_filter_name(:default, @outkey) unless @bot.has_filter?(key)
 
-    output = @bot.filter(key, :item => item, :handle => handle, :date => date,
-                         :title => title, :desc => desc, :link => link,
-                         :category => category, :author => author, :at => at)
+    stream_hash = {
+      :item => item,
+      :handle => handle,
+      :handle_wrap => ['::', ':: '],
+      :date => date,
+      :date_wrap => [nil, ' :: '],
+      :title => title,
+      :title_wrap => Bold,
+      :desc => desc, :link => link,
+      :categories => categories,
+      :category => category, :author => author, :at => at
+    }
+    output = @bot.filter(key, stream_hash)
 
     return output if places.empty?
 
     places.each { |loc|
       output.to_s.each_line { |line|
-        @bot.say loc, line, :overlong => :truncate
+        @bot.__send__(announce_method, loc, line, :overlong => :truncate)
       }
     }
   end
@@ -1069,8 +1220,16 @@ class RSSFeedsPlugin < Plugin
     # reassign the 0.9 RDFs to 1.0, and hope it goes right.
     xml.gsub!("xmlns=\"http://my.netscape.com/rdf/simple/0.9/\"",
               "xmlns=\"http://purl.org/rss/1.0/\"")
+    # make sure the parser doesn't double-convert in case the feed is not UTF-8
+    xml.sub!(/<\?xml (.*?)\?>/) do |match|
+      if /\bencoding=(['"])(.*?)\1/.match(match)
+        match.sub!(/\bencoding=(['"])(?:.*?)\1/,'encoding="UTF-8"')
+      end
+      match
+    end
     feed.mutex.synchronize do
       feed.xml = xml
+      feed.last_success = Time.now
     end
     return true
   end
@@ -1079,29 +1238,42 @@ class RSSFeedsPlugin < Plugin
     return nil unless feed.xml
     feed.mutex.synchronize do
       xml = feed.xml
-      begin
-        ## do validate parse
-        rss = RSS::Parser.parse(xml)
-        debug "parsed and validated #{feed}"
-      rescue RSS::InvalidRSSError
-        ## do non validate parse for invalid RSS 1.0
+      rss = nil
+      errors = []
+      RSS::AVAILABLE_PARSERS.each do |parser|
         begin
-          rss = RSS::Parser.parse(xml, false)
-          debug "parsed but not validated #{feed}"
+          ## do validate parse
+          rss = RSS::Parser.parse(xml, true, true, parser)
+          debug "parsed and validated #{feed} with #{parser}"
+          break
+        rescue RSS::InvalidRSSError
+          begin
+            ## do non validate parse for invalid RSS 1.0
+            rss = RSS::Parser.parse(xml, false, true, parser)
+            debug "parsed but not validated #{feed} with #{parser}"
+            break
+          rescue RSS::Error => e
+            errors << [parser, e, "parsing rss stream failed, whoops =("]
+          end
         rescue RSS::Error => e
-          report_problem("parsing rss stream failed, whoops =(", e, m)
-          return nil
+          errors << [parser, e, "parsing rss stream failed, oioi"]
+        rescue => e
+          errors << [parser, e, "processing error occured, sorry =("]
         end
-      rescue RSS::Error => e
-        report_problem("parsing rss stream failed, oioi", e, m)
-        return nil
-      rescue => e
-        report_problem("processing error occured, sorry =(", e, m)
-        return nil
+      end
+      unless errors.empty?
+        debug errors
+        self.send(:report_problem, errors.last[2], errors.last[1], m)
+        return nil unless rss
       end
       items = []
       if rss.nil?
-        report_problem("#{feed} does not include RSS 1.0 or 0.9x/2.0", nil, m)
+        if xml.match(/xmlns\s*=\s*(['"])http:\/\/www.w3.org\/2005\/Atom\1/) and not defined?(RSS::Atom)
+          report_problem("#{feed.handle} @ #{feed.url} looks like an Atom feed, but your Ruby/RSS library doesn't seem to support it. Consider getting the latest version from http://raa.ruby-lang.org/project/rss/", nil, m)
+        else
+          report_problem("#{feed.handle} @ #{feed.url} doesn't seem to contain an RSS or Atom feed I can read", nil, m)
+        end
+        return nil
       else
         begin
           rss.output_encoding = 'UTF-8'
@@ -1123,11 +1295,11 @@ class RSSFeedsPlugin < Plugin
 
       if items.empty?
         report_problem("no items found in the feed, maybe try weed?", e, m)
-        return nil
+      else
+        feed.title = title.strip
+        feed.items = items
       end
-      feed.title = title
-      feed.items = items
-      return true
+      return items.length
     end
   end
 end
@@ -1189,7 +1361,7 @@ plugin.map 'rss unwatch :handle [in :chan]',
   :action => 'unwatch_rss'
 plugin.map 'rss rmwatch :handle [in :chan]',
   :action => 'unwatch_rss'
-plugin.map 'rss rewatch [:handle]',
+plugin.map 'rss rewatch [:handle] [:delay]',
   :action => 'rewatch_rss'
 plugin.map 'rss types',
   :action => 'rss_types'