]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/rss.rb
Stupid upper/lowercase typo in rss plugin
[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.match(/^(\S+)\s+(\S+)$/)\r
201     if feed.nil?\r
202       m.reply("incorrect usage: " + help(m.plugin))\r
203     end\r
204     handle = feed[1]\r
205     url = feed[2]\r
206     debug "Handle: #{handle.inspect}, Url: #{url.inspect}"\r
207     if @feeds.fetch(handle, nil) && !forced\r
208       m.reply("But there is already a feed named #{handle} with url #{@feeds[handle].url}")\r
209       return\r
210     end\r
211     handle.gsub!("|", '_')\r
212     @@mutex.synchronize {\r
213       @feeds[handle] = RssBlob.new(url,handle)\r
214     }\r
215     m.reply "RSS: Added #{url} with name #{handle}"\r
216     return handle\r
217   end\r
218 \r
219   def handle_rmrss(m)\r
220     feed = handle_rmwatch(m, true)\r
221     if feed.watched?\r
222       m.reply "someone else is watching #{feed.handle}, I won't remove it from my list"\r
223       return\r
224     end\r
225     @@mutex.synchronize {\r
226       @feeds.delete(feed.handle)\r
227     }\r
228     m.okay\r
229     return\r
230   end\r
231 \r
232   def handle_rmwatch(m,pass=false)\r
233     unless m.params\r
234       m.reply "incorrect usage: " + help(m.plugin)\r
235       return\r
236     end\r
237     handle = m.params\r
238     unless @feeds.has_key?(handle)\r
239       m.reply("dunno that feed")\r
240       return\r
241     end\r
242     feed = @feeds[handle]\r
243     if feed.rm_watch(m.replyto)\r
244       m.reply "#{m.replyto} has been removed from the watchlist for #{feed.handle}"\r
245     else\r
246       m.reply("#{m.replyto} wasn't watching #{feed.handle}") unless pass\r
247     end\r
248     if !feed.watched?\r
249       @@mutex.synchronize {\r
250         if @@watchThreads[handle].kind_of? Thread\r
251           @@watchThreads[handle].kill\r
252           debug "rmwatch: Killed thread for #{handle}"\r
253           @@watchThreads.delete(handle)\r
254         end\r
255       }\r
256     end\r
257     return feed\r
258   end\r
259 \r
260   def handle_listrss(m)\r
261     reply = ''\r
262     if @feeds.length == 0\r
263       reply = "No feeds yet."\r
264     else\r
265       @@mutex.synchronize {\r
266         @feeds.each { |handle, feed|\r
267           reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})"\r
268           (reply << " (watched)") if feed.watched_by?(m.replyto)\r
269           reply << "\n"\r
270           debug reply\r
271         }\r
272       }\r
273     end\r
274     m.reply reply\r
275   end\r
276 \r
277   def handle_listrsswatch(m)\r
278     reply = ''\r
279     if watchlist.length == 0\r
280       reply = "No watched feeds yet."\r
281     else\r
282       watchlist.each { |handle, feed|\r
283         (reply << "#{feed.handle}: #{feed.url} (in format: #{feed.type ? feed.type : 'default'})\n") if feed.watched_by?(m.replyto)\r
284         debug reply\r
285       }\r
286     end\r
287     m.reply reply\r
288   end\r
289 \r
290   def handle_rewatch(m=nil)\r
291     kill_threads\r
292 \r
293     # Read watches from list.\r
294     watchlist.each{ |handle, feed|\r
295       watchRss(feed, m)\r
296     }\r
297     m.okay if m\r
298   end\r
299 \r
300   def handle_watchrss(m)\r
301     unless m.params\r
302       m.reply "incorrect usage: " + help(m.plugin)\r
303       return\r
304     end\r
305     if m.params =~ /\s+/\r
306       handle = handle_addrss(m)\r
307     else\r
308       handle = m.params\r
309     end\r
310     feed = nil\r
311     @@mutex.synchronize {\r
312       feed = @feeds.fetch(handle, nil)\r
313     }\r
314     if feed\r
315       @@mutex.synchronize {\r
316         if feed.add_watch(m.replyto)\r
317           watchRss(feed, m)\r
318           m.okay\r
319         else\r
320           m.reply "Already watching #{feed.handle}"\r
321         end\r
322       }\r
323     else\r
324       m.reply "Couldn't watch feed #{handle} (no such feed found)"\r
325     end\r
326   end\r
327 \r
328   private\r
329   def watchRss(feed, m=nil)\r
330     if @@watchThreads.has_key?(feed.handle)\r
331       report_problem("watcher thread for #{feed.handle} is already running", m)\r
332       return\r
333     end\r
334     @@watchThreads[feed.handle] = Thread.new do\r
335       debug 'watchRss thread started.'\r
336       oldItems = []\r
337       firstRun = true\r
338       loop do\r
339         begin\r
340           debug 'Fetching rss feed...'\r
341           title = newItems = nil\r
342           @@mutex.synchronize {\r
343             title, newItems = fetchRss(feed)\r
344           }\r
345           unless newItems\r
346             m.reply "no items in feed"\r
347             break\r
348           end\r
349           debug "Checking if new items are available"\r
350           if firstRun\r
351             debug "First run, we'll see next time"\r
352             firstRun = false\r
353           else\r
354             dispItems = newItems.reject { |item|\r
355               oldItems.include?(item)\r
356             }\r
357             if dispItems.length > 0\r
358               debug "Found #{dispItems.length} new items"\r
359               dispItems.each { |item|\r
360                 debug "showing #{item.title}"\r
361                 @@mutex.synchronize {\r
362                   printFormattedRss(feed.watchers, item, feed.type)\r
363                 }\r
364               }\r
365             else\r
366               debug "No new items found"\r
367             end\r
368           end\r
369           oldItems = newItems\r
370         rescue Exception => e\r
371           error "IO failed: #{e.inspect}"\r
372           debug e.backtrace.join("\n")\r
373         end\r
374 \r
375         seconds = 150 + rand(100)\r
376         debug "Thread going to sleep #{seconds} seconds.."\r
377         sleep seconds\r
378       end\r
379     end\r
380   end\r
381 \r
382   def printRssItem(loc,item)\r
383     if item.kind_of?(RSS::RDF::Item)\r
384       @bot.say loc, item.title.chomp.riphtml.shorten(20) + " @ " + item.link\r
385     else\r
386       @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
387     end\r
388   end\r
389 \r
390   def printFormattedRss(locs, item, type)\r
391     locs.each { |loc|\r
392       case type\r
393       when 'amarokblog'\r
394         @bot.say loc, "::#{item.category.content} just blogged at #{item.link}::"\r
395         @bot.say loc, "::#{item.title.chomp.riphtml} - #{item.description.chomp.riphtml.shorten(60)}::"\r
396       when 'amarokforum'\r
397         @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
398       when 'mediawiki'\r
399         @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
400         debug "mediawiki #{item.title}"\r
401       when "gmame"\r
402         @bot.say loc, "::amarok-devel:: Message #{item.title} sent by #{item.dc_creator}. #{item.description.split("\n")[0].chomp.riphtml.shorten(60)}::"\r
403       else\r
404         printRssItem(loc,item)\r
405       end\r
406     }\r
407   end\r
408 \r
409   def fetchRss(feed, m=nil)\r
410     begin\r
411       # Use 60 sec timeout, cause the default is too low\r
412       xml = @bot.httputil.get_cached(feed.url,60,60)\r
413     rescue URI::InvalidURIError, URI::BadURIError => e\r
414       report_problem("invalid rss feed #{feed.url}", m)\r
415       return\r
416     end\r
417     debug 'fetched'\r
418     unless xml\r
419       report_problem("reading feed #{url} failed", m)\r
420       return\r
421     end\r
422 \r
423     begin\r
424       ## do validate parse\r
425       rss = RSS::Parser.parse(xml)\r
426       debug 'parsed'\r
427     rescue RSS::InvalidRSSError\r
428       ## do non validate parse for invalid RSS 1.0\r
429       begin\r
430         rss = RSS::Parser.parse(xml, false)\r
431       rescue RSS::Error\r
432         report_problem("parsing rss stream failed, whoops =(", m)\r
433         return\r
434       end\r
435     rescue RSS::Error\r
436       report_problem("parsing rss stream failed, oioi", m)\r
437       return\r
438     rescue => e\r
439       report_problem("processing error occured, sorry =(", m)\r
440       debug e.inspect\r
441       debug e.backtrace.join("\n")\r
442       return\r
443     end\r
444     items = []\r
445     if rss.nil?\r
446       report_problem("#{feed.url} does not include RSS 1.0 or 0.9x/2.0",m)\r
447     else\r
448       begin\r
449         rss.output_encoding = "euc-jp"\r
450       rescue RSS::UnknownConvertMethod\r
451         report_problem("bah! something went wrong =(",m)\r
452         return\r
453       end\r
454       rss.channel.title ||= "Unknown"\r
455       title = rss.channel.title\r
456       rss.items.each do |item|\r
457         item.title ||= "Unknown"\r
458         items << item\r
459       end\r
460     end\r
461 \r
462     if items.empty?\r
463       report_problem("no items found in the feed, maybe try weed?",m)\r
464       return\r
465     end\r
466     return [title, items]\r
467   end\r
468 end\r
469 \r
470 plugin = RSSFeedsPlugin.new\r
471 plugin.register("rss")\r
472 plugin.register("addrss")\r
473 plugin.register("rmrss")\r
474 plugin.register("rmwatch")\r
475 plugin.register("listrss")\r
476 plugin.register("rewatch")\r
477 plugin.register("watchrss")\r
478 plugin.register("listwatches")\r
479 \r