]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/freshmeat.rb
hangman: IRCify HTML in definitions
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / freshmeat.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Freshmeat plugin for rbot
5
6 require 'rexml/document'
7
8 class FreshmeatPlugin < Plugin
9   include REXML
10
11   Config.register Config::StringValue.new('freshmeat.api_token',
12     :desc => "Auth token for freshmeat API requests. Without this, no freshmeat calls will be made. Find it in your freshmeat user account settings.",
13     :default => "")
14
15   def api_token
16     return @bot.config['freshmeat.api_token']
17   end
18
19   # Checks if an API token is configure, warns if not, returns true or false
20   def check_api_token(m=nil)
21     if api_token.empty?
22       if m
23         m.reply _("you must set the configuration value freshmeat.api_token to a valid freshmeat auth token, otherwise I cannot make requests to the site")
24       end
25       return false
26     end
27     return true
28   end
29
30   def help(plugin, topic="")
31     "freshmeat search [<max>=4] <string> => search freshmeat for <string>, freshmeat [<max>=4] => return up to <max> freshmeat headlines"
32   end
33
34   REL_ENTRY = %r{<a href="/(release)s/(\d+)/"><font color="#000000">(.*?)</font></a>}
35   PRJ_ENTRY = %r{<a href="/(project)s/(\S+?)/"><b>(.*?)</b></a>}
36
37   # This method defines a filter for fm pages. It's needed because the generic
38   # summarization grabs a comment, not the actual article.
39   #
40   def freshmeat_filter(s)
41     loc = Utils.check_location(s, /freshmeat\.net/)
42     return nil unless loc
43     entries = []
44     s[:text].scan(/#{REL_ENTRY}|#{PRJ_ENTRY}/) { |m|
45       entry = {
46         :type => ($1 || $4).dup,
47         :code => ($2 || $5).dup,
48         :name => ($3 || $6).dup
49       }
50       entries << entry
51     }
52     return nil if entries.empty?
53     title = s[:text].ircify_html_title
54     content = entries.inject([]) { |l, e| l << e[:name] }.join(" | ")
55     return {:title => title, :content => content}
56   end
57
58   def initialize
59     super
60     @bot.register_filter(:freshmeat, :htmlinfo) { |s| freshmeat_filter(s) }
61   end
62
63   def search_freshmeat(m, params)
64     return unless check_api_token(m)
65     max = params[:limit].to_i
66     search = params[:search].to_s
67     max = 8 if max > 8
68     xml = @bot.httputil.get("http://freshmeat.net/search.xml?auth_code=#{api_token}&q=#{CGI.escape(search)}")
69     unless xml
70       m.reply "search for #{search} failed (is the API token configured correctly?)"
71       return
72     end
73     doc = nil
74     begin
75       doc = Document.new xml
76     rescue
77       debug xml
78       error $!
79     end
80     unless doc
81       m.reply "search for #{search} failed"
82       return
83     end
84     matches = Array.new
85     max_width = 250
86     title_width = 0
87     url_width = 0
88     done = 0
89     doc.elements.each("hash/projects/project") {|e|
90       title = e.elements["name"].text
91       title_width = title.length if title.length > title_width
92
93       url = "http://freshmeat.net/projects/#{e.elements['permalink'].text}"
94       url_width = url.length if url.length > url_width
95
96       desc = e.elements["oneliner"].text
97
98       matches << [title, url, desc]
99       done += 1
100       break if done >= max
101     }
102     if matches.length == 0
103       m.reply "not found: #{search}"
104     end
105
106     title_width += 2 # for bold
107
108     matches.each {|mat|
109       title = Bold + mat[0] + Bold
110       url = mat[1]
111       desc = mat[2]
112       reply = sprintf("%s | %s | %s", title.ljust(title_width), url.ljust(url_width), desc)
113       m.reply reply, :overlong => :truncate
114     }
115   end
116
117   # We do manual parsing so that we can work even with the RSS plugin not loaded
118   def freshmeat_rss(m, params)
119     max = params[:limit].to_i
120     max = 8 if max > 8
121
122     text = _("retrieving freshmeat news from the RSS")
123     reason = ""
124     case params[:api_token]
125     when :missing
126       reason = _(" because no API token is configured")
127     when :wrong
128       reason = _(" because the configured API token is wrong")
129     end
130
131     m.reply text + reason
132
133     begin
134       xml = @bot.httputil.get('http://freshmeat.net/?format=atom')
135       unless xml
136         m.reply _("couldn't retrieve freshmeat news feed")
137         return
138       end
139       doc = Document.new xml
140       unless doc
141         m.reply "freshmeat news parse failed"
142         return
143       end
144     rescue
145       error $!
146       m.reply "freshmeat news parse failed"
147       return
148     end
149
150     matches = Array.new
151     max_width = 60
152     title_width = 0
153     done = 0
154     doc.elements.each("feed/entry") {|e|
155       # TODO desc should be replaced by the oneliner, but this means one more hit per project
156       # so we clip out all of the description and leave just the 'changes' part
157       desc = e.elements["content"].text.ircify_html.sub(/.*?#{Bold}Changes:#{Bold}/,'').strip
158       title = e.elements["title"].text.ircify_html.strip
159       title_width = title.length if title.length > title_width
160       matches << [title, desc]
161       done += 1
162       break if done >= max
163     }
164     title_width += 2
165     matches.each {|mat|
166       title = Bold + mat[0] + Bold
167       desc = mat[1]
168       reply = sprintf("%s | %s", title.ljust(title_width), desc)
169       m.reply reply, :overlong => :truncate
170     }
171   end
172
173   def freshmeat(m, params)
174     # use the RSS if no API token is defined
175     return freshmeat_rss(m, params.merge(:api_token => :missing)) unless check_api_token
176     xml = @bot.httputil.get("http://freshmeat.net/index.xml?auth_code=#{api_token}")
177     # use the RSS if we couldn't get the XML
178     return freshmeat_rss(m, params.merge(:api_token => :wrong)) unless xml
179
180     max = params[:limit].to_i
181     max = 8 if max > 8
182     begin
183       doc = Document.new xml
184       unless doc
185         m.reply "freshmeat news parse failed"
186         return
187       end
188     rescue
189       error $!
190       m.reply "freshmeat news parse failed"
191       return
192     end
193
194     matches = Array.new
195     title_width = 0
196     url_width = 0
197     time_width = 0
198     done = 0
199     now = Time.now
200     doc.elements.each("releases/release") {|e|
201       approved = e.elements["approved-at"].text.strip
202       date = Time.parse(approved) rescue nil
203       timeago = date ? (Utils.timeago(date, :start_date => now) rescue nil) : approved
204       time_width = timeago.length if timeago.length > time_width
205
206       changelog = e.elements["changelog"].text.ircify_html
207
208       title = e.elements["project/name"].text.ircify_html
209       title_width = title.length if title.length > title_width
210       url = "http://freshmeat.net/projects/#{e.elements['project/permalink'].text}"
211       url_width = url.length if url.length > url_width
212
213       desc = e.elements["project/oneliner"].text.ircify_html
214
215       matches << [title, timeago, desc, url, changelog]
216
217       done += 1
218       break if done >= max
219     }
220
221     if matches.empty?
222       m.reply _("no news in freshmeat!")
223       return
224     end
225
226     title_width += 2
227     matches.each {|mat|
228       title = Bold + mat[0] + Bold
229       timeago = mat[1]
230       desc = mat[2]
231       url = mat[3]
232       changelog = mat[4]
233       reply = sprintf("%s | %s | %s | %s",
234         timeago.rjust(time_width),
235         title.ljust(title_width),
236         url.ljust(url_width),
237         desc)
238       m.reply reply, :overlong => :truncate
239     }
240   end
241 end
242 plugin = FreshmeatPlugin.new
243 plugin.map 'freshmeat search :limit *search', :action => 'search_freshmeat',
244             :defaults => {:limit => 4}, :requirements => {:limit => /^\d+$/}
245 plugin.map 'freshmeat :limit', :defaults => {:limit => 4},
246                                :requirements => {:limit => /^\d+$/}