4 # :title: Freshmeat plugin for rbot
6 require 'rexml/document'
8 class FreshmeatPlugin < Plugin
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.",
16 return @bot.config['freshmeat.api_token']
19 # Checks if an API token is configure, warns if not, returns true or false
20 def check_api_token(m=nil)
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")
30 def help(plugin, topic="")
31 "freshmeat search [<max>=4] <string> => search freshmeat for <string>, freshmeat [<max>=4] => return up to <max> freshmeat headlines"
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>}
37 # This method defines a filter for fm pages. It's needed because the generic
38 # summarization grabs a comment, not the actual article.
40 def freshmeat_filter(s)
41 loc = Utils.check_location(s, /freshmeat\.net/)
44 s[:text].scan(/#{REL_ENTRY}|#{PRJ_ENTRY}/) { |m|
46 :type => ($1 || $4).dup,
47 :code => ($2 || $5).dup,
48 :name => ($3 || $6).dup
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}
60 @bot.register_filter(:freshmeat, :htmlinfo) { |s| freshmeat_filter(s) }
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
68 xml = @bot.httputil.get("http://freshmeat.net/search.xml?auth_code=#{api_token}&q=#{CGI.escape(search)}")
70 m.reply "search for #{search} failed (is the API token configured correctly?)"
75 doc = Document.new xml
81 m.reply "search for #{search} failed"
89 doc.elements.each("hash/projects/project") {|e|
90 title = e.elements["name"].text
91 title_width = title.length if title.length > title_width
93 url = "http://freshmeat.net/projects/#{e.elements['permalink'].text}"
94 url_width = url.length if url.length > url_width
96 desc = e.elements["oneliner"].text
98 matches << [title, url, desc]
102 if matches.length == 0
103 m.reply "not found: #{search}"
106 title_width += 2 # for bold
109 title = Bold + mat[0] + Bold
112 reply = sprintf("%s | %s | %s", title.ljust(title_width), url.ljust(url_width), desc)
113 m.reply reply, :overlong => :truncate
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
122 text = _("retrieving freshmeat news from the RSS")
124 case params[:api_token]
126 reason = _(" because no API token is configured")
128 reason = _(" because the configured API token is wrong")
131 m.reply text + reason
134 xml = @bot.httputil.get('http://freshmeat.net/?format=atom')
136 m.reply _("couldn't retrieve freshmeat news feed")
139 doc = Document.new xml
141 m.reply "freshmeat news parse failed"
146 m.reply "freshmeat news parse failed"
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]
166 title = Bold + mat[0] + Bold
168 reply = sprintf("%s | %s", title.ljust(title_width), desc)
169 m.reply reply, :overlong => :truncate
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
180 max = params[:limit].to_i
183 doc = Document.new xml
185 m.reply "freshmeat news parse failed"
190 m.reply "freshmeat news parse failed"
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
206 changelog = e.elements["changelog"].text.ircify_html
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
213 desc = e.elements["project/oneliner"].text.ircify_html
215 matches << [title, timeago, desc, url, changelog]
222 m.reply _("no news in freshmeat!")
228 title = Bold + mat[0] + Bold
233 reply = sprintf("%s | %s | %s | %s",
234 timeago.rjust(time_width),
235 title.ljust(title_width),
236 url.ljust(url_width),
238 m.reply reply, :overlong => :truncate
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+$/}