class NBA(callbacks.Plugin): """Get scores from NBA.com.""" _ENDPOINT_BASE_URL = 'https://data.nba.net' _SCOREBOARD_ENDPOINT = (_ENDPOINT_BASE_URL + '/10s/prod/v2/{}/' + 'scoreboard.json') _TODAY_ENDPOINT = (_ENDPOINT_BASE_URL + '/prod/v3/today.json') _FUZZY_DAYS = frozenset(('yesterday', 'tonight', 'today', 'tomorrow')) _TEAM_TRICODES = frozenset( ('CHA', 'ATL', 'IND', 'MEM', 'DET', 'UTA', 'CHI', 'TOR', 'CLE', 'OKC', 'DAL', 'MIN', 'BOS', 'SAS', 'MIA', 'DEN', 'LAL', 'PHX', 'NOP', 'MIL', 'HOU', 'NYK', 'ORL', 'SAC', 'PHI', 'BKN', 'POR', 'GSW', 'LAC', 'WAS')) def __init__(self, irc): self.__parent = super(NBA, self) self.__parent.__init__(irc) self._http = httplib2.Http('.cache') def nba(self, irc, msg, args, optional_team, optional_date): """[<TTT>] [<YYYY-MM-DD>] Get games for a given date. If none is specified, return games scheduled for today. Optionally add team abbreviation to filter for a specific team. """ # Check to see if there's optional input and if there is check # if it's a date or a team, or both. try: team, date = self._parseOptionalArguments(optional_team, optional_date) except ValueError as error: irc.error(str(error)) return try: games = self._getTodayGames() if date is None \ else self._getGamesForDate(date) except ConnectionError as error: irc.error('Could not connect to nba.com') return except: irc.error('Something went wrong') return games = self._filterGamesWithTeam(team, games) games_string = self._resultAsString(games) # Single game query? We can show some extra info. if len(games) == 1: game = games[0] # If the game has ended, we fetch the recap info from NBA.com: if game['ended']: try: recap = self._getRecapInfo(game) games_string += ' | {} {}'.format(ircutils.bold('Recap:'), recap) except: pass else: # Otherwise, when querying a specific game in progress, # we show the broadcaster list. # Also, if it has a text nugget, and it's not # 'Watch live', we show it: broadcasters = game['tv_broadcasters'] broadcasters_string = self._broadcastersToString(broadcasters) games_string += ' [{}]'.format(broadcasters_string) nugget = game['text_nugget'] nugget_is_interesting = nugget and 'Watch live' not in nugget if nugget_is_interesting: games_string += ' | {}'.format(nugget) if date: date = pendulum.from_format(date, 'YYYYMMDD').to_date_string() else: date = pendulum.now('US/Pacific').to_date_string() irc.reply("{0}: {1}".format(date, games_string)) nba = wrap(nba, [ optional('somethingWithoutSpaces'), optional('somethingWithoutSpaces') ]) def nbatv(self, irc, msg, args, team): """[<TTT>] Given a team, if there is a game scheduled for today, return where it is being broadcasted. """ try: team = self._parseTeamInput(team) except ValueError as error: irc.error(str(error)) return games = self._filterGamesWithTeam(team, self._getTodayGames()) if not games: irc.reply('{} is not playing today.'.format(team)) return game = games[0] game_string = self._gameToString(game) broadcasters_string = self._broadcastersToString( game['tv_broadcasters']) irc.reply('{} on: {}'.format(game_string, broadcasters_string)) nbatv = wrap(nbatv, ['somethingWithoutSpaces']) def nbanext(self, irc, msg, args, n, team, team2): """[<n>] <TTT> [<TTT>] Get the next <n> games (1 by default; max. 10) for a given team or, if two teams are provided, matchups between them. """ MAX_GAMES_IN_RESULT = 10 try: if team == team2: irc.error('Both teams should be different.') return team = self._parseTeamInput(team) if team2 is not None: team2 = self._parseTeamInput(team2) team_schedule = self._getTeamSchedule(team) except ValueError as error: irc.error(str(error)) return last_played = team_schedule['lastStandardGamePlayedIndex'] # Keeping only the games that haven't been played: future_games = team_schedule['standard'][last_played + 1:] if n is None: n = 1 end = min(MAX_GAMES_IN_RESULT, n, len(future_games) - 1) if team2 is None: games = future_games else: # Filtering matchups between team and team2: team2_id = self._tricodeToTeamId(team2) games = [g for g in future_games \ if team2_id in [g['vTeam']['teamId'], g['hTeam']['teamId']]] if not games: irc.error('I could not find future games.') return for game in games[:end]: irc.reply(self._upcomingGameToString(game)) nbanext = wrap(nbanext, [ optional('positiveInt'), 'somethingWithoutSpaces', optional('somethingWithoutSpaces') ]) def nbalast(self, irc, msg, args, n, team, team2): """[<n>] <TTT> [<TTT>] Get the last <n> games (1 by default; max. 10) for a given team or, if two teams are provided, matchups between them. """ MAX_GAMES_IN_RESULT = 10 try: if team == team2: irc.error('Both teams should be different.') return team = self._parseTeamInput(team) if team2 is not None: team2 = self._parseTeamInput(team2) team_schedule = self._getTeamSchedule(team) except ValueError as error: irc.error(str(error)) return last_played = team_schedule['lastStandardGamePlayedIndex'] # Keeping only the games that have been played: team_past_games = team_schedule['standard'][:last_played + 1] # Making sure the number of games we will show is a valid one: if n is None: n = 1 n = min(MAX_GAMES_IN_RESULT, n) if team2 is None: games = team_past_games else: # Filtering matchups between team and team2: team2_id = self._tricodeToTeamId(team2) games = [g for g in team_past_games \ if team2_id in [g['vTeam']['teamId'], g['hTeam']['teamId']]] if not games: irc.error('I could not find past games.') return for game in reversed(games[-n:]): # Most-recent game first. irc.reply(self._pastGameToString(game)) nbalast = wrap(nbalast, [ optional('positiveInt'), 'somethingWithoutSpaces', optional('somethingWithoutSpaces') ]) @classmethod def _parseOptionalArguments(cls, optional_team, optional_date): """Parse the optional arguments, which could be None, and return a (team, date) tuple. In case of finding an invalid argument, it throws a ValueError exception. """ # No arguments: if optional_team is None: return (None, None) # Both arguments: if (optional_date is not None) and (optional_team is not None): team = cls._parseTeamInput(optional_team) date = cls._parseDateInput(optional_date) return (team, date) # Only one argument: if cls._isPotentialDate(optional_team): # Should be a date. team = None date = cls._parseDateInput(optional_team) else: # Should be a team. team = cls._parseTeamInput(optional_team) date = None return (team, date) def _getTodayGames(self): return self._getGames(self._getTodayDate()) def _getGamesForDate(self, date): return self._getGames(date) @staticmethod def _filterGamesWithTeam(team, games): """Given a list of games, return those that involve a given team. If team is None, return the list with no modifications. """ if team is None: return games return [ g for g in games if team == g['home_team'] or team == g['away_team'] ] ############################ # Content-getting helpers ############################ def _getTodayJSON(self): today_url = self._ENDPOINT_BASE_URL + '/10s/prod/v3/today.json' return self._getJSON(today_url) def _getGames(self, date): """Given a date, populate the url with it and try to download its content. If successful, parse the JSON data and extract the relevant fields for each game. Returns a list of games. """ url = self._getEndpointURL(date) # If asking for today's results, revalidate the cached data. # ('If-Mod.-Since' flag.). This allows to get real-time scores. revalidate_cache = (date == self._getTodayDate()) response = self._getURL(url, revalidate_cache) json_data = self._extractJSON(response) return self._parseGames(json_data) @classmethod def _getEndpointURL(cls, date): return cls._SCOREBOARD_ENDPOINT.format(date) def _getTeamSchedule(self, tricode): """Fetch the json with the given team's schedule""" # First we fetch `today.json` to extract the path to teams' # schedules and `seasonScheduleYear`: today_json = self._getTodayJSON() schedule_path = today_json['links']['teamScheduleYear2'] season_year = today_json['seasonScheduleYear'] # We also need to convert the `tricode` to a `team_id`: team_id = self._tricodeToTeamId(tricode) # (The path looks like this: # '/prod/v1/{{seasonScheduleYear}}/teams/{{teamId}}/schedule.json') # Now we can fill-in the url: schedule_path = schedule_path.replace('{{teamId}}', team_id) schedule_path = schedule_path.replace('{{seasonScheduleYear}}', str(season_year)) return self._getJSON(self._ENDPOINT_BASE_URL + schedule_path)['league'] def _tricodeToTeamId(self, tricode): """Given a valid team tricode, get the `teamId` used in NBA.com""" teams_path = self._getJSON(self._TODAY_ENDPOINT)['links']['teams'] teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path) for team in teams_json['league']['standard']: if team['tricode'] == tricode: return team['teamId'] raise ValueError('{} is not a valid tricode'.format(tricode)) def _teamIdToTricode(self, team_id): """Given a valid teamId, get the team's tricode""" teams_path = self._getJSON(self._TODAY_ENDPOINT)['links']['teams'] teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path) for team in teams_json['league']['standard']: if team['teamId'] == team_id: return team['tricode'] raise ValueError('{} is not a valid teamId'.format(team_id)) def _getURL(self, url, force_revalidation=False): """Use httplib2 to download the URL's content. The `force_revalidation` parameter forces the data to be validated before being returned from the cache. In the worst case the data has not changed in the server, and we get a '304 - Not Modified' response. """ user_agent = 'Mozilla/5.0 \ (X11; Ubuntu; Linux x86_64; rv:45.0) \ Gecko/20100101 Firefox/45.0' header = {'User-Agent': user_agent} if force_revalidation: header['Cache-Control'] = 'max-age=0' response, content = self._http.request(url, 'GET', headers=header) if response.fromcache: self.log.debug('%s - 304/Cache Hit', url) if response.status == 200: return content self.log.error('HTTP Error (%s): %s', url, error.code) raise ConnectionError('Could not access URL') @staticmethod def _extractJSON(body): return json.loads(body) def _getJSON(self, url): """Fetch `url` and return its contents decoded as json.""" return self._extractJSON(self._getURL(url)) @classmethod def _parseGames(cls, json_data): """Extract all relevant fields from NBA.com's scoreboard.json and return a list of games. """ games = [] for g in json_data['games']: # Starting times are in UTC. By default, we will show # Eastern times. # (In the future we could add a user option to select # timezones.) try: starting_time = cls._ISODateToEasternTime(g['startTimeUTC']) except: starting_time = 'TBD' if g['isStartTimeTBD'] else '' game_info = { 'game_id': g['gameId'], 'home_team': g['hTeam']['triCode'], 'away_team': g['vTeam']['triCode'], 'home_score': g['hTeam']['score'], 'away_score': g['vTeam']['score'], 'starting_year': g['startDateEastern'][0:4], 'starting_month': g['startDateEastern'][4:6], 'starting_day': g['startDateEastern'][6:8], 'starting_time': starting_time, 'starting_time_TBD': g['isStartTimeTBD'], 'clock': g['clock'], 'period': g['period'], 'buzzer_beater': g['isBuzzerBeater'], 'ended': (g['statusNum'] == 3), 'text_nugget': g['nugget']['text'].strip(), 'tv_broadcasters': cls._extractGameBroadcasters(g) } games.append(game_info) return games @staticmethod def _extractGameBroadcasters(game_json): """Extract the list of broadcasters from the API. Return a dictionary of broadcasts: (['vTeam', 'hTeam', 'national', 'canadian']) to the short name of the broadcaster. """ json_data = game_json['watch']['broadcast']['broadcasters'] game_broadcasters = dict() for category in json_data: broadcasters_list = json_data[category] if broadcasters_list and 'shortName' in broadcasters_list[0]: game_broadcasters[category] = broadcasters_list[0]['shortName'] return game_broadcasters ############################ # Formatting helpers ############################ @classmethod def _resultAsString(cls, games): if not games: return "No games found" # sort games list and put F(inal) games at end sorted_games = sorted(games, key=lambda k: k['ended']) return ' | '.join([cls._gameToString(g) for g in sorted_games]) @classmethod def _gameToString(cls, game): """ Given a game, format the information into a string according to the context. For example: * "MEM @ CLE 07:00 PM ET" (a game that has not started yet), * "HOU 132 GSW 127 F OT2" (a game that ended and went to 2 overtimes), * "POR 36 LAC 42 8:01 Q2" (a game in progress). """ away_team = game['away_team'] home_team = game['home_team'] if game['period']['current'] == 0: # The game hasn't started yet starting_time = game['starting_time'] \ if not game['starting_time_TBD'] \ else "TBD" return "{} @ {} {}".format(away_team, home_team, starting_time) # The game started => It has points: away_score = game['away_score'] home_score = game['home_score'] away_string = "{} {}".format(away_team, away_score) home_string = "{} {}".format(home_team, home_score) # Bold for the winning team: if int(away_score) > int(home_score): away_string = ircutils.bold(away_string) elif int(home_score) > int(away_score): home_string = ircutils.bold(home_string) game_string = "{} {} {}".format( away_string, home_string, cls._clockBoardToString(game['clock'], game['period'], game['ended'])) # Highlighting 'buzzer-beaters': if game['buzzer_beater'] and not game['ended']: game_string = ircutils.mircColor(game_string, fg='yellow', bg='black') return game_string @classmethod def _clockBoardToString(cls, clock, period, game_ended): """Get a string with current period and, if the game is still in progress, the remaining time in it. """ period_number = period['current'] # Game hasn't started => There is no clock yet. if period_number == 0: return '' # Halftime if period['isHalftime']: return ircutils.mircColor('Halftime', 'orange') period_string = cls._periodToString(period_number) # Game finished: if game_ended: if period_number == 4: return ircutils.mircColor('F', 'red') return ircutils.mircColor("F {}".format(period_string), 'red') # Game in progress: if period['isEndOfPeriod']: return ircutils.mircColor("E{}".format(period_string), 'blue') # Period in progress, show clock: return "{} {}".format(clock, ircutils.mircColor(period_string, 'green')) @staticmethod def _periodToString(period): """Get a string describing the current period in the game. Period is an integer counting periods from 1 (so 5 would be OT1). The output format is as follows: {Q1...Q4} (regulation); {OT, OT2, OT3...} (overtimes). """ if period <= 4: return "Q{}".format(period) ot_number = period - 4 if ot_number == 1: return "OT" return "OT{}".format(ot_number) @staticmethod def _broadcastersToString(broadcasters): """Given a broadcasters dictionary (category->name), where category is in ['vTeam', 'hTeam', 'national', 'canadian'], return a printable string representation of that list. """ items = [] for category in ['vTeam', 'hTeam', 'national', 'canadian']: if category in broadcasters: items.append(broadcasters[category]) return ', '.join(items) def _upcomingGameToString(self, game): """Given a team's upcoming game, return a string with the opponent's tricode and the date of the game. """ date = self._ISODateToEasternDatetime(game['startTimeUTC']) home_tricode = self._teamIdToTricode(game['hTeam']['teamId']) away_tricode = self._teamIdToTricode(game['vTeam']['teamId']) if game['isHomeTeam']: home_tricode = ircutils.bold(home_tricode) else: away_tricode = ircutils.bold(away_tricode) return '{} | {} @ {}'.format(date, away_tricode, home_tricode) def _pastGameToString(self, game): """Given a team's upcoming game, return a string with the opponent's tricode and the result. """ date = self._ISODateToEasternDate(game['startTimeUTC']) home_tricode = self._teamIdToTricode(game['hTeam']['teamId']) away_tricode = self._teamIdToTricode(game['vTeam']['teamId']) home_score = int(game['hTeam']['score']) away_score = int(game['vTeam']['score']) if game['isHomeTeam']: was_victory = (home_score > away_score) else: was_victory = (away_score > home_score) if home_score > away_score: home_tricode = ircutils.bold(home_tricode) home_score = ircutils.bold(home_score) else: away_tricode = ircutils.bold(away_tricode) away_score = ircutils.bold(away_score) result = ircutils.mircColor('W', 'green') if was_victory \ else ircutils.mircColor('L', 'red') points = '{} {} {} {}'.format(away_tricode, away_score, home_tricode, home_score) if game['seasonStageId'] == 1: points += ' (Preseason)' return '{} {} | {}'.format(date, result, points) ############################ # Date-manipulation helpers ############################ @classmethod def _getTodayDate(cls): """Get the current date formatted as "YYYYMMDD". Because the API separates games by day of start, we will consider and return the date in the Pacific timezone. The objective is to avoid reading future games anticipatedly when the day rolls over at midnight, which would cause us to ignore games in progress that may have started on the previous day. Taking the west coast time guarantees that the day will advance only when the whole continental US is already on that day. """ today = cls._pacificTimeNow().date() today_iso = today.isoformat() return today_iso.replace('-', '') @staticmethod def _easternTimeNow(): return pendulum.now('US/Eastern') @staticmethod def _pacificTimeNow(): return pendulum.now('US/Pacific') @staticmethod def _ISODateToEasternDate(iso): """Convert the ISO date in UTC time that the API outputs into an Eastern-time date. (The default human-readable format for the listing of games). """ date = pendulum.parse(iso) date_eastern = date.in_tz('US/Eastern') eastern_date = date_eastern.strftime('%a %m/%d') return "{}".format(eastern_date) @staticmethod def _ISODateToEasternTime(iso): """Convert the ISO date in UTC time that the API outputs into an Eastern time formatted with am/pm. (The default human-readable format for the listing of games). """ date = pendulum.parse(iso) date_eastern = date.in_tz('US/Eastern') eastern_time = date_eastern.strftime('%-I:%M %p') return "{} ET".format(eastern_time) @staticmethod def _ISODateToEasternDatetime(iso): """Convert the ISO date in UTC time that the API outputs into a string with a date and Eastern time formatted with am/pm. """ date = pendulum.parse(iso) date_eastern = date.in_tz('US/Eastern') eastern_datetime = date_eastern.strftime('%a %m/%d, %I:%M %p') return "{} ET".format(eastern_datetime) @staticmethod def _stripDateSeparators(date_string): return date_string.replace('-', '') @classmethod def _EnglishDateToDate(cls, date): """Convert a human-readable like 'yesterday' to a datetime object and return a 'YYYYMMDD' string. """ if date == 'yesterday': day_delta = -1 elif date == 'today' or date == 'tonight': day_delta = 0 elif date == 'tomorrow': day_delta = 1 # Calculate the day difference and return a string date_string = cls._pacificTimeNow().add( days=day_delta).strftime('%Y%m%d') return date_string @classmethod def _isValidTricode(cls, team): return team in cls._TEAM_TRICODES ############################ # Input-parsing helpers ############################ @classmethod def _isPotentialDate(cls, string): """Given a user-provided string, check whether it could be a date. """ return (string.lower() in cls._FUZZY_DAYS or string.replace('-', '').isdigit()) @classmethod def _parseTeamInput(cls, team): """Given a user-provided string, try to extract an upper-case team tricode from it. If not valid, throws a ValueError exception. """ t = team.upper() if not cls._isValidTricode(t): raise ValueError('{} is not a valid team'.format(team)) return t @classmethod def _parseDateInput(cls, date): """Verify that the given string is a valid date formatted as YYYY-MM-DD. Also, the API seems to go back until 2014-10-04, so we will check that the input is not a date earlier than that. In case of failure, throws a ValueError exception. """ date = date.lower() if date in cls._FUZZY_DAYS: date = cls._EnglishDateToDate(date) elif date.replace('-', '').isdigit(): try: parsed_date = pendulum.from_format(date, 'YYYY-MM-DD') except: raise ValueError('Incorrect date format, should be YYYY-MM-DD') # The current API goes back until 2014-10-04. Is it in range? if parsed_date < pendulum.datetime(2014, 10, 4): raise ValueError('I can only go back until 2014-10-04') else: raise ValueError('Date is not valid') return cls._stripDateSeparators(date) def _getRecapInfo(self, game): """Given a finished game, fetch its recap summary and a link to its video recap. It returns a string with the format '{summary} (link to video)'. The link is shortened by calling _shortenURL(str) -> str. """ recap_base_url = 'https://www.nba.com/video/'\ '{year}/{month}/{day}/'\ '{game_id}-{away_team}-{home_team}-recap.xml' url = recap_base_url.format(year=game['starting_year'], month=game['starting_month'], day=game['starting_day'], game_id=game['game_id'], away_team=game['away_team'].lower(), home_team=game['home_team'].lower()) xml = self._getURL(url) tree = ElementTree.fromstring(xml) res = [] summary = tree.find('description') if summary is not None: res.append(summary.text) video_recap = tree.find("*file[@bitrate='1920x1080_5904']") if video_recap is not None: url = self._shortenURL(video_recap.text) res.append('({})'.format(url)) return ' '.join(res) @staticmethod def _shortenURL(url): """ Run a link through an URL shortener and return the new url.""" # Complete with the code that uses your desired # shortener service. return url
class Nickometer(callbacks.Plugin): """Will tell you how lame a nick is by the command `@nickometer [nick]`.""" def punish(self, damage, reason): self.log.debug('%s lameness points awarded: %s', damage, reason) return damage @internationalizeDocstring def nickometer(self, irc, msg, args, nick): """[<nick>] Tells you how lame said nick is. If <nick> is not given, uses the nick of the person giving the command. """ score = 0L if not nick: nick = msg.nick originalNick = nick if not nick: irc.error('Give me a nick to judge as the argument, please.') return specialCost = [('69', 500), ('dea?th', 500), ('dark', 400), ('n[i1]ght', 300), ('n[i1]te', 500), ('f**k', 500), ('sh[i1]t', 500), ('coo[l1]', 500), ('kew[l1]', 500), ('lame', 500), ('dood', 500), ('dude', 500), ('[l1](oo?|u)[sz]er', 500), ('[l1]eet', 500), ('e[l1]ite', 500), ('[l1]ord', 500), ('pron', 1000), ('warez', 1000), ('xx', 100), ('\\[rkx]0', 1000), ('\\0[rkx]', 1000)] letterNumberTranslator = utils.str.MultipleReplacer(dict(list(zip( '023457+8', 'ozeasttb')))) for special in specialCost: tempNick = nick if special[0][0] != '\\': tempNick = letterNumberTranslator(tempNick) if tempNick and re.search(special[0], tempNick, re.IGNORECASE): score += self.punish(special[1], 'matched special case /%s/' % special[0]) # I don't really know about either of these next two statements, # but they don't seem to do much harm. # Allow Perl referencing nick=re.sub('^\\\\([A-Za-z])', '\1', nick); # C-- ain't so bad either nick=re.sub('^C--$', 'C', nick); # Punish consecutive non-alphas matches=re.findall('[^\w\d]{2,}',nick) for match in matches: score += self.punish(slowPow(10, len(match)), '%s consecutive non-alphas ' % len(match)) # Remove balanced brackets ... while True: nickInitial = nick nick=re.sub('^([^()]*)(\()(.*)(\))([^()]*)$', '\1\3\5', nick, 1) nick=re.sub('^([^{}]*)(\{)(.*)(\})([^{}]*)$', '\1\3\5', nick, 1) nick=re.sub('^([^[\]]*)(\[)(.*)(\])([^[\]]*)$', '\1\3\5', nick, 1) if nick == nickInitial: break self.log.debug('Removed some matching brackets %r => %r', nickInitial, nick) # ... and punish for unmatched brackets unmatched = re.findall('[][(){}]', nick) if len(unmatched) > 0: score += self.punish(slowPow(10, len(unmatched)), '%s unmatched parentheses' % len(unmatched)) # Punish k3wlt0k k3wlt0k_weights = (5, 5, 2, 5, 2, 3, 1, 2, 2, 2) for i in range(len(k3wlt0k_weights)): hits=re.findall(repr(i), nick) if (hits and len(hits)>0): score += self.punish(k3wlt0k_weights[i] * len(hits) * 30, '%s occurrences of %s ' % (len(hits), i)) # An alpha caps is not lame in middle or at end, provided the first # alpha is caps. nickOriginalCase = nick match = re.search('^([^A-Za-z]*[A-Z].*[a-z].*?)[-_]?([A-Z])', nick) if match: nick = ''.join([nick[:match.start(2)], nick[match.start(2)].lower(), nick[match.start(2)+1:]]) match = re.search('^([^A-Za-z]*)([A-Z])([a-z])', nick) if match: nick = ''.join([nick[:match.start(2)], nick[match.start(2):match.end(2)].lower(), nick[match.end(2):]]) # Punish uppercase to lowercase shifts and vice-versa, modulo # exceptions above # the commented line is the equivalent of the original, but i think # they intended my version, otherwise, the first caps alpha will # still be punished #cshifts = caseShifts(nickOriginalCase); cshifts = caseShifts(nick); if cshifts > 1 and re.match('.*[A-Z].*', nick): score += self.punish(slowPow(9, cshifts), '%s case shifts' % cshifts) # Punish lame endings if re.match('.*[XZ][^a-zA-Z]*$', nickOriginalCase): score += self.punish(50, 'the last alphanumeric character was lame') # Punish letter to numeric shifts and vice-versa nshifts = numberShifts(nick); if nshifts > 1: score += self.punish(slowPow(9, nshifts), '%s letter/number shifts' % nshifts) # Punish extraneous caps caps = re.findall('[A-Z]', nick) if caps and len(caps) > 0: score += self.punish(slowPow(7, len(caps)), '%s extraneous caps' % len(caps)) # one trailing underscore is ok. i also added a - for parasite- nick = re.sub('[-_]$','',nick) # Punish anything that's left remains = re.findall('[^a-zA-Z0-9]', nick) if remains and len(remains) > 0: score += self.punish(50*len(remains) + slowPow(9, len(remains)), '%s extraneous symbols' % len(remains)) # Use an appropriate function to map [0, +inf) to [0, 100) percentage = 100 * (1 + math.tanh((score - 400.0) / 400.0)) * \ (1 - 1 / (1 + score / 5.0)) // 2 # if it's above 99.9%, show as many digits as is interesting score_string=re.sub('(99\\.9*\\d|\\.\\d).*','\\1',repr(percentage)) irc.reply(_('The "lame nick-o-meter" reading for "%s" is %s%%.') % (originalNick, score_string)) self.log.debug('Calculated lameness score for %s as %s ' '(raw score was %s)', originalNick, score_string, score) nickometer = wrap(nickometer, [additional('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'])
try: lines = madison(package) if not lines: irc.reply('Did not get a response -- is "%s" a valid package?' % package) return field_styles = ('package', 'version', 'distribution', 'section') for line in lines: out = [] fields = line.strip().split('|', len(field_styles)) for style, data in zip(field_styles, fields): out.append('[%s]%s' % (style, data)) irc.reply(colourise('[reset]|'.join(out)), prefixNick=False) except Exception, e: irc.reply("Error: %s" % e.message) madison = wrap(madison, ['text']) def get_pool_url(self, package): if package.startswith('lib'): return (package[:4], package) else: return (package[:1], package) def _maintainer(self, irc, msg, args, items): for package in items: info = Maintainer().get_maintainer(package) if info: display_name = format_email_address("%s <%s>" % (info['name'], info['email']), max_domain=18) login = info['email'] if login.endswith('@debian.org'):
class DebianDevelChanges(supybot.callbacks.Plugin): threaded = True def __init__(self, irc): super(DebianDevelChanges, self).__init__(irc) self.irc = irc self.topic_lock = threading.Lock() fr = FifoReader() fifo_loc = '/var/run/debian-devel-changes-bot/fifo' fr.start(self._email_callback, fifo_loc) self.requests_session = requests.Session() self.requests_session.verify = True self.queued_topics = {} self.last_n_messages = [] self.stable_rc_bugs = StableRCBugs(self.requests_session) self.testing_rc_bugs = TestingRCBugs(self.requests_session) self.new_queue = NewQueue(self.requests_session) self.data_sources = (self.stable_rc_bugs, self.testing_rc_bugs, self.new_queue) # Schedule datasource updates for klass, interval, name in get_datasources(): try: schedule.removePeriodicEvent(name) except KeyError: pass def wrapper(klass=klass): klass().update() self._topic_callback() schedule.addPeriodicEvent(wrapper, interval, name, now=False) schedule.addEvent(wrapper, time.time() + 1) def wrapper(source): def implementation(): source.update() self._topic_callback() return implementation for source in self.data_sources: schedule.addPeriodicEvent(wrapper(source), source.INTERVAL, source.NAME, now=False) schedule.addEvent(wrapper(source), time.time() + 1) def die(self): FifoReader().stop() for _, _, name in get_datasources(): try: schedule.removePeriodicEvent(name) except KeyError: # A newly added event may not exist, ignore exception. pass for source in self.data_sources: try: schedule.removePeriodicEvent(source.NAME) except KeyError: pass def _email_callback(self, fileobj): try: email = parse_mail(fileobj) msg = get_message(email, new_queue=self.new_queue) if not msg: return txt = colourise(msg.for_irc()) # Simple flood/duplicate detection if txt in self.last_n_messages: return self.last_n_messages.insert(0, txt) self.last_n_messages = self.last_n_messages[:20] for channel in self.irc.state.channels: package_regex = self.registryValue( 'package_regex', channel, ) or 'a^' # match nothing by default package_match = re.search(package_regex, msg.package) maintainer_match = False maintainer_regex = self.registryValue( 'maintainer_regex', channel) if maintainer_regex: info = Maintainer().get_maintainer(msg.package) if info: maintainer_match = re.search(maintainer_regex, info['email']) if not package_match and not maintainer_match: continue distribution_regex = self.registryValue( 'distribution_regex', channel, ) if distribution_regex: if not hasattr(msg, 'distribution'): # If this channel has a distribution regex, don't # bother continuing unless the message actually has a # distribution. This filters security messages, etc. continue if not re.search(distribution_regex, msg.distribution): # Distribution doesn't match regex; don't send this # message. continue ircmsg = supybot.ircmsgs.privmsg(channel, txt) self.irc.queueMsg(ircmsg) except Exception as e: log.exception('Uncaught exception: %s ' % e) def _topic_callback(self): sections = { self.testing_rc_bugs.get_number_bugs: 'RC bug count', self.stable_rc_bugs.get_number_bugs: 'Stable RC bug count', self.new_queue.get_size: 'NEW queue', RmQueue().get_size: 'RM queue', } with self.topic_lock: values = {} for callback, prefix in sections.iteritems(): values[callback] = callback() for channel in self.irc.state.channels: new_topic = topic = self.irc.state.getTopic(channel) for callback, prefix in sections.iteritems(): if values[callback]: new_topic = rewrite_topic(new_topic, prefix, values[callback]) if topic != new_topic: log.info("Queueing change of topic in #%s to '%s'" % (channel, new_topic)) self.queued_topics[channel] = new_topic event_name = '%s_topic' % channel try: schedule.removeEvent(event_name) except KeyError: pass schedule.addEvent(lambda channel=channel: self._update_topic(channel), time.time() + 60, event_name) def _update_topic(self, channel): with self.topic_lock: try: new_topic = self.queued_topics[channel] log.info("Changing topic in #%s to '%s'" % (channel, new_topic)) self.irc.queueMsg(supybot.ircmsgs.topic(channel, new_topic)) except KeyError: pass def greeting(self, prefix, irc, msg, args): num_bugs = self.testing_rc_bugs.get_number_bugs() if type(num_bugs) is int: advice = random.choice(( 'Why not go and fix one?', 'Why not peek at the list and find one?', 'Stop IRCing and fix one! :]', 'You realise they don\'t fix themselves, right?', 'How about fixing yourself some caffeine and then poking at the bug list?', )) txt = "%s %s! There are currently %d RC bugs in stretch. %s" % \ (prefix, msg.nick, num_bugs, advice) else: txt = "%s %s!" % (prefix, msg.name) irc.reply(txt, prefixNick=False) def morning(self, *args): self.greeting('Good morning,', *args) morning = wrap(morning) yawn = wrap(morning) wakeywakey = wrap(morning) def night(self, *args): self.greeting( 'Good night,', *args) night = wrap(night) nn = wrap(night) goodnight = wrap(night) def sup(self, *args): self.greeting("'sup", *args) sup = wrap(sup) lo = wrap(sup) def rc(self, irc, msg, args): num_bugs = self.testing_rc_bugs.get_number_bugs() if type(num_bugs) is int: irc.reply("There are %d release-critical bugs in the testing distribution. " \ "See https://udd.debian.org/bugs.cgi?release=stretch¬main=ign&merged=ign&rc=1" % num_bugs) else: irc.reply("No data at this time.") rc = wrap(rc) bugs = wrap(rc) def update(self, irc, msg, args): if not ircdb.checkCapability(msg.prefix, 'owner'): irc.reply("You are not authorised to run this command.") return for klass, interval, name in get_datasources(): klass().update() irc.reply("Updated %s." % name) for source in self.data_sources: source.update() irc.reply("Updated %s." % source.NAME) self._topic_callback() update = wrap(update) def madison(self, irc, msg, args, package): try: lines = madison(package) if not lines: irc.reply('Did not get a response -- is "%s" a valid package?' % package) return field_styles = ('package', 'version', 'distribution', 'section') for line in lines: out = [] fields = line.strip().split('|', len(field_styles)) for style, data in zip(field_styles, fields): out.append('[%s]%s' % (style, data)) irc.reply(colourise('[reset]|'.join(out)), prefixNick=False) except Exception, e: irc.reply("Error: %s" % e.message)
class SubredditAnnouncer(callbacks.Plugin): """Add the help for "@plugin help SubredditAnnouncer" here This should describe *how* to use this plugin.""" def __init__(self, irc): self.__parent = super(SubredditAnnouncer, self) self.__parent.__init__(irc) self.savefile = conf.supybot.directories.data.dirize( "subredditAnnouncer.db") self.headers = {"User-Agent": "SubredditAnnouncer ([email protected])"} def checkForPosts(): self.checkReddit(irc) try: schedule.addPeriodicEvent(checkForPosts, self.registryValue('checkinterval') * 60, 'redditCheck', False) except AssertionError: schedule.removeEvent('redditCheck') schedule.addPeriodicEvent(checkForPosts, self.registryValue('checkinterval') * 60, 'redditCheck', False) try: if self.registryValue('dsn') != "": if "raven" in dir(): # Check that raven was actually imported self.raven = raven.Client(self.registryValue("dsn")) else: self.log.error( "dsn defined but raven not installed! Please pip install raven" ) except NonExistentRegistryEntry: pass def post(self, irc, channel, msg): try: irc.queueMsg(ircmsgs.privmsg(channel, str(msg))) except Exception as e: self.log.warning("Failed to send to " + channel + ": " + str(type(e))) self.log.warning(str(e.args)) self.log.warning(str(e)) def checkReddit(self, irc): try: data = json.load(open(self.savefile)) except Exception: domain = "http://www.reddit.com" if self.registryValue('domain') is not "": domain = self.registryValue('domain') data = {domain: {"announced": [], "subreddits": []}} parser = ConfigParser.SafeConfigParser() parser.read([self.registryValue('configfile')]) for channel in parser.sections(): if channel != "global": try: addtoindex = [] sub = parser.get(channel, 'subreddits') domain = self.registryValue('domain') if parser.has_option(channel, 'domain'): domain = parser.get(channel, 'domain') if domain not in data: data[domain] = {"announced": [], "subreddits": []} self.log.info("Creating data store for " + domain) messageformat = "[NEW] [{redditname}] [/r/{subreddit}] {bold}{title}{bold} - {shortlink}" if parser.has_section("global"): if parser.has_option("global", "format"): messageformat = parser.get("global", "format") if parser.has_option(channel, "format"): messageformat = parser.get(channel, "format") url = domain + "/r/" + sub + "/new.json?sort=new" self.log.info("Checking " + url + " for " + channel) request = requests.get(url, headers=self.headers) listing = request.json() for post in listing['data']['children']: if not post['data']['id'] in data[domain]['announced']: shortlink = self.registryValue( 'domain') + "/" + post['data']['id'] if self.registryValue('shortdomain') is not None: shortlink = self.registryValue( 'shortdomain') + "/" shortlink += post['data']['id'] if parser.has_option(channel, 'shortdomain'): shortlink = parser.get(channel, 'shortdomain') + "/" shortlink += post['data']['id'] redditname = "" if self.registryValue('redditname') is not "": redditname = self.registryValue('redditname') if parser.has_option(channel, 'redditname'): redditname = parser.get(channel, 'redditname') if post['data']['subreddit'] in data[domain][ 'subreddits']: msg = messageformat.format( redditname=redditname, subreddit=sanatize( post['data']['subreddit']), title=sanatize(post['data']['title']), author=sanatize(post['data']['author']), link=sanatize(post['data']['url']), shortlink=shortlink, score=sanatize(post['data']['score']), ups=sanatize(post['data']['ups']), downs=sanatize(post['data']['downs']), comments=sanatize( post['data']['num_comments']), domain=domain, bold=chr(002), underline="\037", reverse="\026", white="\0030", black="\0031", blue="\0032", red="\0034", dred="\0035", purple="\0036", dyellow="\0037", yellow="\0038", lgreen="\0039", dgreen="\00310", green="\00311", lpurple="\00313", dgrey="\00314", lgrey="\00315", close="\003") self.post(irc, channel, msg) else: self.log.info( "Not posting " + self.registryValue('shortdomain') + "/" + post['data']['id'] + " because it's our first time looking at /r/" + post['data']['subreddit']) if not post['data']['subreddit'] in addtoindex: addtoindex.append( post['data']['subreddit']) data[domain]['announced'].append( post['data']['id']) except Exception as e: if hasattr(self, 'raven'): self.raven.captureException() else: self.log.warning("Whoops! Something f****d up: " + str(e)) if domain not in data: data[domain] = {"announced": [], "subreddits": []} self.log.info("Creating data store for " + domain) if sub not in data[domain]['subreddits']: data[domain]['subreddits'].extend(addtoindex) savefile = open(self.savefile, "w") savefile.write(json.dumps(data)) savefile.close() def check(self, irc, msg, args): """takes no args Checks the specified subreddit and announces new posts""" if ircdb.checkCapability(msg.prefix, "owner"): irc.reply("Checking!") self.checkReddit(irc) else: irc.reply("F**k off you unauthorized piece of shit") check = wrap(check) def start(self, irc, msg, args): """takes no arguments A command to start the node checker.""" # don't forget to redefine the event wrapper if ircdb.checkCapability(msg.prefix, "owner"): def checkForPosts(): self.checkReddit(irc) try: schedule.addPeriodicEvent( checkForPosts, self.registryValue('checkinterval') * 60, 'redditCheck', False) except AssertionError: irc.reply('The reddit checker was already running!') else: irc.reply('Reddit checker started!') else: irc.reply("F**k off you unauthorized piece of shit") start = wrap(start) def stop(self, irc, msg, args): """takes no arguments A command to stop the node checker.""" if ircdb.checkCapability(msg.prefix, "owner"): try: schedule.removeEvent('redditCheck') except KeyError: irc.reply('Error: the reddit checker wasn\'t running!') else: irc.reply('Reddit checker stopped.') else: irc.reply("F**k off you unauthorized piece of shit") stop = wrap(stop)
def altbug(self, irc, msg, args, bugno): """<bug number> Shows information about specified bug from ALT Linux bugzilla. """ try: buginfo = self._getBugInfo(bugno) except utils.web.Error, err: irc.error(err.args[0]) return irc.reply('%(bug_id)s: %(bug_severity)s, %(bug_status)s' '%(resolution)s; %(product)s - %(component)s; created on ' '%(creation_time)s by %(reporter)s, assigned to ' '%(assigned_to)s, last changed on %(last_change_time)s; ' 'summary: "%(summary)s"' % buginfo) altbug = wrap(altbug, [('id', 'bug')]) def _getBugInfo(self, bugno): def _formatEmail(e): if e.get('name'): return '%s <%s>' % (self._encode(e.get('name')), e.text) return e.text bugXML = utils.web.getUrlFd(self.bugzillaRoot + 'show_bug.cgi?ctype=xml&excludefield=attachmentdata&' 'excludefield=long_desc&excludefield=attachment&id=' + str(bugno)) etree = ElementTree(file=bugXML) bugRoot = etree.find('bug') buginfo = { 'bug_id': bugRoot.find('bug_id').text,
def decorator(func): return wrap(func, *args, **kwargs)
class YBot(callbacks.Plugin): """sup ygg bot""" threaded = True def __init__(self, irc): global _shout_err self.__parent = super(YBot, self) self.__parent.__init__(irc) self.yggb = YggBrowser(log=self.log) self._shout = YggShout(robs=self.yggb, log=self.log) _shout_err = 0 self._col = dict() def yggv(self, irc, msg, args): """ Prints the plugin version """ irc.reply(yggscr.__version__) yggv = wrap(yggv) def yconn(self, irc, msg, args): """ Print connection details """ irc.reply("{}".format(self.yggb)) yconn = wrap(yconn) def yprox(self, irc, msg, args, https_proxy): """[https proxy] Sets or removes proxy (http, socks, ..) """ if https_proxy: self.yggb.proxify(https_proxy) else: self.yggb.proxify(None) irc.replySuccess() yprox = wrap(yprox, ['owner', optional('anything')]) def ysearch(self, irc, msg, args, n, detail, p): # noqa """[n(int)] [detail (True/False)] q:pattern [c:cat [s:subcat]] [opt:val]* Searches on ygg and return first page results - Will only return the first nmax results and waits 1s between each reply """ q = {} try: for t in p.split(): k, v = t.rsplit(':', 1) if k in q.keys(): if isinstance(q[k], list): q[k].append(v) else: q[k] = [q[k], v] else: q[k] = v except ValueError: irc.error("Wrong syntax") return q['name'] = q.pop('q') q['category'] = q.pop('c', "") q['sub_category'] = q.pop('s', "") if n is None: n = 3 if detail is None: detail = False try: torrents = self.yggb.search_torrents(detail=detail, q=q, nmax=int(n)) except (requests.exceptions.ProxyError, requests.exceptions.ConnectionError) as e: irc.error("Network Error: %s" % e) return except YggException as e: irc.error("Ygg Exception raised: %s" % e) return if torrents is None: irc.reply("No results") return for idx, torrent in enumerate(torrents[:n]): sleep(1) irc.reply( "%2d - %s [%s Size:%s C:%s S:%s L:%s Comm:%s Uploader:%s] : %s" % (1 + idx, torrent.title, torrent.publish_date, torrent.size, torrent.completed, torrent.seed, torrent.leech, torrent.comm, torrent.uploader, torrent.href)) ysearch = wrap(ysearch, [optional('int'), optional('boolean'), 'text']) def ycat(self, irc, msg, args): """Will list available cat/subcat combinaisons """ irc.reply("Available (cat, subcat) combinaisons:{}".format( list_cat_subcat())) ycat = wrap(ycat) def ylogin(self, irc, msg, args, yuser, ypass): """[user pass] Logins to ygg using given credentials or stored one """ if not yuser and not ypass: yuser = self.registryValue('cred.user') ypass = self.registryValue('cred.pass') if not yuser or not ypass: irc.error("You need to set cred.user and cred.pass") return elif not ypass: irc.error("Wrong syntax") return try: self.yggb.login(yuser, ypass) except (requests.exceptions.ProxyError, requests.exceptions.ConnectionError) as e: irc.error("Network Error: %s" % e) return except YggException as e: irc.error("Ygg Exception raised: %s" % e) return except Exception as e: irc.error("Could not login to Ygg with credentials: %s" % e) return irc.replySuccess() self.log.info("Connected as {}".format(yuser)) ylogin = wrap( ylogin, ['owner', optional('anything'), optional('anything')]) def ylogout(self, irc, msg, args): """ Logout from ygg """ self.yggb.logout() irc.replySuccess() ylogout = wrap(ylogout, ['owner']) def ystats(self, irc, msg, args): """ Return ratio stats """ if self.yggb.idstate == "Anonymous": irc.error("You need to be authenticated at ygg") else: try: r = self.yggb.get_stats() except (requests.exceptions.ProxyError, requests.exceptions.ConnectionError) as e: irc.error("Network Error: %s" % e) return except YggException as e: irc.error("Ygg Exception raised: %s" % e) return except Exception as e: irc.error("Could not get stats: %s" % e) return irc.reply('↑ {:7.2f}GB ↓ {:7.2f}GB % {:6.4f}'.format( r['up'], r['down'], r['ratio'])) irc.reply( '↑ Instant {}KBps Mean {}KBps ↓ Instant {}KBps Mean {}KBps'. format(r['i_up'], r['m_up'], r['i_down'], r['m_down'])) ystats = wrap(ystats) def yresp(self, irc, msg, args): """ Print http response on console """ self.log.info("ygg request response:%s" % self.yggb.response()) irc.replySuccess() yresp = wrap(yresp) def yping(self, irc, msg, args, n, quiet): """[n] [quiet: boolean(default False)] GET / """ t = [] statuses = defaultdict(int) mmin, mmax, mmean = float("inf"), float("-inf"), float("inf") if n is None: n = 1 if n > 100: n = 100 if n > 10 and quiet is False: n = 10 for _ in range(n): try: t1 = time() sts = self.yggb.ping() t2 = time() dt = 1000 * (t2 - t1) mmax = max(mmax, dt) mmin = min(mmin, dt) t.append(dt) if not quiet: irc.reply("{:>2} ping {} time={:>7.2f}ms http {}".format( 1 + _ if n > 1 else "", self.yggb.browser.url, dt, sts), prefixNick=False) statuses[sts] += 1 except Exception as e: mmax = float("inf") irc.reply("{:>2} timeout! [{}]".format(1 + _, e), prefixNick=False) if n == 1: return if t: mmean = sum(t) / len(t) str_statuses = ' | '.join('{}:{}'.format(key, value) for key, value in statuses.items()) irc.reply( "{} packet{} transmitted, {} received, {:.2%} packet loss, http codes {}" .format(n, "s" if n > 1 else "", len(t), 1 - len(t) / n, str_statuses), prefixNick=False) irc.reply("rtt min/avg/max = {:.2f}/{:.2f}/{:.2f} ms".format( mmin, mmean, mmax), prefixNick=False) yping = wrap(yping, [optional('PositiveInt'), optional('boolean')]) def colorize_user(self, user, group, w_colour): colours = ('blue', 'green', 'brown', 'purple', 'orange', 'yellow', 'light green', 'teal', 'light blue', 'pink', 'dark gray', 'light gray') # 1: unknown, 2: Membre, 3: supermod, 4: mod, 5: tp, 8: nouveau membre, 9: desactivé gcolours = { 1: 'blue', 3: 'orange', 4: 'green', 5: 'pink', 8: 'purple', 9: 'brown' } # Don't colorize members unless w_colour for color tracking if group == 2: if w_colour: mhash = sha256() mhash.update(user.encode()) mhash = mhash.digest()[0] mhash = hash % len(colours) user = ircutils.mircColor(user, colours[mhash]) else: pass elif group not in gcolours.keys(): user = ircutils.mircColor(user, gcolours[1]) else: user = ircutils.mircColor(user, gcolours[group]) # High grade in bold if group in [1, 3, 4, 5]: user = ircutils.bold(user) return user def shoutify(self, shoutm, w_colour): user = "******".format(shoutm.user) user = self.colorize_user(user, shoutm.group, w_colour) fmt = self.registryValue('shout.fmt') msg = shoutm.message.replace('\n', ' ').replace('\n', ' ') return fmt.format(time=shoutm.mtime, id=shoutm.id, fuser=user, user=shoutm.user, group=shoutm.group, message=msg) def yshout(self, irc, msg, args, n, w_colour=False, hfile=None): """[int n] [boolean user_colorized] [injected html file] Print last shout messages and detects gap. Time is UTC. User will be colorized if boolean is True. """ global _shout_err rate_err = self.registryValue('shout.rate_err') if hfile: try: with open(hfile, "r") as fn: html = fn.read() except Exception: irc.error("Can't read file %s" % hfile) return shoutm = ShoutMessage(shout=None, soup=BeautifulSoup(html, 'html.parser')) irc.reply(self.shoutify(shoutm, False), prefixNick=False) return try: self._shout.get_shouts() diff = self._shout.do_diff() _shout_err = 0 except Exception as e: self.log.info("Could not dump shout, aborting. Error %s. Tid %s", e, threading.get_ident()) _shout_err += 1 if _shout_err % rate_err == 0: irc.error( "Shout ({} messages suppressed) (Exception {})".format( rate_err, e)) irc.error("Connection details: {}".format(self.yggb)) return if n is None: n = len(diff) for removed, shoutm in diff[len(diff) - n:]: prefix = "REMOVED!!: " if removed else "" irc.reply(prefix + self.shoutify(shoutm, w_colour), prefixNick=False) sleep(1) yshout = wrap( yshout, ['owner', optional('int'), optional('boolean'), optional('filename')]) def ydebug(self, irc, msg, args, debug): """[debug: boolean to set debug] Get or set bot debug level """ if debug is None: irc.reply( "Debug level for %s is %s" % (self.log.name, yggscr.ylogging.loggerlevel_as_text(self.log))) else: yggscr.ylogging.set_log_debug(debug) irc.replySuccess() ydebug = wrap(ydebug, [optional('boolean')])
class project(callbacks.Commands): """Project commands""" @internationalizeDocstring def add(self, irc, msg, args, channel, project_slug, project_url): """[<channel>] <project-slug> <project-url> Announces the changes of the project with the slug <project-slug> and the url <project-url> to <channel>. """ if not instance._check_capability(irc, msg): return projects = instance._load_projects(channel) if project_slug in projects: irc.error( _('This project is already announced to this channel.') ) return # Save new project mapping projects[project_slug] = project_url instance._save_projects(projects, channel) irc.replySuccess() add = wrap(add, ['channel', 'somethingWithoutSpaces', 'httpUrl']) @internationalizeDocstring def remove(self, irc, msg, args, channel, project_slug): """[<channel>] <project-slug> Stops announcing the changes of the project slug <project-slug> to <channel>. """ if not instance._check_capability(irc, msg): return projects = instance._load_projects(channel) if project_slug not in projects: irc.error( _('This project is not registered to this channel.')) return # Remove project mapping del projects[project_slug] instance._save_projects(projects, channel) irc.replySuccess() remove = wrap(remove, ['channel', 'somethingWithoutSpaces']) @internationalizeDocstring def list(self, irc, msg, args, channel): """[<channel>] Lists the registered projects in <channel>. """ if not instance._check_capability(irc, msg): return projects = instance._load_projects(channel) if projects is None or len(projects) == 0: irc.error(_('This channel has no registered projects.')) return for project_slug, project_url in projects.items(): irc.reply("%s: %s" % (project_slug, project_url)) list = wrap(list, ['channel'])
class DSWeather(callbacks.Plugin): """Weather info from DarkSky""" threaded = True def __init__(self, irc): self.__parent = super(DSWeather, self) self.__parent.__init__(irc) locationdb_file = conf.supybot.directories.data.dirize( "DSWeather-locations.json") self.log.debug("location db: " + locationdb_file) if os.path.exists(locationdb_file): with open(locationdb_file) as f: self.locationdb = json.load(f) else: self.log.info("No location db found, creating...") self.locationdb = {} world.flushers.append(self._sync_locationdb) def _sync_locationdb(self): locationdb_filename = conf.supybot.directories.data.dirize( "DSWeather-locations.json") with open(locationdb_filename, 'w') as f: json.dump(self.locationdb, f) def die(self): world.flushers.remove(self._sync_locationdb) self._sync_locationdb() def _get_location(self, location): self.log.debug("checking location " + str(location)) loc = location.lower() if loc in self.locationdb: self.log.debug("Using cached details for %s" % loc) return self.locationdb[loc] url = 'https://nominatim.openstreetmap.org/search/%s?format=jsonv2' % utils.web.urlquote( location) self.log.debug("trying url %s" % url) r = requests.get(url) if len(r.json()) == 0: self.locationdb[loc] = None return None data = r.json()[0] self.log.debug("Found location: %s" % (data['display_name'])) self.locationdb[loc] = data return data def _get_weather(self, latitude, longitude, extra=None): baseurl = "https://api.darksky.net/forecast/" opts = {'exclude': 'minutely,hourly,daily'} alerts = [] r = requests.get(baseurl + self.registryValue('apikey') + "/%s,%s" % (str(latitude), str(longitude)), params=opts) if 'alerts' in r.json(): alerts = r.json()['alerts'] return (r.json()['currently']['temperature'], r.json()['currently']['summary'], alerts) def weather(self, irc, msg, args, things): """get the weather for a location""" location = ' '.join(things) loc_data = self._get_location(location) if loc_data is None: irc.reply( "Sorry, \"%s\" is not found. Please try your search on https://nominatim.openstreetmap.org/" % location) else: (temp, status, alerts) = self._get_weather(loc_data['lat'], loc_data['lon']) tempC = round((float(temp) - 32) * 5 / 9, 1) tempF = round(float(temp), 1) irc.reply("The weather in \"%s\" currently %sF/%sC and %s" % (loc_data['display_name'], tempF, tempC, status), sendImmediately=True) # Handle alerts self.log.debug("show alerts: %s" % (str(self.registryValue('alerts')))) if self.registryValue('alerts'): for alert in alerts: msg = "Alert: %s [%s] for this location until %s: %s" % ( alert['title'], alert['severity'], time.ctime(alert['expires']), alert['description']) irc.reply(msg, sendImmediately=True) weather = wrap(weather, [any('something')])
class Irccat(callbacks.Plugin): ''' Main plugin. Runs the dataflow from TCP port -> irc in a separate thread, governed by twisted's reactor.run(). Commands are executed in main thread. The critical zone is self.config, a _Config instance. ''' # pylint: disable=E1101,R0904 threaded = True admin = 'owner' # The capability required to manage data. def __init__(self, irc): callbacks.Plugin.__init__(self, irc) self.log = log.getPluginLogger('irccat.irccat') self.config = _Config() self.pipe = multiprocessing.Pipe() self.pipe[1].send(self.config) self.process = multiprocessing.Process( target = io_process, args = (self.config.port, self.pipe)) self.process.start() self.listen_abort = False self.thread = threading.Thread(target = self.listener_thread) self.thread.start() def replace_topic(self, irc, channel, pattern, replacement): """ Looks for pattern in channel topic and replaces it with replacement string. """ curtopic = irc.state.getTopic(channel) newtopic = re.sub(pattern, lambda m: replacement, curtopic, count=1, flags=re.IGNORECASE) irc.queueMsg(ircmsgs.topic(channel, newtopic)) def listener_thread(self): ''' Take messages from process, write them to irc.''' while not self.listen_abort: try: if not self.pipe[1].poll(0.5): continue msg, channels = self.pipe[1].recv() for channel in channels: for irc in world.ircs: if channel in irc.state.channels: if self.config.topic_regex: self.replace_topic(irc, channel, self.config.topic_regex, msg) elif self.config.privmsg: irc.queueMsg(ircmsgs.privmsg(channel, msg)) else: irc.queueMsg(ircmsgs.notice(channel, msg)) else: self.log.warning( "Can't write to non-joined channel: " + channel) except EOFError: self.listen_abort = True except Exception: self.log.debug("LISTEN: Exception", exc_info = True) self.listen_abort = True self.log.debug("LISTEN: exiting") def die(self, cmd = False): # pylint: disable=W0221 ''' Tear down reactor thread and die. ''' self.log.debug("Dying...") self.process.terminate() self.listen_abort = True self.thread.join() if not cmd: callbacks.Plugin.die(self) def sectiondata(self, irc, msg, args, section_name, password, channels): """ <section name> <password> <channel[,channel...]> Update a section with name, password and a comma-separated list of channels which should be connected to this section. Creates new section if it doesn't exist. """ salts = 'abcdcefghijklmnopqrstauvABCDEFGHIJKLMNOPQRSTUVXYZ123456789' salt = random.choice(salts) + random.choice(salts) cipher_pw = crypt.crypt(password, salt) self.config.update(section_name, cipher_pw, channels) self.pipe[1].send(self.config) irc.replySuccess() sectiondata = wrap(sectiondata, [admin, 'somethingWithoutSpaces', 'somethingWithoutSpaces', commalist('validChannel')]) def sectionkill(self, irc, msg, args, section_name): """ <section name> Removes an existing section given it's name. """ try: self.config.remove(section_name) except KeyError: irc.reply("Error: no such section") return self.pipe[1].send(self.config) irc.replySuccess() sectionkill = wrap(sectionkill, [admin, 'somethingWithoutSpaces']) def sectionshow(self, irc, msg, args, section_name): """ <section name> Show data for a section. """ try: password, channels = self.config.get(section_name) except KeyError: irc.reply("Error: no such section") return msg = password + ' ' + ','.join(channels) irc.reply(msg) sectionshow = wrap(sectionshow, [admin, 'somethingWithoutSpaces']) def sectionlist(self, irc, msg, args): """ <takes no arguments> Print list of sections. """ msg = ' '.join(self.config.keys()) irc.reply(msg if msg else 'No sections defined') sectionlist = wrap(sectionlist, [admin]) def sectionhelp(self, irc, msg, args): """ <takes no argument> print help url """ irc.reply(_HELP_URL) sectionhelp = wrap(sectionhelp, [])
class Jira(callbacks.Plugin): """This plugin communicates with Jira. It will automatically snarf Jira ticket numbers, and reply with some basic information about the ticket.""" threaded = True def __init__(self, irc): self.__parent = super(Jira, self) self.__parent.__init__(irc) self.server = self.registryValue('server') self.checkTime = 30 # move to config try: schedule.removeEvent('recent') except KeyError: pass def myEventCaller(): self.recentOnly(irc) schedule.addPeriodicEvent(myEventCaller, self.checkTime, 'recent') self.irc = irc def searchissue(self, irc, msg, args): """Search for JIRA issues by keyword/s in issue summary. Outputs up to three results.""" jira = JIRA(self.server) if len(args) == 0: replytext = ( "Search for JIRA issues by keyword/s. Outputs up to three results. Example: \x02searchissue Allwinner H6 sound" ) irc.reply(replytext, prefixNick=False) return # construct search string for JIRA API searchstring = "project=Armbian " for arg in args: searchstring += " AND summary ~ " + arg searchstring += " order by created" resultlist = [] for issue in jira.search_issues(searchstring, maxResults=3): recentDate = issue.fields.created splitdate = recentDate.split('T') resultlist.append([ issue.key, issue.fields.issuetype, issue.fields.summary.strip(), issue.fields.creator, splitdate[0], issue.fields.status ]) if len(resultlist) == 0: replytext = ("\x02Nothing found.") irc.reply(replytext, prefixNick=False) else: for issue in resultlist: replytext = ( "\x1F\x02\x034{0}\x0F\x03 \x02\x036[{1}] \x03\"{2}\" \x0Freported by \x02\x033{3}\x03\x0F at \x02{4}\x0F. Status: \x1F\x02{5}\x0F" .format(issue[0], issue[1], issue[2], issue[3], issue[4], issue[5])) irc.reply(replytext, prefixNick=False) #searchissue = wrap(searchissue, ['text']) def doPrivmsg(self, irc, msg): if callbacks.addressed(irc, msg): return snarfChannel = self.registryValue('snarfChannel') if not msg.channel == snarfChannel: return """ log.error(msg.channel) log.error(msg.nick) log.error(msg.args[1]) log.error(self.registryValue('snarfRegex')) log.error(re.search(self.registryValue('snarfRegex'), msg.args[1])) """ x = re.search(self.registryValue('snarfRegex'), msg.args[1]) if x: jira = JIRA(self.server) try: issue = jira.issue(x.group(0)) recentDate = issue.fields.created splitdate = recentDate.split('T') replytext = ( "\x1F\x02\x034{0}\x0F\x03 \x02\x036[{1}] \x03\"{2}\" \x0Freported by \x02\x033{3}\x03\x0F at \x02{4}\x0F. Status: \x1F\x02{5}\x0F" .format(issue.key, issue.fields.issuetype, issue.fields.summary.strip(), issue.fields.creator, splitdate[0], issue.fields.status)) # .strip() to get rid of accidential added leading or trailing whitespaces in issue summary irc.reply(replytext, prefixNick=False) except: replytext = ( "Detected regex match for Armbian issue: \x1F\x02\x034{0}\x0F\x03. Could not find it on Jira though. :-(" .format(x.group(0))) irc.reply(replytext, prefixNick=False) return def recent(self, irc, msg, args): """Fetch the most recent issue""" jira = JIRA(self.server) for issue in jira.search_issues('project=Armbian order by created', maxResults=1): recentDate = issue.fields.created splitdate = recentDate.split('T') replytext = ( "\x1F\x02\x034{0}\x0F\x03 \x02\x036[{1}] \x03\"{2}\" \x0Freported by \x02\x033{3}\x03\x0F at \x02{4}\x0F. Status: \x1F\x02{5}\x0F" .format(issue.key, issue.fields.issuetype, issue.fields.summary.strip(), issue.fields.creator, splitdate[0], issue.fields.status)) irc.reply(replytext, prefixNick=False) recent = wrap(recent) def recentOnly(self, irc): """Fetch the most recent issue Not a real command, just used for scheduled recurring search""" jira = JIRA(self.server) # There must be a more decent way to read the file that consist of one line only and # to get rid of the file at all and keep that in memory. script_dir = os.path.dirname(__file__) rel_path = "issue.txt" abs_file_path = os.path.join(script_dir, rel_path) try: with open(abs_file_path, "r+", encoding="utf-8") as file: for line in file: lastknownissue = line logmsg = "Last known issue key:" + lastknownissue log.debug(logmsg) except: with open(abs_file_path, "w+", encoding="utf-8") as file: lastknownissue = "" log.debug("Created empty issue temp file.") for issue in jira.search_issues('project=Armbian order by created', maxResults=1): if issue.key != lastknownissue: recentDate = issue.fields.created splitdate = recentDate.split('T') replytext = ( "\x1F\x02\x034{0}\x0F\x03 \x02\x036[{1}] \x03\"{2}\" \x0Freported by \x02\x033{3}\x03\x0F at \x02{4}\x0F. Status: \x1F\x02{5}\x0F" .format(issue.key, issue.fields.issuetype, issue.fields.summary.strip(), issue.fields.creator, splitdate[0], issue.fields.status)) # irc.reply(replytext, prefixNick=False) # shall not be used in schedule events irc.queueMsg( ircmsgs.privmsg(self.registryValue('channel'), replytext)) with open(abs_file_path, "w+", encoding="utf-8") as file: file.write(issue.key) else: log.debug( "Recurring new issue search successful. No new issue found." )
try: lines = madison(package) if not lines: irc.reply('Did not get a response -- is "%s" a valid package?' % package) return field_styles = ('package', 'version', 'distribution', 'section') for line in lines: out = [] fields = line.strip().split('|', len(field_styles)) for style, data in zip(field_styles, fields): out.append('[%s]%s' % (style, data)) irc.reply(colourise('[reset]|'.join(out)), prefixNick=False) except Exception, e: irc.reply("Error: %s" % e.message) madison = wrap(madison, ['text']) def bug(self, irc, msg, args, bug_string): try: msg = bug_synopsis(bug_string) if msg: irc.reply(colourise(msg.for_irc()), prefixNick=False) except ValueError: irc.reply('Could not parse bug number') except Exception, e: irc.reply("Error: %s" % e.message) bug = wrap(bug, ['text']) def get_pool_url(self, package): if package.startswith('lib'):
class Dice(callbacks.Plugin): """This plugin supports rolling the dice using !roll 4d20+3 as well as automatically rolling such combinations it sees in the channel (if autoRoll option is enabled for that channel) or query (if autoRollInPrivate option is enabled). """ rollReStandard = re.compile( r'((?P<rolls>\d+)#)?(?P<spec>[+-]?(\d*d\d+|\d+)([+-](\d*d\d+|\d+))*)$') rollReSR = re.compile(r'(?P<rolls>\d+)#sd$') rollReSRX = re.compile(r'(?P<rolls>\d+)#sdx$') rollReSRE = re.compile(r'(?P<pool>\d+),(?P<thr>\d+)#sde$') rollRe7Sea = re.compile( r'((?P<count>\d+)#)?(?P<prefix>[-+])?(?P<rolls>\d+)(?P<k>k{1,2})(?P<keep>\d+)(?P<mod>[+-]\d+)?$' ) rollRe7Sea2ed = re.compile( r'(?P<rolls>([-+]|\d)+)s(?P<skill>\d)(?P<vivre>-)?(l(?P<lashes>\d+))?(?P<explode>ex)?(?P<cursed>r15)?$' ) rollReWoD = re.compile(r'(?P<rolls>\d+)w(?P<explode>\d|-)?$') rollReDH = re.compile(r'(?P<rolls>\d*)vs\((?P<thr>([-+]|\d)+)\)$') rollReWG = re.compile(r'(?P<rolls>\d+)#wg$') validationDH = re.compile(r'^[+\-]?\d{1,4}([+\-]\d{1,4})*$') validation7sea2ed = re.compile(r'^[+\-]?\d{1,2}([+\-]\d{1,2})*$') MAX_DICE = 1000 MIN_SIDES = 2 MAX_SIDES = 100 MAX_ROLLS = 30 def __init__(self, irc): super(Dice, self).__init__(irc) self.deck = Deck() def _roll(self, dice, sides, mod=0): """ Roll a die several times, return sum of the results plus the static modifier. Arguments: dice -- number of dice rolled; sides -- number of sides each die has; mod -- number added to the total result (optional); """ res = int(mod) for _ in range(dice): res += random.randrange(1, sides + 1) return res def _rollMultiple(self, dice, sides, rolls=1, mod=0): """ Roll several dice several times, return a list of results. Specified number of dice with specified sides is rolled several times. Each time the sum of results is calculated, with optional modifier added. The list of these sums is returned. Arguments: dice -- number of dice rolled each time; sides -- number of sides each die has; rolls -- number of times dice are rolled; mod -- number added to the each total result (optional); """ return [self._roll(dice, sides, mod) for i in range(rolls)] @staticmethod def _formatMod(mod): """ Format a numeric modifier for printing expressions such as 1d20+3. Nonzero numbers are formatted with a sign, zero is formatted as an empty string. """ return ('%+d' % mod) if mod != 0 else '' def _process(self, irc, text): """ Process a message and reply with roll results, if any. The message is split to the words and each word is checked against all known expression forms (first applicable form is used). All results are printed together in the IRC reply. """ checklist = [ (self.rollReStandard, self._parseStandardRoll), (self.rollReSR, self._parseShadowrunRoll), (self.rollReSRX, self._parseShadowrunXRoll), (self.rollReSRE, self._parseShadowrunExtRoll), (self.rollRe7Sea, self._parse7SeaRoll), (self.rollRe7Sea2ed, self._parse7Sea2edRoll), (self.rollReWoD, self._parseWoDRoll), (self.rollReDH, self._parseDHRoll), (self.rollReWG, self._parseWGRoll), ] results = [] for word in text.split(): for expr, parser in checklist: m = expr.match(word) if m: r = parser(m) if r: results.append(r) break if results: irc.reply('; '.join(results)) def _parseStandardRoll(self, m): """ Parse rolls such as 3#2d6+1d4+2. This is a roll (or several rolls) of several dice with optional static modifiers. It yields one number (the sum of results and modifiers) for each roll series. """ rolls = int(m.group('rolls') or 1) spec = m.group('spec') if not spec[0] in '+-': spec = '+' + spec r = re.compile( r'(?P<sign>[+-])((?P<dice>\d*)d(?P<sides>\d+)|(?P<mod>\d+))') totalMod = 0 totalDice = {} for m in r.finditer(spec): if not m.group('mod') is None: totalMod += int(m.group('sign') + m.group('mod')) continue dice = int(m.group('dice') or 1) sides = int(m.group('sides')) if dice > self.MAX_DICE or sides > self.MAX_SIDES or sides < self.MIN_SIDES: return if m.group('sign') == '-': sides *= -1 totalDice[sides] = totalDice.get(sides, 0) + dice if len(totalDice) == 0: return results = [] for _ in range(rolls): result = totalMod for sides, dice in totalDice.items(): if sides > 0: result += self._roll(dice, sides) else: result -= self._roll(dice, -sides) results.append(result) specFormatted = '' self.log.debug(repr(totalDice)) for sides, dice in sorted(list(totalDice.items()), key=itemgetter(0), reverse=True): if sides > 0: if len(specFormatted) > 0: specFormatted += '+' specFormatted += '%dd%d' % (dice, sides) else: specFormatted += '-%dd%d' % (dice, -sides) specFormatted += self._formatMod(totalMod) return '[%s] %s' % (specFormatted, ', '.join([str(i) for i in results])) def _parseShadowrunRoll(self, m): """ Parse Shadowrun-specific roll such as 3#sd. """ rolls = int(m.group('rolls')) if rolls < 1 or rolls > self.MAX_DICE: return L = self._rollMultiple(1, 6, rolls) self.log.debug(format("%L", [str(i) for i in L])) return self._processSRResults(L, rolls) def _parseShadowrunXRoll(self, m): """ Parse Shadowrun-specific 'exploding' roll such as 3#sdx. """ rolls = int(m.group('rolls')) if rolls < 1 or rolls > self.MAX_DICE: return L = self._rollMultiple(1, 6, rolls) self.log.debug(format("%L", [str(i) for i in L])) reroll = L.count(6) while reroll: rerolled = self._rollMultiple(1, 6, reroll) self.log.debug(format("%L", [str(i) for i in rerolled])) L.extend([r for r in rerolled if r >= 5]) reroll = rerolled.count(6) return self._processSRResults(L, rolls, True) @staticmethod def _processSRResults(results, pool, isExploding=False): hits = results.count(6) + results.count(5) ones = results.count(1) isHit = hits > 0 isGlitch = ones >= (pool + 1) / 2 explStr = ', exploding' if isExploding else '' if isHit: hitsStr = format('%n', (hits, 'hit')) glitchStr = ', glitch' if isGlitch else '' return '(pool %d%s) %s%s' % (pool, explStr, hitsStr, glitchStr) if isGlitch: return '(pool %d%s) critical glitch!' % (pool, explStr) return '(pool %d%s) 0 hits' % (pool, explStr) def _parseShadowrunExtRoll(self, m): """ Parse Shadowrun-specific Extended test roll such as 14,3#sde. """ pool = int(m.group('pool')) if pool < 1 or pool > self.MAX_DICE: return threshold = int(m.group('thr')) if threshold < 1 or threshold > self.MAX_DICE: return result = 0 passes = 0 glitches = [] critGlitch = None while result < threshold: L = self._rollMultiple(1, 6, pool) self.log.debug(format('%L', [str(i) for i in L])) hits = L.count(6) + L.count(5) result += hits passes += 1 isHit = hits > 0 isGlitch = L.count(1) >= (pool + 1) / 2 if isGlitch: if not isHit: critGlitch = passes break glitches.append(ordinal(passes)) glitchStr = format(', glitch at %L', glitches) if len(glitches) > 0 else '' if critGlitch is None: return format('(pool %i, threshold %i) %n, %n%s', pool, threshold, (passes, 'pass'), (result, 'hit'), glitchStr) else: return format( '(pool %i, threshold %i) critical glitch at %s pass%s, %n so far', pool, threshold, ordinal(critGlitch), glitchStr, (result, 'hit')) def _parse7Sea2edRoll(self, m): """ Parse 7th Sea 2ed roll (4s2 is its simplest form). Full spec: https://redd.it/80l7jm """ rolls = m.group('rolls') if rolls is None: return # additional validation if not re.match(self.validation7sea2ed, rolls): return roll_count = eval(rolls) if roll_count < 1 or roll_count > self.MAX_ROLLS: return skill = int(m.group('skill')) vivre = m.group('vivre') == '-' explode = m.group('explode') == 'ex' lashes = 0 if m.group('lashes') is None else int(m.group('lashes')) cursed = m.group('cursed') is not None self.log.debug( format( '7sea2ed: %i (%s) dices at %i skill. lashes = %i. explode is %s. vivre is %s', roll_count, str(rolls), skill, lashes, "enabled" if explode else "disabled", "enabled" if vivre else "disabled")) roller = SevenSea2EdRaiseRoller(lambda x: self._rollMultiple(1, 10, x), skill_rank=skill, explode=explode, lash_count=lashes, joie_de_vivre=vivre, raise_target=15 if cursed else 10) return '[%s]: %s' % (m.group(0), str( roller.roll_and_count(roll_count))) def _parse7SeaRoll(self, m): """ Parse 7th Sea-specific roll (4k2 is its simplest form). """ rolls = int(m.group('rolls')) if rolls < 1 or rolls > self.MAX_ROLLS: return count = int(m.group('count') or 1) keep = int(m.group('keep')) mod = int(m.group('mod') or 0) prefix = m.group('prefix') k = m.group('k') explode = prefix != '-' if keep < 1 or keep > self.MAX_ROLLS: return if keep > rolls: keep = rolls if rolls > 10: keep += rolls - 10 rolls = 10 if keep > 10: mod += (keep - 10) * 10 keep = 10 unkept = (prefix == '+' or k == 'kk') and keep < rolls explodeStr = ', not exploding' if not explode else '' results = [] for _ in range(count): L = self._rollMultiple(1, 10, rolls) if explode: for i in range(len(L)): if L[i] == 10: while True: rerolled = self._roll(1, 10) L[i] += rerolled if rerolled < 10: break self.log.debug(format("%L", [str(i) for i in L])) L.sort(reverse=True) keptDice, unkeptDice = L[:keep], L[keep:] unkeptStr = ' | %s' % ', '.join([str(i) for i in unkeptDice ]) if unkept else '' keptStr = ', '.join([str(i) for i in keptDice]) results.append('(%d) %s%s' % (sum(keptDice) + mod, keptStr, unkeptStr)) return '[%dk%d%s%s] %s' % (rolls, keep, self._formatMod(mod), explodeStr, '; '.join(results)) def _parseWoDRoll(self, m): """ Parse New World of Darkness roll (5w) """ rolls = int(m.group('rolls')) if rolls < 1 or rolls > self.MAX_ROLLS: return if m.group('explode') == '-': explode = 0 elif m.group('explode') is not None and m.group('explode').isdigit(): explode = int(m.group('explode')) if explode < 8 or explode > 10: explode = 10 else: explode = 10 L = self._rollMultiple(1, 10, rolls) self.log.debug(format("%L", [str(i) for i in L])) successes = len([x for x in L if x >= 8]) if explode: for i in range(len(L)): if L[i] >= explode: while True: rerolled = self._roll(1, 10) self.log.debug(str(rerolled)) if rerolled >= 8: successes += 1 if rerolled < explode: break if explode == 0: explStr = ', not exploding' elif explode != 10: explStr = ', %d-again' % explode else: explStr = '' result = format('%n', (successes, 'success')) if successes > 0 else 'FAIL' return '(%d%s) %s' % (rolls, explStr, result) def _parseDHRoll(self, m): """ Parse Dark Heresy roll (3vs(20+30-10)) """ rolls = int(m.group('rolls') or 1) if rolls < 1 or rolls > self.MAX_ROLLS: return thresholdExpr = m.group('thr') # additional validation if not re.match(self.validationDH, thresholdExpr): return threshold = eval(thresholdExpr) rollResults = self._rollMultiple(1, 100, rolls) results = [threshold - roll for roll in rollResults] return '%s (%s vs %d)' % (', '.join( [str(i) for i in results]), ', '.join([str(i) for i in rollResults]), threshold) def _parseWGRoll(self, m): """ Parse WH40K: Wrath & Glory roll (10#wg) """ rolls = int(m.group('rolls') or 1) if rolls < 1 or rolls > self.MAX_ROLLS: return L = self._rollMultiple(1, 6, rolls) self.log.debug(format("%L", [str(i) for i in L])) return self._processWGResults(L, rolls) @staticmethod def _processWGResults(results, pool): wrathstrings = ["❶", "❷", "❸", "❹", "❺", "❻"] strTag = "" wrathDie = results.pop(0) n6 = results.count(6) n5 = results.count(5) n4 = results.count(4) icons = 2 * n6 + n5 + n4 Glory = wrathDie == 6 Complication = wrathDie == 1 iconssymb = wrathstrings[wrathDie - 1] + " " if Glory: strTag += "| Glory" icons += 2 elif wrathDie > 3: icons += 1 elif Complication: strTag += "| Complication" iconssymb += n6 * "➅ " + n5 * "5 " + n4 * "4 " isNonZero = icons > 0 if isNonZero: iconsStr = str(icons) + " icon(s): " + iconssymb + strTag return '[pool %d] %s' % (pool, iconsStr) def _autoRollEnabled(self, irc, channel): """ Check if automatic rolling is enabled for this context. """ return ((irc.isChannel(channel) and self.registryValue('autoRoll', channel)) or (not irc.isChannel(channel) and self.registryValue('autoRollInPrivate'))) def roll(self, irc, msg, args, text): """<dice>d<sides>[<modifier>] Rolls a die with <sides> number of sides <dice> times, summarizes the results and adds optional modifier <modifier> For example, 2d6 will roll 2 six-sided dice; 10d10-3 will roll 10 ten-sided dice and subtract 3 from the total result. """ if self._autoRollEnabled(irc, msg.args[0]): return self._process(irc, text) roll = wrap(roll, ['somethingWithoutSpaces']) def shuffle(self, irc, msg, args): """takes no arguments Restores and shuffles the deck. """ self.deck.shuffle() irc.reply('shuffled') shuffle = wrap(shuffle) def draw(self, irc, msg, args, count): """[<count>] Draws <count> cards (1 if omitted) from the deck and shows them. """ cards = [next(self.deck) for i in range(count)] irc.reply(', '.join(cards)) draw = wrap(draw, [additional('positiveInt', 1)]) deal = draw def doPrivmsg(self, irc, msg): if not self._autoRollEnabled(irc, msg.args[0]): return if ircmsgs.isAction(msg): text = ircmsgs.unAction(msg) else: text = msg.args[1] self._process(irc, text)
class DebianDevelChanges(supybot.callbacks.Plugin): threaded = True def __init__(self, irc): super().__init__(irc) self.irc = irc self.topic_lock = threading.Lock() self.mainloop = None self.mainloop_thread = None mainloop = GObject.MainLoop() if not mainloop.is_running(): log.info("Starting Glib main loop") mainloop_thread = threading.Thread(target=mainloop.run, name="Glib maing loop") mainloop_thread.start() self.mainloop_thread = mainloop_thread self.mainloop = mainloop self.requests_session = requests.Session() self.requests_session.verify = True self.queued_topics = {} self.last_n_messages = [] # data sources pseudo_packages.pp = PseudoPackages(self.requests_session) self.pseudo_packages = pseudo_packages.pp self.stable_rc_bugs = StableRCBugs(self.requests_session) self.testing_rc_bugs = TestingRCBugs(self.requests_session) self.new_queue = NewQueue(self.requests_session) self.dinstall = Dinstall(self.requests_session) self.rm_queue = RmQueue(self.requests_session) self.apt_archive = AptArchive( self.registryValue("apt_configuration_directory"), self.registryValue("apt_cache_directory"), ) self.data_sources = ( self.pseudo_packages, self.stable_rc_bugs, self.testing_rc_bugs, self.new_queue, self.dinstall, self.rm_queue, self.apt_archive, ) # Schedule datasource updates def wrapper(source): def implementation(): try: source.update() except Exception as e: log.exception("Failed to update {}: {}".format( source.NAME, e)) self._topic_callback() return implementation for source in self.data_sources: # schedule periodic events schedule.addPeriodicEvent(wrapper(source), source.INTERVAL, source.NAME, now=False) # and run them now once schedule.addEvent(wrapper(source), time.time() + 1) log.info("Starting D-Bus service") self.dbus_service = BTSDBusService(self._email_callback) self.dbus_bus = SystemBus() self.dbus_bus.publish(self.dbus_service.interface_name, self.dbus_service) self.dbus_service.start() def die(self): log.info("Stopping D-Bus service") self.dbus_service.stop() if self.mainloop is not None: log.info("Stopping Glib main loop") self.mainloop.quit() self.mainloop_thread.join(timeout=1.0) if self.mainloop_thread.is_alive(): log.warn("Glib main loop thread is still alive.") self.mainloop = None self.mainloop_thread = None for source in self.data_sources: try: schedule.removePeriodicEvent(source.NAME) except KeyError: pass super().die() def _email_callback(self, fileobj): try: emailmsg = parse_mail(fileobj) msg = get_message(emailmsg, new_queue=self.new_queue) if not msg: return txt = colourise(msg.for_irc()) # Simple flood/duplicate detection if txt in self.last_n_messages: return self.last_n_messages.insert(0, txt) self.last_n_messages = self.last_n_messages[:20] packages = [package.strip() for package in msg.package.split(",")] maintainer_info = None if hasattr(msg, "maintainer"): maintainer_info = (split_address(msg.maintainer), ) else: maintainer_info = [] for package in packages: try: maintainer_info.append( self.apt_archive.get_maintainer(package)) except NewDataSource.DataError as e: log.info("Failed to query maintainer for {}.".format( package)) for channel in self.irc.state.channels: # match package or nothing by default package_regex = self.registryValue("package_regex", channel) or "a^" package_match = False for package in packages: package_match = re.search(package_regex, package) if package_match: break maintainer_match = False maintainer_regex = self.registryValue("maintainer_regex", channel) if (maintainer_regex and maintainer_info is not None and len(maintainer_info) >= 0): for mi in maintainer_info: maintainer_match = re.search(maintainer_regex, mi["email"]) if maintainer_match: break if not package_match and not maintainer_match: continue distribution_regex = self.registryValue( "distribution_regex", channel) if distribution_regex: if not hasattr(msg, "distribution"): # If this channel has a distribution regex, don't # bother continuing unless the message actually has a # distribution. This filters security messages, etc. continue if not re.search(distribution_regex, msg.distribution): # Distribution doesn't match regex; don't send this # message. continue send_privmsg = self.registryValue("send_privmsg", channel) # Send NOTICE per default and if 'send_privmsg' is set for the # channel, send PRIVMSG instead. if send_privmsg: ircmsg = supybot.ircmsgs.privmsg(channel, txt) else: ircmsg = supybot.ircmsgs.notice(channel, txt) self.irc.queueMsg(ircmsg) except Exception as e: log.exception("Uncaught exception: %s " % e) def _topic_callback(self): sections = { self.testing_rc_bugs.get_number_bugs: "RC bug count", self.stable_rc_bugs.get_number_bugs: "stable RC bug count", self.new_queue.get_size: "NEW queue", self.new_queue.get_backports_size: "backports NEW queue", self.rm_queue.get_size: "RM queue", self.dinstall.get_status: "dinstall", } channels = set() with self.topic_lock: values = {} for callback, prefix in sections.items(): new_value = callback() if new_value is not None: values[prefix] = new_value for channel in self.irc.state.channels: new_topic = topic = self.irc.state.getTopic(channel) for prefix, value in values.items(): new_topic = rewrite_topic(new_topic, prefix, value) if topic != new_topic: self.queued_topics[channel] = new_topic if channel not in channels: log.info("Queueing change of topic in #%s to '%s'" % (channel, new_topic)) channels.add(channel) for channel in channels: event_name = "{}_topic".format(channel) try: schedule.removeEvent(event_name) except KeyError: pass def update_topic(channel=channel): self._update_topic(channel) schedule.addEvent(update_topic, time.time() + 60, event_name) def _update_topic(self, channel): with self.topic_lock: try: new_topic = self.queued_topics[channel] log.info("Changing topic in #%s to '%s'" % (channel, new_topic)) self.irc.queueMsg(supybot.ircmsgs.topic(channel, new_topic)) except KeyError: pass def rc(self, irc, msg, args): """Link to UDD RC bug overview.""" num_bugs = self.testing_rc_bugs.get_number_bugs() if type(num_bugs) is int: irc.reply( "There are %d release-critical bugs in the testing distribution. " "See https://udd.debian.org/bugs.cgi?release=bullseye¬main=ign&merged=ign&rc=1" % num_bugs) else: irc.reply("No data at this time.") rc = wrap(rc) bugs = wrap(rc) def update(self, irc, msg, args): """Trigger an update.""" if not ircdb.checkCapability(msg.prefix, "owner"): irc.reply("You are not authorised to run this command.") return for source in self.data_sources: source.update() irc.reply("Updated %s." % source.NAME) self._topic_callback() update = wrap(update) def madison(self, irc, msg, args, package): """List packages.""" try: lines = madison(package) if not lines: irc.reply( 'Did not get a response -- is "%s" a valid package?' % package) return field_styles = ("package", "version", "distribution", "section") for line in lines: out = [] fields = line.strip().split("|", len(field_styles)) for style, data in zip(field_styles, fields): out.append("[%s]%s" % (style, data)) irc.reply(colourise("[reset]|".join(out)), prefixNick=False) except Exception as e: irc.reply("Error: %s" % e.message) madison = wrap(madison, ["text"]) def get_pool_url(self, package): if package.startswith("lib"): return (package[:4], package) else: return (package[:1], package) def _maintainer(self, irc, msg, args, items): """Get maintainer for package.""" for package in items: info = self.apt_archive.get_maintainer(package) if info: display_name = format_email_address( "%s <%s>" % (info["name"], info["email"]), max_domain=18) login = info["email"] if login.endswith("@debian.org"): login = login.replace("@debian.org", "") msg = ( "[desc]Maintainer for[reset] [package]%s[reset] [desc]is[reset] [by]%s[reset]: " % (package, display_name)) msg += "[url]https://qa.debian.org/developer.php?login=%s[/url]" % login else: msg = 'Unknown source package "%s"' % package irc.reply(colourise(msg), prefixNick=False) maintainer = wrap(_maintainer, [many("anything")]) maint = wrap(_maintainer, [many("anything")]) who_maintains = wrap(_maintainer, [many("anything")]) def _qa(self, irc, msg, args, items): """Get link to QA page.""" for package in items: url = "https://packages.qa.debian.org/%s/%s.html" % self.get_pool_url( package) msg = "[desc]QA page for[reset] [package]%s[reset]: [url]%s[/url]" % ( package, url, ) irc.reply(colourise(msg), prefixNick=False) qa = wrap(_qa, [many("anything")]) overview = wrap(_qa, [many("anything")]) package = wrap(_qa, [many("anything")]) pkg = wrap(_qa, [many("anything")]) srcpkg = wrap(_qa, [many("anything")]) def _changelog(self, irc, msg, args, items): """Get link to changelog.""" for package in items: url = ( "https://packages.debian.org/changelogs/pool/main/%s/%s/current/changelog" % self.get_pool_url(package)) msg = ( "[desc]debian/changelog for[reset] [package]%s[reset]: [url]%s[/url]" % (package, url)) irc.reply(colourise(msg), prefixNick=False) changelog = wrap(_changelog, [many("anything")]) changes = wrap(_changelog, [many("anything")]) def _copyright(self, irc, msg, args, items): """Link to copyright files.""" for package in items: url = ( "https://packages.debian.org/changelogs/pool/main/%s/%s/current/copyright" % self.get_pool_url(package)) msg = ( "[desc]debian/copyright for[reset] [package]%s[reset]: [url]%s[/url]" % (package, url)) irc.reply(colourise(msg), prefixNick=False) copyright = wrap(_copyright, [many("anything")]) def _buggraph(self, irc, msg, args, items): """Link to bug graph.""" for package in items: msg = ( "[desc]Bug graph for[reset] [package]%s[reset]: [url]https://qa.debian.org/data/bts/graphs/%s/%s.png[/url]" % (package, package[0], package)) irc.reply(colourise(msg), prefixNick=False) buggraph = wrap(_buggraph, [many("anything")]) bug_graph = wrap(_buggraph, [many("anything")]) def _buildd(self, irc, msg, args, items): """Link to buildd page.""" for package in items: msg = ( "[desc]buildd status for[reset] [package]%s[reset]: [url]https://buildd.debian.org/pkg.cgi?pkg=%s[/url]" % (package, package)) irc.reply(colourise(msg), prefixNick=False) buildd = wrap(_buildd, [many("anything")]) def _popcon(self, irc, msg, args, package): """Get popcon data.""" try: msg = popcon(package, self.requests_session) if msg: irc.reply(colourise(msg.for_irc()), prefixNick=False) except Exception as e: irc.reply("Error: unable to obtain popcon data for %s" % package) popcon = wrap(_popcon, ["text"]) def _testing(self, irc, msg, args, items): """Check testing migration status.""" for package in items: msg = ( "[desc]Testing migration status for[reset] [package]%s[reset]: [url]https://qa.debian.org/excuses.php?package=%s[/url]" % (package, package)) irc.reply(colourise(msg), prefixNick=False) testing = wrap(_testing, [many("anything")]) migration = wrap(_testing, [many("anything")]) def _new(self, irc, msg, args): """Link to NEW queue.""" line = ( "[desc]NEW queue is[reset]: [url]%s[/url]. [desc]Current size is:[reset] %d" % ("https://ftp-master.debian.org/new.html", self.new_queue.get_size())) irc.reply(colourise(line)) new = wrap(_new) new_queue = wrap(_new) newqueue = wrap(_new)
class Owner(callbacks.Plugin): """Owner-only commands for core Supybot. This is a core Supybot module that should not be removed!""" # This plugin must be first; its priority must be lowest; otherwise odd # things will happen when adding callbacks. def __init__(self, irc=None): if irc is not None: assert not irc.getCallback(self.name()) self.__parent = super(Owner, self) self.__parent.__init__(irc) # Setup command flood detection. self.commands = ircutils.FloodQueue(conf.supybot.abuse.flood.interval()) conf.supybot.abuse.flood.interval.addCallback(self.setFloodQueueTimeout) # Setup plugins and default plugins for commands. # # This needs to be done before we connect to any networks so that the # children of supybot.plugins (the actual plugins) exist and can be # loaded. for (name, s) in registry._cache.items(): if 'alwaysLoadDefault' in name or 'alwaysLoadImportant' in name: continue if name.startswith('supybot.plugins'): try: (_, _, name) = registry.split(name) except ValueError: # unpack list of wrong size. continue # This is just for the prettiness of the configuration file. # There are no plugins that are all-lowercase, so we'll at # least attempt to capitalize them. if name == name.lower(): name = name.capitalize() conf.registerPlugin(name) if name.startswith('supybot.commands.defaultPlugins'): try: (_, _, _, name) = registry.split(name) except ValueError: # unpack list of wrong size. continue registerDefaultPlugin(name, s) # Setup Irc objects, connected to networks. If world.ircs is already # populated, chances are that we're being reloaded, so don't do this. if not world.ircs: for network in conf.supybot.networks(): try: self._connect(network) except socket.error as e: self.log.error('Could not connect to %s: %s.', network, e) except Exception as e: self.log.exception('Exception connecting to %s:', network) self.log.error('Could not connect to %s: %s.', network, e) def callPrecedence(self, irc): return ([], [cb for cb in irc.callbacks if cb is not self]) def outFilter(self, irc, msg): if msg.command == 'PRIVMSG' and not world.testing: if ircutils.strEqual(msg.args[0], irc.nick): self.log.warning('Tried to send a message to myself: %r.', msg) return None return msg def reset(self): # This has to be done somewhere, I figure here is as good place as any. callbacks.IrcObjectProxy._mores.clear() self.__parent.reset() def _connect(self, network, serverPort=None, password='', ssl=False): try: group = conf.supybot.networks.get(network) group.servers()[0] except (registry.NonExistentRegistryEntry, IndexError): if serverPort is None: raise ValueError('connect requires a (server, port) ' \ 'if the network is not registered.') conf.registerNetwork(network, password, ssl) server = '%s:%s' % serverPort conf.supybot.networks.get(network).servers.append(server) assert conf.supybot.networks.get(network).servers(), \ 'No servers are set for the %s network.' % network self.log.debug('Creating new Irc for %s.', network) newIrc = irclib.Irc(network) driver = drivers.newDriver(newIrc) self._loadPlugins(newIrc) return newIrc def _loadPlugins(self, irc): self.log.debug('Loading plugins (connecting to %s).', irc.network) alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant() important = conf.supybot.commands.defaultPlugins.importantPlugins() for (name, value) in conf.supybot.plugins.getValues(fullNames=False): if irc.getCallback(name) is None: load = value() if not load and name in important: if alwaysLoadImportant: s = '%s is configured not to be loaded, but is being '\ 'loaded anyway because ' \ 'supybot.plugins.alwaysLoadImportant is True.' self.log.warning(s, name) load = True if load: # We don't load plugins that don't start with a capital # letter. if name[0].isupper() and not irc.getCallback(name): # This is debug because each log logs its beginning. self.log.debug('Loading %s.', name) try: m = plugin.loadPluginModule(name, ignoreDeprecation=True) plugin.loadPluginClass(irc, m) except callbacks.Error as e: # This is just an error message. log.warning(str(e)) except plugins.NoSuitableDatabase as e: s = 'Failed to load %s: no suitable database(%s).' % (name, e) log.warning(s) except ImportError as e: e = str(e) if e.endswith(name): s = 'Failed to load {0}: No plugin named {0} exists.'.format( utils.str.dqrepr(name)) elif "No module named 'config'" in e: s = ("Failed to load %s: This plugin may be incompatible " "with your current Python version." % name) else: s = 'Failed to load %s: import error (%s).' % (name, e) log.warning(s) except Exception as e: log.exception('Failed to load %s:', name) else: # Let's import the module so configuration is preserved. try: _ = plugin.loadPluginModule(name) except Exception as e: log.debug('Attempted to load %s to preserve its ' 'configuration, but load failed: %s', name, e) world.starting = False def do376(self, irc, msg): msgs = conf.supybot.networks.get(irc.network).channels.joins() if msgs: for msg in msgs: irc.queueMsg(msg) do422 = do377 = do376 def setFloodQueueTimeout(self, *args, **kwargs): self.commands.timeout = conf.supybot.abuse.flood.interval() def doBatch(self, irc, msg): if not conf.supybot.protocols.irc.experimentalExtensions(): return batch = msg.tagged('batch') # Always not-None on a BATCH message if msg.args[0].startswith('+'): # Start of a batch, we're not interested yet. return if batch.type != 'draft/multiline': # This is not a multiline batch, also not interested. return assert msg.args[0].startswith("-"), ( "BATCH's first argument should start with either - or +, but " "it is %s." ) % msg.args[0] # End of multiline batch. It may be a long command. payloads = [] first_privmsg = None for message in batch.messages: if message.command != "PRIVMSG": # We're only interested in PRIVMSGs for the payloads. # (eg. exclude NOTICE) continue elif not payloads: # This is the first PRIVMSG of the batch first_privmsg = message payloads.append(message.args[1]) elif 'draft/multiline-concat' in message.server_tags: # This message is not a new line, but the continuation # of the previous one. payloads.append(message.args[1]) else: # New line; stop here. We're not processing extra lines # either as the rest of the command or as new commands. # This may change in the future. break payload = ''.join(payloads) if not payload: self.log.error( 'Got empty multiline payload. This is a bug, please ' 'report it along with logs.' ) return assert first_privmsg, "This shouldn't be None unless payload is empty" # Let's build a synthetic message from the various parts of the # batch, to look like the multiline batch was a single (large) # PRIVMSG: # * copy the tags and server tags of the 'BATCH +' command, # * copy the prefix and channel of any of the PRIVMSGs # inside the batch # * create a new args[1] target = first_privmsg.args[0] synthetic_msg = ircmsgs.IrcMsg( msg=batch.messages[0], # tags, server_tags, time prefix=first_privmsg.prefix, command='PRIVMSG', args=(target, payload) ) self._doPrivmsgs(irc, synthetic_msg) def doPrivmsg(self, irc, msg): if conf.supybot.protocols.irc.experimentalExtensions(): if 'batch' in msg.server_tags \ and any(batch.type =='draft/multiline' for batch in irc.state.getParentBatches(msg)): # We will handle the message in doBatch when the entire batch ends. return self._doPrivmsgs(irc, msg) def _doPrivmsgs(self, irc, msg): """If the given message is a command, triggers Limnoria's command-dispatching for that command. Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can potentially be an artificial message synthesized in doBatch from a multiline batch. Usually, a command is a single message, so ``payload=msg.params[0]`` However, when ``msg`` is part of a multiline message, the payload is the concatenation of multiple messages. See <https://ircv3.net/specs/extensions/multiline>. """ assert self is irc.callbacks[0], \ 'Owner isn\'t first callback: %r' % irc.callbacks if ircmsgs.isCtcp(msg): return s = callbacks.addressed(irc, msg) if s: ignored = ircdb.checkIgnored(msg.prefix) if ignored: self.log.info('Ignoring command from %s.', msg.prefix) return maximum = conf.supybot.abuse.flood.command.maximum() self.commands.enqueue(msg) if conf.supybot.abuse.flood.command() \ and self.commands.len(msg) > maximum \ and not ircdb.checkCapability(msg.prefix, 'trusted'): punishment = conf.supybot.abuse.flood.command.punishment() banmask = conf.supybot.protocols.irc.banmask \ .makeBanmask(msg.prefix) self.log.info('Ignoring %s for %s seconds due to an apparent ' 'command flood.', banmask, punishment) ircdb.ignores.add(banmask, time.time() + punishment) if conf.supybot.abuse.flood.command.notify(): irc.reply('You\'ve given me %s commands within the last ' '%i seconds; I\'m now ignoring you for %s.' % (maximum, conf.supybot.abuse.flood.interval(), utils.timeElapsed(punishment, seconds=False))) return try: tokens = callbacks.tokenize(s, channel=msg.channel, network=irc.network) self.Proxy(irc, msg, tokens) except SyntaxError as e: if conf.supybot.reply.error.detailed(): irc.error(str(e)) else: irc.replyError(msg=msg) self.log.info('Syntax error: %s', e) def logmark(self, irc, msg, args, text): """<text> Logs <text> to the global Supybot log at critical priority. Useful for marking logfiles for later searching. """ self.log.critical(text) irc.replySuccess() logmark = wrap(logmark, ['text']) def announce(self, irc, msg, args, text): """<text> Sends <text> to all channels the bot is currently on and not lobotomized in. """ u = ircdb.users.getUser(msg.prefix) template = self.registryValue('announceFormat') text = ircutils.standardSubstitute( irc, msg, template, env={'owner': u.name, 'text': text}) for channel in irc.state.channels: c = ircdb.channels.getChannel(channel) if not c.lobotomized: irc.queueMsg(ircmsgs.privmsg(channel, text)) irc.noReply() announce = wrap(announce, ['text']) def defaultplugin(self, irc, msg, args, optlist, command, plugin): """[--remove] <command> [<plugin>] Sets the default plugin for <command> to <plugin>. If --remove is given, removes the current default plugin for <command>. If no plugin is given, returns the current default plugin set for <command>. See also, supybot.commands.defaultPlugins.importantPlugins. """ remove = False for (option, arg) in optlist: if option == 'remove': remove = True (_, cbs) = irc.findCallbacksForArgs([command]) if remove: try: conf.supybot.commands.defaultPlugins.unregister(command) irc.replySuccess() except registry.NonExistentRegistryEntry: s = 'I don\'t have a default plugin set for that command.' irc.error(s) elif not cbs: irc.errorInvalid('command', command) elif plugin: if not plugin.isCommand(command): irc.errorInvalid('command in the %s plugin' % plugin.name(), command) registerDefaultPlugin(command, plugin.name()) irc.replySuccess() else: try: irc.reply(conf.supybot.commands.defaultPlugins.get(command)()) except registry.NonExistentRegistryEntry: s = 'I don\'t have a default plugin set for that command.' irc.error(s) defaultplugin = wrap(defaultplugin, [getopts({'remove': ''}), 'commandName', additional('plugin')]) def ircquote(self, irc, msg, args, s): """<string to be sent to the server> Sends the raw string given to the server. """ try: m = ircmsgs.IrcMsg(s) except Exception as e: irc.error(utils.exnToString(e)) else: irc.queueMsg(m) irc.noReply() ircquote = wrap(ircquote, ['text']) def quit(self, irc, msg, args, text): """[<text>] Exits the bot with the QUIT message <text>. If <text> is not given, the default quit message (supybot.plugins.Owner.quitMsg) will be used. If there is no default quitMsg set, your nick will be used. The standard substitutions ($version, $nick, etc.) are all handled appropriately. """ text = text or self.registryValue('quitMsg') or msg.nick text = ircutils.standardSubstitute(irc, msg, text) irc.noReply() m = ircmsgs.quit(text) world.upkeep() for irc in world.ircs[:]: irc.queueMsg(m) irc.die() quit = wrap(quit, [additional('text')]) def flush(self, irc, msg, args): """takes no arguments Runs all the periodic flushers in world.flushers. This includes flushing all logs and all configuration changes to disk. """ world.flush() irc.replySuccess() flush = wrap(flush) def upkeep(self, irc, msg, args, level): """[<level>] Runs the standard upkeep stuff (flushes and gc.collects()). If given a level, runs that level of upkeep (currently, the only supported level is "high", which causes the bot to flush a lot of caches as well as do normal upkeep stuff). """ L = [] if level == 'high': L.append(format('Regexp cache flushed: %n cleared.', (len(re._cache), 'regexp'))) re.purge() L.append(format('Pattern cache flushed: %n cleared.', (len(ircutils._patternCache), 'compiled pattern'))) ircutils._patternCache.clear() L.append(format('hostmaskPatternEqual cache flushed: %n cleared.', (len(ircutils._hostmaskPatternEqualCache), 'result'))) ircutils._hostmaskPatternEqualCache.clear() L.append(format('ircdb username cache flushed: %n cleared.', (len(ircdb.users._nameCache), 'username to id mapping'))) ircdb.users._nameCache.clear() L.append(format('ircdb hostmask cache flushed: %n cleared.', (len(ircdb.users._hostmaskCache), 'hostmask to id mapping'))) ircdb.users._hostmaskCache.clear() L.append(format('linecache line cache flushed: %n cleared.', (len(linecache.cache), 'line'))) linecache.clearcache() if minisix.PY2: sys.exc_clear() collected = world.upkeep() if gc.garbage: L.append('Garbage! %r.' % gc.garbage) if collected is not None: # Some time between 5.2 and 7.1, Pypy (3?) started returning None # when gc.collect() is called. L.append(format('%n collected.', (collected, 'object'))) if L: irc.reply(' '.join(L)) else: irc.replySuccess() upkeep = wrap(upkeep, [additional(('literal', ['high']))]) def load(self, irc, msg, args, optlist, name): """[--deprecated] <plugin> Loads the plugin <plugin> from any of the directories in conf.supybot.directories.plugins; usually this includes the main installed directory and 'plugins' in the current directory. --deprecated is necessary if you wish to load deprecated plugins. """ ignoreDeprecation = False for (option, argument) in optlist: if option == 'deprecated': ignoreDeprecation = True if name.endswith('.py'): name = name[:-3] if irc.getCallback(name): irc.error('%s is already loaded.' % name.capitalize()) return try: module = plugin.loadPluginModule(name, ignoreDeprecation) except plugin.Deprecated: irc.error('%s is deprecated. Use --deprecated ' 'to force it to load.' % name.capitalize()) return except ImportError as e: if str(e).endswith(name): irc.error('No plugin named %s exists.' % utils.str.dqrepr(name)) elif "No module named 'config'" in str(e): irc.error('This plugin may be incompatible with your current Python ' 'version. Try running 2to3 on it.') else: irc.error(str(e)) return cb = plugin.loadPluginClass(irc, module) name = cb.name() # Let's normalize this. conf.registerPlugin(name, True) irc.replySuccess() load = wrap(load, [getopts({'deprecated': ''}), 'something']) def reload(self, irc, msg, args, name): """<plugin> Unloads and subsequently reloads the plugin by name; use the 'list' command to see a list of the currently loaded plugins. """ if ircutils.strEqual(name, self.name()): irc.error('You can\'t reload the %s plugin.' % name) return callbacks = irc.removeCallback(name) if callbacks: module = sys.modules[callbacks[0].__module__] if hasattr(module, 'reload'): x = module.reload() try: module = plugin.loadPluginModule(name) if hasattr(module, 'reload') and 'x' in locals(): module.reload(x) if hasattr(module, 'config'): from importlib import reload reload(module.config) for callback in callbacks: callback.die() del callback gc.collect() # This makes sure the callback is collected. callback = plugin.loadPluginClass(irc, module) irc.replySuccess() except ImportError: for callback in callbacks: irc.addCallback(callback) irc.error('No plugin named %s exists.' % name) else: irc.error('There was no plugin %s.' % name) reload = wrap(reload, ['something']) def unload(self, irc, msg, args, name): """<plugin> Unloads the callback by name; use the 'list' command to see a list of the currently loaded plugins. Obviously, the Owner plugin can't be unloaded. """ if ircutils.strEqual(name, self.name()): irc.error('You can\'t unload the %s plugin.' % name) return # Let's do this so even if the plugin isn't currently loaded, it doesn't # stay attempting to load. old_callback = irc.getCallback(name) if old_callback: # Normalize the plugin case to prevent duplicate registration # entries, https://github.com/ProgVal/Limnoria/issues/1295 name = old_callback.name() conf.registerPlugin(name, False) callbacks = irc.removeCallback(name) if callbacks: for callback in callbacks: callback.die() del callback gc.collect() irc.replySuccess() return irc.error('There was no plugin %s.' % name) unload = wrap(unload, ['something']) def defaultcapability(self, irc, msg, args, action, capability): """{add|remove} <capability> Adds or removes (according to the first argument) <capability> from the default capabilities given to users (the configuration variable supybot.capabilities stores these). """ if action == 'add': conf.supybot.capabilities().add(capability) irc.replySuccess() elif action == 'remove': try: conf.supybot.capabilities().remove(capability) irc.replySuccess() except KeyError: if ircdb.isAntiCapability(capability): irc.error('That capability wasn\'t in ' 'supybot.capabilities.') else: anticap = ircdb.makeAntiCapability(capability) conf.supybot.capabilities().add(anticap) irc.replySuccess() defaultcapability = wrap(defaultcapability, [('literal', ['add','remove']), 'capability']) def disable(self, irc, msg, args, plugin, command): """[<plugin>] <command> Disables the command <command> for all users (including the owners). If <plugin> is given, only disables the <command> from <plugin>. If you want to disable a command for most users but not for yourself, set a default capability of -plugin.command or -command (if you want to disable the command in all plugins). """ if command in ('enable', 'identify'): irc.error('You can\'t disable %s.' % command) return if plugin: if plugin.isCommand(command): pluginCommand = '%s.%s' % (plugin.name(), command) conf.supybot.commands.disabled().add(pluginCommand) plugin._disabled.add(command, plugin.name()) else: irc.error('%s is not a command in the %s plugin.' % (command, plugin.name())) return else: conf.supybot.commands.disabled().add(command) self._disabled.add(command) irc.replySuccess() disable = wrap(disable, [optional('plugin'), 'commandName']) def enable(self, irc, msg, args, plugin, command): """[<plugin>] <command> Enables the command <command> for all users. If <plugin> if given, only enables the <command> from <plugin>. This command is the inverse of disable. """ try: if plugin: plugin._disabled.remove(command, plugin.name()) command = '%s.%s' % (plugin.name(), command) else: self._disabled.remove(command) conf.supybot.commands.disabled().remove(command) irc.replySuccess() except KeyError: irc.error('That command wasn\'t disabled.') enable = wrap(enable, [optional('plugin'), 'commandName']) def rename(self, irc, msg, args, command_plugin, command, newName): """<plugin> <command> <new name> Renames <command> in <plugin> to the <new name>. """ if not command_plugin.isCommand(command): what = 'command in the %s plugin' % command_plugin.name() irc.errorInvalid(what, command) if hasattr(command_plugin, newName): irc.error('The %s plugin already has an attribute named %s.' % (command_plugin, newName)) return plugin.registerRename(command_plugin.name(), command, newName) plugin.renameCommand(command_plugin, command, newName) irc.replySuccess() rename = wrap(rename, ['plugin', 'commandName', 'commandName']) def unrename(self, irc, msg, args, plugin): """<plugin> Removes all renames in <plugin>. The plugin will be reloaded after this command is run. """ try: conf.supybot.commands.renames.unregister(plugin.name()) except registry.NonExistentRegistryEntry: irc.errorInvalid('plugin', plugin.name()) self.reload(irc, msg, [plugin.name()]) # This makes the replySuccess. unrename = wrap(unrename, ['plugin']) def reloadlocale(self, irc, msg, args): """takes no argument Reloads the locale of the bot.""" i18n.reloadLocales() irc.replySuccess()
page = response.read() response.close() opener.close() # Trim page = page[page.find(r'<div id=currency_converter_result>'):] page = page[:page.find(r'<input')-1] # if the tag is present but contains no data, its length will be 34 if len(page) == 34: page = 'Invalid Currency.' # in the event of a conversion failure, '\nCould not convert.' appears elif page.find(r'Could not convert.') != -1: page = 'Could not convert.' else: # remove tags and use the data page = page.replace(r'<div id=currency_converter_result>', '', 1) page = page.replace(r'<span class=bld>', '', 1) page = page.replace(r'</span>', '', 1) irc.reply(page) del page, url, timeout ex = wrap(ex, ['somethingWithoutSpaces', optional('somethingWithoutSpaces'), optional('somethingWithoutSpaces')]) Class = Ex # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
class board(callbacks.Commands): """Board commands""" @internationalizeDocstring def add(self, irc, msg, args, channel, board_slug, board_url): """[<channel>] <board-slug> <board-url> Announces the changes of the board with the slug <board-slug> and the url <board-url> to <channel>. """ if not instance._check_capability(irc, msg): return boards = instance._load_boards(channel) if board_slug in boards: irc.error( _('This board is already announced to this channel.')) return # Save new board mapping boards[board_slug] = board_url instance._save_boards(boards, channel) irc.replySuccess() add = wrap(add, ['channel', 'somethingWithoutSpaces', 'httpUrl']) @internationalizeDocstring def remove(self, irc, msg, args, channel, board_slug): """[<channel>] <board-slug> Stops announcing the changes of the board slug <board-slug> to <channel>. """ if not instance._check_capability(irc, msg): return boards = instance._load_boards(channel) if board_slug not in boards: irc.error( _('This board is not registered to this channel.')) return # Remove board mapping del boards[board_slug] instance._save_boards(boards, channel) irc.replySuccess() remove = wrap(remove, ['channel', 'somethingWithoutSpaces']) @internationalizeDocstring def list(self, irc, msg, args, channel): """[<channel>] Lists the registered boards in <channel>. """ if not instance._check_capability(irc, msg): return boards = instance._load_boards(channel) if boards is None or len(boards) == 0: irc.error(_('This channel has no registered boards.')) return for board_slug, board_url in boards.items(): irc.reply("%s: %s" % (board_slug, board_url)) list = wrap(list, ['channel'])