def get_pkg_pushers(pkg, branch): watchers = [] committers = [] watchergroups = [] committergroups = [] if config.get('acl_system') == 'dummy': return ((['guest', 'admin'], ['guest', 'admin']), (['guest', 'admin'], ['guest', 'admin'])) from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) acls = pkgdb.get_package(pkg, branches=branch) for package in acls['packages']: for acl in package.get('acls', []): if acl['status'] == 'Approved': if acl['acl'] == 'watchcommits': name = acl['fas_name'] if name.startswith('group::'): watchergroups.append(name.split('::')[1]) else: watchers.append(name) elif acl['acl'] == 'commit': name = acl['fas_name'] if name.startswith('group::'): committergroups.append(name.split('::')[1]) else: committers.append(name) return (committers, watchers), (committergroups, watchergroups)
def get_critpath_components(collection='master', component_type='rpm'): """ Return a list of critical path packages for a given collection. Args: collection (basestring): The collection/branch to search. Defaults to 'master'. component_type (basestring): The component type to search for. This only affects PDC queries. Defaults to 'rpm'. Returns: list: The critpath components for the given collection and type. """ critpath_components = [] critpath_type = config.get('critpath.type') if critpath_type != 'pdc' and component_type != 'rpm': log.warning('The critpath.type of "{0}" does not support searching for' ' non-RPM components'.format(component_type)) if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_components = results['pkgs'][collection] elif critpath_type == 'pdc': critpath_components = get_critpath_components_from_pdc( collection, component_type) else: critpath_components = config.get('critpath_pkgs') return critpath_components
def get_pkg_pushers(pkg, branch): watchers = [] committers = [] watchergroups = [] committergroups = [] if config.get('acl_system') == 'dummy': return ((['guest', 'admin'], ['guest', 'admin']), (['guest', 'admin'], ['guest', 'admin'])) from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) acls = pkgdb.get_package(pkg, branches=branch) for package in acls['packages']: for acl in package.get('acls', []): if acl['status'] == 'Approved': if acl['acl'] == 'watchcommits': name = acl['fas_name'] if name.startswith('group::'): watchergroups.append(name.split('::')[1]) else: watchers.append(name) elif acl['acl'] == 'commit': name = acl['fas_name'] if name.startswith('group::'): committergroups.append(name.split('::')[1]) else: committers.append(name) return (committers, watchers), (committergroups, watchergroups)
def __init__(self, irc): super(Fedora, self).__init__(irc) # caches, automatically downloaded on __init__, manually refreshed on # .refresh self.userlist = None self.bugzacl = None # To get the information, we need a username and password to FAS. # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY! self.fasurl = self.registryValue('fas.url') self.username = self.registryValue('fas.username') self.password = self.registryValue('fas.password') self.fasclient = AccountSystem(self.fasurl, username=self.username, password=self.password) self.pkgdb = PkgDB() # URLs # self.url = {} self.github_oauth_token = self.registryValue('github.oauth_token') self.karma_tokens = ('++', '--') if self.allow_negative else ('++',) # fetch necessary caches self._refresh() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config)
def get_critpath_pkgs(collection='master'): """Return a list of critical path packages for a given collection""" critpath_pkgs = [] critpath_type = config.get('critpath.type') if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_pkgs = results['pkgs'][collection] else: critpath_pkgs = config.get('critpath_pkgs', '').split() return critpath_pkgs
def get_critpath_pkgs(collection='master'): """Return a list of critical path packages for a given collection""" critpath_pkgs = [] critpath_type = config.get('critpath.type') if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_pkgs = results['pkgs'][collection] else: critpath_pkgs = config.get('critpath_pkgs', '').split() return critpath_pkgs
def get_critpath_pkgs(collection='master'): critpath_pkgs = [] critpath_type = config.get('critpath.type', None) if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_pkgs = results['pkgs'][collection] else: # HACK: Avoid the current critpath policy for EPEL if not collection.startswith('EL'): # Note: ''.split() == [] critpath_pkgs = config.get('critpath', '').split() return critpath_pkgs
def get_critpath_pkgs(collection='master'): critpath_pkgs = [] critpath_type = config.get('critpath.type', None) if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_pkgs = results['pkgs'][collection] else: # HACK: Avoid the current critpath policy for EPEL if not collection.startswith('EL'): # Note: ''.split() == [] critpath_pkgs = config.get('critpath', '').split() return critpath_pkgs
def __init__(self, irc): super(Fedora, self).__init__(irc) # caches, automatically downloaded on __init__, manually refreshed on # .refresh self.userlist = None self.bugzacl = None # To get the information, we need a username and password to FAS. # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY! self.fasurl = self.registryValue('fas.url') self.username = self.registryValue('fas.username') self.password = self.registryValue('fas.password') self.fasclient = AccountSystem(self.fasurl, username=self.username, password=self.password) self.pkgdb = PkgDB() # URLs # self.url = {} self.github_oauth_token = self.registryValue('github.oauth_token') # fetch necessary caches self._refresh() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config)
def get_critpath_components(collection='master', component_type='rpm', components=None): """ Return a list of critical path packages for a given collection, filtered by components. Args: collection (basestring): The collection/branch to search. Defaults to 'master'. component_type (basestring): The component type to search for. This only affects PDC queries. Defaults to 'rpm'. components (frozenset or None): The list of components we are interested in. If None (the default), all components for the given collection and type are returned. Returns: list: The critpath components for the given collection and type. """ critpath_components = [] critpath_type = config.get('critpath.type') if critpath_type != 'pdc' and component_type != 'rpm': log.warning('The critpath.type of "{0}" does not support searching for' ' non-RPM components'.format(component_type)) if critpath_type == 'pkgdb': from pkgdb2client import PkgDB pkgdb = PkgDB(config.get('pkgdb_url')) results = pkgdb.get_critpath_packages(branches=collection) if collection in results['pkgs']: critpath_components = results['pkgs'][collection] elif critpath_type == 'pdc': critpath_components = get_critpath_components_from_pdc( collection, component_type, components) else: critpath_components = config.get('critpath_pkgs') # Filter the list of components down to what was requested, in case the specific path did # not take our request into account. if components is not None: critpath_components = [ c for c in critpath_components if c in components ] return critpath_components
def __init__(self, bot): self.bot = bot fas_url = bot.config['fas']['url'] fas_username = bot.config['fas']['username'] fas_password = bot.config['fas']['password'] self.fasclient = AccountSystem(fas_url, username=fas_username, password=fas_password) #self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] self.pkgdb = PkgDB() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config)
def __init__(self, bot): self.bot = bot fas_url = bot.config['fas']['url'] fas_username = bot.config['fas']['username'] fas_password = bot.config['fas']['password'] self.fasclient = AccountSystem( fas_url, username=fas_username, password=fas_password) #self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] self.pkgdb = PkgDB() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config)
def main(): ''' Main function ''' # Set up parser for global args parser = setup_parser() # Parse the commandline try: arg = parser.parse_args() except argparse.ArgumentTypeError as err: print("\nError: {0}".format(err)) return 2 logging.basicConfig() if arg.debug: LOG.setLevel(logging.DEBUG) PKGDBLOG.setLevel(logging.DEBUG) elif arg.verbose: LOG.setLevel(logging.INFO) global PKGDBCLIENT if arg.pkgdburl: LOG.info("Querying pkgdb at: %s", arg.pkgdburl) PKGDBCLIENT = PkgDB(arg.pkgdburl, login_callback=pkgdb2client.ask_password) PKGDBCLIENT.insecure = arg.insecure if arg.bzurl: if not arg.bzurl.endswith('xmlrpc.cgi'): arg.bzurl = '%s/xmlrpc.cgi' % arg.bzurl LOG.info("Querying bugzilla at: %s", arg.pkgdburl) utils._get_bz(arg.bzurl, insecure=arg.insecure) if arg.fasurl: LOG.info("Querying FAS at: %s", arg.pkgdburl) utils.FASCLIENT.base_url = arg.fasurl utils.FASCLIENT.insecure = arg.insecure return_code = 0 if arg.password: PKGDBCLIENT.password = arg.password if arg.username: PKGDBCLIENT.username = arg.username try: arg.func(arg) except KeyboardInterrupt: print("\nInterrupted by user.") return_code = 1 except ServerError as err: LOG.debug('ServerError') print('{0}'.format(err)) return_code = 3 except ActionError as err: LOG.debug('ActionError') print('{0}'.format(err.message)) return_code = 7 except AppError as err: LOG.debug('AppError') print('{0}: {1}'.format(err.name, err.message)) return_code = 4 except PkgDBException as err: LOG.debug('PkgDBException') print('{0}'.format(err)) return_code = 8 except ValueError as err: print('Error: {0}'.format(err)) print('Did you log in?') return_code = 6 except Exception as err: print('Error: {0}'.format(err)) logging.exception("Generic error catched:") return_code = 5 return return_code
def main(): ''' Main function ''' # Set up parser for global args parser = setup_parser() # Parse the commandline try: arg = parser.parse_args() except argparse.ArgumentTypeError as err: print("\nError: {0}".format(err)) return 2 if arg.nocolor: global RED, BOLD, RESET RED = "" BOLD = "" RESET = "" logging.basicConfig() if arg.debug: LOG.setLevel(logging.DEBUG) PKGDBLOG.setLevel(logging.DEBUG) elif arg.verbose: LOG.setLevel(logging.INFO) global pkgdbclient if arg.pkgdburl != pkgdb2client.PKGDB_URL: print("Querying pkgdb at: %s" % arg.pkgdburl) pkgdbclient = PkgDB( arg.pkgdburl, login_callback=pkgdb2client.ask_password) pkgdbclient.insecure = arg.insecure if arg.bzurl != pkgdb2client.BZ_URL: if not arg.bzurl.endswith('xmlrpc.cgi'): arg.bzurl = '%s/xmlrpc.cgi' % arg.bzurl print("Querying bugzilla at: %s" % arg.bzurl) utils._get_bz(arg.bzurl, insecure=arg.insecure) if arg.fasurl != pkgdb2client.FAS_URL: print("Querying FAS at: %s" % arg.fasurl) utils._get_fas(arg.fasurl, insecure=arg.insecure) if arg.kojihuburl != pkgdb2client.KOJI_HUB: print("Querying koji at: %s" % arg.kojihuburl) global KOJI_HUB KOJI_HUB = arg.kojihuburl return_code = 0 if arg.password: pkgdbclient.password = arg.password if arg.username: pkgdbclient.username = arg.username try: arg.func(arg) except KeyboardInterrupt: print("\nInterrupted by user.") return_code = 1 except ServerError as err: LOG.debug('ServerError') print('{0}'.format(err)) return_code = 3 except ActionError as err: LOG.debug('ActionError') print('{0}'.format(err.message)) return_code = 7 except argparse.ArgumentError as err: LOG.debug('ArgparseError') print('{0}'.format(err.message)) return_code = 9 except AppError as err: LOG.debug('AppError') print('{0}: {1}'.format(err.name, err.message)) return_code = 4 except PkgDBException as err: LOG.debug('PkgDBException') print('{0}'.format(err)) return_code = 8 except ValueError as err: print('Error: {0}'.format(err)) print('Did you log in?') return_code = 6 except Exception as err: print('Error: {0}'.format(err)) logging.exception("Generic error catched:") return_code = 5 return return_code
from fedora.client import (AppError, ServerError) import argparse import requests import logging import koji import itertools from pkgdb2client import PkgDB, PkgDBException, __version__ import pkgdb2client import pkgdb2client.utils KOJI_HUB = 'http://koji.fedoraproject.org/kojihub' pkgdbclient = PkgDB('https://admin.fedoraproject.org/pkgdb', login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-cli") ACTIONLIST = ['watchbugzilla', 'watchcommits', 'commit', 'approveacls'] class ActionError(Exception): ''' This class is raised when an ACL action is requested but not in the list of allowed action. '''
def main(): ''' Main function ''' # Set up parser for global args parser = setup_parser() # Parse the commandline try: arg = parser.parse_args() except argparse.ArgumentTypeError as err: print("\nError: {0}".format(err)) return 2 logging.basicConfig() if arg.debug: LOG.setLevel(logging.DEBUG) PKGDBLOG.setLevel(logging.DEBUG) elif arg.verbose: LOG.setLevel(logging.INFO) global PKGDBCLIENT if arg.pkgdburl: LOG.info("Querying pkgdb at: %s", arg.pkgdburl) PKGDBCLIENT = PkgDB( arg.pkgdburl, login_callback=pkgdb2client.ask_password) PKGDBCLIENT.insecure = arg.insecure if arg.bzurl: if not arg.bzurl.endswith('xmlrpc.cgi'): arg.bzurl = '%s/xmlrpc.cgi' % arg.bzurl LOG.info("Querying bugzilla at: %s", arg.pkgdburl) utils._get_bz(arg.bzurl, insecure=arg.insecure) if arg.fasurl: LOG.info("Querying FAS at: %s", arg.pkgdburl) utils.FASCLIENT.base_url = arg.fasurl utils.FASCLIENT.insecure = arg.insecure return_code = 0 if arg.password: PKGDBCLIENT.password = arg.password if arg.username: PKGDBCLIENT.username = arg.username try: arg.func(arg) except KeyboardInterrupt: print("\nInterrupted by user.") return_code = 1 except ServerError as err: LOG.debug('ServerError') print('{0}'.format(err)) return_code = 3 except ActionError as err: LOG.debug('ActionError') print('{0}'.format(err.message)) return_code = 7 except AppError as err: LOG.debug('AppError') print('{0}: {1}'.format(err.name, err.message)) return_code = 4 except PkgDBException as err: LOG.debug('PkgDBException') print('{0}'.format(err)) return_code = 8 except ValueError as err: print('Error: {0}'.format(err)) print('Did you log in?') return_code = 6 except Exception as err: print('Error: {0}'.format(err)) logging.exception("Generic error catched:") return_code = 5 return return_code
""" from fedora.client import (AppError, ServerError) import argparse import requests import logging import koji import itertools from pkgdb2client import PkgDB, PkgDBException, __version__ import pkgdb2client import pkgdb2client.utils as utils pkgdbclient = PkgDB( pkgdb2client.PKGDB_URL, login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-cli") ACTIONLIST = ['watchbugzilla', 'watchcommits', 'commit', 'approveacls'] KOJI_HUB = pkgdb2client.KOJI_HUB class ActionError(Exception):
class FedoraPlugin: """A plugin is a class which take the IrcBot as argument """ requires = [ 'irc3.plugins.core', 'irc3.plugins.command', ] def __init__(self, bot): self.bot = bot fas_url = bot.config['fas']['url'] fas_username = bot.config['fas']['username'] fas_password = bot.config['fas']['password'] self.fasclient = AccountSystem( fas_url, username=fas_username, password=fas_password) #self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] self.pkgdb = PkgDB() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config) @staticmethod def _future_meetings(location): if not location.endswith('@irc.freenode.net'): location = '*****@*****.**' % location meetings = FedoraPlugin._query_fedocal(location=location) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now < dt: yield dt, meeting @staticmethod def _meetings_for(calendar): meetings = FedoraPlugin._query_fedocal(calendar=calendar) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) start = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") string = "%s %s" % (meeting['meeting_date_end'], meeting['meeting_time_stop']) end = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now >= start and now <= end: yield meeting @staticmethod def _query_fedocal(**kwargs): url = 'https://apps.fedoraproject.org/calendar/api/meetings' return requests.get(url, params=kwargs).json()['meetings'] @command def admins(self, mask, target, args): """admins <group name> Return the administrators list for the selected group %%admins <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'administrator': sponsors += person['username'] + ' ' msg = 'Administrators for %s: %s' % (name, sponsors) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def badges(self, mask, target, args): """badges <username> Return badges statistics about a user. %%badges <username> """ name = args['<username>'] url = "https://badges.fedoraproject.org/user/" + name d = requests.get(url + "/json").json() if 'error' in d: response = d['error'] else: template = "{name} has unlocked {n} Fedora Badges: {url}" n = len(d['assertions']) response = template.format(name=name, url=url, n=n) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def branches(self, mask, target, args): """branches <package> Return the branches a package is in. %%branches <package> """ package = args['<package>'] try: pkginfo = self.pkgdb.get_package(package) except AppError: msg = "No such package exists." self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return branch_list = [] for listing in pkginfo['packages']: branch_list.append(listing['collection']['branchname']) branch_list.sort() msg = ' '.join(branch_list) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def fas(self, mask, target, args): """fas <pattern> Searches a pattern in the list of FAS user %%fas <pattern> """ users = self.fasclient.people_query( constraints={ #'username': args['<pattern>'], 'ircnick': args['<pattern>'], }, columns=['username', 'ircnick', 'email'] ) if users: msg = ', '.join( [ '%s (%s) <%s>' % (user.username, user.ircnick, user.email) for user in users ] ) else: msg = 'No user matching found' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def fasinfo(self, mask, target, args): """fasinfo <pattern> Return more information about the specified user %%fasinfo <username> """ name = args['<username>'] try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info for user: "******"' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return if not person: msg = 'User "%s" doesn\'t exist' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return person['creation'] = person['creation'].split(' ')[0] string = ( "User: %(username)s, Name: %(human_name)s" ", email: %(email)s, Creation: %(creation)s" ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s" ", Locale: %(locale)s" ", GPG key ID: %(gpg_keyid)s, Status: %(status)s") % person self.bot.privmsg(target, '%s: %s' % (mask.nick, string)) # List of unapproved groups is easy unapproved = '' for group in person['unapproved_memberships']: unapproved = unapproved + "%s " % group['name'] if unapproved != '': msg = 'Unapproved Groups: %s' % unapproved self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) # List of approved groups requires a separate query to extract roles constraints = { 'username': name, 'group': '%', 'role_status': 'approved'} columns = ['username', 'group', 'role_type'] roles = [] try: roles = self.fasclient.people_query( constraints=constraints, columns=columns) except: msg = 'Error getting group memberships.' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return approved = '' for role in roles: if role['role_type'] == 'sponsor': approved += '+' + role['group'] + ' ' elif role['role_type'] == 'administrator': approved += '@' + role['group'] + ' ' else: approved += role['group'] + ' ' if approved == '': approved = "None" msg = 'Approved Groups: %s' % approved self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def group(self, mask, target, args): """group <group short name> Return information about a Fedora Account System group. %%group <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_by_name(name) msg = '%s: %s' % (name, group['display_name']) except AppError: msg = 'There is no group "%s".' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def hellomynameis(self, mask, target, args): """hellomynameis <username> Return brief information about a Fedora Account System username. Useful for things like meeting roll call and calling attention to yourself. %%hellomynameis <username> """ name = args['<username>'] msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Something blew up, please try again' if not person: msg = 'Sorry, but you don\'t exist' else: msg = '%(username)s \'%(human_name)s\' <%(email)s>' % person if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def himynameis(self, mask, target, args): """himynameis <username> Return information about a Fedora Account System group. %%himynameis <username> """ name = args['<username>'] msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Something blew up, please try again' if not person: msg = 'Sorry, but you don\'t exist' else: msg = '%(username)s \'Slim Shady\' <%(email)s>' % person if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def localtime(self, mask, target, args): """localtime <username> Returns the current time of the user. The timezone is queried from FAS. %%localtime <username> """ name = args['<username>'] try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info user user: "******"' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return if not person: msg = 'User "%s" doesn\'t exist' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return timezone_name = person['timezone'] if timezone_name is None: msg = 'User "%s" doesn\'t share his timezone' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: msg = 'The timezone of "%s" was unknown: "%s"' % ( name, timezone_name) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return msg = 'The current local time of "%s" is: "%s" (timezone: %s)' % ( name, time.strftime('%H:%M'), timezone_name) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def members(self, mask, target, args): """members <group short name> Return the list of members for the selected group %%members <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) members = '' for person in group: if person['role_type'] == 'administrator': members += '@' + person['username'] + ' ' elif person['role_type'] == 'sponsor': members += '+' + person['username'] + ' ' else: members += person['username'] + ' ' msg = 'Members of %s: %s' % (name, members) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def nextmeeting(self, mask, target, args): """nextmeeting <channel> Return the next meeting scheduled for a particular channel. %%nextmeeting <channel> """ channel = args['<channel>'] channel = channel.strip('#').split('@')[0] meetings = sorted(self._future_meetings(channel), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled for #%s." % channel self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) return for date, meeting in islice(meetings, 0, 3): response = "In #%s is %s (starting %s)" % ( channel, meeting['meeting_name'], arrow.get(date).humanize(), ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) base = "https://apps.fedoraproject.org/calendar/location/" url = base + urllib.quote("%[email protected]/" % channel) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def nextmeetings(self, mask, target, args): """nextmeetings Return the next meetings scheduled for any channel(s). %%nextmeetings """ msg = 'One moment, please... Looking up the channel list.' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) url = 'https://apps.fedoraproject.org/calendar/api/locations/' locations = requests.get(url).json()['locations'] meetings = sorted(chain(*[ self._future_meetings(location) for location in locations if 'irc.freenode.net' in location ]), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled at all." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) return for date, meeting in islice(meetings, 0, 5): response = "In #%s is %s (starting %s)" % ( meeting['meeting_location'].split('@')[0].strip(), meeting['meeting_name'], arrow.get(date).humanize(), ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def pushduty(self, mask, target, args): """pushduty Return the list of people who are on releng push duty right now. %%pushduty """ def get_persons(): for meeting in self._meetings_for('release-engineering'): yield meeting['meeting_name'] persons = list(get_persons()) url = "https://apps.fedoraproject.org/" + \ "calendar/release-engineering/" if not persons: response = "Nobody is listed as being on push duty right now..." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) return persons = ", ".join(persons) response = "The following people are on push duty: %s" % persons self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def quote(self, mask, target, args): """quote <SYMBOL> [daily, weekly, monthly, quarterly] Return some datagrepper statistics on fedmsg categories. %%quote <symbol> <frame> """ symbol = args['<symbol>'] frame = 'daily' if '<frame>' in args: frame = args['<frame>'] # Second, build a lookup table for symbols. By default, we'll use the # fedmsg category names, take their first 3 characters and uppercase # them. That will take things like "wiki" and turn them into "WIK" and # "bodhi" and turn them into "BOD". This handles a lot for us. We'll # then override those that don't make sense manually here. For # instance "fedoratagger" by default would be "FED", but that's no # good. We want "TAG". # Why all this trouble? Well, as new things get added to the fedmsg # bus, we don't want to have keep coming back here and modifying this # code. Hopefully this dance will at least partially future-proof us. symbols = dict([ (processor.__name__.lower(), processor.__name__[:3].upper()) for processor in fedmsg.meta.processors ]) symbols.update({ 'fedoratagger': 'TAG', 'fedbadges': 'BDG', 'buildsys': 'KOJ', 'pkgdb': 'PKG', 'meetbot': 'MTB', 'planet': 'PLN', 'trac': 'TRC', 'mailman': 'MM3', }) # Now invert the dict so we can lookup the argued symbol. # Yes, this is vulnerable to collisions. symbols = dict([(sym, name) for name, sym in symbols.items()]) # These aren't user-facing topics, so drop 'em. del symbols['LOG'] del symbols['UNH'] del symbols['ANN'] # And this one is unused... key_fmt = lambda d: ', '.join(sorted(d.keys())) if symbol not in symbols: response = "No such symbol %r. Try one of %s" msg = response % (symbol, key_fmt(symbols)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return # Now, build another lookup of our various timeframes. frames = dict( daily=datetime.timedelta(days=1), weekly=datetime.timedelta(days=7), monthly=datetime.timedelta(days=30), quarterly=datetime.timedelta(days=91), ) if frame not in frames: response = "No such timeframe %r. Try one of %s" msg = response % (frame, key_fmt(frames)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return category = [symbols[symbol]] t2 = datetime.datetime.utcnow() t1 = t2 - frames[frame] t0 = t1 - frames[frame] # Count the number of messages between t0 and t1, and between t1 and t2 query1 = dict(start=t0, end=t1, category=category) query2 = dict(start=t1, end=t2, category=category) # Do this async for superfast datagrepper queries. tpool = ThreadPool() batched_values = tpool.map(datagrepper_query, [ dict(start=x, end=y, category=category) for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION) ] + [query1, query2]) count2 = batched_values.pop() count1 = batched_values.pop() # Just rename the results. We'll use the rest for the sparkline. sparkline_values = batched_values yester_phrases = dict( daily="yesterday", weekly="the week preceding this one", monthly="the month preceding this one", quarterly="the 3 months preceding these past three months", ) phrases = dict( daily="24 hours", weekly="week", monthly="month", quarterly="3 months", ) if count1 and count2: percent = ((float(count2) / count1) - 1) * 100 elif not count1 and count2: # If the older of the two time periods had zero messages, but there # are some in the more current period.. well, that's an infinite # percent increase. percent = float('inf') elif not count1 and not count2: # If counts are zero for both periods, then the change is 0%. percent = 0 else: # Else, if there were some messages in the old time period, but # none in the current... then that's a 100% drop off. percent = -100 sign = lambda value: value >= 0 and '+' or '-' template = u"{sym}, {name} {sign}{percent:.2f}% over {phrase}" response = template.format( sym=symbol, name=symbols[symbol], sign=sign(percent), percent=abs(percent), phrase=yester_phrases[frame], ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) # Now, make a graph out of it. sparkline = Utils.sparkline(sparkline_values) template = u" {sparkline} ⤆ over {phrase}" response = template.format( sym=symbol, sparkline=sparkline, phrase=phrases[frame] ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # And a final line for "x-axis tics" t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1)) t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2)) padding = u" " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3) template = u" ↑ {t1}{padding}↑ {t2}" response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def sponsors(self, mask, target, args): """sponsors <group short name> Return the sponsors list for the selected group %%sponsors <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'sponsor': sponsors += person['username'] + ' ' elif person['role_type'] == 'administrator': sponsors += '@' + person['username'] + ' ' msg = 'Sponsors for %s: %s' % (name, sponsors) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def vacation(self, mask, target, args): """vacation Return the list of people who are on vacation right now according to fedocal. %%vacation """ def get_persons(): for meeting in self._meetings_for('vacation'): for manager in meeting['meeting_manager']: yield manager persons = list(get_persons()) if not persons: response = "Nobody is listed as being on vacation right now..." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) url = "https://apps.fedoraproject.org/calendar/vacation/" self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) return persons = ", ".join(persons) response = "The following people are on vacation: %s" % persons self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) url = "https://apps.fedoraproject.org/calendar/vacation/" self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def what(self, mask, target, args): """what <package> Returns a description of a given package. %%what <package> """ package = args['<package>'] msg = None try: summary = self.bugzacl['Fedora'][package]['summary'] msg = "%s: %s" % (package, summary) except KeyError: msg = "No such package exists." if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def whoowns(self, mask, target, args): """whoowns <package> Return more information about the specified user %%whoowns <package> """ package = args['<package>'] try: mainowner = self.bugzacl['Fedora'][package]['owner'] except KeyError: msg = "No such package exists." self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return others = [] for key in self.bugzacl: if key == 'Fedora': continue try: owner = self.bugzacl[key][package]['owner'] if owner == mainowner: continue except KeyError: continue others.append("%s in %s" % (owner, key)) if others == []: self.bot.privmsg(target, '%s: %s' % (mask.nick, mainowner)) else: msg = "%s (%s)" % (mainowner, ', '.join(others)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def wikilink(self, mask, target, args): """wikilink <username> Return MediaWiki link syntax for a FAS user's page on the wiki. %%wikilink <username> """ name = args['<username>'] person = msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info for user: "******"' % name if not person: msg = 'User "%s" doesn\'t exist' % name else: msg = "[[User:%s|%s]]" % (person["username"], person["human_name"] or '') if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg))
class TestPkgdDBAuth(unittest.TestCase): ''' Authenticated pkgdb2 tests. ''' @auth_only def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True) self.pkgdb.login(USERNAME, PASSWORD) @auth_only def test_01_create_collection(self): ''' Test the create_collection function. ''' out = self.pkgdb.create_collection( clt_name='Test', version=VERSION, clt_status='Active', branchname=COL_NAME, dist_tag='.tst' + COL_NAME[:20], git_branch_name='test', kojiname='test', ) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], ['Collection "%s" created' % COL_NAME]) self.assertRaises( PkgDBException, self.pkgdb.create_collection, clt_name='Test', version=VERSION, clt_status='Active', branchname=COL_NAME, dist_tag='.tst' + COL_NAME[:20], git_branch_name='test', kojiname='test', ) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], ['Collection "%s" created' % COL_NAME]) @auth_only def test_02_create_package(self): ''' Test the create_package function. ''' out = self.pkgdb.create_package(pkgname=PKG_NAME, summary='Test package', description='Test package desc', review_url='https://bz.com', status='Approved', shouldopen=False, branches=COL_NAME, poc='pingou', upstream_url='http://guake.org', critpath=False) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], ['Package created']) @auth_only def test_03_orphan_packages(self): ''' Test the orphan_packages function. ''' out = self.pkgdb.orphan_packages('guake', ['master', 'el6']) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou changed point of contact of package: guake from: ' 'pingou to: orphan on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'pingou to: orphan on branch: el6' ]) @auth_only def test_04_unorphan_packages(self): ''' Test the unorphan_packages function. ''' out = self.pkgdb.unorphan_packages('guake', ['master', 'el6'], 'pingou') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'Package guake has been unorphaned on master by pingou', 'Package guake has been unorphaned on el6 by pingou' ]) @auth_only def test_05_retire_packages(self): ''' Test the retire_packages function. ''' out = self.pkgdb.retire_packages('guake', 'master') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou updated package: guake status from: Approved to ' 'Retired on branch: master' ]) @auth_only def test_06_unretire_packages(self): ''' Test the unretire_packages function. ''' out = self.pkgdb.unretire_packages('guake', 'master') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou updated package: guake status from: Retired to ' 'Approved on branch: master' ]) @auth_only def test_07_update_acl(self): ''' Test the update_acl function. ''' # After un-retiring the package on master, we need to re-set Ralph's # pending ACL request out = self.pkgdb.update_acl('guake', ['master', 'el6'], 'commit', 'Awaiting Review', 'ralph') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou set for ralph acl: commit of package: guake from: ' 'Obsolete to: Awaiting Review on branch: master', 'Nothing to update on branch: el6 for acl: commit' ]) # Check the output when we try to change an ACL to what it is already out = self.pkgdb.update_acl('guake', ['master', 'el6'], 'commit', 'Awaiting Review', 'ralph') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'Nothing to update on branch: master for acl: commit', 'Nothing to update on branch: el6 for acl: commit' ]) @auth_only def test_08_update_collection_status(self): ''' Test the update_collection_status function. ''' out = self.pkgdb.update_collection_status(COL_NAME, 'EOL') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], ['Collection updated from "Active" to "EOL"']) @auth_only def test_09_update_package_poc(self): ''' Test the update_package_poc function. ''' out = self.pkgdb.update_package_poc('guake', ['master', 'el6'], 'ralph') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou changed point of contact of package: guake from: ' 'orphan to: ralph on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'pingou to: ralph on branch: el6' ]) out = self.pkgdb.update_package_poc('guake', ['master', 'el6'], 'pingou') self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'user: pingou changed point of contact of package: guake from: ' 'ralph to: pingou on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'ralph to: pingou on branch: el6' ]) @auth_only def test_10_update_critpath(self): ''' Test the update_critpath function. ''' # Check before changing the critpath out = self.pkgdb.get_package('guake') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual(critpaths, [False, False, False, False, False]) self.assertEqual(branches, ['master', 'el6', 'f19', 'f20', 'f21']) out = self.pkgdb.update_critpath('guake', ['master', 'el6'], True) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'guake: critpath updated on master to True', 'guake: critpath updated on el6 to True' ]) # Check after changing the critpath out = self.pkgdb.get_package('guake') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual(critpaths, [True, True, False, False, False]) self.assertEqual(branches, ['master', 'el6', 'f19', 'f20', 'f21']) out = self.pkgdb.get_package('guake') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) self.assertEqual(out['packages'][0]['collection']['branchname'], 'master') self.assertEqual(out['packages'][0]['package']['name'], 'guake') self.assertEqual(out['packages'][0]['point_of_contact'], 'pingou') out = self.pkgdb.update_critpath('guake', ['master', 'el6'], False) self.assertEqual(sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual(out['messages'], [ 'guake: critpath updated on master to False', 'guake: critpath updated on el6 to False' ]) # Check after reste critpath to False out = self.pkgdb.get_package('guake') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual(critpaths, [False, False, False, False, False]) self.assertEqual(branches, ['master', 'el6', 'f19', 'f20', 'f21'])
class TestPkgdDB(unittest.TestCase): ''' Un-authenticated pkgdb2 tests. ''' def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True) def test_get_collection(self): ''' Test the get_collections function. ''' out = self.pkgdb.get_collections() self.assertEqual( sorted(out.keys()), ['collections', 'output']) self.assertTrue(len(out['collections']) >= 30) out = self.pkgdb.get_collections(pattern='f19') self.assertEqual(len(out['collections']), 1) self.assertEqual(out['collections'][0]['branchname'], 'f19') self.assertEqual( sorted(out.keys()), ['collections', 'output']) out = self.pkgdb.get_collections( clt_status=['EOL', 'Under Development']) self.assertEqual( sorted(out.keys()), ['collections', 'output']) self.assertTrue(len(out['collections']) >= 25) def test_get_package(self): ''' Test the get_package function. ''' out = self.pkgdb.get_package('guake') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) self.assertEqual( out['packages'][0]['collection']['branchname'], 'master') self.assertEqual( out['packages'][0]['package']['name'], 'guake') self.assertEqual( out['packages'][0]['point_of_contact'], 'pingou') out = self.pkgdb.get_package('guake', 'f20') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 1) self.assertEqual( out['packages'][0]['collection']['branchname'], 'f20') self.assertEqual( out['packages'][0]['package']['name'], 'guake') out = self.pkgdb.get_package('guake', ['f20', 'f19']) self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 2) self.assertEqual( out['packages'][0]['collection']['branchname'], 'f19') self.assertEqual( out['packages'][1]['collection']['branchname'], 'f20') self.assertEqual( out['packages'][0]['package']['name'], 'guake') self.assertEqual( out['packages'][1]['package']['name'], 'guake') def test_get_packager_acls(self): ''' Test the get_packager_acls function. ''' out = self.pkgdb.get_packager_acls('pingou') self.assertEqual( sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertEqual(len(out['acls']), 250) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packager_acls('pingou', acls=['approveacls']) self.assertEqual( sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertTrue(len(out['acls']) >= 239) self.assertEqual(out['page_total'], 2) out = self.pkgdb.get_packager_acls('pingou', page=3) self.assertEqual( sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertTrue(len(out['acls']) >= 250) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packager_acls('pingou', count=True) self.assertEqual( sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 1043) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packager_acls('pingou', poc=True, count=True) self.assertEqual( sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 750) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packager_acls('pingou', poc=False, count=True) self.assertEqual( sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 239) self.assertEqual(out['page_total'], 1) def test_get_packager_stats(self): ''' Test the get_packager_stats function. ''' out = self.pkgdb.get_packager_stats('pingou') self.assertEqual( sorted(out.keys()), ['el5', 'el6', 'epel7', 'f19', 'f20', 'f21', 'master', 'output']) self.assertEqual( sorted(out['master'].keys()), ['co-maintainer', 'point of contact']) self.assertTrue(out['master']['point of contact'] >= 50) def test_get_packagers(self): ''' Test the get_packagers function. ''' out = self.pkgdb.get_packagers('ping*') self.assertEqual( sorted(out.keys()), ['output', 'packagers']) self.assertEqual(out['packagers'], ['pingou']) def test_get_packages(self): ''' Test the get_packages function. ''' out = self.pkgdb.get_packages('gua*') expected_keys = [ 'acls', 'creation_date', 'description', 'monitor', 'name', 'review_url', 'status', 'summary', 'upstream_url', ] self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 10) self.assertEqual(out['packages'][0]['name'], 'guacamole-client') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['packages'][1]['name'], 'guacamole-common') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', branches='el6') self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 7) self.assertEqual(out['packages'][0]['name'], 'guacamole-common') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['packages'][1]['name'], 'guacamole-ext') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', poc='pingou') self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 1) self.assertEqual(out['packages'][0]['name'], 'guake') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', status='Retired') self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 5) self.assertEqual(out['packages'][0]['name'], 'guacd') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('g*', orphaned=True) self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertTrue(len(out['packages']) >= 44) self.assertEqual(out['packages'][0]['name'], 'ghex') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) #self.assertEqual(out['packages'][1]['name'], 'glom') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', poc='pingou', acls=True) self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 1) self.assertEqual(out['packages'][0]['name'], 'guake') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('g*', page=2) self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 250) self.assertEqual(out['packages'][0]['name'], 'ghc-parameterized-data') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 2) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packages('g*', count=True) self.assertEqual( sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertTrue(out['packages'] >= 1340) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1)
def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True)
# license. """ from fedora.client import (AppError, ServerError) import argparse import logging from six.moves import input from pkgdb2client import PkgDB, PkgDBException, __version__ from pkgdb2client.cli import ActionError import pkgdb2client import pkgdb2client.utils as utils PKGDBCLIENT = PkgDB('https://admin.fedoraproject.org/pkgdb', login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-admin") def setup_parser(): ''' Set the main arguments. ''' parser = argparse.ArgumentParser(prog="pkgdb-admin")
class TestPkgdDBAuth(unittest.TestCase): ''' Authenticated pkgdb2 tests. ''' @auth_only def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True) self.pkgdb.login(USERNAME, PASSWORD) @auth_only def test_01_create_collection(self): ''' Test the create_collection function. ''' out = self.pkgdb.create_collection( clt_name='Test', version=VERSION, clt_status='Active', branchname=COL_NAME, dist_tag='.tst' + COL_NAME[:20], git_branch_name='test', kojiname='test', ) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Collection "%s" created' % COL_NAME]) self.assertRaises( PkgDBException, self.pkgdb.create_collection, clt_name='Test', version=VERSION, clt_status='Active', branchname=COL_NAME, dist_tag='.tst' + COL_NAME[:20], git_branch_name='test', kojiname='test', ) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Collection "%s" created' % COL_NAME]) @auth_only def test_02_create_package(self): ''' Test the create_package function. ''' out = self.pkgdb.create_package( pkgname=PKG_NAME, summary='Test package', description='Test package desc', review_url='https://bz.com', status='Approved', shouldopen=False, branches=COL_NAME, poc='pingou', upstream_url='http://guake.org', critpath=False) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Package created']) @auth_only def test_03_orphan_packages(self): ''' Test the orphan_packages function. ''' out = self.pkgdb.orphan_packages('guake', ['master', 'el6']) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou changed point of contact of package: guake from: ' 'pingou to: orphan on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'pingou to: orphan on branch: el6']) @auth_only def test_04_unorphan_packages(self): ''' Test the unorphan_packages function. ''' out = self.pkgdb.unorphan_packages( 'guake', ['master', 'el6'], 'pingou') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Package guake has been unorphaned on master by pingou', 'Package guake has been unorphaned on el6 by pingou']) @auth_only def test_05_retire_packages(self): ''' Test the retire_packages function. ''' out = self.pkgdb.retire_packages('guake', 'master') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou updated package: guake status from: Approved to ' 'Retired on branch: master']) @auth_only def test_06_unretire_packages(self): ''' Test the unretire_packages function. ''' out = self.pkgdb.unretire_packages('guake', 'master') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou updated package: guake status from: Retired to ' 'Approved on branch: master']) @auth_only def test_07_update_acl(self): ''' Test the update_acl function. ''' # After un-retiring the package on master, we need to re-set Ralph's # pending ACL request out = self.pkgdb.update_acl( 'guake', ['master', 'el6'], 'commit', 'Awaiting Review', 'ralph') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou set for ralph acl: commit of package: guake from: ' 'Obsolete to: Awaiting Review on branch: master', 'Nothing to update on branch: el6 for acl: commit']) # Check the output when we try to change an ACL to what it is already out = self.pkgdb.update_acl( 'guake', ['master', 'el6'], 'commit', 'Awaiting Review', 'ralph') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Nothing to update on branch: master for acl: commit', 'Nothing to update on branch: el6 for acl: commit']) @auth_only def test_08_update_collection_status(self): ''' Test the update_collection_status function. ''' out = self.pkgdb.update_collection_status(COL_NAME, 'EOL') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['Collection updated from "Active" to "EOL"']) @auth_only def test_09_update_package_poc(self): ''' Test the update_package_poc function. ''' out = self.pkgdb.update_package_poc( 'guake', ['master', 'el6'], 'ralph') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou changed point of contact of package: guake from: ' 'orphan to: ralph on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'pingou to: ralph on branch: el6']) out = self.pkgdb.update_package_poc( 'guake', ['master', 'el6'], 'pingou') self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['user: pingou changed point of contact of package: guake from: ' 'ralph to: pingou on branch: master', 'user: pingou changed point of contact of package: guake from: ' 'ralph to: pingou on branch: el6']) @auth_only def test_10_update_critpath(self): ''' Test the update_critpath function. ''' # Check before changing the critpath out = self.pkgdb.get_package('guake') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual( critpaths, [False, False, False, False, False] ) self.assertEqual( branches, ['master', 'el6', 'f19', 'f20', 'f21'] ) out = self.pkgdb.update_critpath( 'guake', ['master', 'el6'], True) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['guake: critpath updated on master to True', 'guake: critpath updated on el6 to True'] ) # Check after changing the critpath out = self.pkgdb.get_package('guake') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual( critpaths, [True, True, False, False, False] ) self.assertEqual( branches, ['master', 'el6', 'f19', 'f20', 'f21'] ) out = self.pkgdb.get_package('guake') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) self.assertEqual( out['packages'][0]['collection']['branchname'], 'master') self.assertEqual( out['packages'][0]['package']['name'], 'guake') self.assertEqual( out['packages'][0]['point_of_contact'], 'pingou') out = self.pkgdb.update_critpath( 'guake', ['master', 'el6'], False) self.assertEqual( sorted(out.keys()), ['messages', 'output']) self.assertEqual(out['output'], 'ok') self.assertEqual( out['messages'], ['guake: critpath updated on master to False', 'guake: critpath updated on el6 to False'] ) # Check after reste critpath to False out = self.pkgdb.get_package('guake') self.assertEqual( sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) critpaths = [el['critpath'] for el in out['packages']] branches = [el['collection']['branchname'] for el in out['packages']] self.assertEqual( critpaths, [False, False, False, False, False] ) self.assertEqual( branches, ['master', 'el6', 'f19', 'f20', 'f21'] )
class FedoraPlugin: """A plugin is a class which take the IrcBot as argument """ requires = [ 'irc3.plugins.core', 'irc3.plugins.command', ] def __init__(self, bot): self.bot = bot fas_url = bot.config['fas']['url'] fas_username = bot.config['fas']['username'] fas_password = bot.config['fas']['password'] self.fasclient = AccountSystem(fas_url, username=fas_username, password=fas_password) #self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] self.pkgdb = PkgDB() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config) @staticmethod def _future_meetings(location): if not location.endswith('@irc.freenode.net'): location = '*****@*****.**' % location meetings = FedoraPlugin._query_fedocal(location=location) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now < dt: yield dt, meeting @staticmethod def _meetings_for(calendar): meetings = FedoraPlugin._query_fedocal(calendar=calendar) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) start = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") string = "%s %s" % (meeting['meeting_date_end'], meeting['meeting_time_stop']) end = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now >= start and now <= end: yield meeting @staticmethod def _query_fedocal(**kwargs): url = 'https://apps.fedoraproject.org/calendar/api/meetings' return requests.get(url, params=kwargs).json()['meetings'] @command def admins(self, mask, target, args): """admins <group name> Return the administrators list for the selected group %%admins <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'administrator': sponsors += person['username'] + ' ' msg = 'Administrators for %s: %s' % (name, sponsors) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def badges(self, mask, target, args): """badges <username> Return badges statistics about a user. %%badges <username> """ name = args['<username>'] url = "https://badges.fedoraproject.org/user/" + name d = requests.get(url + "/json").json() if 'error' in d: response = d['error'] else: template = "{name} has unlocked {n} Fedora Badges: {url}" n = len(d['assertions']) response = template.format(name=name, url=url, n=n) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def branches(self, mask, target, args): """branches <package> Return the branches a package is in. %%branches <package> """ package = args['<package>'] try: pkginfo = self.pkgdb.get_package(package) except AppError: msg = "No such package exists." self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return branch_list = [] for listing in pkginfo['packages']: branch_list.append(listing['collection']['branchname']) branch_list.sort() msg = ' '.join(branch_list) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def fas(self, mask, target, args): """fas <pattern> Searches a pattern in the list of FAS user %%fas <pattern> """ users = self.fasclient.people_query( constraints={ #'username': args['<pattern>'], 'ircnick': args['<pattern>'], }, columns=['username', 'ircnick', 'email']) if users: msg = ', '.join([ '%s (%s) <%s>' % (user.username, user.ircnick, user.email) for user in users ]) else: msg = 'No user matching found' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def fasinfo(self, mask, target, args): """fasinfo <pattern> Return more information about the specified user %%fasinfo <username> """ name = args['<username>'] try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info for user: "******"' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return if not person: msg = 'User "%s" doesn\'t exist' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return person['creation'] = person['creation'].split(' ')[0] string = ("User: %(username)s, Name: %(human_name)s" ", email: %(email)s, Creation: %(creation)s" ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s" ", Locale: %(locale)s" ", GPG key ID: %(gpg_keyid)s, Status: %(status)s") % person self.bot.privmsg(target, '%s: %s' % (mask.nick, string)) # List of unapproved groups is easy unapproved = '' for group in person['unapproved_memberships']: unapproved = unapproved + "%s " % group['name'] if unapproved != '': msg = 'Unapproved Groups: %s' % unapproved self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) # List of approved groups requires a separate query to extract roles constraints = { 'username': name, 'group': '%', 'role_status': 'approved' } columns = ['username', 'group', 'role_type'] roles = [] try: roles = self.fasclient.people_query(constraints=constraints, columns=columns) except: msg = 'Error getting group memberships.' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return approved = '' for role in roles: if role['role_type'] == 'sponsor': approved += '+' + role['group'] + ' ' elif role['role_type'] == 'administrator': approved += '@' + role['group'] + ' ' else: approved += role['group'] + ' ' if approved == '': approved = "None" msg = 'Approved Groups: %s' % approved self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def group(self, mask, target, args): """group <group short name> Return information about a Fedora Account System group. %%group <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_by_name(name) msg = '%s: %s' % (name, group['display_name']) except AppError: msg = 'There is no group "%s".' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def hellomynameis(self, mask, target, args): """hellomynameis <username> Return brief information about a Fedora Account System username. Useful for things like meeting roll call and calling attention to yourself. %%hellomynameis <username> """ name = args['<username>'] msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Something blew up, please try again' if not person: msg = 'Sorry, but you don\'t exist' else: msg = '%(username)s \'%(human_name)s\' <%(email)s>' % person if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def himynameis(self, mask, target, args): """himynameis <username> Return information about a Fedora Account System group. %%himynameis <username> """ name = args['<username>'] msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Something blew up, please try again' if not person: msg = 'Sorry, but you don\'t exist' else: msg = '%(username)s \'Slim Shady\' <%(email)s>' % person if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def localtime(self, mask, target, args): """localtime <username> Returns the current time of the user. The timezone is queried from FAS. %%localtime <username> """ name = args['<username>'] try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info user user: "******"' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return if not person: msg = 'User "%s" doesn\'t exist' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return timezone_name = person['timezone'] if timezone_name is None: msg = 'User "%s" doesn\'t share his timezone' % name self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: msg = 'The timezone of "%s" was unknown: "%s"' % (name, timezone_name) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return msg = 'The current local time of "%s" is: "%s" (timezone: %s)' % ( name, time.strftime('%H:%M'), timezone_name) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def members(self, mask, target, args): """members <group short name> Return the list of members for the selected group %%members <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) members = '' for person in group: if person['role_type'] == 'administrator': members += '@' + person['username'] + ' ' elif person['role_type'] == 'sponsor': members += '+' + person['username'] + ' ' else: members += person['username'] + ' ' msg = 'Members of %s: %s' % (name, members) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def nextmeeting(self, mask, target, args): """nextmeeting <channel> Return the next meeting scheduled for a particular channel. %%nextmeeting <channel> """ channel = args['<channel>'] channel = channel.strip('#').split('@')[0] meetings = sorted(self._future_meetings(channel), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled for #%s." % channel self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) return for date, meeting in islice(meetings, 0, 3): response = "In #%s is %s (starting %s)" % ( channel, meeting['meeting_name'], arrow.get(date).humanize(), ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) base = "https://apps.fedoraproject.org/calendar/location/" url = base + urllib.quote("%[email protected]/" % channel) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def nextmeetings(self, mask, target, args): """nextmeetings Return the next meetings scheduled for any channel(s). %%nextmeetings """ msg = 'One moment, please... Looking up the channel list.' self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) url = 'https://apps.fedoraproject.org/calendar/api/locations/' locations = requests.get(url).json()['locations'] meetings = sorted(chain(*[ self._future_meetings(location) for location in locations if 'irc.freenode.net' in location ]), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled at all." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) return for date, meeting in islice(meetings, 0, 5): response = "In #%s is %s (starting %s)" % ( meeting['meeting_location'].split('@')[0].strip(), meeting['meeting_name'], arrow.get(date).humanize(), ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def pushduty(self, mask, target, args): """pushduty Return the list of people who are on releng push duty right now. %%pushduty """ def get_persons(): for meeting in self._meetings_for('release-engineering'): yield meeting['meeting_name'] persons = list(get_persons()) url = "https://apps.fedoraproject.org/" + \ "calendar/release-engineering/" if not persons: response = "Nobody is listed as being on push duty right now..." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) return persons = ", ".join(persons) response = "The following people are on push duty: %s" % persons self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def quote(self, mask, target, args): """quote <SYMBOL> [daily, weekly, monthly, quarterly] Return some datagrepper statistics on fedmsg categories. %%quote <symbol> <frame> """ symbol = args['<symbol>'] frame = 'daily' if '<frame>' in args: frame = args['<frame>'] # Second, build a lookup table for symbols. By default, we'll use the # fedmsg category names, take their first 3 characters and uppercase # them. That will take things like "wiki" and turn them into "WIK" and # "bodhi" and turn them into "BOD". This handles a lot for us. We'll # then override those that don't make sense manually here. For # instance "fedoratagger" by default would be "FED", but that's no # good. We want "TAG". # Why all this trouble? Well, as new things get added to the fedmsg # bus, we don't want to have keep coming back here and modifying this # code. Hopefully this dance will at least partially future-proof us. symbols = dict([(processor.__name__.lower(), processor.__name__[:3].upper()) for processor in fedmsg.meta.processors]) symbols.update({ 'fedoratagger': 'TAG', 'fedbadges': 'BDG', 'buildsys': 'KOJ', 'pkgdb': 'PKG', 'meetbot': 'MTB', 'planet': 'PLN', 'trac': 'TRC', 'mailman': 'MM3', }) # Now invert the dict so we can lookup the argued symbol. # Yes, this is vulnerable to collisions. symbols = dict([(sym, name) for name, sym in symbols.items()]) # These aren't user-facing topics, so drop 'em. del symbols['LOG'] del symbols['UNH'] del symbols['ANN'] # And this one is unused... key_fmt = lambda d: ', '.join(sorted(d.keys())) if symbol not in symbols: response = "No such symbol %r. Try one of %s" msg = response % (symbol, key_fmt(symbols)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return # Now, build another lookup of our various timeframes. frames = dict( daily=datetime.timedelta(days=1), weekly=datetime.timedelta(days=7), monthly=datetime.timedelta(days=30), quarterly=datetime.timedelta(days=91), ) if frame not in frames: response = "No such timeframe %r. Try one of %s" msg = response % (frame, key_fmt(frames)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return category = [symbols[symbol]] t2 = datetime.datetime.utcnow() t1 = t2 - frames[frame] t0 = t1 - frames[frame] # Count the number of messages between t0 and t1, and between t1 and t2 query1 = dict(start=t0, end=t1, category=category) query2 = dict(start=t1, end=t2, category=category) # Do this async for superfast datagrepper queries. tpool = ThreadPool() batched_values = tpool.map(datagrepper_query, [ dict(start=x, end=y, category=category) for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION) ] + [query1, query2]) count2 = batched_values.pop() count1 = batched_values.pop() # Just rename the results. We'll use the rest for the sparkline. sparkline_values = batched_values yester_phrases = dict( daily="yesterday", weekly="the week preceding this one", monthly="the month preceding this one", quarterly="the 3 months preceding these past three months", ) phrases = dict( daily="24 hours", weekly="week", monthly="month", quarterly="3 months", ) if count1 and count2: percent = ((float(count2) / count1) - 1) * 100 elif not count1 and count2: # If the older of the two time periods had zero messages, but there # are some in the more current period.. well, that's an infinite # percent increase. percent = float('inf') elif not count1 and not count2: # If counts are zero for both periods, then the change is 0%. percent = 0 else: # Else, if there were some messages in the old time period, but # none in the current... then that's a 100% drop off. percent = -100 sign = lambda value: value >= 0 and '+' or '-' template = u"{sym}, {name} {sign}{percent:.2f}% over {phrase}" response = template.format( sym=symbol, name=symbols[symbol], sign=sign(percent), percent=abs(percent), phrase=yester_phrases[frame], ) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) # Now, make a graph out of it. sparkline = Utils.sparkline(sparkline_values) template = u" {sparkline} ⤆ over {phrase}" response = template.format(sym=symbol, sparkline=sparkline, phrase=phrases[frame]) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # And a final line for "x-axis tics" t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1)) t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2)) padding = u" " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3) template = u" ↑ {t1}{padding}↑ {t2}" response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding) self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) @command def sponsors(self, mask, target, args): """sponsors <group short name> Return the sponsors list for the selected group %%sponsors <group_name> """ name = args['<group_name>'] msg = None try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'sponsor': sponsors += person['username'] + ' ' elif person['role_type'] == 'administrator': sponsors += '@' + person['username'] + ' ' msg = 'Sponsors for %s: %s' % (name, sponsors) except AppError: msg = 'There is no group %s.' % name if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def vacation(self, mask, target, args): """vacation Return the list of people who are on vacation right now according to fedocal. %%vacation """ def get_persons(): for meeting in self._meetings_for('vacation'): for manager in meeting['meeting_manager']: yield manager persons = list(get_persons()) if not persons: response = "Nobody is listed as being on vacation right now..." self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) url = "https://apps.fedoraproject.org/calendar/vacation/" self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) return persons = ", ".join(persons) response = "The following people are on vacation: %s" % persons self.bot.privmsg(target, '%s: %s' % (mask.nick, response)) url = "https://apps.fedoraproject.org/calendar/vacation/" self.bot.privmsg(target, '%s: - %s' % (mask.nick, url)) @command def what(self, mask, target, args): """what <package> Returns a description of a given package. %%what <package> """ package = args['<package>'] msg = None try: summary = self.bugzacl['Fedora'][package]['summary'] msg = "%s: %s" % (package, summary) except KeyError: msg = "No such package exists." if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def whoowns(self, mask, target, args): """whoowns <package> Return more information about the specified user %%whoowns <package> """ package = args['<package>'] try: mainowner = self.bugzacl['Fedora'][package]['owner'] except KeyError: msg = "No such package exists." self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) return others = [] for key in self.bugzacl: if key == 'Fedora': continue try: owner = self.bugzacl[key][package]['owner'] if owner == mainowner: continue except KeyError: continue others.append("%s in %s" % (owner, key)) if others == []: self.bot.privmsg(target, '%s: %s' % (mask.nick, mainowner)) else: msg = "%s (%s)" % (mainowner, ', '.join(others)) self.bot.privmsg(target, '%s: %s' % (mask.nick, msg)) @command def wikilink(self, mask, target, args): """wikilink <username> Return MediaWiki link syntax for a FAS user's page on the wiki. %%wikilink <username> """ name = args['<username>'] person = msg = None try: person = self.fasclient.person_by_username(name) except: msg = 'Error getting info for user: "******"' % name if not person: msg = 'User "%s" doesn\'t exist' % name else: msg = "[[User:%s|%s]]" % (person["username"], person["human_name"] or '') if msg is not None: self.bot.privmsg(target, '%s: %s' % (mask.nick, msg))
def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True)
class TestPkgdDB(unittest.TestCase): ''' Un-authenticated pkgdb2 tests. ''' def setUp(self): """ set up data used in the tests. setUp is called before each test function execution. """ self.pkgdb = PkgDB(PKGDB_URL, insecure=True) def test_get_collection(self): ''' Test the get_collections function. ''' out = self.pkgdb.get_collections() self.assertEqual(sorted(out.keys()), ['collections', 'output']) self.assertTrue(len(out['collections']) >= 30) out = self.pkgdb.get_collections(pattern='f19') self.assertEqual(len(out['collections']), 1) self.assertEqual(out['collections'][0]['branchname'], 'f19') self.assertEqual(sorted(out.keys()), ['collections', 'output']) out = self.pkgdb.get_collections( clt_status=['EOL', 'Under Development']) self.assertEqual(sorted(out.keys()), ['collections', 'output']) self.assertTrue(len(out['collections']) >= 25) def test_get_package(self): ''' Test the get_package function. ''' out = self.pkgdb.get_package('guake') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 5) self.assertEqual(out['packages'][0]['collection']['branchname'], 'master') self.assertEqual(out['packages'][0]['package']['name'], 'guake') self.assertEqual(out['packages'][0]['point_of_contact'], 'pingou') out = self.pkgdb.get_package('guake', 'f20') self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 1) self.assertEqual(out['packages'][0]['collection']['branchname'], 'f20') self.assertEqual(out['packages'][0]['package']['name'], 'guake') out = self.pkgdb.get_package('guake', ['f20', 'f19']) self.assertEqual(sorted(out.keys()), ['output', 'packages']) self.assertEqual(out['output'], 'ok') self.assertEqual(len(out['packages']), 2) self.assertEqual(out['packages'][0]['collection']['branchname'], 'f19') self.assertEqual(out['packages'][1]['collection']['branchname'], 'f20') self.assertEqual(out['packages'][0]['package']['name'], 'guake') self.assertEqual(out['packages'][1]['package']['name'], 'guake') def test_get_packager_acls(self): ''' Test the get_packager_acls function. ''' out = self.pkgdb.get_packager_acls('pingou') self.assertEqual(sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertEqual(len(out['acls']), 250) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packager_acls('pingou', acls=['approveacls']) self.assertEqual(sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertTrue(len(out['acls']) >= 239) self.assertEqual(out['page_total'], 2) out = self.pkgdb.get_packager_acls('pingou', page=3) self.assertEqual(sorted(out.keys()), ['acls', 'output', 'page', 'page_total']) self.assertTrue(len(out['acls']) >= 250) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packager_acls('pingou', count=True) self.assertEqual(sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 1043) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packager_acls('pingou', poc=True, count=True) self.assertEqual(sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 750) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packager_acls('pingou', poc=False, count=True) self.assertEqual(sorted(out.keys()), ['acls_count', 'output', 'page', 'page_total']) self.assertTrue(out['acls_count'] >= 239) self.assertEqual(out['page_total'], 1) def test_get_packager_stats(self): ''' Test the get_packager_stats function. ''' out = self.pkgdb.get_packager_stats('pingou') self.assertEqual( sorted(out.keys()), ['el5', 'el6', 'epel7', 'f19', 'f20', 'f21', 'master', 'output']) self.assertEqual(sorted(out['master'].keys()), ['co-maintainer', 'point of contact']) self.assertTrue(out['master']['point of contact'] >= 50) def test_get_packagers(self): ''' Test the get_packagers function. ''' out = self.pkgdb.get_packagers('ping*') self.assertEqual(sorted(out.keys()), ['output', 'packagers']) self.assertEqual(out['packagers'], ['pingou']) def test_get_packages(self): ''' Test the get_packages function. ''' out = self.pkgdb.get_packages('gua*') expected_keys = [ 'acls', 'creation_date', 'description', 'monitor', 'name', 'review_url', 'status', 'summary', 'upstream_url', ] self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 10) self.assertEqual(out['packages'][0]['name'], 'guacamole-client') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['packages'][1]['name'], 'guacamole-common') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', branches='el6') self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 7) self.assertEqual(out['packages'][0]['name'], 'guacamole-common') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['packages'][1]['name'], 'guacamole-ext') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', poc='pingou') self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 1) self.assertEqual(out['packages'][0]['name'], 'guake') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', status='Retired') self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 5) self.assertEqual(out['packages'][0]['name'], 'guacd') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('g*', orphaned=True) self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertTrue(len(out['packages']) >= 44) self.assertEqual(out['packages'][0]['name'], 'ghex') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) #self.assertEqual(out['packages'][1]['name'], 'glom') self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('gua*', poc='pingou', acls=True) self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 1) self.assertEqual(out['packages'][0]['name'], 'guake') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1) out = self.pkgdb.get_packages('g*', page=2) self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertEqual(len(out['packages']), 250) self.assertEqual(out['packages'][0]['name'], 'ghc-parameterized-data') self.assertEqual(sorted(out['packages'][0].keys()), expected_keys) self.assertEqual(out['page'], 2) self.assertEqual(out['page_total'], 6) out = self.pkgdb.get_packages('g*', count=True) self.assertEqual(sorted(out.keys()), ['output', 'packages', 'page', 'page_total']) self.assertTrue(out['packages'] >= 1340) self.assertEqual(out['page'], 1) self.assertEqual(out['page_total'], 1)
""" from fedora.client import AppError, ServerError import argparse import requests import logging import koji from pkgdb2client import PkgDB, PkgDBException, __version__ from pkgdb2client.cli import ActionError import pkgdb2client import pkgdb2client.utils as utils PKGDBCLIENT = PkgDB("https://admin.fedoraproject.org/pkgdb", login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-admin") def setup_parser(): """ Set the main arguments. """ parser = argparse.ArgumentParser(prog="pkgdb-admin")
# license. """ from fedora.client import (AppError, ServerError) import argparse import requests import logging import koji from pkgdb2client import PkgDB, PkgDBException, __version__ from pkgdb2client.cli import ActionError import pkgdb2client import pkgdb2client.utils as utils PKGDBCLIENT = PkgDB('https://admin.fedoraproject.org/pkgdb', login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-admin") def setup_parser(): ''' Set the main arguments. ''' parser = argparse.ArgumentParser(prog="pkgdb-admin")
# Set Koschei monitoring flag for <PACKAGES> to <VALUE> in pkgdb2 instance at # <PKGDB_URL>. Login credentials are provided in <FAS_CONF> file. # Requires: packagedb-cli >= 2.9. PACKAGES = ['pkg1', 'pkg2'] VALUE = True PKGDB_URL = 'https://admin.stg.fedoraproject.org/pkgdb' FAS_CONF = '/etc/fas.conf' from pkgdb2client import PkgDB from ConfigParser import ConfigParser # Obtain FAS credentials conf = ConfigParser() conf.read(FAS_CONF) login = conf.get('global', 'login') password = conf.get('global', 'password') # Initiate authenticated pkgdb2 session pkgdb = PkgDB(PKGDB_URL) pkgdb.login(login, password) # Set package monitoring status one-by-one for package in PACKAGES: result = pkgdb.set_koschei_status(package, VALUE) message = result.get('messages', 'Invalid output') print "%s: %s" % (package, message) print "Done."
class Fedora(callbacks.Plugin): """Use this plugin to retrieve Fedora-related information.""" threaded = True def __init__(self, irc): super(Fedora, self).__init__(irc) # caches, automatically downloaded on __init__, manually refreshed on # .refresh self.userlist = None self.bugzacl = None # To get the information, we need a username and password to FAS. # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY! self.fasurl = self.registryValue('fas.url') self.username = self.registryValue('fas.username') self.password = self.registryValue('fas.password') self.fasclient = AccountSystem(self.fasurl, username=self.username, password=self.password) self.pkgdb = PkgDB() # URLs #self.url = {} # fetch necessary caches self._refresh() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config) def _refresh(self): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(None) self.log.info("Downloading user data") request = self.fasclient.send_request('/user/list', req_params={'search': '*'}, auth=True, timeout=240) users = request['people'] + request['unapproved_people'] del request self.log.info("Caching necessary user data") self.users = {} self.faslist = {} for user in users: name = user['username'] self.users[name] = {} self.users[name]['id'] = user['id'] key = ' '.join([ user['username'], user['email'] or '', user['human_name'] or '', user['ircnick'] or '' ]) key = key.lower() value = "%s '%s' <%s>" % (user['username'], user['human_name'] or '', user['email'] or '') self.faslist[key] = value self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] socket.setdefaulttimeout(timeout) def refresh(self, irc, msg, args): """takes no arguments Refresh the necessary caches.""" self._refresh() irc.replySuccess() refresh = wrap(refresh) def _load_json(self, url): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(45) json = simplejson.loads(utils.web.getUrl(url)) socket.setdefaulttimeout(timeout) return json def whoowns(self, irc, msg, args, package): """<package> Retrieve the owner of a given package """ try: mainowner = self.bugzacl['Fedora'][package]['owner'] except KeyError: irc.reply("No such package exists.") return others = [] for key in self.bugzacl: if key == 'Fedora': continue try: owner = self.bugzacl[key][package]['owner'] if owner == mainowner: continue except KeyError: continue others.append("%s in %s" % (owner, key)) if others == []: irc.reply(mainowner) else: irc.reply("%s (%s)" % (mainowner, ', '.join(others))) whoowns = wrap(whoowns, ['text']) def branches(self, irc, msg, args, package): """<package> Return the branches a package is in.""" try: pkginfo = self.pkgdb.get_package(package) except AppError: irc.reply("No such package exists.") return branch_list = [] for listing in pkginfo['packages']: branch_list.append(listing['collection']['branchname']) branch_list.sort() irc.reply(' '.join(branch_list)) return branches = wrap(branches, ['text']) def what(self, irc, msg, args, package): """<package> Returns a description of a given package. """ try: summary = self.bugzacl['Fedora'][package]['summary'] irc.reply("%s: %s" % (package, summary)) except KeyError: irc.reply("No such package exists.") return what = wrap(what, ['text']) def fas(self, irc, msg, args, find_name): """<query> Search the Fedora Account System usernames, full names, and email addresses for a match.""" find_name = to_unicode(find_name) matches = [] for entry in self.faslist.keys(): if entry.find(find_name.lower()) != -1: matches.append(entry) if len(matches) == 0: irc.reply("'%s' Not Found!" % find_name) else: output = [] for match in matches: output.append(self.faslist[match]) irc.reply(' - '.join(output).encode('utf-8')) fas = wrap(fas, ['text']) def hellomynameis(self, irc, msg, args, name): """<username> Return brief information about a Fedora Account System username. Useful for things like meeting roll call and calling attention to yourself.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'%(human_name)s\' <%(email)s>' % person).encode('utf-8')) hellomynameis = wrap(hellomynameis, ['text']) def himynameis(self, irc, msg, args, name): """<username> Will the real Slim Shady please stand up?""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'Slim Shady\' <%(email)s>' % person).encode('utf-8')) himynameis = wrap(himynameis, ['text']) def localtime(self, irc, msg, args, name): """<username> Returns the current time of the user. The timezone is queried from FAS.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info user user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return timezone_name = person['timezone'] if timezone_name is None: irc.reply('User "%s" doesn\'t share his timezone' % name) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: irc.reply('The timezone of "%s" was unknown: "%s"' % (name, timezone)) return irc.reply('The current local time of "%s" is: "%s" (timezone: %s)' % (name, time.strftime('%H:%M'), timezone_name)) localtime = wrap(localtime, ['text']) def fasinfo(self, irc, msg, args, name): """<username> Return information on a Fedora Account System username.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return person['creation'] = person['creation'].split(' ')[0] string = ("User: %(username)s, Name: %(human_name)s" ", email: %(email)s, Creation: %(creation)s" ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s" ", Locale: %(locale)s" ", GPG key ID: %(gpg_keyid)s, Status: %(status)s") % person irc.reply(string.encode('utf-8')) # List of unapproved groups is easy unapproved = '' for group in person['unapproved_memberships']: unapproved = unapproved + "%s " % group['name'] if unapproved != '': irc.reply('Unapproved Groups: %s' % unapproved) # List of approved groups requires a separate query to extract roles constraints = { 'username': name, 'group': '%', 'role_status': 'approved' } columns = ['username', 'group', 'role_type'] roles = [] try: roles = self.fasclient.people_query(constraints=constraints, columns=columns) except: irc.reply('Error getting group memberships.') return approved = '' for role in roles: if role['role_type'] == 'sponsor': approved += '+' + role['group'] + ' ' elif role['role_type'] == 'administrator': approved += '@' + role['group'] + ' ' else: approved += role['group'] + ' ' if approved == '': approved = "None" irc.reply('Approved Groups: %s' % approved) fasinfo = wrap(fasinfo, ['text']) def group(self, irc, msg, args, name): """<group short name> Return information about a Fedora Account System group.""" try: group = self.fasclient.group_by_name(name) irc.reply('%s: %s' % (name, group['display_name'])) except AppError: irc.reply('There is no group "%s".' % name) group = wrap(group, ['text']) def admins(self, irc, msg, args, name): """<group short name> Return the administrators list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'administrator': sponsors += person['username'] + ' ' irc.reply('Administrators for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) admins = wrap(admins, ['text']) def sponsors(self, irc, msg, args, name): """<group short name> Return the sponsors list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'sponsor': sponsors += person['username'] + ' ' elif person['role_type'] == 'administrator': sponsors += '@' + person['username'] + ' ' irc.reply('Sponsors for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) sponsors = wrap(sponsors, ['text']) def members(self, irc, msg, args, name): """<group short name> Return a list of members of the specified group""" try: group = self.fasclient.group_members(name) members = '' for person in group: if person['role_type'] == 'administrator': members += '@' + person['username'] + ' ' elif person['role_type'] == 'sponsor': members += '+' + person['username'] + ' ' else: members += person['username'] + ' ' irc.reply('Members of %s: %s' % (name, members)) except AppError: irc.reply('There is no group %s.' % name) members = wrap(members, ['text']) def showticket(self, irc, msg, args, baseurl, number): """<baseurl> <number> Return the name and URL of a trac ticket or bugzilla bug. """ url = format(baseurl, str(number)) size = conf.supybot.protocols.http.peekSize() text = utils.web.getUrl(url, size=size) parser = Title() try: parser.feed(text) except sgmllib.SGMLParseError: irc.reply(format('Encountered a problem parsing %u', url)) if parser.title: irc.reply(utils.web.htmlToText(parser.title.strip()) + ' - ' + url) else: irc.reply( format( 'That URL appears to have no HTML title ' + 'within the first %i bytes.', size)) showticket = wrap(showticket, ['httpUrl', 'int']) def swedish(self, irc, msg, args): """takes no arguments Humor mmcgrath.""" # Import this here to avoid a circular import problem. from __init__ import __version__ irc.reply(str('kwack kwack')) irc.reply(str('bork bork bork')) irc.reply(str('(supybot-fedora version %s)' % __version__)) swedish = wrap(swedish) def wikilink(self, irc, msg, args, name): """<username> Return MediaWiki link syntax for a FAS user's page on the wiki.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return string = "[[User:%s|%s]]" % (person["username"], person["human_name"] or '') irc.reply(string.encode('utf-8')) wikilink = wrap(wikilink, ['text']) def mirroradmins(self, irc, msg, args, hostname): """<hostname> Return MirrorManager list of FAS usernames which administer <hostname>. <hostname> must be the FQDN of the host.""" url = ("https://admin.fedoraproject.org/mirrormanager/mirroradmins?" "tg_format=json&host=" + hostname) result = self._load_json(url)['values'] if len(result) == 0: irc.reply('Hostname "%s" not found' % hostname) return string = 'Mirror Admins of %s: ' % hostname string += ' '.join(result) irc.reply(string.encode('utf-8')) mirroradmins = wrap(mirroradmins, ['text']) def nextmeeting(self, irc, msg, args, channel): """<channel> Return the next meeting scheduled for a particular channel. """ channel = channel.strip('#').split('@')[0] meetings = list(self._future_meetings(channel)) if not meetings: response = "There are no meetings scheduled for #%s." % channel irc.reply(response.encode('utf-8')) return date, meeting = meetings[0] response = "The next meeting in #%s is %s (starting %s)" % ( channel, meeting['meeting_name'], arrow.get(date).humanize(), ) irc.reply(response.encode('utf-8')) base = "https://apps.fedoraproject.org/calendar/location/" url = base + urllib.quote("%[email protected]/" % channel) irc.reply("- " + url.encode('utf-8')) nextmeeting = wrap(nextmeeting, ['text']) @staticmethod def _future_meetings(channel): response = requests.get( 'https://apps.fedoraproject.org/calendar/api/meetings', params=dict(location='*****@*****.**' % channel, )) data = response.json() now = datetime.datetime.utcnow() for meeting in data['meetings']: string = meeting['meeting_date'] + " " + meeting[ 'meeting_time_start'] dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now < dt: yield dt, meeting def badges(self, irc, msg, args, name): """<username> Return badges statistics about a user. """ url = "https://badges.fedoraproject.org/user/" + name d = requests.get(url + "/json").json() if 'error' in d: response = d['error'] else: template = "{name} has unlocked {n} Fedora Badges: {url}" n = len(d['assertions']) response = template.format(name=name, url=url, n=n) irc.reply(response.encode('utf-8')) badges = wrap(badges, ['text']) def quote(self, irc, msg, args, arguments): """<SYMBOL> [daily, weekly, monthly, quarterly] Return some datagrepper statistics on fedmsg categories. """ # First, some argument parsing. Supybot should be able to do this for # us, but I couldn't figure it out. The supybot.plugins.additional # object is the thing to use... except its weird. tokens = arguments.split(None, 1) if len(tokens) == 1: symbol, frame = tokens[0], 'daily' else: symbol, frame = tokens # Second, build a lookup table for symbols. By default, we'll use the # fedmsg category names, take their first 3 characters and uppercase # them. That will take things like "wiki" and turn them into "WIK" and # "bodhi" and turn them into "BOD". This handles a lot for us. We'll # then override those that don't make sense manually here. For # instance "fedoratagger" by default would be "FED", but that's no # good. We want "TAG". # Why all this trouble? Well, as new things get added to the fedmsg # bus, we don't want to have keep coming back here and modifying this # code. Hopefully this dance will at least partially future-proof us. symbols = dict([(processor.__name__.lower(), processor.__name__[:3].upper()) for processor in fedmsg.meta.processors]) symbols.update({ 'fedoratagger': 'TAG', 'fedbadges': 'BDG', 'buildsys': 'KOJ', 'pkgdb': 'PKG', 'meetbot': 'MTB', 'planet': 'PLN', 'trac': 'TRC', 'mailman': 'MM3', }) # Now invert the dict so we can lookup the argued symbol. # Yes, this is vulnerable to collisions. symbols = dict([(sym, name) for name, sym in symbols.items()]) # These aren't user-facing topics, so drop 'em. del symbols['LOG'] del symbols['UNH'] del symbols['ANN'] # And this one is unused... key_fmt = lambda d: ', '.join(sorted(d.keys())) if not symbol in symbols: response = "No such symbol %r. Try one of %s" irc.reply((response % (symbol, key_fmt(symbols))).encode('utf-8')) return # Now, build another lookup of our various timeframes. frames = dict( daily=datetime.timedelta(days=1), weekly=datetime.timedelta(days=7), monthly=datetime.timedelta(days=30), quarterly=datetime.timedelta(days=91), ) if not frame in frames: response = "No such timeframe %r. Try one of %s" irc.reply((response % (frame, key_fmt(frames))).encode('utf-8')) return category = [symbols[symbol]] t2 = datetime.datetime.now() t1 = t2 - frames[frame] t0 = t1 - frames[frame] # Count the number of messages between t0 and t1, and between t1 and t2 query1 = dict(start=t0, end=t1, category=category) query2 = dict(start=t1, end=t2, category=category) # Do this async for superfast datagrepper queries. tpool = ThreadPool() batched_values = tpool.map(datagrepper_query, [ dict(start=x, end=y, category=category) for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION) ] + [query1, query2]) count2 = batched_values.pop() count1 = batched_values.pop() # Just rename the results. We'll use the rest for the sparkline. sparkline_values = batched_values yester_phrases = dict( daily="yesterday", weekly="the week preceding this one", monthly="the month preceding this one", quarterly="the 3 months preceding these past three months", ) phrases = dict( daily="24 hours", weekly="week", monthly="month", quarterly="3 months", ) if count1 and count2: percent = ((float(count2) / count1) - 1) * 100 elif not count1 and count2: # If the older of the two time periods had zero messages, but there # are some in the more current period.. well, that's an infinite # percent increase. percent = float('inf') elif not count1 and not count2: # If counts are zero for both periods, then the change is 0%. percent = 0 else: # Else, if there were some messages in the old time period, but # none in the current... then that's a 100% drop off. percent = -100 sign = lambda value: value >= 0 and '+' or '-' template = u"{sym}, {name} {sign}{percent:.2f}% over {phrase}" response = template.format( sym=symbol, name=symbols[symbol], sign=sign(percent), percent=abs(percent), phrase=yester_phrases[frame], ) irc.reply(response.encode('utf-8')) # Now, make a graph out of it. sparkline = Utils.sparkline(sparkline_values) template = u" {sparkline} ⤆ over {phrase}" response = template.format(sym=symbol, sparkline=sparkline, phrase=phrases[frame]) irc.reply(response.encode('utf-8')) to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # And a final line for "x-axis tics" t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1)) t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2)) padding = u" " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3) template = u" ↑ {t1}{padding}↑ {t2}" response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding) irc.reply(response.encode('utf-8')) quote = wrap(quote, ['text'])
class Fedora(callbacks.Plugin): """Use this plugin to retrieve Fedora-related information.""" threaded = True def __init__(self, irc): super(Fedora, self).__init__(irc) # caches, automatically downloaded on __init__, manually refreshed on # .refresh self.userlist = None self.bugzacl = None # To get the information, we need a username and password to FAS. # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY! self.fasurl = self.registryValue('fas.url') self.username = self.registryValue('fas.username') self.password = self.registryValue('fas.password') self.fasclient = AccountSystem(self.fasurl, username=self.username, password=self.password) self.pkgdb = PkgDB() # URLs # self.url = {} self.github_oauth_token = self.registryValue('github.oauth_token') # fetch necessary caches self._refresh() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config) def _refresh(self): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(None) self.log.info("Downloading user data") request = self.fasclient.send_request('/user/list', req_params={'search': '*'}, auth=True, timeout=240) users = request['people'] + request['unapproved_people'] del request self.log.info("Caching necessary user data") self.users = {} self.faslist = {} for user in users: name = user['username'] self.users[name] = {} self.users[name]['id'] = user['id'] key = ' '.join([user['username'], user['email'] or '', user['human_name'] or '', user['ircnick'] or '']) key = key.lower() value = "%s '%s' <%s>" % (user['username'], user['human_name'] or '', user['email'] or '') self.faslist[key] = value self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] socket.setdefaulttimeout(timeout) def refresh(self, irc, msg, args): """takes no arguments Refresh the necessary caches.""" self._refresh() irc.replySuccess() refresh = wrap(refresh) def _load_json(self, url): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(45) json = simplejson.loads(utils.web.getUrl(url)) socket.setdefaulttimeout(timeout) return json def pulls(self, irc, msg, args, slug): """<username/repo> List the pending pull requests on a github repo. """ if slug.count('/') != 1: irc.reply('Must be GitHub repo of the format <username/repo>') return username, repo = slug.strip().split('/') tmpl = "https://api.github.com/repos/{username}/{repo}/" + \ "pulls?per_page=100" url = tmpl.format(username=username, repo=repo) auth = dict(access_token=self.github_oauth_token) results = [] link = dict(next=url) while 'next' in link: response = requests.get(link['next'], params=auth) if response.status_code == 404: irc.reply('No such GitHub repository %r' % slug) return # And.. if we didn't get good results, just bail. if response.status_code != 200: raise IOError( "Non-200 status code %r; %r; %r" % ( response.status_code, url, response.json)) if callable(response.json): # Newer python-requests results += response.json() else: # Older python-requests results += response.json field = response.headers.get('link', None) link = dict() if field: link = dict([ ( part.split('; ')[1][5:-1], part.split('; ')[0][1:-1], ) for part in field.split(', ') ]) if not results: irc.reply('No pending pull requests on {slug}'.format(slug=slug)) else: n = 4 for pull in results[:n]: irc.reply('@{user}\'s "{title}" {url}'.format( user=pull['user']['login'], title=pull['title'], url=pull['html_url'])) if len(results) > n: irc.reply('... and %i more.' % (len(results) - n)) pulls = wrap(pulls, ['text']) def whoowns(self, irc, msg, args, package): """<package> Retrieve the owner of a given package """ try: mainowner = self.bugzacl['Fedora'][package]['owner'] except KeyError: irc.reply("No such package exists.") return others = [] for key in self.bugzacl: if key == 'Fedora': continue try: owner = self.bugzacl[key][package]['owner'] if owner == mainowner: continue except KeyError: continue others.append("%s in %s" % (owner, key)) if others == []: irc.reply(mainowner) else: irc.reply("%s (%s)" % (mainowner, ', '.join(others))) whoowns = wrap(whoowns, ['text']) def branches(self, irc, msg, args, package): """<package> Return the branches a package is in.""" try: pkginfo = self.pkgdb.get_package(package) except AppError: irc.reply("No such package exists.") return branch_list = [] for listing in pkginfo['packages']: branch_list.append(listing['collection']['branchname']) branch_list.sort() irc.reply(' '.join(branch_list)) return branches = wrap(branches, ['text']) def what(self, irc, msg, args, package): """<package> Returns a description of a given package. """ try: summary = self.bugzacl['Fedora'][package]['summary'] irc.reply("%s: %s" % (package, summary)) except KeyError: irc.reply("No such package exists.") return what = wrap(what, ['text']) def fas(self, irc, msg, args, find_name): """<query> Search the Fedora Account System usernames, full names, and email addresses for a match.""" find_name = to_unicode(find_name) matches = [] for entry in self.faslist.keys(): if entry.find(find_name.lower()) != -1: matches.append(entry) if len(matches) == 0: irc.reply("'%s' Not Found!" % find_name) else: output = [] for match in matches: output.append(self.faslist[match]) irc.reply(' - '.join(output).encode('utf-8')) fas = wrap(fas, ['text']) def hellomynameis(self, irc, msg, args, name): """<username> Return brief information about a Fedora Account System username. Useful for things like meeting roll call and calling attention to yourself.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'%(human_name)s\' <%(email)s>' % person).encode('utf-8')) hellomynameis = wrap(hellomynameis, ['text']) def himynameis(self, irc, msg, args, name): """<username> Will the real Slim Shady please stand up?""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'Slim Shady\' <%(email)s>' % person).encode('utf-8')) himynameis = wrap(himynameis, ['text']) def localtime(self, irc, msg, args, name): """<username> Returns the current time of the user. The timezone is queried from FAS.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info user user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return timezone_name = person['timezone'] if timezone_name is None: irc.reply('User "%s" doesn\'t share his timezone' % name) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: irc.reply('The timezone of "%s" was unknown: "%s"' % (name, timezone)) return irc.reply('The current local time of "%s" is: "%s" (timezone: %s)' % (name, time.strftime('%H:%M'), timezone_name)) localtime = wrap(localtime, ['text']) def fasinfo(self, irc, msg, args, name): """<username> Return information on a Fedora Account System username.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return person['creation'] = person['creation'].split(' ')[0] string = ("User: %(username)s, Name: %(human_name)s" ", email: %(email)s, Creation: %(creation)s" ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s" ", Locale: %(locale)s" ", GPG key ID: %(gpg_keyid)s, Status: %(status)s") % person irc.reply(string.encode('utf-8')) # List of unapproved groups is easy unapproved = '' for group in person['unapproved_memberships']: unapproved = unapproved + "%s " % group['name'] if unapproved != '': irc.reply('Unapproved Groups: %s' % unapproved) # List of approved groups requires a separate query to extract roles constraints = {'username': name, 'group': '%', 'role_status': 'approved'} columns = ['username', 'group', 'role_type'] roles = [] try: roles = self.fasclient.people_query(constraints=constraints, columns=columns) except: irc.reply('Error getting group memberships.') return approved = '' for role in roles: if role['role_type'] == 'sponsor': approved += '+' + role['group'] + ' ' elif role['role_type'] == 'administrator': approved += '@' + role['group'] + ' ' else: approved += role['group'] + ' ' if approved == '': approved = "None" irc.reply('Approved Groups: %s' % approved) fasinfo = wrap(fasinfo, ['text']) def group(self, irc, msg, args, name): """<group short name> Return information about a Fedora Account System group.""" try: group = self.fasclient.group_by_name(name) irc.reply('%s: %s' % (name, group['display_name'])) except AppError: irc.reply('There is no group "%s".' % name) group = wrap(group, ['text']) def admins(self, irc, msg, args, name): """<group short name> Return the administrators list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'administrator': sponsors += person['username'] + ' ' irc.reply('Administrators for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) admins = wrap(admins, ['text']) def sponsors(self, irc, msg, args, name): """<group short name> Return the sponsors list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'sponsor': sponsors += person['username'] + ' ' elif person['role_type'] == 'administrator': sponsors += '@' + person['username'] + ' ' irc.reply('Sponsors for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) sponsors = wrap(sponsors, ['text']) def members(self, irc, msg, args, name): """<group short name> Return a list of members of the specified group""" try: group = self.fasclient.group_members(name) members = '' for person in group: if person['role_type'] == 'administrator': members += '@' + person['username'] + ' ' elif person['role_type'] == 'sponsor': members += '+' + person['username'] + ' ' else: members += person['username'] + ' ' irc.reply('Members of %s: %s' % (name, members)) except AppError: irc.reply('There is no group %s.' % name) members = wrap(members, ['text']) def showticket(self, irc, msg, args, baseurl, number): """<baseurl> <number> Return the name and URL of a trac ticket or bugzilla bug. """ url = format(baseurl, str(number)) size = conf.supybot.protocols.http.peekSize() text = utils.web.getUrl(url, size=size) parser = Title() try: parser.feed(text) except sgmllib.SGMLParseError: irc.reply(format('Encountered a problem parsing %u', url)) if parser.title: irc.reply(utils.web.htmlToText(parser.title.strip()) + ' - ' + url) else: irc.reply(format('That URL appears to have no HTML title ' + 'within the first %i bytes.', size)) showticket = wrap(showticket, ['httpUrl', 'int']) def swedish(self, irc, msg, args): """takes no arguments Humor mmcgrath.""" # Import this here to avoid a circular import problem. from __init__ import __version__ irc.reply(str('kwack kwack')) irc.reply(str('bork bork bork')) irc.reply(str('(supybot-fedora version %s)' % __version__)) swedish = wrap(swedish) def wikilink(self, irc, msg, args, name): """<username> Return MediaWiki link syntax for a FAS user's page on the wiki.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return string = "[[User:%s|%s]]" % (person["username"], person["human_name"] or '') irc.reply(string.encode('utf-8')) wikilink = wrap(wikilink, ['text']) def mirroradmins(self, irc, msg, args, hostname): """<hostname> Return MirrorManager list of FAS usernames which administer <hostname>. <hostname> must be the FQDN of the host.""" url = ("https://admin.fedoraproject.org/mirrormanager/mirroradmins?" "tg_format=json&host=" + hostname) result = self._load_json(url)['values'] if len(result) == 0: irc.reply('Hostname "%s" not found' % hostname) return string = 'Mirror Admins of %s: ' % hostname string += ' '.join(result) irc.reply(string.encode('utf-8')) mirroradmins = wrap(mirroradmins, ['text']) def pushduty(self, irc, msg, args): """ Return the list of people who are on releng push duty right now. """ def get_persons(): for meeting in self._meetings_for('release-engineering'): yield meeting['meeting_name'] persons = list(get_persons()) url = "https://apps.fedoraproject.org/" + \ "calendar/release-engineering/" if not persons: response = "Nobody is listed as being on push duty right now..." irc.reply(response.encode('utf-8')) irc.reply("- " + url.encode('utf-8')) return persons = ", ".join(persons) response = "The following people are on push duty: %s" % persons irc.reply(response.encode('utf-8')) irc.reply("- " + url.encode('utf-8')) pushduty = wrap(pushduty) def vacation(self, irc, msg, args): """ Return the list of people who are on vacation right now. """ def get_persons(): for meeting in self._meetings_for('vacation'): for manager in meeting['meeting_manager']: yield manager persons = list(get_persons()) if not persons: response = "Nobody is listed as being on vacation right now..." irc.reply(response.encode('utf-8')) url = "https://apps.fedoraproject.org/calendar/vacation/" irc.reply("- " + url.encode('utf-8')) return persons = ", ".join(persons) response = "The following people are on vacation: %s" % persons irc.reply(response.encode('utf-8')) url = "https://apps.fedoraproject.org/calendar/vacation/" irc.reply("- " + url.encode('utf-8')) vacation = wrap(vacation) def nextmeeting(self, irc, msg, args, channel): """<channel> Return the next meeting scheduled for a particular channel. """ channel = channel.strip('#').split('@')[0] meetings = list(self._future_meetings(channel)) if not meetings: response = "There are no meetings scheduled for #%s." % channel irc.reply(response.encode('utf-8')) return date, meeting = meetings[0] response = "The next meeting in #%s is %s (starting %s)" % ( channel, meeting['meeting_name'], arrow.get(date).humanize(), ) irc.reply(response.encode('utf-8')) base = "https://apps.fedoraproject.org/calendar/location/" url = base + urllib.quote("%[email protected]/" % channel) irc.reply("- " + url.encode('utf-8')) nextmeeting = wrap(nextmeeting, ['text']) @staticmethod def _future_meetings(channel): location = '*****@*****.**' % channel meetings = Fedora._query_fedocal(location=location) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now < dt: yield dt, meeting @staticmethod def _meetings_for(calendar): meetings = Fedora._query_fedocal(calendar=calendar) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) start = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") string = "%s %s" % (meeting['meeting_date_end'], meeting['meeting_time_stop']) end = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now >= start and now <= end: yield meeting @staticmethod def _query_fedocal(**kwargs): url = 'https://apps.fedoraproject.org/calendar/api/meetings' return requests.get(url, params=kwargs).json()['meetings'] def badges(self, irc, msg, args, name): """<username> Return badges statistics about a user. """ url = "https://badges.fedoraproject.org/user/" + name d = requests.get(url + "/json").json() if 'error' in d: response = d['error'] else: template = "{name} has unlocked {n} Fedora Badges: {url}" n = len(d['assertions']) response = template.format(name=name, url=url, n=n) irc.reply(response.encode('utf-8')) badges = wrap(badges, ['text']) def quote(self, irc, msg, args, arguments): """<SYMBOL> [daily, weekly, monthly, quarterly] Return some datagrepper statistics on fedmsg categories. """ # First, some argument parsing. Supybot should be able to do this for # us, but I couldn't figure it out. The supybot.plugins.additional # object is the thing to use... except its weird. tokens = arguments.split(None, 1) if len(tokens) == 1: symbol, frame = tokens[0], 'daily' else: symbol, frame = tokens # Second, build a lookup table for symbols. By default, we'll use the # fedmsg category names, take their first 3 characters and uppercase # them. That will take things like "wiki" and turn them into "WIK" and # "bodhi" and turn them into "BOD". This handles a lot for us. We'll # then override those that don't make sense manually here. For # instance "fedoratagger" by default would be "FED", but that's no # good. We want "TAG". # Why all this trouble? Well, as new things get added to the fedmsg # bus, we don't want to have keep coming back here and modifying this # code. Hopefully this dance will at least partially future-proof us. symbols = dict([ (processor.__name__.lower(), processor.__name__[:3].upper()) for processor in fedmsg.meta.processors ]) symbols.update({ 'fedoratagger': 'TAG', 'fedbadges': 'BDG', 'buildsys': 'KOJ', 'pkgdb': 'PKG', 'meetbot': 'MTB', 'planet': 'PLN', 'trac': 'TRC', 'mailman': 'MM3', }) # Now invert the dict so we can lookup the argued symbol. # Yes, this is vulnerable to collisions. symbols = dict([(sym, name) for name, sym in symbols.items()]) # These aren't user-facing topics, so drop 'em. del symbols['LOG'] del symbols['UNH'] del symbols['ANN'] # And this one is unused... key_fmt = lambda d: ', '.join(sorted(d.keys())) if symbol not in symbols: response = "No such symbol %r. Try one of %s" irc.reply((response % (symbol, key_fmt(symbols))).encode('utf-8')) return # Now, build another lookup of our various timeframes. frames = dict( daily=datetime.timedelta(days=1), weekly=datetime.timedelta(days=7), monthly=datetime.timedelta(days=30), quarterly=datetime.timedelta(days=91), ) if frame not in frames: response = "No such timeframe %r. Try one of %s" irc.reply((response % (frame, key_fmt(frames))).encode('utf-8')) return category = [symbols[symbol]] t2 = datetime.datetime.utcnow() t1 = t2 - frames[frame] t0 = t1 - frames[frame] # Count the number of messages between t0 and t1, and between t1 and t2 query1 = dict(start=t0, end=t1, category=category) query2 = dict(start=t1, end=t2, category=category) # Do this async for superfast datagrepper queries. tpool = ThreadPool() batched_values = tpool.map(datagrepper_query, [ dict(start=x, end=y, category=category) for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION) ] + [query1, query2]) count2 = batched_values.pop() count1 = batched_values.pop() # Just rename the results. We'll use the rest for the sparkline. sparkline_values = batched_values yester_phrases = dict( daily="yesterday", weekly="the week preceding this one", monthly="the month preceding this one", quarterly="the 3 months preceding these past three months", ) phrases = dict( daily="24 hours", weekly="week", monthly="month", quarterly="3 months", ) if count1 and count2: percent = ((float(count2) / count1) - 1) * 100 elif not count1 and count2: # If the older of the two time periods had zero messages, but there # are some in the more current period.. well, that's an infinite # percent increase. percent = float('inf') elif not count1 and not count2: # If counts are zero for both periods, then the change is 0%. percent = 0 else: # Else, if there were some messages in the old time period, but # none in the current... then that's a 100% drop off. percent = -100 sign = lambda value: value >= 0 and '+' or '-' template = u"{sym}, {name} {sign}{percent:.2f}% over {phrase}" response = template.format( sym=symbol, name=symbols[symbol], sign=sign(percent), percent=abs(percent), phrase=yester_phrases[frame], ) irc.reply(response.encode('utf-8')) # Now, make a graph out of it. sparkline = Utils.sparkline(sparkline_values) template = u" {sparkline} ⤆ over {phrase}" response = template.format( sym=symbol, sparkline=sparkline, phrase=phrases[frame] ) irc.reply(response.encode('utf-8')) to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # And a final line for "x-axis tics" t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1)) t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2)) padding = u" " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3) template = u" ↑ {t1}{padding}↑ {t2}" response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding) irc.reply(response.encode('utf-8')) quote = wrap(quote, ['text'])
class Fedora(callbacks.Plugin): """Use this plugin to retrieve Fedora-related information.""" threaded = True def __init__(self, irc): super(Fedora, self).__init__(irc) # caches, automatically downloaded on __init__, manually refreshed on # .refresh self.userlist = None self.bugzacl = None # To get the information, we need a username and password to FAS. # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY! self.fasurl = self.registryValue('fas.url') self.username = self.registryValue('fas.username') self.password = self.registryValue('fas.password') self.fasclient = AccountSystem(self.fasurl, username=self.username, password=self.password) self.pkgdb = PkgDB() # URLs # self.url = {} self.github_oauth_token = self.registryValue('github.oauth_token') self.karma_tokens = ('++', '--') if self.allow_negative else ('++',) # fetch necessary caches self._refresh() # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors. fm_config = fedmsg.config.load_config() fedmsg.meta.make_processors(**fm_config) def _refresh(self): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(None) self.log.info("Downloading user data") request = self.fasclient.send_request('/user/list', req_params={'search': '*'}, auth=True, timeout=240) users = request['people'] + request['unapproved_people'] del request self.log.info("Caching necessary user data") self.users = {} self.faslist = {} self.nickmap = {} for user in users: name = user['username'] self.users[name] = {} self.users[name]['id'] = user['id'] key = ' '.join([user['username'], user['email'] or '', user['human_name'] or '', user['ircnick'] or '']) key = key.lower() value = "%s '%s' <%s>" % (user['username'], user['human_name'] or '', user['email'] or '') self.faslist[key] = value if user['ircnick']: self.nickmap[user['ircnick']] = name self.log.info("Downloading package owners cache") data = requests.get( 'https://admin.fedoraproject.org/pkgdb/api/bugzilla?format=json', verify=True).json() self.bugzacl = data['bugzillaAcls'] socket.setdefaulttimeout(timeout) def refresh(self, irc, msg, args): """takes no arguments Refresh the necessary caches.""" irc.reply("Downloading caches. This could take a while...") self._refresh() irc.replySuccess() refresh = wrap(refresh) @property def karma_db_path(self): return self.registryValue('karma.db_path') @property def allow_unaddressed_karma(self): return self.registryValue('karma.unaddressed') @property def allow_negative(self): return self.registryValue('karma.allow_negative') def _load_json(self, url): timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(45) json = simplejson.loads(utils.web.getUrl(url)) socket.setdefaulttimeout(timeout) return json def pulls(self, irc, msg, args, slug): """<username[/repo]> List the latest pending pull requests on github/pagure repos. """ slug = slug.strip() if not slug or slug.count('/') != 0: irc.reply('Must be a GitHub org/username or pagure tag') return irc.reply('One moment, please... Looking up %s.' % slug) fail_on_github, fail_on_pagure = False, False github_repos, pagure_repos = [], [] try: github_repos = list(self.yield_github_repos(slug)) except IOError as e: self.log.exception(e.message) fail_on_github = True try: pagure_repos = list(self.yield_pagure_repos(slug)) except IOError as e: self.log.exception(e.message) fail_on_pagure = True if fail_on_github and fail_on_pagure: irc.reply('Could not find %s on GitHub or pagure.io' % slug) return results = sum([ list(self.yield_github_pulls(slug, r)) for r in github_repos ], []) + sum([ list(self.yield_pagure_pulls(slug, r)) for r in pagure_repos ], []) # Reverse-sort by time (newest-first) comparator = lambda a, b: cmp(b['age_numeric'], a['age_numeric']) results.sort(comparator) if not results: irc.reply('No pending pull requests on {slug}'.format(slug=slug)) else: n = 6 # Show 6 pull requests for pull in results[:n]: irc.reply(u'@{user}\'s "{title}" {url} filed {age}'.format( user=pull['user'], title=pull['title'], url=pull['url'], age=pull['age'], ).encode('utf-8')) if len(results) > n: irc.reply('... and %i more.' % (len(results) - n)) pulls = wrap(pulls, ['text']) def yield_github_repos(self, username): self.log.info("Finding github repos for %r" % username) tmpl = "https://api.github.com/users/{username}/repos?per_page=100" url = tmpl.format(username=username) auth = dict(access_token=self.github_oauth_token) for result in self.yield_github_results(url, auth): yield result['name'] def yield_github_pulls(self, username, repo): self.log.info("Finding github pull requests for %r %r" % (username, repo)) tmpl = "https://api.github.com/repos/{username}/{repo}/" + \ "pulls?per_page=100" url = tmpl.format(username=username, repo=repo) auth = dict(access_token=self.github_oauth_token) for result in self.yield_github_results(url, auth): yield dict( user=result['user']['login'], title=result['title'], url=result['html_url'], age=arrow.get(result['created_at']).humanize(), age_numeric=arrow.get(result['created_at']), ) def yield_github_results(self, url, auth): results = [] link = dict(next=url) while 'next' in link: response = requests.get(link['next'], params=auth) if response.status_code == 404: raise IOError("404 for %r" % link['next']) # And.. if we didn't get good results, just bail. if response.status_code != 200: raise IOError( "Non-200 status code %r; %r; %r" % ( response.status_code, link['next'], response.json)) results = response.json() for result in results: yield result field = response.headers.get('link', None) link = dict() if field: link = dict([ ( part.split('; ')[1][5:-1], part.split('; ')[0][1:-1], ) for part in field.split(', ') ]) def yield_pagure_repos(self, tag): self.log.info("Finding pagure repos for %r" % tag) tmpl = "https://pagure.io/api/0/projects?tags={tag}" url = tmpl.format(tag=tag) for result in self.yield_pagure_results(url, 'projects'): yield result['name'] def yield_pagure_pulls(self, tag, repo): self.log.info("Finding pagure pull requests for %r %r" % (tag, repo)) tmpl = "https://pagure.io/api/0/{repo}/pull-requests" url = tmpl.format(tag=tag, repo=repo) for result in self.yield_pagure_results(url, 'requests'): yield dict( user=result['user']['name'], title=result['title'], url='https://pagure.io/{repo}/pull-request/{id}'.format( repo=result['project']['name'], id=result['id']), age=arrow.get(result['date_created']).humanize(), age_numeric=arrow.get(result['date_created']), ) def yield_pagure_results(self, url, key): response = requests.get(url) if response.status_code == 404: raise IOError("404 for %r" % url) # And.. if we didn't get good results, just bail. if response.status_code != 200: raise IOError( "Non-200 status code %r; %r; %r" % ( response.status_code, url, response.text)) results = response.json() results = results[key] for result in results: yield result def whoowns(self, irc, msg, args, package): """<package> Retrieve the owner of a given package """ try: mainowner = self.bugzacl['Fedora'][package]['owner'] except KeyError: irc.reply("No such package exists.") return others = [] for key in self.bugzacl: if key == 'Fedora': continue try: owner = self.bugzacl[key][package]['owner'] if owner == mainowner: continue except KeyError: continue others.append("%s in %s" % (owner, key)) if others == []: irc.reply(mainowner) else: irc.reply("%s (%s)" % (mainowner, ', '.join(others))) whoowns = wrap(whoowns, ['text']) def branches(self, irc, msg, args, package): """<package> Return the branches a package is in.""" try: pkginfo = self.pkgdb.get_package(package) except AppError: irc.reply("No such package exists.") return branch_list = [] for listing in pkginfo['packages']: branch_list.append(listing['collection']['branchname']) branch_list.sort() irc.reply(' '.join(branch_list)) return branches = wrap(branches, ['text']) def what(self, irc, msg, args, package): """<package> Returns a description of a given package. """ try: summary = self.bugzacl['Fedora'][package]['summary'] irc.reply("%s: %s" % (package, summary)) except KeyError: irc.reply("No such package exists.") return what = wrap(what, ['text']) def fas(self, irc, msg, args, find_name): """<query> Search the Fedora Account System usernames, full names, and email addresses for a match.""" find_name = to_unicode(find_name) matches = [] for entry in self.faslist.keys(): if entry.find(find_name.lower()) != -1: matches.append(entry) if len(matches) == 0: irc.reply("'%s' Not Found!" % find_name) else: output = [] for match in matches: output.append(self.faslist[match]) irc.reply(' - '.join(output).encode('utf-8')) fas = wrap(fas, ['text']) def hellomynameis(self, irc, msg, args, name): """<username> Return brief information about a Fedora Account System username. Useful for things like meeting roll call and calling attention to yourself.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'%(human_name)s\' <%(email)s>' % person).encode('utf-8')) hellomynameis = wrap(hellomynameis, ['text']) def himynameis(self, irc, msg, args, name): """<username> Will the real Slim Shady please stand up?""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Something blew up, please try again') return if not person: irc.reply('Sorry, but you don\'t exist') return irc.reply(('%(username)s \'Slim Shady\' <%(email)s>' % person).encode('utf-8')) himynameis = wrap(himynameis, ['text']) def dctime(self, irc, msg, args, dcname): """<dcname> Returns the current time of the datacenter identified by dcname. Supported DCs: PHX2, RDU, AMS, osuosl, ibiblio.""" timezone_name = '' dcname_lower = dcname.lower() if dcname_lower == 'phx2': timezone_name = 'US/Arizona' elif dcname_lower in ['rdu', 'ibiblio']: timezone_name = 'US/Eastern' elif dcname_lower == 'osuosl': timezone_name = 'US/Pacific' elif dcname_lower in ['ams', 'internetx']: timezone_name = 'Europe/Amsterdam' else: irc.reply('Datacenter %s is unknown' % dcname) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: irc.reply('The timezone of "%s" was unknown: "%s"' % ( dcname, timezone_name)) return irc.reply('The current local time of "%s" is: "%s" (timezone: %s)' % (dcname, time.strftime('%H:%M'), timezone_name)) dctime = wrap(dctime, ['text']) def localtime(self, irc, msg, args, name): """<username> Returns the current time of the user. The timezone is queried from FAS.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info user user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return timezone_name = person['timezone'] if timezone_name is None: irc.reply('User "%s" doesn\'t share his timezone' % name) return try: time = datetime.datetime.now(pytz.timezone(timezone_name)) except: irc.reply('The timezone of "%s" was unknown: "%s"' % ( name, timezone_name)) return irc.reply('The current local time of "%s" is: "%s" (timezone: %s)' % (name, time.strftime('%H:%M'), timezone_name)) localtime = wrap(localtime, ['text']) def fasinfo(self, irc, msg, args, name): """<username> Return information on a Fedora Account System username.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return person['creation'] = person['creation'].split(' ')[0] string = ("User: %(username)s, Name: %(human_name)s" ", email: %(email)s, Creation: %(creation)s" ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s" ", Locale: %(locale)s" ", GPG key ID: %(gpg_keyid)s, Status: %(status)s") % person irc.reply(string.encode('utf-8')) # List of unapproved groups is easy unapproved = '' for group in person['unapproved_memberships']: unapproved = unapproved + "%s " % group['name'] if unapproved != '': irc.reply('Unapproved Groups: %s' % unapproved) # List of approved groups requires a separate query to extract roles constraints = {'username': name, 'group': '%', 'role_status': 'approved'} columns = ['username', 'group', 'role_type'] roles = [] try: roles = self.fasclient.people_query(constraints=constraints, columns=columns) except: irc.reply('Error getting group memberships.') return approved = '' for role in roles: if role['role_type'] == 'sponsor': approved += '+' + role['group'] + ' ' elif role['role_type'] == 'administrator': approved += '@' + role['group'] + ' ' else: approved += role['group'] + ' ' if approved == '': approved = "None" irc.reply('Approved Groups: %s' % approved) fasinfo = wrap(fasinfo, ['text']) def group(self, irc, msg, args, name): """<group short name> Return information about a Fedora Account System group.""" try: group = self.fasclient.group_by_name(name) irc.reply('%s: %s' % (name, group['display_name'])) except AppError: irc.reply('There is no group "%s".' % name) group = wrap(group, ['text']) def admins(self, irc, msg, args, name): """<group short name> Return the administrators list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'administrator': sponsors += person['username'] + ' ' irc.reply('Administrators for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) admins = wrap(admins, ['text']) def sponsors(self, irc, msg, args, name): """<group short name> Return the sponsors list for the selected group""" try: group = self.fasclient.group_members(name) sponsors = '' for person in group: if person['role_type'] == 'sponsor': sponsors += person['username'] + ' ' elif person['role_type'] == 'administrator': sponsors += '@' + person['username'] + ' ' irc.reply('Sponsors for %s: %s' % (name, sponsors)) except AppError: irc.reply('There is no group %s.' % name) sponsors = wrap(sponsors, ['text']) def members(self, irc, msg, args, name): """<group short name> Return a list of members of the specified group""" try: group = self.fasclient.group_members(name) members = '' for person in group: if person['role_type'] == 'administrator': members += '@' + person['username'] + ' ' elif person['role_type'] == 'sponsor': members += '+' + person['username'] + ' ' else: members += person['username'] + ' ' irc.reply('Members of %s: %s' % (name, members)) except AppError: irc.reply('There is no group %s.' % name) members = wrap(members, ['text']) def showticket(self, irc, msg, args, baseurl, number): """<baseurl> <number> Return the name and URL of a trac ticket or bugzilla bug. """ url = format(baseurl, str(number)) size = conf.supybot.protocols.http.peekSize() text = utils.web.getUrl(url, size=size) parser = Title() try: parser.feed(text) except sgmllib.SGMLParseError: irc.reply(format('Encountered a problem parsing %u', url)) if parser.title: irc.reply(utils.web.htmlToText(parser.title.strip()) + ' - ' + url) else: irc.reply(format('That URL appears to have no HTML title ' + 'within the first %i bytes.', size)) showticket = wrap(showticket, ['httpUrl', 'int']) def swedish(self, irc, msg, args): """takes no arguments Humor mmcgrath.""" # Import this here to avoid a circular import problem. from __init__ import __version__ irc.reply(str('kwack kwack')) irc.reply(str('bork bork bork')) irc.reply(str('(supybot-fedora version %s)' % __version__)) swedish = wrap(swedish) def invalidCommand(self, irc, msg, tokens): """ Handle any command not otherwise handled. We use this to accept karma commands directly. """ channel = msg.args[0] if not irc.isChannel(channel): return agent = msg.nick line = tokens[-1].strip() words = line.split() for word in words: if word[-2:] in self.karma_tokens: self._do_karma(irc, channel, agent, word, line, explicit=True) def doPrivmsg(self, irc, msg): """ Handle everything. The name is misleading. This hook actually gets called for all IRC activity in every channel. """ # We don't handle this if we've been addressed because invalidCommand # will handle it for us. This prevents us from accessing the db twice # and therefore crashing. if (msg.addressed or msg.repliedTo): return channel = msg.args[0] if irc.isChannel(channel) and self.allow_unaddressed_karma: irc = callbacks.SimpleProxy(irc, msg) agent = msg.nick line = msg.args[1].strip() # First try to handle karma commands words = line.split() for word in words: if word[-2:] in self.karma_tokens: self._do_karma( irc, channel, agent, word, line, explicit=False) blacklist = self.registryValue('naked_ping_channel_blacklist') if irc.isChannel(channel) and not channel in blacklist: # Also, handle naked pings for # https://github.com/fedora-infra/supybot-fedora/issues/26 pattern = '\w* ?[:,] ?ping\W*$' if re.match(pattern, line): admonition = self.registryValue('naked_ping_admonition') irc.reply(admonition) def get_current_release(self): url = 'https://admin.fedoraproject.org/pkgdb/api/collections/' query = { 'clt_status': 'Active', 'pattern': 'f*', } response = requests.get(url, params=query) data = response.json() collections = data['collections'] collections.sort(key=lambda c: int(c['version'])) return collections[-1]['branchname'].encode('utf-8') def open_karma_db(self): data = shelve.open(self.karma_db_path) if 'backwards' in data: # This is the old style data. convert it to the new form. release = self.get_current_release() data['forwards-' + release] = copy.copy(data['forwards']) data['backwards-' + release] = copy.copy(data['backwards']) del data['forwards'] del data['backwards'] data.sync() return data def karma(self, irc, msg, args, name): """<username> Return the total karma for a FAS user.""" data = None try: data = self.open_karma_db() if name in self.nickmap: name = self.nickmap[name] release = self.get_current_release() votes = data['backwards-' + release].get(name, {}) alltime = [] for key in data: if 'backwards-' not in key: continue alltime.append(data[key].get(name, {})) finally: if data: data.close() inc = len([v for v in votes.values() if v == 1]) dec = len([v for v in votes.values() if v == -1]) total = inc - dec inc, dec = 0, 0 for release in alltime: inc += len([v for v in release.values() if v == 1]) dec += len([v for v in release.values() if v == -1]) alltime_total = inc - dec irc.reply("Karma for %s has been increased %i times and " "decreased %i times this release cycle for a " "total of %i (%i all time)" % ( name, inc, dec, total, alltime_total)) karma = wrap(karma, ['text']) def _do_karma(self, irc, channel, agent, recip, line, explicit=False): recip, direction = recip[:-2], recip[-2:] if not recip: return # Extract 'puiterwijk' out of 'have a cookie puiterwijk++' recip = recip.strip().split()[-1] # Exclude 'c++', 'g++' or 'i++' (c,g,i), issue #30 if str(recip).lower() in ['c','g','i']: return increment = direction == '++' # If not, then it must be decrement # Check that these are FAS users if not agent in self.nickmap and not agent in self.users: self.log.info( "Saw %s from %s, but %s not in FAS" % (recip, agent, agent)) if explicit: irc.reply("Couldn't find %s in FAS" % agent) return if not recip in self.nickmap and not recip in self.users: self.log.info( "Saw %s from %s, but %s not in FAS" % (recip, agent, recip)) if explicit: irc.reply("Couldn't find %s in FAS" % recip) return # Transform irc nicks into fas usernames if possible. if agent in self.nickmap: agent = self.nickmap[agent] if recip in self.nickmap: recip = self.nickmap[recip] if agent == recip: irc.reply("You may not modify your own karma.") return release = self.get_current_release() # Check our karma db to make sure this hasn't already been done. data = None try: data = shelve.open(self.karma_db_path) fkey = 'forwards-' + release bkey = 'backwards-' + release if fkey not in data: data[fkey] = {} if bkey not in data: data[bkey] = {} if agent not in data[fkey]: forwards = data[fkey] forwards[agent] = {} data[fkey] = forwards if recip not in data[bkey]: backwards = data[bkey] backwards[recip] = {} data[bkey] = backwards vote = 1 if increment else -1 if data[fkey][agent].get(recip) == vote: ## People found this response annoying. ## https://github.com/fedora-infra/supybot-fedora/issues/25 #irc.reply( # "You have already given %i karma to %s" % (vote, recip)) return forwards = data[fkey] forwards[agent][recip] = vote data[fkey] = forwards backwards = data[bkey] backwards[recip][agent] = vote data[bkey] = backwards # Count the number of karmas for old so-and-so. total_this_release = sum(data[bkey][recip].values()) total_all_time = 0 for key in data: if 'backwards-' not in key: continue total_all_time += sum(data[key].get(recip, {}).values()) finally: if data: data.close() fedmsg.publish( name="supybot.%s" % socket.gethostname(), modname="irc", topic="karma", msg={ 'agent': agent, 'recipient': recip, 'total': total_all_time, # The badge rules use this value 'total_this_release': total_this_release, 'vote': vote, 'channel': channel, 'line': line, 'release': release, }, ) url = self.registryValue('karma.url') irc.reply( 'Karma for %s changed to %r ' '(for the %s release cycle): %s' % ( recip, total_this_release, release, url)) def wikilink(self, irc, msg, args, name): """<username> Return MediaWiki link syntax for a FAS user's page on the wiki.""" try: person = self.fasclient.person_by_username(name) except: irc.reply('Error getting info for user: "******"' % name) return if not person: irc.reply('User "%s" doesn\'t exist' % name) return string = "[[User:%s|%s]]" % (person["username"], person["human_name"] or '') irc.reply(string.encode('utf-8')) wikilink = wrap(wikilink, ['text']) def mirroradmins(self, irc, msg, args, hostname): """<hostname> Return MirrorManager list of FAS usernames which administer <hostname>. <hostname> must be the FQDN of the host.""" url = ("https://admin.fedoraproject.org/mirrormanager/api/" "mirroradmins?name=" + hostname) result = self._load_json(url) if not 'admins' in result: irc.reply(result.get('message', 'Something went wrong')) return string = 'Mirror Admins of %s: ' % hostname string += ' '.join(result['admins']) irc.reply(string.encode('utf-8')) mirroradmins = wrap(mirroradmins, ['text']) def pushduty(self, irc, msg, args): """ Return the list of people who are on releng push duty right now. """ def get_persons(): for meeting in self._meetings_for('release-engineering'): yield meeting['meeting_name'] persons = list(get_persons()) url = "https://apps.fedoraproject.org/" + \ "calendar/release-engineering/" if not persons: response = "Nobody is listed as being on push duty right now..." irc.reply(response.encode('utf-8')) irc.reply("- " + url.encode('utf-8')) return persons = ", ".join(persons) response = "The following people are on push duty: %s" % persons irc.reply(response.encode('utf-8')) irc.reply("- " + url.encode('utf-8')) pushduty = wrap(pushduty) def vacation(self, irc, msg, args): """ Return the list of people who are on vacation right now. """ def get_persons(): for meeting in self._meetings_for('vacation'): for manager in meeting['meeting_manager']: yield manager persons = list(get_persons()) if not persons: response = "Nobody is listed as being on vacation right now..." irc.reply(response.encode('utf-8')) url = "https://apps.fedoraproject.org/calendar/vacation/" irc.reply("- " + url.encode('utf-8')) return persons = ", ".join(persons) response = "The following people are on vacation: %s" % persons irc.reply(response.encode('utf-8')) url = "https://apps.fedoraproject.org/calendar/vacation/" irc.reply("- " + url.encode('utf-8')) vacation = wrap(vacation) def nextmeetings(self, irc, msg, args): """ Return the next meetings scheduled for any channel(s). """ irc.reply('One moment, please... Looking up the channel list.') url = 'https://apps.fedoraproject.org/calendar/api/locations/' locations = requests.get(url).json()['locations'] meetings = sorted(chain(*[ self._future_meetings(location) for location in locations if 'irc.freenode.net' in location ]), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled at all." irc.reply(response.encode('utf-8')) return for date, meeting in islice(meetings, 0, 5): response = "In #%s is %s (starting %s)" % ( meeting['meeting_location'].split('@')[0].strip(), meeting['meeting_name'], arrow.get(date).humanize(), ) irc.reply(response.encode('utf-8')) nextmeetings = wrap(nextmeetings, []) def nextmeeting(self, irc, msg, args, channel): """<channel> Return the next meeting scheduled for a particular channel. """ channel = channel.strip('#').split('@')[0] meetings = sorted(self._future_meetings(channel), key=itemgetter(0)) test, meetings = tee(meetings) try: test.next() except StopIteration: response = "There are no meetings scheduled for #%s." % channel irc.reply(response.encode('utf-8')) return for date, meeting in islice(meetings, 0, 3): response = "In #%s is %s (starting %s)" % ( channel, meeting['meeting_name'], arrow.get(date).humanize(), ) irc.reply(response.encode('utf-8')) base = "https://apps.fedoraproject.org/calendar/location/" url = base + urllib.quote("%[email protected]/" % channel) irc.reply("- " + url.encode('utf-8')) nextmeeting = wrap(nextmeeting, ['text']) @staticmethod def _future_meetings(location): if not location.endswith('@irc.freenode.net'): location = '*****@*****.**' % location meetings = Fedora._query_fedocal(location=location) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now < dt: yield dt, meeting @staticmethod def _meetings_for(calendar): meetings = Fedora._query_fedocal(calendar=calendar) now = datetime.datetime.utcnow() for meeting in meetings: string = "%s %s" % (meeting['meeting_date'], meeting['meeting_time_start']) start = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") string = "%s %s" % (meeting['meeting_date_end'], meeting['meeting_time_stop']) end = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") if now >= start and now <= end: yield meeting @staticmethod def _query_fedocal(**kwargs): url = 'https://apps.fedoraproject.org/calendar/api/meetings' return requests.get(url, params=kwargs).json()['meetings'] def badges(self, irc, msg, args, name): """<username> Return badges statistics about a user. """ url = "https://badges.fedoraproject.org/user/" + name d = requests.get(url + "/json").json() if 'error' in d: response = d['error'] else: template = "{name} has unlocked {n} Fedora Badges: {url}" n = len(d['assertions']) response = template.format(name=name, url=url, n=n) irc.reply(response.encode('utf-8')) badges = wrap(badges, ['text']) def quote(self, irc, msg, args, arguments): """<SYMBOL> [daily, weekly, monthly, quarterly] Return some datagrepper statistics on fedmsg categories. """ # First, some argument parsing. Supybot should be able to do this for # us, but I couldn't figure it out. The supybot.plugins.additional # object is the thing to use... except its weird. tokens = arguments.split(None, 1) if len(tokens) == 1: symbol, frame = tokens[0], 'daily' else: symbol, frame = tokens # Second, build a lookup table for symbols. By default, we'll use the # fedmsg category names, take their first 3 characters and uppercase # them. That will take things like "wiki" and turn them into "WIK" and # "bodhi" and turn them into "BOD". This handles a lot for us. We'll # then override those that don't make sense manually here. For # instance "fedoratagger" by default would be "FED", but that's no # good. We want "TAG". # Why all this trouble? Well, as new things get added to the fedmsg # bus, we don't want to have keep coming back here and modifying this # code. Hopefully this dance will at least partially future-proof us. symbols = dict([ (processor.__name__.lower(), processor.__name__[:3].upper()) for processor in fedmsg.meta.processors ]) symbols.update({ 'fedoratagger': 'TAG', 'fedbadges': 'BDG', 'buildsys': 'KOJ', 'pkgdb': 'PKG', 'meetbot': 'MTB', 'planet': 'PLN', 'trac': 'TRC', 'mailman': 'MM3', }) # Now invert the dict so we can lookup the argued symbol. # Yes, this is vulnerable to collisions. symbols = dict([(sym, name) for name, sym in symbols.items()]) # These aren't user-facing topics, so drop 'em. del symbols['LOG'] del symbols['UNH'] del symbols['ANN'] # And this one is unused... key_fmt = lambda d: ', '.join(sorted(d.keys())) if symbol not in symbols: response = "No such symbol %r. Try one of %s" irc.reply((response % (symbol, key_fmt(symbols))).encode('utf-8')) return # Now, build another lookup of our various timeframes. frames = dict( daily=datetime.timedelta(days=1), weekly=datetime.timedelta(days=7), monthly=datetime.timedelta(days=30), quarterly=datetime.timedelta(days=91), ) if frame not in frames: response = "No such timeframe %r. Try one of %s" irc.reply((response % (frame, key_fmt(frames))).encode('utf-8')) return category = [symbols[symbol]] t2 = datetime.datetime.utcnow() t1 = t2 - frames[frame] t0 = t1 - frames[frame] # Count the number of messages between t0 and t1, and between t1 and t2 query1 = dict(start=t0, end=t1, category=category) query2 = dict(start=t1, end=t2, category=category) # Do this async for superfast datagrepper queries. tpool = ThreadPool() batched_values = tpool.map(datagrepper_query, [ dict(start=x, end=y, category=category) for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION) ] + [query1, query2]) count2 = batched_values.pop() count1 = batched_values.pop() # Just rename the results. We'll use the rest for the sparkline. sparkline_values = batched_values yester_phrases = dict( daily="yesterday", weekly="the week preceding this one", monthly="the month preceding this one", quarterly="the 3 months preceding these past three months", ) phrases = dict( daily="24 hours", weekly="week", monthly="month", quarterly="3 months", ) if count1 and count2: percent = ((float(count2) / count1) - 1) * 100 elif not count1 and count2: # If the older of the two time periods had zero messages, but there # are some in the more current period.. well, that's an infinite # percent increase. percent = float('inf') elif not count1 and not count2: # If counts are zero for both periods, then the change is 0%. percent = 0 else: # Else, if there were some messages in the old time period, but # none in the current... then that's a 100% drop off. percent = -100 sign = lambda value: value >= 0 and '+' or '-' template = u"{sym}, {name} {sign}{percent:.2f}% over {phrase}" response = template.format( sym=symbol, name=symbols[symbol], sign=sign(percent), percent=abs(percent), phrase=yester_phrases[frame], ) irc.reply(response.encode('utf-8')) # Now, make a graph out of it. sparkline = Utils.sparkline(sparkline_values) template = u" {sparkline} ⤆ over {phrase}" response = template.format( sym=symbol, sparkline=sparkline, phrase=phrases[frame] ) irc.reply(response.encode('utf-8')) to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # And a final line for "x-axis tics" t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1)) t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2)) padding = u" " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3) template = u" ↑ {t1}{padding}↑ {t2}" response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding) irc.reply(response.encode('utf-8')) quote = wrap(quote, ['text'])
from fedora.client import (AppError, ServerError) import argparse import requests import logging import koji import itertools from pkgdb2client import PkgDB, PkgDBException, __version__ import pkgdb2client import pkgdb2client.utils KOJI_HUB = 'http://koji.fedoraproject.org/kojihub' pkgdbclient = PkgDB('https://admin.fedoraproject.org/pkgdb', login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-cli") ACTIONLIST = ['watchbugzilla', 'watchcommits', 'commit', 'approveacls'] class ActionError(Exception): ''' This class is raised when an ACL action is requested but not in the list of allowed action. '''
""" from fedora.client import (AppError, ServerError) import argparse import requests import logging import koji from pkgdb2client import PkgDB, PkgDBException, __version__ from pkgdb2client.cli import ActionError import pkgdb2client import pkgdb2client.utils as utils PKGDBCLIENT = PkgDB('https://admin.fedoraproject.org/pkgdb', login_callback=pkgdb2client.ask_password) BOLD = "\033[1m" RED = "\033[0;31m" RESET = "\033[0;0m" # Initial simple logging stuff logging.basicConfig() PKGDBLOG = logging.getLogger("pkgdb2client") LOG = logging.getLogger("pkgdb-admin") def setup_parser(): ''' Set the main arguments. ''' parser = argparse.ArgumentParser(prog="pkgdb-admin")