1 # RSS feed plugin for RubyBot
\r
2 # (c) 2004 Stanislav Karchebny <berkus@madfire.net>
\r
3 # (c) 2005 Ian Monroe <ian@monroe.nu>
\r
4 # (c) 2005 Mark Kretschmann <markey@web.de>
\r
5 # Licensed under MIT License.
\r
10 require 'rss/dublincore'
\r
12 # require 'rss/dublincore/2.0'
\r
14 warning "Unable to load RSS libraries, RSS plugin functionality crippled"
\r
19 if self.length > limit
\r
20 self+". " =~ /^(.{#{limit}}[^.!;?]*[.!;?])/mi
\r
27 self.gsub(/<[^>]+>/, '').gsub(/&/,'&').gsub(/"/,'"').gsub(/</,'<').gsub(/>/,'>').gsub(/&ellip;/,'...').gsub(/'/, "'").gsub("\n",'')
\r
31 self.gsub(/'/, "''")
\r
41 def initialize(url,handle=nil,type=nil,watchers=[])
\r
49 @watchers = watchers
\r
56 def watched_by?(who)
\r
57 @watchers.include?(who)
\r
64 @watchers << who unless watched_by?(who)
\r
69 @watchers.delete(who)
\r
73 # [@handle,@url,@type,@watchers]
\r
77 class RSSFeedsPlugin < Plugin
\r
78 @@watchThreads = Hash.new
\r
81 # Keep a 1:1 relation between commands and handlers
\r
83 "rss" => "handle_rss",
\r
84 "addrss" => "handle_addrss",
\r
85 "rmrss" => "handle_rmrss",
\r
86 "rmwatch" => "handle_rmwatch",
\r
87 "listrss" => "handle_listrss",
\r
88 "listwatches" => "handle_listrsswatch",
\r
89 "rewatch" => "handle_rewatch",
\r
90 "watchrss" => "handle_watchrss",
\r
96 if @registry.has_key?(:feeds)
\r
97 @feeds = @registry[:feeds]
\r
105 @feeds.select { |h, f| f.watched? }
\r
113 @registry[:feeds] = @feeds
\r
117 @@mutex.synchronize {
\r
118 # Abort all running threads.
\r
119 @@watchThreads.each { |url, thread|
\r
120 debug "Killing thread for #{url}"
\r
123 # @@watchThreads.each { |url, thread|
\r
124 # debug "Joining on killed thread for #{url}"
\r
127 @@watchThreads = Hash.new
\r
131 def help(plugin,topic="")
\r
132 "RSS Reader: rss name [limit] => read a named feed [limit maximum posts, default 5], addrss [force] name url => add a feed, listrss => list all available feeds, rmrss name => remove the named feed, watchrss url [type] => watch a rss feed for changes (type may be 'amarokblog', 'amarokforum', 'mediawiki', 'gmame' or empty - it defines special formatting of feed items), rewatch => restart all rss watches, rmwatch url => stop watching for changes in url, listwatches => see a list of watched feeds"
\r
135 def report_problem(report, m=nil)
\r
144 meth = self.method(@@handlers[m.plugin])
\r
150 m.reply("incorrect usage: " + help(m.plugin))
\r
154 if m.params =~ /\s+(\d+)$/
\r
156 if limit < 1 || limit > 15
\r
157 m.reply("weird, limit not in [1..15], reverting to default")
\r
160 m.params.gsub!(/\s+\d+$/, '')
\r
164 if m.params =~ /^https?:\/\//
\r
166 @@mutex.synchronize {
\r
167 @feeds[url] = RssBlob.new(url)
\r
171 feed = @feeds.fetch(m.params, nil)
\r
173 m.reply(m.params + "? what is that feed about?")
\r
178 m.reply("Please wait, querying...")
\r
179 title = items = nil
\r
180 @@mutex.synchronize {
\r
181 title, items = fetchRss(feed, m)
\r
183 return unless items
\r
184 m.reply("Channel : #{title}")
\r
185 # TODO: optional by-date sorting if dates present
\r
186 items[0...limit].each do |item|
\r
187 printRssItem(m.replyto,item)
\r
191 def handle_addrss(m)
\r
193 m.reply "incorrect usage: " + help(m.plugin)
\r
196 if m.params =~ /^force /
\r
198 m.params.gsub!(/^force /, '')
\r
200 feed = m.params.scan(/\S+/)
\r
201 if feed.nil? or feed.length < 2
\r
202 m.reply("incorrect usage: " + help(m.plugin))
\r
206 handle.gsub!("|", '_')
\r
208 type = feed[2] || nil
\r
209 debug "Handle: #{handle.inspect}, Url: #{url.inspect}, Type: #{type.inspect}"
\r
210 if @feeds.fetch(handle, nil) && !forced
\r
211 m.reply("But there is already a feed named #{handle} with url #{@feeds[handle].url}")
\r
214 @@mutex.synchronize {
\r
215 @feeds[handle] = RssBlob.new(url,handle,type)
\r
217 reply = "Added RSS #{url} named #{handle}"
\r
219 reply << " (format: #{type})"
\r
225 def handle_rmrss(m)
\r
226 feed = handle_rmwatch(m, true)
\r
228 m.reply "someone else is watching #{feed.handle}, I won't remove it from my list"
\r
231 @@mutex.synchronize {
\r
232 @feeds.delete(feed.handle)
\r
238 def handle_rmwatch(m,pass=false)
\r
240 m.reply "incorrect usage: " + help(m.plugin)
\r
244 unless @feeds.has_key?(handle)
\r
245 m.reply("dunno that feed")
\r
248 feed = @feeds[handle]
\r
249 if feed.rm_watch(m.replyto)
\r
250 m.reply "#{m.replyto} has been removed from the watchlist for #{feed.handle}"
\r
252 m.reply("#{m.replyto} wasn't watching #{feed.handle}") unless pass
\r
255 @@mutex.synchronize {
\r
256 if @@watchThreads[handle].kind_of? Thread
\r
257 @@watchThreads[handle].kill
\r
258 debug "rmwatch: Killed thread for #{handle}"
\r
259 @@watchThreads.delete(handle)
\r
266 def handle_listrss(m)
\r
268 if @feeds.length == 0
\r
269 reply = "No feeds yet."
\r
271 @@mutex.synchronize {
\r
272 @feeds.each { |handle, feed|
\r
273 reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"
\r
274 (reply << " (watched)") if feed.watched_by?(m.replyto)
\r
283 def handle_listrsswatch(m)
\r
285 if watchlist.length == 0
\r
286 reply = "No watched feeds yet."
\r
288 watchlist.each { |handle, feed|
\r
289 (reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})\n") if feed.watched_by?(m.replyto)
\r
296 def handle_rewatch(m=nil)
\r
299 # Read watches from list.
\r
300 watchlist.each{ |handle, feed|
\r
306 def handle_watchrss(m)
\r
308 m.reply "incorrect usage: " + help(m.plugin)
\r
311 if m.params =~ /\s+/
\r
312 handle = handle_addrss(m)
\r
317 @@mutex.synchronize {
\r
318 feed = @feeds.fetch(handle, nil)
\r
321 @@mutex.synchronize {
\r
322 if feed.add_watch(m.replyto)
\r
326 m.reply "Already watching #{feed.handle}"
\r
330 m.reply "Couldn't watch feed #{handle} (no such feed found)"
\r
335 def watchRss(feed, m=nil)
\r
336 if @@watchThreads.has_key?(feed.handle)
\r
337 report_problem("watcher thread for #{feed.handle} is already running", m)
\r
340 @@watchThreads[feed.handle] = Thread.new do
\r
341 debug 'watchRss thread started.'
\r
346 debug 'Fetching rss feed...'
\r
347 title = newItems = nil
\r
348 @@mutex.synchronize {
\r
349 title, newItems = fetchRss(feed)
\r
352 m.reply "no items in feed"
\r
355 debug "Checking if new items are available"
\r
357 debug "First run, we'll see next time"
\r
360 otxt = oldItems.map { |item| item.to_s }
\r
361 dispItems = newItems.reject { |item|
\r
362 otxt.include?(item.to_s)
\r
364 if dispItems.length > 0
\r
365 debug "Found #{dispItems.length} new items"
\r
366 dispItems.each { |item|
\r
367 debug "showing #{item.title}"
\r
368 @@mutex.synchronize {
\r
369 printFormattedRss(feed.watchers, item, feed.type)
\r
373 debug "No new items found"
\r
376 oldItems = newItems.dup
\r
377 rescue Exception => e
\r
378 error "IO failed: #{e.inspect}"
\r
379 debug e.backtrace.join("\n")
\r
382 seconds = 150 + rand(100)
\r
383 debug "Thread going to sleep #{seconds} seconds.."
\r
389 def printRssItem(loc,item)
\r
390 if item.kind_of?(RSS::RDF::Item)
\r
391 @bot.say loc, item.title.chomp.riphtml.shorten(20) + " @ " + item.link
\r
393 @bot.say loc, "#{item.pubDate.to_s.chomp+": " if item.pubDate}#{item.title.chomp.riphtml.shorten(20)+" :: " if item.title}#{" @ "+item.link.chomp if item.link}"
\r
397 def printFormattedRss(locs, item, type)
\r
401 @bot.say loc, "::#{item.category.content} just blogged at #{item.link}::"
\r
402 @bot.say loc, "::#{item.title.chomp.riphtml} - #{item.description.chomp.riphtml.shorten(60)}::"
\r
404 @bot.say loc, "::Forum:: #{item.pubDate.to_s.chomp+": " if item.pubDate}#{item.title.chomp.riphtml+" :: " if item.title}#{" @ "+item.link.chomp if item.link}"
\r
406 @bot.say loc, "::Wiki:: #{item.title} has been edited by #{item.dc_creator}. #{item.description.split("\n")[0].chomp.riphtml.shorten(60)} #{item.link} ::"
\r
407 debug "mediawiki #{item.title}"
\r
409 @bot.say loc, "::amarok-devel:: Message #{item.title} sent by #{item.dc_creator}. #{item.description.split("\n")[0].chomp.riphtml.shorten(60)}::"
\r
411 printRssItem(loc,item)
\r
416 def fetchRss(feed, m=nil)
\r
418 # Use 60 sec timeout, cause the default is too low
\r
419 xml = @bot.httputil.get_cached(feed.url,60,60)
\r
420 rescue URI::InvalidURIError, URI::BadURIError => e
\r
421 report_problem("invalid rss feed #{feed.url}", m)
\r
426 report_problem("reading feed #{url} failed", m)
\r
431 ## do validate parse
\r
432 rss = RSS::Parser.parse(xml)
\r
434 rescue RSS::InvalidRSSError
\r
435 ## do non validate parse for invalid RSS 1.0
\r
437 rss = RSS::Parser.parse(xml, false)
\r
439 report_problem("parsing rss stream failed, whoops =(", m)
\r
443 report_problem("parsing rss stream failed, oioi", m)
\r
446 report_problem("processing error occured, sorry =(", m)
\r
448 debug e.backtrace.join("\n")
\r
453 report_problem("#{feed.url} does not include RSS 1.0 or 0.9x/2.0",m)
\r
456 rss.output_encoding = "euc-jp"
\r
457 rescue RSS::UnknownConvertMethod
\r
458 report_problem("bah! something went wrong =(",m)
\r
461 rss.channel.title ||= "Unknown"
\r
462 title = rss.channel.title
\r
463 rss.items.each do |item|
\r
464 item.title ||= "Unknown"
\r
470 report_problem("no items found in the feed, maybe try weed?",m)
\r
473 return [title, items]
\r
477 plugin = RSSFeedsPlugin.new
\r
478 plugin.register("rss")
\r
479 plugin.register("addrss")
\r
480 plugin.register("rmrss")
\r
481 plugin.register("rmwatch")
\r
482 plugin.register("listrss")
\r
483 plugin.register("rewatch")
\r
484 plugin.register("watchrss")
\r
485 plugin.register("listwatches")
\r