Example #1
0
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
Example #2
0
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')])
Example #3
0
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'])
Example #4
0
        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'):
Example #5
0
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&notmain=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)
Example #6
0
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)
Example #7
0
    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)
Example #9
0
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')])
Example #10
0
        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'])
Example #11
0
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')])
 def decorator(func):
     return wrap(func, *args, **kwargs)
Example #13
0
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, [])
Example #14
0
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."
                )
Example #15
0
        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'):
Example #16
0
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)
Example #17
0
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&notmain=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)
Example #18
0
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()
Example #19
0
            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:
Example #20
0
        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'])