]> 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 de7678a81fee31ac62f444bbaa27f57664458ae1..f7e559f7601ed86a10a1e17c26da681e25e812cf 100644 (file)
@@ -151,6 +151,28 @@ 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)
@@ -162,18 +184,27 @@ module ::RSS
       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},
+      :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
@@ -294,8 +325,13 @@ 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 suppored is bot.config['rss.show_updated']: when false,
+  # Currently only supported is bot.config['rss.show_updated']: when false,
   # only the guid/link is accounted for.
 
   def make_uid(item)
@@ -326,10 +362,43 @@ 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
@@ -339,13 +408,31 @@ class RSSFeedsPlugin < Plugin
 
   # 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 under botclass.
+  # 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'
+  # 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
+  #
   def define_filters
     @outkey ||= :"rss.out"
 
@@ -593,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
 
@@ -711,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
@@ -830,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
@@ -858,7 +963,7 @@ class RSSFeedsPlugin < Plugin
         debug "fetching #{feed}"
 
         first_run = !feed.last_success
-        if (@bot.config['rss.announce_timeout'] > 0 &&
+        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
@@ -972,44 +1077,45 @@ class RSSFeedsPlugin < Plugin
       return seconds
   end
 
-  def printFormattedRss(feed, item, opts=nil)
+  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, options={})
     # 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 += " :: "
+    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 = {}
@@ -1023,7 +1129,7 @@ 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 = {}
@@ -1052,6 +1158,7 @@ class RSSFeedsPlugin < Plugin
     link = item.link!
     link.strip! if link
 
+    categories = item.categories!
     category = item.category! || item.dc_subject!
     category.strip! if category
     author = item.dc_creator! || item.author!
@@ -1065,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