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 __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'): 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))
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 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")