]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/rss.rb
636385a5a6afb9591855a989a0d4b22d1e4c82c0
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / rss.rb
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
6 \r
7 require 'rss/parser'\r
8 require 'rss/1.0'\r
9 require 'rss/2.0'\r
10 require 'rss/dublincore'\r
11 begin\r
12   # require 'rss/dublincore/2.0'\r
13 rescue\r
14   warning "Unable to load RSS libraries, RSS plugin functionality crippled"\r
15 end\r
16 \r
17 class ::String\r
18   def shorten(limit)\r
19     if self.length > limit\r
20       self+". " =~ /^(.{#{limit}}[^.!;?]*[.!;?])/mi\r
21       return $1\r
22     end\r
23     self\r
24   end\r
25 \r
26   def riphtml\r
27     self.gsub(/<[^>]+>/, '').gsub(/&amp;/,'&').gsub(/&quot;/,'"').gsub(/&lt;/,'<').gsub(/&gt;/,'>').gsub(/&ellip;/,'...').gsub(/&apos;/, "'").gsub("\n",'')\r
28   end\r
29 \r
30   def mysqlize\r
31     self.gsub(/'/, "''")\r
32   end\r
33 end\r
34 \r
35 class ::RssBlob\r
36   attr :url\r
37   attr :handle\r
38   attr :type\r
39   attr :watchers\r
40 \r
41   def initialize(url,handle=nil,type=nil,watchers=[])\r
42     @url = url\r
43     if handle\r
44       @handle = handle\r
45     else\r
46       @handle = url\r
47     end\r
48     @type = type\r
49     @watchers = watchers\r
50   end\r
51 \r
52   def watched?\r
53     !@watchers.empty?\r
54   end\r
55 \r
56   def watched_by?(who)\r
57     @watchers.include?(who)\r
58   end\r
59 \r
60   def add_watch(who)\r
61     if watched_by?(who)\r
62       return nil\r
63     end\r
64     @watchers << who unless watched_by?(who)\r
65     return who\r
66   end\r
67 \r
68   def rm_watch(who)\r
69     @watchers.delete(who)\r
70   end\r
71 \r
72   #  def to_ary\r
73   #    [@handle,@url,@type,@watchers]\r
74   #  end\r
75 end\r
76 \r
77 class RSSFeedsPlugin < Plugin\r
78   @@watchThreads = Hash.new\r
79   @@mutex = Mutex.new\r
80 \r
81   # Keep a 1:1 relation between commands and handlers\r
82   @@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
91   }\r
92 \r
93   def initialize\r
94     super\r
95     kill_threads\r
96     if @registry.has_key?(:feeds)\r
97       @feeds = @registry[:feeds]\r
98     else\r
99       @feeds = Hash.new\r
100     end\r
101     handle_rewatch\r
102   end\r
103 \r
104   def watchlist\r
105     @feeds.select { |h, f| f.watched? }\r
106   end\r
107 \r
108   def cleanup\r
109     kill_threads\r
110   end\r
111 \r
112   def save\r
113     @registry[:feeds] = @feeds\r
114   end\r
115 \r
116   def kill_threads\r
117     @@mutex.synchronize {\r
118       # Abort all running threads.\r
119       @@watchThreads.each { |url, thread|\r
120         debug "Killing thread for #{url}"\r
121         thread.kill\r
122       }\r
123       # @@watchThreads.each { |url, thread|\r
124       #   debug "Joining on killed thread for #{url}"\r
125       #   thread.join\r
126       # }\r
127       @@watchThreads = Hash.new\r
128     }\r
129   end\r
130 \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
133   end\r
134 \r
135   def report_problem(report, m=nil)\r
136       if m\r
137         m.reply report\r
138       else\r
139         warning report\r
140       end\r
141   end\r
142 \r
143   def privmsg(m)\r
144     meth = self.method(@@handlers[m.plugin])\r
145     meth.call(m)\r
146   end\r
147 \r
148   def handle_rss(m)\r
149     unless m.params\r
150       m.reply("incorrect usage: " + help(m.plugin))\r
151       return\r
152     end\r
153     limit = 5\r
154     if m.params =~ /\s+(\d+)$/\r
155       limit = $1.to_i\r
156       if limit < 1 || limit > 15\r
157         m.reply("weird, limit not in [1..15], reverting to default")\r
158         limit = 5\r
159       end\r
160       m.params.gsub!(/\s+\d+$/, '')\r
161     end\r
162 \r
163     url = ''\r
164     if m.params =~ /^https?:\/\//\r
165       url = m.params\r
166       @@mutex.synchronize {\r
167         @feeds[url] = RssBlob.new(url)\r
168         feed = @feeds[url]\r
169       }\r
170     else\r
171       feed = @feeds.fetch(m.params, nil)\r
172       unless feed\r
173         m.reply(m.params + "? what is that feed about?")\r
174         return\r
175       end\r
176     end\r
177 \r
178     m.reply("Please wait, querying...")\r
179     title = items = nil\r
180     @@mutex.synchronize {\r
181       title, items = fetchRss(feed, m)\r
182     }\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
188     end\r
189   end\r
190 \r
191   def handle_addrss(m)\r
192     unless m.params\r
193       m.reply "incorrect usage: " + help(m.plugin)\r
194       return\r
195     end\r
196     if m.params =~ /^force /\r
197       forced = true\r
198       m.params.gsub!(/^force /, '')\r
199     end\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
203       return\r
204     end\r
205     handle = feed[0]\r
206     handle.gsub!("|", '_')\r
207     url = feed[1]\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
212       return\r
213     end\r
214     @@mutex.synchronize {\r
215       @feeds[handle] = RssBlob.new(url,handle,type)\r
216     }\r
217     reply = "Added RSS #{url} named #{handle}"\r
218     if type\r
219       reply << " (format: #{type})"\r
220     end\r
221     m.reply reply\r
222     return handle\r
223   end\r
224 \r
225   def handle_rmrss(m)\r
226     feed = handle_rmwatch(m, true)\r
227     if feed.watched?\r
228       m.reply "someone else is watching #{feed.handle}, I won't remove it from my list"\r
229       return\r
230     end\r
231     @@mutex.synchronize {\r
232       @feeds.delete(feed.handle)\r
233     }\r
234     m.okay\r
235     return\r
236   end\r
237 \r
238   def handle_rmwatch(m,pass=false)\r
239     unless m.params\r
240       m.reply "incorrect usage: " + help(m.plugin)\r
241       return\r
242     end\r
243     handle = m.params\r
244     unless @feeds.has_key?(handle)\r
245       m.reply("dunno that feed")\r
246       return\r
247     end\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
251     else\r
252       m.reply("#{m.replyto} wasn't watching #{feed.handle}") unless pass\r
253     end\r
254     if !feed.watched?\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
260         end\r
261       }\r
262     end\r
263     return feed\r
264   end\r
265 \r
266   def handle_listrss(m)\r
267     reply = ''\r
268     if @feeds.length == 0\r
269       reply = "No feeds yet."\r
270     else\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
275           reply << "\n"\r
276           debug reply\r
277         }\r
278       }\r
279     end\r
280     m.reply reply\r
281   end\r
282 \r
283   def handle_listrsswatch(m)\r
284     reply = ''\r
285     if watchlist.length == 0\r
286       reply = "No watched feeds yet."\r
287     else\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
290         debug reply\r
291       }\r
292     end\r
293     m.reply reply\r
294   end\r
295 \r
296   def handle_rewatch(m=nil)\r
297     kill_threads\r
298 \r
299     # Read watches from list.\r
300     watchlist.each{ |handle, feed|\r
301       watchRss(feed, m)\r
302     }\r
303     m.okay if m\r
304   end\r
305 \r
306   def handle_watchrss(m)\r
307     unless m.params\r
308       m.reply "incorrect usage: " + help(m.plugin)\r
309       return\r
310     end\r
311     if m.params =~ /\s+/\r
312       handle = handle_addrss(m)\r
313     else\r
314       handle = m.params\r
315     end\r
316     feed = nil\r
317     @@mutex.synchronize {\r
318       feed = @feeds.fetch(handle, nil)\r
319     }\r
320     if feed\r
321       @@mutex.synchronize {\r
322         if feed.add_watch(m.replyto)\r
323           watchRss(feed, m)\r
324           m.okay\r
325         else\r
326           m.reply "Already watching #{feed.handle}"\r
327         end\r
328       }\r
329     else\r
330       m.reply "Couldn't watch feed #{handle} (no such feed found)"\r
331     end\r
332   end\r
333 \r
334   private\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
338       return\r
339     end\r
340     @@watchThreads[feed.handle] = Thread.new do\r
341       debug 'watchRss thread started.'\r
342       oldItems = []\r
343       firstRun = true\r
344       loop do\r
345         begin\r
346           debug 'Fetching rss feed...'\r
347           title = newItems = nil\r
348           @@mutex.synchronize {\r
349             title, newItems = fetchRss(feed)\r
350           }\r
351           unless newItems\r
352             m.reply "no items in feed"\r
353             break\r
354           end\r
355           debug "Checking if new items are available"\r
356           if firstRun\r
357             debug "First run, we'll see next time"\r
358             firstRun = false\r
359           else\r
360             otxt = oldItems.map { |item| item.to_s }\r
361             dispItems = newItems.reject { |item|\r
362               otxt.include?(item.to_s)\r
363             }\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
370                 }\r
371               }\r
372             else\r
373               debug "No new items found"\r
374             end\r
375           end\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
380         end\r
381 \r
382         seconds = 150 + rand(100)\r
383         debug "Thread going to sleep #{seconds} seconds.."\r
384         sleep seconds\r
385       end\r
386     end\r
387   end\r
388 \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
392     else\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
394     end\r
395   end\r
396 \r
397   def printFormattedRss(locs, item, type)\r
398     locs.each { |loc|\r
399       case type\r
400       when 'amarokblog'\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
403       when 'amarokforum'\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
405       when 'mediawiki'\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
408       when "gmame"\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
410       else\r
411         printRssItem(loc,item)\r
412       end\r
413     }\r
414   end\r
415 \r
416   def fetchRss(feed, m=nil)\r
417     begin\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
422       return\r
423     end\r
424     debug 'fetched'\r
425     unless xml\r
426       report_problem("reading feed #{url} failed", m)\r
427       return\r
428     end\r
429 \r
430     begin\r
431       ## do validate parse\r
432       rss = RSS::Parser.parse(xml)\r
433       debug 'parsed'\r
434     rescue RSS::InvalidRSSError\r
435       ## do non validate parse for invalid RSS 1.0\r
436       begin\r
437         rss = RSS::Parser.parse(xml, false)\r
438       rescue RSS::Error\r
439         report_problem("parsing rss stream failed, whoops =(", m)\r
440         return\r
441       end\r
442     rescue RSS::Error\r
443       report_problem("parsing rss stream failed, oioi", m)\r
444       return\r
445     rescue => e\r
446       report_problem("processing error occured, sorry =(", m)\r
447       debug e.inspect\r
448       debug e.backtrace.join("\n")\r
449       return\r
450     end\r
451     items = []\r
452     if rss.nil?\r
453       report_problem("#{feed.url} does not include RSS 1.0 or 0.9x/2.0",m)\r
454     else\r
455       begin\r
456         rss.output_encoding = "euc-jp"\r
457       rescue RSS::UnknownConvertMethod\r
458         report_problem("bah! something went wrong =(",m)\r
459         return\r
460       end\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
465         items << item\r
466       end\r
467     end\r
468 \r
469     if items.empty?\r
470       report_problem("no items found in the feed, maybe try weed?",m)\r
471       return\r
472     end\r
473     return [title, items]\r
474   end\r
475 end\r
476 \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
486 \r