# Gentoo centric plugin for rbot # Copyright (c) 2008 Mark Loeser # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # This does not always actually get run! #class Hash # def grep(key) # keys = self.keys.grep(key) # result = {} # keys.each do |k| # result[k] = self[k] # #warning("#{k} => #{self[k]}") # end # return result # end #end #VALID_PACKAGE_SRC = "https://tinderbox.dev.gentoo.org/misc/qsearch.txt" #GLSA_SRC = "https://www.gentoo.org/security/en/glsa/glsa-@GLSA_ID@.xml?passthru=1" VALID_PACKAGE_SRC = "/dev/shm/qsearch.txt" GLSA_SRC = "#{ENV['PORTDIR']}/metadata/glsa/glsa-@GLSA_ID@.xml" PROJECTS_SRC = 'https://api.gentoo.org/metastructure/projects.xml' PGO_RESOLVE_URI = 'https://packages.gentoo.org/packages/resolve.json?atom=%s' PGO_DATA_URI = 'https://packages.gentoo.org/packages/%s.json' class GentooPlugin < Plugin Config.register Config::StringValue.new('gentoo.scriptdir', :requires_rescan => true, :desc => "Directory for finding external scripts.") Config.register Config::StringValue.new('gentoo.python', :requires_rescan => true, :desc => "Patch to Python binary") def scriptdir sd = @bot.config['gentoo.scriptdir'] sd = "@BOTCLASS@/gentoo-scripts" unless sd sd.sub!('@BOTCLASS@', @bot.botclass) return sd end def python py = @bot.config['gentoo.python'] py = '/usr/bin/python' unless py return py end def pgo_resolve(atom) uri = PGO_RESOLVE_URI % [URI.escape(atom, '\W+')] res = @bot.httputil.get(uri) return JSON.parse(res) end def pgo_resolve_one(m, atom) pkgs = pgo_resolve(atom)['packages'] if pkgs.empty? m.reply "No matching packages for '#{atom}'" elsif pkgs.length > 1 m.reply "Ambiguous name '#{atom}'. Possible options: #{pkgs.map{ |x| x['atom'] }.sort.join(' ')}" else return pkgs[0] end return nil end def pgo_get(m, atom) pkg = pgo_resolve_one(m, atom) return if pkg.nil? uri = PGO_DATA_URI % [pkg['atom']] res = @bot.httputil.get(uri) return JSON.parse(res) end def meta_print(m, pkg) # TODO: handle description? maints = pkg['maintainers'].map{|x| x['email'].chomp('@gentoo.org')}.join(', ') maints = '(none)' if maints.empty? m.reply "#{pkg['atom']}; maintainers: #{maints}" end def meta(m, params) atom = params[:pkg] pkg = pgo_get(m, atom) return if pkg.nil? meta_print(m, pkg) end def validpkg(m, params) atom = params[:pkg] pkg = pgo_resolve_one(m, atom) return if pkg.nil? m.reply "#{atom} => #{pkg['atom']} is valid" end def meta_verbose(m, params) atom = params[:pkg] pkg = pgo_get(m, atom) return if pkg.nil? meta_print(m, pkg) pkg['maintainers'].each { |maint| next if maint['type'] != 'project' debug("meta -v calling proj for #{maint['email']}") p = params.clone p[:project] = maint['email'] project(m, p) } end def changelog(m, params) cp = params[:pkg] cp = validate_package(m, cp) return if cp.nil? f = IO.popen("#{python} #{scriptdir}/changelog.py '#{cp}'") m.reply "#{f.readlines}" f.close end def devaway(m, params) dev = params[:dev].downcase res = @bot.httputil.get("https://dev.gentoo.org/devaway/index-csv.php?who=#{dev}") if res.length > 0 then m.reply "#{dev}: #{res}" else m.reply "#{dev} has no devaway!" end end def initialize super @@cached = {} @@cached['projects'] = [0, nil] @@cached['pkgindex'] = [0, nil] @@cached['alias'] = [0, nil] end def project(m, params) now = Time.now.tv_sec unless @@cached['projects'] and @@cached['projects'][0] > now-600 #m.reply "Fetch #{@@cached['projects'][0]} > #{now-600}" res = @bot.httputil.get(PROJECTS_SRC) projects = REXML::Document.new(res) @@cached['projects'] = [now, projects] else #m.reply "Cache #{@@cached['projects'][0]} > #{now-600}" projects = @@cached['projects'][1] end req_project = params[:project] unless req_project.include?('@') req_project += '@gentoo.org' end # Parse data # xpath queries with REXML appear to be extremely slow, which is why we took the approach below def expand_project_recursively(projects, proj_email) project = nil projects.elements[1].each_element { |elem| if elem.get_elements('email')[0].text == proj_email project = elem break end } if project emails = [] for maintainer in project.get_elements("member") emails << maintainer.get_elements('email')[0].text.chomp('@gentoo.org') end for subproject in project.get_elements("subproject") if subproject.attributes["inherit-members"] == "1" sub_emails = expand_project_recursively(projects, subproject.attributes["ref"]) if sub_emails.nil? emails << "<#{subproject.attributes["ref"]}>" else emails += sub_emails end end end return emails else return nil end end emails = expand_project_recursively(projects, req_project) if emails.nil? m.reply "No such project: #{req_project}" elsif emails.empty? m.reply "(#{req_project}) [no members]" else m.reply "(#{req_project}) #{emails.sort.uniq.join(', ')}" end end def expand_alias(m, params) now = Time.now.tv_sec unless @@cached['alias'] and @@cached['alias'][0] > now-600 #m.reply "Fetch #{@@cached['alias'][0]} > #{now-600}" #res = @bot.httputil.get('https://dev.gentoo.org/~solar/.alias') res = @bot.httputil.get('https://dev.gentoo.org/.alias.cache') alias_hash = {} for line in res.split("\n") split_line = line.split(' = ') alias_hash[split_line[0]] = split_line[1] end @@cached['alias'] = [now, alias_hash] else #m.reply "Cache #{@@cached['alias'][0]} > #{now-600}" alias_hash = @@cached['alias'][1] end m.reply "#{params[:alias]} = #{alias_hash[params[:alias]]}" end def glsa(m, params) id = params[:glsa_id].gsub(/^(GLSA *)?/i,'') source = GLSA_SRC.sub('@GLSA_ID@', params[:glsa_id]) res = fetch_file_or_url(source) if res glsa_body = REXML::Document.new(res) refs = nil for ref in glsa_body.get_elements('/glsa/references/uri') if refs.nil? refs = '' refs << ref.text else refs << ', ' << ref.text end end m.reply "#{glsa_body.get_elements("/glsa/title")[0].text} #{refs}" else m.reply "Unable to find GLSA #{params[:glsa_id]}" end end def glsa_search(m, params) m.reply 'TODO' end def fetch_file_or_url(f) if (f =~ /^http/) == 0 return @bot.httputil.get(f) else return File.read(f) end end def get_pkgindex(m) now = Time.now.tv_sec #m.reply "In validate_package" @@cached['pkgindex'] = [0, nil] unless unless @@cached.key?('pkgindex') and @@cached['pkgindex'][0] > now-600 #m.reply "Fetch #{@@cached['pkgindex'][0]} > #{now-600}" pkgindex_a = fetch_file_or_url(VALID_PACKAGE_SRC).split("\n") pkgindex = {} pkgindex_a.each do |pkg| cp, desc = pkg.split(' ', 2) pkgindex[cp] = desc end @@cached['pkgindex'] = [now, pkgindex] else #m.reply "Cache #{@@cached['pkgindex'][0]} > #{now-600}" pkgindex = @@cached['pkgindex'][1] end return pkgindex end def validate_package(m, pn) begin pkgindex = get_pkgindex(m) pn = pn.gsub('+','\\\+') rx = (pn =~ /\//) ? /^#{pn}$/ : /\/#{pn}$/ packages = pkgindex.keys.grep(rx) case packages.length when 1 return packages[0] when 0 m.reply "No matching packages for '#{pn}'." return nil else m.reply "Ambiguous name '#{pn}'. Possible options: #{packages.join(' ')}" return nil end rescue ::Exception => e m.reply e.message end end def depcommon(m, type, url, params) cp = params[:pkg] cp = validate_package(m, cp) return if cp.nil? # Watch out for network problems begin resp = @bot.httputil.get_response(url+cp) if Net::HTTPNotFound === resp packages = '' elsif not Net::HTTPOK === resp m.reply "HTTP error: #{resp}" return else packages = resp.body.split("\n") end rescue ::Exception => e m.reply "Error: #{e.message}" return end if packages.length == 0 m.reply "No packages have a reverse #{type} on #{cp}." elsif packages.join(' ').length > 400 m.reply "Too many packages have reverse #{type} on #{cp}, go to #{url+cp} instead." else m.reply "Reverse #{type} for #{cp}: #{packages.join(' ')}" end end def ddep(m, params) depcommon(m, 'DEPEND', 'https://qa-reports.gentoo.org/output/genrdeps/dindex/', params) end def pdep(m, params) depcommon(m, 'PDEPEND', 'https://qa-reports.gentoo.org/output/genrdeps/pindex/', params) end def rdep(m, params) depcommon(m, 'RDEPEND', 'https://qa-reports.gentoo.org/output/genrdeps/rindex/', params) end def earch(m, params) cp = params[:pkg] cp = validate_package(m, cp) return if cp.nil? f = IO.popen("#{python} #{scriptdir}/earch --nocolor --quiet -c '#{cp}'") output = f.readlines f.close if output[0] =~ /^!!!/ m.reply "Unable to find package #{cp}" return end output[0].gsub!(/^.*#{cp}/,cp) output.map!{ |l| l.gsub(/^#{cp}-/,'').chomp } m.reply "#{cp} #{output.join(' ')}" end @@commands = %w(meta changelog devaway proj expn glsa earch rdep ddep pdep) @@help_gentoo = { "gentoo" => "Available commands: " + @@commands.map { |s| "#{Bold}#{s}#{Bold}" }.join(", ") "meta" => [ "meta #{Bold}[cat/]package#{Bold} : Print metadata for the given package", "meta -v #{Bold}[cat/]package#{Bold} : Print metadata for the given package and the members of the maintaining projects.", ].join("\n"), "changelog" => "changelog #{Bold}[cat/]package#{Bold} : Produce changelog statistics for a given package", "devaway" => "devaway #{Bold}devname|list#{Bold} : Print the .away for a developer (if any). Using 'list' shows the developers who are away.", "proj" => "proj #{Bold}project-email#{Bold} : Print the members of a project.", "expn" => "expn #{Bold}alias#{Bold} : Print the addresses on a Gentoo mail alias.", "glsa" => [ "glsa #{Bold}GLSA-ID#{Bold} : Prints the title and reference IDs for a given GLSA.", "glsa -s #{Bold}[cat/]package#{Bold} : Prints all GLSA IDs for a given package.", ].join("\n"), "earch" => "earch #{Bold}[cat/]package#{Bold} : Prints the versions and effective keywords for a given package.", "rdep" => "rdep #{Bold}[cat/]package#{Bold} : Prints the reverse RDEPENDs for a given package.", "ddep" => "ddep #{Bold}[cat/]package#{Bold} : Prints the reverse DEPENDS for a given package.", "pdep" => "pdep #{Bold}[cat/]package#{Bold} : Prints the reverse PDEPENDs for a given package.", } def help(plugin, topic = "") cmd = plugin cmd += " "+topic if topic.length > 0 if @@help_gentoo.has_key?(cmd) return @@help_gentoo[cmd] else return @@help_gentoo['gentoo'] end end end plugin = GentooPlugin.new plugin.default_auth( 'modify', false ) plugin.default_auth( 'view', true ) REGEX_CP = /^(?:[-[:alnum:]]+\/)?[-+_[:alnum:]]+$/ REGEX_DEV = /^[-._[:alnum:]]+$/ REGEX_PROJECT = /^[-_@.[:alnum:]]+$/ REGEX_GLSA = /^(GLSA ?)?[-1234567890]+$/i plugin.map 'meta -v :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'meta_verbose', :thread => 'yes', :auth_path => 'view' plugin.map 'meta :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'meta', :thread => 'yes', :auth_path => 'view' plugin.map 'validpkg :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'validpkg', :auth_path => 'view' plugin.map 'changelog :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'changelog', :thread => 'yes', :auth_path => 'view' plugin.map 'devaway :dev', :requirements => { :dev => REGEX_DEV, }, :action => 'devaway', :thread => 'yes', :auth_path => 'view' plugin.map 'away :dev', :requirements => { :dev => REGEX_DEV, }, :action => 'devaway', :thread => 'yes', :auth_path => 'view' plugin.map 'proj :project', :requirements => { :project => REGEX_PROJECT, }, :action => 'project', :thread => 'yes', :auth_path => 'view' plugin.map 'expn :alias', :requirements => { :alias => REGEX_DEV, }, :action => 'expand_alias', :thread => 'yes', :auth_path => 'view' plugin.map 'glsa :glsa_id', :requirements => { :alias => REGEX_GLSA, }, :action => 'glsa', :thread => 'yes', :auth_path => 'view' plugin.map 'glsa -s :text', :requirements => { :text => /^[^ ]+$/, }, :action => 'glsa_search', :thread => 'yes', :auth_path => 'view' plugin.map 'ddep :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'ddep', :thread => 'yes', :auth_path => 'view' plugin.map 'pdep :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'pdep', :thread => 'yes', :auth_path => 'view' plugin.map 'rdep :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'rdep', :thread => 'yes', :auth_path => 'view' plugin.map 'earch :pkg', :requirements => { :pkg => REGEX_CP, }, :action => 'earch', :thread => 'yes', :auth_path => 'view' # vim: ft=ruby ts=2 sts=2 et: