Exemplo n.º 1
0
class Countdown(callbacks.Plugin):
    def __init__(self, irc, *args, **kwargs):
        self.__parent = super(Countdown, self)
        self.__parent.__init__(irc, *args, **kwargs)
        self._resolved = {}

    def _countdown_resp(self, irc, remaining_seconds, end_response):
        if remaining_seconds > 0:
            delta = timedelta(seconds=remaining_seconds)
            irc.reply(format_timedelta(delta), prefixNick=False)
        else:
            irc.reply(end_response, prefixNick=False)

    @decowrap(['positiveInt', additional('text')])
    def countdown(self, irc, msg, args, seconds, final_message=None):
        """<seconds> [final_message]

        Counts down
        """
        if final_message is None:
            final_message = 'GO!'
        now = time()
        callback_part = partial(self._countdown_resp, irc)
        trigger_resolve_at = now + seconds - min(seconds, 3)
        schedule.addEvent(self._populate_resolved, trigger_resolve_at)
        for alarm_point in countdown_alarm_points(seconds):
            schedule.addEvent(
                partial(callback_part, alarm_point, final_message),
                now + seconds - alarm_point)
Exemplo n.º 2
0
class SimpleUser(callbacks.Plugin):
    """A very simple user authentication. As in, it fully depends on
    hostmask."""
    @internationalizeDocstring
    def hostmask(self, irc, msg, args, nick):
        """[<nick>]

        Returns the hostmask of <nick>.  If <nick> isn't given, return the
        hostmask of the person giving the command.
        """
        if not nick:
            nick = msg.nick
        irc.reply(irc.state.nickToHostmask(nick))

    hostmask = wrap(hostmask, [additional("seenNick")])

    @internationalizeDocstring
    def whoami(self, irc, msg, args):
        """takes no arguments

        Returns the name of the user calling the command.
        """
        try:
            user = ircdb.users.getUser(msg.prefix)
            irc.reply(user.name)
        except KeyError:
            error = _("I don't recognize you.")
            irc.reply(error)

    whoami = wrap(whoami)
Exemplo n.º 3
0
class Countdown(callbacks.Plugin):
    def __init__(self, irc, *args, **kwargs):
        self.__parent = super(Countdown, self)
        self.__parent.__init__(irc, *args, **kwargs)
        self._resolved = {}
        self._destination_ips = [
            ('::ffff:225.0.0.1', 5551),
            ('::ffff:24.4.154.5', 15555),
            ('ff02::1', 5551),
        ]
        self._destination_hosts = [('hidoi.moebros.org', 15555),
                                   ('me.cwma.me', 15555)]

    def _populate_resolved(self):
        for dest in self._destination_hosts:
            try:
                self._resolved[dest[0]] = \
                    '::ffff:' + socket.gethostbyname(dest[0])
            except socket.gaierror:
                pass

    def _destinations(self):
        for host, post in self._destination_ips:
            yield host, post
        for host, port in self._destination_hosts:
            yield self._resolved[host], port

    def _send_go_packets(self):
        sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        for dest in self._destinations():
            print "sending to %r" % (dest, )
            sock.sendto('GO!', 0, dest)

    def _countdown_resp(self, irc, remaining_seconds, end_response):
        if remaining_seconds > 0:
            delta = timedelta(seconds=remaining_seconds)
            irc.reply(format_timedelta(delta), prefixNick=False)
        else:
            irc.reply(end_response, prefixNick=False)
            self._send_go_packets()

    @decowrap(['positiveInt', additional('text')])
    def countdown(self, irc, msg, args, seconds, final_message=None):
        """<seconds> [final_message]

        Counts down
        """
        if final_message is None:
            final_message = 'GO!'
        now = time()
        callback_part = partial(self._countdown_resp, irc)
        trigger_resolve_at = now + seconds - min(seconds, 3)
        schedule.addEvent(self._populate_resolved, trigger_resolve_at)
        for alarm_point in countdown_alarm_points(seconds):
            schedule.addEvent(
                partial(callback_part, alarm_point, final_message),
                now + seconds - alarm_point)
Exemplo n.º 4
0
class Nickometer(callbacks.Plugin):
    def punish(self, damage, reason):
        self.log.debug('%s lameness points awarded: %s', damage, reason)
        return damage

    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 = string.maketrans('023457+8', 'ozeasttb')
        for special in specialCost:
            tempNick = nick
            if special[0][0] != '\\':
                tempNick = tempNick.translate(letterNumberTranslator)

            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 1:
            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( ` 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 insteresting
        score_string = re.sub('(99\\.9*\\d|\\.\\d).*', '\\1', ` 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')])
Exemplo n.º 5
0
class Dicebot(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})*$')

    convertMoney = re.compile(
        r'((?P<prefix>(?P<p_curr>[$€£₴₽¥元])(?P<p_amount>(\d+[\.,]\d+)|([\.,]\d+)|(\d+)))|(?P<suffix>(?P<s_amount>(\d+[\.,]\d+)|([\.,]\d+)|(\d+))( ?(?P<s_curr>[^\d\s]+))))(?P<output>( [^\d\s]+)*)'
    )

    MAX_DICE = 1000
    MIN_SIDES = 2
    MAX_SIDES = 100
    MAX_ROLLS = 30

    def __init__(self, irc):
        super(Dicebot, self).__init__(irc)
        self.deck = Deck()
        self.money = MoneyConverter(HttpRequester())

    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')))

    @wrap(['somethingWithoutSpaces'])
    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)

    @wrap
    def shuffle(self, irc, msg, args):
        """takes no arguments

        Restores and shuffles the deck.
        """
        self.deck.shuffle()
        irc.reply('shuffled')

    @wrap([additional('positiveInt', 1)])
    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))

    deal = draw

    @wrap([rest('anything')])
    def money(self, irc, msg, args, user_input):
        """
        Converts money using some online service. Syntax is:
        !m <amount> (<to>)*
        amount is either $200, 200$, 200 $ or 200 USD
        if <to> is omitted, then it is assumed to be ['usd', 'eur'].
        """
        self.log.debug(user_input)
        if user_input is None:
            self.log.debug('user_input is None')
            return

        user_input = self.convertMoney.match(user_input)

        if user_input.group('prefix') is not None:
            input = user_input.group('p_curr')
            amount = float(user_input.group('p_amount'))
        elif user_input.group('suffix') is not None:
            input = user_input.group('s_curr')
            amount = float(user_input.group('s_amount'))
        else:
            self.log.debug('user_input does not have prefix and suffix.')
            return

        outputs = ('' if user_input.group('output') is None else
                   user_input.group('output')).split()
        outputs = ['usd', 'eur'] if len(outputs) == 0 else [
            x for x in outputs if x is not None and len(x) > 0
        ]
        irc.reply(self.money.convert(amount, input, outputs))

    m = money

    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)
Exemplo n.º 6
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 'batch' in msg.server_tags:
            parent_batches = irc.state.getParentBatches(msg)
            parent_batch_types = [batch.type for batch in parent_batches]
            if 'draft/multiline' in parent_batch_types \
                    and conf.supybot.protocols.irc.experimentalExtensions():
                # We will handle the message in doBatch when the entire
                # batch ends.
                return
            if 'chathistory' in parent_batch_types:
                # Either sent automatically by the server upon join,
                # or triggered by a plugin (why?!)
                # Either way, replying to commands from the history would
                # look weird, because it may have been sent a while ago,
                # and we may have already answered to it.
                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()
Exemplo n.º 7
0
class Wikifetch(callbacks.Plugin):
    """Grabs data from Wikipedia and other MediaWiki-powered sites."""
    threaded = True

    def _wiki(self, irc, msg, search, baseurl):
        """Fetches and replies content from a MediaWiki-powered website."""
        reply = ''

        # Different instances of MediaWiki use different URLs... This tries
        # to make the parser work for most sites, but still use resonable defaults
        # such as filling in http:// and appending /wiki to links...
        # Special cases: Wikia, Wikipedia, Wikimedia (i.e. Wikimedia Commons), Arch Linux Wiki
        if any(sitename in baseurl
               for sitename in ('wikia.com', 'wikipedia.org',
                                'wikimedia.org')):
            baseurl += '/wiki'
        elif 'wiki.archlinux.org' in baseurl:
            baseurl += '/index.php'
        if not baseurl.lower().startswith(('http://', 'https://')):
            baseurl = 'http://' + baseurl

        # first, we get the page
        addr = '%s/Special:Search?search=%s' % \
                    (baseurl, quote_plus(search))
        self.log.debug('Wikifetch: using URL %s', addr)
        article = utils.web.getUrl(addr)
        if sys.version_info[0] >= 3:
            article = article.decode()
        # parse the page
        tree = lxml.html.document_fromstring(article)
        # check if it gives a "Did you mean..." redirect
        didyoumean = tree.xpath('//div[@class="searchdidyoumean"]/a'
                                '[@title="Special:Search"]')
        if didyoumean:
            redirect = didyoumean[0].text_content().strip()
            if sys.version_info[0] < 3:
                if isinstance(redirect, unicode):
                    redirect = redirect.encode('utf-8', 'replace')
                if isinstance(search, unicode):
                    search = search.encode('utf-8', 'replace')
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s". '
                           'Did you mean "%s"? ') % (search, redirect)
            addr = "%s/%s" % (baseurl, didyoumean[0].get('href'))
            article = utils.web.getUrl(addr)
            if sys.version_info[0] >= 3:
                article = article.decode()
            tree = lxml.html.document_fromstring(article)
            search = redirect
        # check if it's a page of search results (rather than an article), and
        # if so, retrieve the first result
        searchresults = tree.xpath('//div[@class="searchresults"]/ul/li/a')
        if searchresults:
            redirect = searchresults[0].text_content().strip()
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s", but here\'s the '
                           'result for "%s": ') % (search, redirect)
            addr = self.registryValue('url', msg.args[0]) + \
                   searchresults[0].get('href')
            article = utils.web.getUrl(addr)
            if sys.version_info[0] >= 3:
                article = article.decode()

            tree = lxml.html.document_fromstring(article)
            search = redirect
        # otherwise, simply return the title and whether it redirected
        elif self.registryValue('showRedirects', msg.args[0]):
            redirect = re.search(
                '\(%s <a href=[^>]*>([^<]*)</a>\)' % _('Redirected from'),
                article)
            if redirect:
                try:
                    redirect = tree.xpath(
                        '//span[@class="mw-redirectedfrom"]/a')[0]
                    redirect = redirect.text_content().strip()
                    title = tree.xpath('//*[@class="firstHeading"]')
                    title = title[0].text_content().strip()
                    if sys.version_info[0] < 3:
                        if isinstance(title, unicode):
                            title = title.encode('utf-8', 'replace')
                        if isinstance(redirect, unicode):
                            redirect = redirect.encode('utf-8', 'replace')
                    reply += '"%s" (Redirected from "%s"): ' % (title,
                                                                redirect)
                except IndexError:
                    pass
        # extract the address we got it from - most sites have the perm link
        # inside the page itself
        try:
            addr = tree.find(".//div[@class='printfooter']/a").attrib['href']
            addr = re.sub('([&?]|(amp;)?)oldid=\d+$', '', addr)
        except:
            pass
        # check if it's a disambiguation page
        disambig = tree.xpath('//table[@id="disambigbox"]') or \
            tree.xpath('//table[@id="setindexbox"]')
        if disambig:
            disambig = tree.xpath('//div[@id="bodyContent"]/div/ul/li')
            # Hackishly bold all <a> tags
            r = []
            for item in disambig:
                for link in item.findall('a'):
                    if link.text is not None:
                        link.text = "&#x02;%s&#x02;" % link.text
                item = item.text_content().replace('&#x02;', '\x02')
                # Normalize and strip whitespace, to prevent newlines and such
                # from corrupting the display.
                item = utils.str.normalizeWhitespace(item).strip()
                r.append(item)
            reply += format(
                _('%u is a disambiguation page. '
                  'Possible results include: %L'), addr, r)
        # or just as bad, a page listing events in that year
        elif re.search(
                _('This article is about the year [\d]*\. '
                  'For the [a-zA-Z ]* [\d]*, see'), article):
            reply += _('"%s" is a page full of events that happened in that '
                       'year.  If you were looking for information about the '
                       'number itself, try searching for "%s_(number)", but '
                       'don\'t expect anything useful...') % (search, search)
        # Catch talk pages
        elif 'ns-talk' in tree.find("body").attrib['class']:
            reply += format(_('This article appears to be a talk page: %u'),
                            addr)
        else:
            p = tree.xpath("//div[@id='mw-content-text']/p[1]")
            if len(p) == 0 or 'wiki/Special:Search' in addr:
                if 'wikipedia:wikiproject' in addr.lower():
                    reply += format(
                        _('This page appears to be a WikiProject page, '
                          'but it is too complex for us to parse: %u'), addr)
                else:
                    irc.error(_('Not found, or page malformed.'), Raise=True)
            else:
                p = p[0]
                # Replace <b> tags with IRC-style bold, this has to be
                # done indirectly because unescaped '\x02' is invalid in XML
                for b_tag in p.xpath('//b'):
                    b_tag.text = "&#x02;%s&#x02;" % (b_tag.text or '')
                p = p.text_content()
                p = p.replace('&#x02;', '\x02')
                # Get rid of newlines, etc., that can corrupt the output.
                p = utils.str.normalizeWhitespace(p)
                p = p.strip()
                if sys.version_info[0] < 3:
                    if isinstance(p, unicode):
                        p = p.encode('utf-8', 'replace')
                    if isinstance(reply, unicode):
                        reply = reply.encode('utf-8', 'replace')
                reply += format('%s %s %u', p, _('Retrieved from'), addr)
        reply = reply.replace('&amp;', '&')

        # Remove inline citations (text[1][2][3], etc.)
        reply = re.sub('\[\d+\]', '', reply)

        return reply

    @internationalizeDocstring
    @wrap([getopts({'site': 'somethingWithoutSpaces'}), 'text'])
    def wiki(self, irc, msg, args, optlist, search):
        """[--site <site>] <search term>

        Returns the first paragraph of a wiki article. Optionally, a --site
        argument can be given to override the default (usually Wikipedia) -
        try using '--site lyrics.wikia.com' or '--site wiki.archlinux.org'."""
        optlist = dict(optlist)
        baseurl = optlist.get('site') or self.registryValue('url', msg.args[0])
        text = self._wiki(irc, msg, search, baseurl)

        irc.reply(text)

    @internationalizeDocstring
    @wrap([additional('somethingWithoutSpaces')])
    def random(self, irc, msg, args, site):
        """[<site>]

        Returns the first paragraph of a random wiki article. Optionally, the --site
        argument can be given to override the default (usually Wikipedia)."""
        baseurl = site or self.registryValue('url', msg.args[0])
        text = self._wiki(irc, msg, 'Special:Random', baseurl)

        irc.reply(text)
Exemplo n.º 8
0
class Wikifetch(callbacks.Plugin):
    """Grabs data from Wikipedia and other MediaWiki-powered sites."""
    threaded = True

    # This defines a series of suffixes this should be added after the domain name.
    SPECIAL_URLS = {
        'wikia.com': '/wiki',
        'wikipedia.org': '/wiki',
        'wiki.archlinux.org': '/index.php',
        'wiki.gentoo.org': '/wiki',
        'mediawiki.org': '/wiki',
        'wikimedia.org': '/wiki',
    }

    def _get_article_tree(self, baseurl, query, use_mw_parsing=True):
        """
        Returns a wiki article tree given the base URL and search query. baseurl can be None,
        in which case, searching is skipped and the search query will be treated as a raw address.
        """

        if baseurl is None:
            addr = query
        else:
            # Different instances of MediaWiki use different URLs... This tries
            # to make the parser work for most sites, but still use resonable defaults
            # such as filling in http:// and appending /wiki to links...
            baseurl = baseurl.lower()
            for match, suffix in self.SPECIAL_URLS.items():
                if match in baseurl:
                    baseurl += suffix
                    break

            # Add http:// to the URL if a scheme isn't specified
            if not baseurl.startswith(('http://', 'https://')):
                baseurl = 'http://' + baseurl

            if use_mw_parsing:
                # first, we get the page
                addr = '%s/Special:Search?search=%s' % \
                            (baseurl, quote_plus(query))
            else:
                addr = '%s/%s' % (baseurl, query)

        self.log.debug('Wikifetch: using URL %s', addr)

        try:
            article = utils.web.getUrl(addr, timeout=3)
        except utils.web.Error:
            self.log.exception('Failed to fetch link %s', addr)
            raise

        article = article.decode()

        tree = lxml.html.document_fromstring(article)
        return (tree, article, addr)

    def _wiki(self, irc, msg, search, baseurl, use_mw_parsing=True):
        """Fetches and replies content from a MediaWiki-powered website."""
        reply = ''

        # First, fetch and parse the page
        tree, article, addr = self._get_article_tree(
            baseurl, search, use_mw_parsing=use_mw_parsing)

        # check if it gives a "Did you mean..." redirect
        didyoumean = tree.xpath('//div[@class="searchdidyoumean"]/a'
                                '[@title="Special:Search"]')
        if didyoumean:
            redirect = didyoumean[0].text_content().strip()
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s". '
                           'Did you mean "%s"? ') % (search, redirect)

            tree, article, addr = self._get_article_tree(
                baseurl, didyoumean[0].get('href'))
            search = redirect

        # check if it's a page of search results (rather than an article), and
        # if so, retrieve the first result
        searchresults = tree.xpath('//div[@class="searchresults"]/ul/li/a') or \
            tree.xpath('//article/ul/li/a') # Special case for Wikia (2017-01-27)
        self.log.debug('Wikifetch: got search results %s', searchresults)

        if searchresults:
            redirect = searchresults[0].text_content().strip()
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s", but here\'s the '
                           'result for "%s": ') % (search, redirect)
            # Follow the search result and fetch that article. Note: use the original
            # base url to prevent prefixes like "/wiki" from being added twice.
            self.log.debug('Wikifetch: following search result:')
            tree, article, addr = self._get_article_tree(
                None, searchresults[0].get('href'))
            search = redirect
        # otherwise, simply return the title and whether it redirected
        elif self.registryValue('showRedirects', msg.args[0]):
            redirect = re.search(
                r'\(%s <a href=[^>]*>([^<]*)</a>\)' % _('Redirected from'),
                article)
            if redirect:
                try:
                    redirect = tree.xpath(
                        '//span[@class="mw-redirectedfrom"]/a')[0]
                    redirect = redirect.text_content().strip()
                    title = tree.xpath('//*[@class="firstHeading"]')
                    title = title[0].text_content().strip()
                    reply += '"%s" (Redirected from "%s"): ' % (title,
                                                                redirect)
                except IndexError:
                    pass
        # extract the address we got it from - most sites have the perm link
        # inside the page itself
        try:
            addr = tree.find(".//link[@rel='canonical']").attrib['href']
        except (ValueError, AttributeError):
            self.log.debug(
                'Wikifetch: failed <link rel="canonical"> link extraction, skipping'
            )
            try:
                addr = tree.find(
                    ".//div[@class='printfooter']/a").attrib['href']
                addr = re.sub(r'([&?]|(amp;)?)oldid=\d+$', '', addr)
            except (ValueError, AttributeError):
                self.log.debug(
                    'Wikifetch: failed printfooter link extraction, skipping')
                # If any of the above post-processing tricks fail, just ignore
                pass

        text_content = tree
        if use_mw_parsing:
            text_content = tree.xpath(
                "//div[@class='mw-parser-output']") or tree.xpath(
                    "//div[@id='mw-content-text']")
            if text_content:
                text_content = text_content[0]
        self.log.debug('Wikifetch: Using %s as text_content', text_content)

        # check if it's a disambiguation page
        disambig = tree.xpath('//table[@id="disambigbox"]') or \
            tree.xpath('//table[@id="setindexbox"]') or \
            tree.xpath('//div[contains(@class, "disambig")]')  # Wikia (2017-01-27)
        if disambig:
            reply += format(_('%u is a disambiguation page. '), addr)
            disambig = text_content.xpath('./ul/li')

            disambig_results = []
            for item in disambig:
                for link in item.findall('a'):
                    if link.text is not None:
                        # Hackishly bold all <a> tags
                        link.text = "&#x02;%s&#x02;" % link.text
                item = item.text_content().replace('&#x02;', '\x02')
                # Normalize and strip whitespace, to prevent newlines and such
                # from corrupting the display.
                item = utils.str.normalizeWhitespace(item).strip()
                disambig_results.append(item)
            if disambig_results:
                reply += format(_('Possible results include: %s'),
                                '; '.join(disambig_results))

        # Catch talk pages
        elif 'ns-talk' in tree.find("body").attrib.get('class', ''):
            reply += format(_('This article appears to be a talk page: %u'),
                            addr)
        else:
            # Get the first paragraph as text.
            paragraphs = []
            for p in text_content.xpath("./p"):
                self.log.debug('Wikifetch: looking at paragraph %s',
                               p.text_content())

                # Skip geographic coordinates, e.g. on articles for countries
                if p.xpath(".//span[@class='geo-dec']"):
                    continue
                # 2018-07-19: some articles have an empty p tag with this class and no content (why?)
                elif 'mw-empty-elt' in p.attrib.get('class', ''):
                    continue
                # Skip <p> tags with no content, for obvious reasons
                elif not p.text_content().strip():
                    continue

                paragraphs.append(p)

            if (not paragraphs) or 'wiki/Special:Search' in addr:
                if 'wikipedia:wikiproject' in addr.lower():
                    reply += format(
                        _('This page appears to be a WikiProject page, '
                          'but it is too complex for us to parse: %u'), addr)
                else:
                    irc.error(_('Not found, or page malformed.'), Raise=True)
            else:
                p = paragraphs[0]
                # Replace <b> tags with IRC-style bold, this has to be
                # done indirectly because unescaped '\x02' is invalid in XML
                for b_tag in p.xpath('//b'):
                    b_tag.text = "&#x02;%s&#x02;" % (b_tag.text or '')
                p = p.text_content()
                p = p.replace('&#x02;', '\x02')
                # Get rid of newlines, etc., that can corrupt the output.
                p = utils.str.normalizeWhitespace(p)
                p = p.strip()

                if not p:
                    reply = _('<Page was too complex to parse>')

                reply += format('%s %s %u', p, _('Retrieved from'), addr)
        reply = reply.replace('&amp;', '&')

        # Remove inline citations (text[1][2][3]) as well as inline notes (text[note 1]).
        reply = re.sub(r'\[[a-z ]*?\d+\]', '', reply)

        return reply

    @internationalizeDocstring
    @wrap([
        getopts({
            'site': 'somethingWithoutSpaces',
            'no-mw-parsing': ''
        }), 'text'
    ])
    def wiki(self, irc, msg, args, optlist, search):
        """[--site <site>] [--no-mw-parsing] <search term>

        Returns the first paragraph of a wiki article. Optionally, a --site
        argument can be given to override the default (usually Wikipedia) -
        try using '--site lyrics.wikia.com' or '--site wiki.archlinux.org'.

        If the --no-mw-parsing option is given, MediaWiki-specific parsing is
        disabled. This has the following effects:
          1) No attempt at searching for a relevant Wiki page is made, and
             an article with the same name as the search term is directly
             retrieved.
          2) The plugin will retrieve the first <p> tag found on a page,
             regardless of where it's found, and print it as text. This may
             not work on all sites, as some use <p> for navbars and headings
             as well.
        """
        optlist = dict(optlist)
        baseurl = optlist.get('site') or self.registryValue('url', msg.args[0])
        text = self._wiki(
            irc,
            msg,
            search,
            baseurl,
            use_mw_parsing=not optlist.get('no-mw-parsing'),
        )

        irc.reply(text)

    @internationalizeDocstring
    @wrap([additional('somethingWithoutSpaces')])
    def random(self, irc, msg, args, site):
        """[<site>]

        Returns the first paragraph of a random wiki article. Optionally, the 'site'
        argument can be given to override the default (usually Wikipedia)."""
        baseurl = site or self.registryValue('url', msg.args[0])
        text = self._wiki(irc, msg, 'Special:Random', baseurl)

        irc.reply(text)
Exemplo n.º 9
0
class WunderWeather(callbacks.Plugin):
    """Uses the Wunderground XML API to get weather conditions for a given location.
    Always gets current conditions, and by default shows a 7-day forecast as well."""
    threaded = True

    ##########    GLOBAL VARIABLES    ##########

    _weatherCurrentCondsURL = 'http://api.wunderground.com/auto/wui/geo/WXCurrentObXML/index.xml?query=%s'
    _weatherForecastURL = 'http://api.wunderground.com/auto/wui/geo/ForecastXML/index.xml?query=%s'

    ##########    EXPOSED METHODS    ##########

    def weather(self, irc, msg, args, options, location):
        """[--current|--forecast|--all] [US zip code | US/Canada city, state | Foreign city, country]

        Returns the approximate weather conditions for a given city from Wunderground.
        --current, --forecast, and --all control what kind of information the command
        shows.
        """
        matchedLocation = self._commandSetup(irc, msg, location)
        locationName = self._getNodeValue(matchedLocation[0], 'full',
                                          'Unknown Location')

        output = []
        showCurrent = False
        showForecast = False

        if not options:
            # use default output
            showCurrent = self.registryValue('showCurrentByDefault',
                                             self.__channel)
            showForecast = self.registryValue('showForecastByDefault',
                                              self.__channel)
        else:
            for (type, arg) in options:
                if type == 'current':
                    showCurrent = True
                elif type == 'forecast':
                    showForecast = True
                elif type == 'all':
                    showCurrent = True
                    showForecast = True

        if showCurrent and showForecast:
            output.append(u('Weather for ') + locationName)
        elif showCurrent:
            output.append(u('Current weather for ') + locationName)
        elif showForecast:
            output.append(u('Forecast for ') + locationName)

        if showCurrent:
            output.append(self._getCurrentConditions(matchedLocation[0]))

        if showForecast:
            # _getForecast returns a list, so we have to call extend rather than append
            output.extend(self._getForecast(matchedLocation[1]))

        if not showCurrent and not showForecast:
            irc.error(
                "Something weird happened... I'm not supposed to show current conditions or a forecast!"
            )

        irc.reply(self._formatUnicodeOutput(output))

    weather = wrap(weather, [
        getopts({
            'current': '',
            'forecast': '',
            'all': ''
        }),
        additional('text')
    ])

    ##########    SUPPORTING METHODS    ##########

    def _checkLocation(self, location):
        if not location:
            location = self.userValue('lastLocation', self.__msg.prefix)
        if not location:
            raise callbacks.ArgumentError
        self.setUserValue('lastLocation',
                          self.__msg.prefix,
                          location,
                          ignoreNoUser=True)

        # Check for shortforms, because Wunderground will attempt to check
        # for US locations without a full country name.

        # checkShortforms may return Unicode characters in the country name.
        # Need Latin 1 for Supybot's URL handlers to work
        webLocation = shortforms.checkShortforms(location)
        conditions = self._getDom(self._weatherCurrentCondsURL %
                                  utils.web.urlquote(webLocation))
        observationLocation = conditions.getElementsByTagName(
            'observation_location')[0]

        # if there's no city name in the XML, we didn't get a match
        if observationLocation.getElementsByTagName(
                'city')[0].childNodes.length < 1:
            # maybe the country shortform given conflicts with a state shortform and wasn't replaced before
            webLocation = shortforms.checkConflictingShortforms(location)

            # if no conflicting short names match, we have the same query as before
            if webLocation == None:
                return None

            conditions = self._getDom(self._weatherCurrentCondsURL %
                                      utils.web.urlquote(webLocation))
            observationLocation = conditions.getElementsByTagName(
                'observation_location')[0]

            # if there's still no match, nothing more we can do
            if observationLocation.getElementsByTagName(
                    'city')[0].childNodes.length < 1:
                return None

        # if we get this far, we got a match. Return the DOM and location
        return (conditions, webLocation)

    def _commandSetup(self, irc, msg, location):
        channel = None
        if irc.isChannel(msg.args[0]):
            channel = msg.args[0]

        # set various variables for submethods use
        self.__irc = irc
        self.__msg = msg
        self.__channel = channel

        matchedLocation = self._checkLocation(location)
        if not matchedLocation:
            self._noLocation()

        return matchedLocation

    # format temperatures using _formatForMetricOrImperial
    def _formatCurrentConditionTemperatures(self, dom, string):
        tempC = self._getNodeValue(dom, string + '_c', u('N/A')) + u('\xb0C')
        tempF = self._getNodeValue(dom, string + '_f', u('N/A')) + u('\xb0F')
        return self._formatForMetricOrImperial(tempF, tempC)

    def _formatForecastTemperatures(self, dom, type):
        tempC = self._getNodeValue(
            dom.getElementsByTagName(type)[0], 'celsius',
            u('N/A')) + u('\xb0C')
        tempF = self._getNodeValue(
            dom.getElementsByTagName(type)[0], 'fahrenheit',
            u('N/A')) + u('\xb0F')
        return self._formatForMetricOrImperial(tempF, tempC)

    # formats any imperial or metric values according to the config
    def _formatForMetricOrImperial(self, imperial, metric):
        returnValues = []

        if self.registryValue('imperial', self.__channel):
            returnValues.append(imperial)
        if self.registryValue('metric', self.__channel):
            returnValues.append(metric)

        if not returnValues:
            returnValues = (imperial, metric)

        return u(' / ').join(returnValues)

    def _formatPressures(self, dom):
        # lots of function calls, but it just divides pressure_mb by 10 and rounds it
        pressureKpa = str(
            round(
                float(self._getNodeValue(dom, 'pressure_mb', u('0'))) / 10,
                1)) + 'kPa'
        pressureIn = self._getNodeValue(dom, 'pressure_in', u('0')) + 'in'
        return self._formatForMetricOrImperial(pressureIn, pressureKpa)

    def _formatSpeeds(self, dom, string):
        mphValue = float(self._getNodeValue(dom, string, u('0')))
        speedM = u('%dmph') % round(mphValue)
        speedK = u('%dkph') % round(
            mphValue * 1.609344)  # thanks Wikipedia for the conversion rate
        return self._formatForMetricOrImperial(speedM, speedK)

    def _formatUpdatedTime(self, dom):
        observationTime = self._getNodeValue(dom, 'observation_epoch', None)
        localTime = self._getNodeValue(dom, 'local_epoch', None)
        if not observationTime or not localTime:
            return self._getNodeValue(dom, 'observation_time',
                                      'Unknown Time').lstrip(
                                          u('Last Updated on '))

        seconds = int(localTime) - int(observationTime)
        minutes = int(seconds / 60)
        seconds -= minutes * 60
        hours = int(minutes / 60)
        minutes -= hours * 60

        if seconds == 1:
            seconds = '1 sec'
        else:
            seconds = '%d secs' % seconds

        if minutes == 1:
            minutes = '1 min'
        else:
            minutes = '%d mins' % minutes

        if hours == 1:
            hours = '1 hr'
        else:
            hours = '%d hrs' % hours

        if hours == '0 hrs':
            if minutes == '0 mins':
                return '%s ago' % seconds
            return '%s, %s ago' % (minutes, seconds)
        return '%s, %s, %s ago' % (hours, minutes, seconds)

    def _getCurrentConditions(self, dom):
        output = []

        temp = self._formatCurrentConditionTemperatures(dom, 'temp')
        if self._getNodeValue(dom, 'heat_index_string') != 'NA':
            temp += u(' (Heat Index: %s)'
                      ) % self._formatCurrentConditionTemperatures(
                          dom, 'heat_index')
        if self._getNodeValue(dom, 'windchill_string') != 'NA':
            temp += u(' (Wind Chill: %s)'
                      ) % self._formatCurrentConditionTemperatures(
                          dom, 'windchill')
        output.append(u('Temperature: ') + temp)

        output.append(
            u('Humidity: ') +
            self._getNodeValue(dom, 'relative_humidity', u('N/A%')))
        if self.registryValue('showPressure', self.__channel):
            output.append(u('Pressure: ') + self._formatPressures(dom))
        output.append(
            u('Conditions: ') +
            self._getNodeValue(dom, 'weather').capitalize())
        output.append(
            u('Wind: ') +
            self._getNodeValue(dom, 'wind_dir', u('None')).capitalize() +
            ', ' + self._formatSpeeds(dom, 'wind_mph'))
        output.append(u('Updated: ') + self._formatUpdatedTime(dom))
        return u('; ').join(output)

    def _getDom(self, url):
        try:
            xmlString = utils.web.getUrl(url)
            return dom.parseString(xmlString)
        except utils.web.Error as e:
            error = e.args[0].capitalize()
            if error[-1] != '.':
                error = error + '.'
            self.__irc.error(error, Raise=True)

    def _getForecast(self, location):
        dom = self._getDom(self._weatherForecastURL %
                           utils.web.urlquote(location))
        output = []
        count = 0
        max = self.registryValue('forecastDays', self.__channel)

        forecast = dom.getElementsByTagName('simpleforecast')[0]

        for day in forecast.getElementsByTagName('forecastday'):
            if count >= max and max != 0:
                break
            forecastOutput = []

            forecastOutput.append(
                'Forecast for ' +
                self._getNodeValue(day, 'weekday').capitalize() + ': ' +
                self._getNodeValue(day, 'conditions').capitalize())
            forecastOutput.append(
                'High of ' + self._formatForecastTemperatures(day, 'high'))
            forecastOutput.append('Low of ' +
                                  self._formatForecastTemperatures(day, 'low'))
            output.append('; '.join(forecastOutput))
            count += 1

        return output

    ##########    STATIC METHODS    ##########

    def _formatUnicodeOutput(output):
        # UTF-8 encoding is required for Supybot to handle \xb0 (degrees) and other special chars
        # We can't (yet) pass it a Unicode string on its own (an oddity, to be sure)
        s = u(' | ').join(output)
        if sys.version_info[0] < 3:
            s = s.encode('utf-8')
        return s

    _formatUnicodeOutput = staticmethod(_formatUnicodeOutput)

    def _getNodeValue(dom, value, default=u('Unknown')):
        subTag = dom.getElementsByTagName(value)
        if len(subTag) < 1:
            return default
        subTag = subTag[0].firstChild
        if subTag == None:
            return default
        return subTag.nodeValue

    _getNodeValue = staticmethod(_getNodeValue)

    def _noLocation():
        raise NoLocation(noLocationError)

    _noLocation = staticmethod(_noLocation)
Exemplo n.º 10
0
class Wikifetch(callbacks.Plugin):
    """Grabs data from Wikipedia and other MediaWiki-powered sites."""
    threaded = True

    # This defines a series of suffixes this should be added after the domain name.
    SPECIAL_URLS = {
        'wikia.com': '/wiki',
        'wikipedia.org': '/wiki',
        'wiki.archlinux.org': '/index.php',
        'wiki.gentoo.org': '/wiki',
        'mediawiki.org': '/wiki',
        'wikimedia.org': '/wiki',
    }

    def _get_article_tree(self, baseurl, search):
        """
        Returns the article tree given the base URL and search query. baseurl can be None,
        in which case, the search query will be treated as a raw string.
        """

        if baseurl is None:
            addr = search
        else:
            # Different instances of MediaWiki use different URLs... This tries
            # to make the parser work for most sites, but still use resonable defaults
            # such as filling in http:// and appending /wiki to links...
            # Special cases: Wikia, Wikipedia, Wikimedia (i.e. Wikimedia Commons), Arch Linux Wiki
            if '/' not in search:
                baseurl = baseurl.lower()
                for match, suffix in self.SPECIAL_URLS.items():
                    if match in baseurl:
                        baseurl += suffix
                        break

            # Add http:// to the URL if a scheme isn't specified
            if not baseurl.startswith(('http://', 'https://')):
                baseurl = 'http://' + baseurl

            # first, we get the page
            addr = '%s/Special:Search?search=%s' % \
                        (baseurl, quote_plus(search))

        self.log.debug('Wikifetch: using URL %s', addr)

        try:
            article = utils.web.getUrl(addr, timeout=3)
        except utils.web.Error:
            self.log.exception('Failed to fetch link %s', addr)
            raise

        if sys.version_info[0] >= 3:
            article = article.decode()

        tree = lxml.html.document_fromstring(article)
        return (tree, article, addr)

    def _wiki(self, irc, msg, search, baseurl):
        """Fetches and replies content from a MediaWiki-powered website."""
        reply = ''

        # First, fetch and parse the page
        tree, article, addr = self._get_article_tree(baseurl, search)

        # check if it gives a "Did you mean..." redirect
        didyoumean = tree.xpath('//div[@class="searchdidyoumean"]/a'
                                '[@title="Special:Search"]')
        if didyoumean:
            redirect = didyoumean[0].text_content().strip()
            if sys.version_info[0] < 3:
                if isinstance(redirect, unicode):
                    redirect = redirect.encode('utf-8', 'replace')
                if isinstance(search, unicode):
                    search = search.encode('utf-8', 'replace')
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s". '
                           'Did you mean "%s"? ') % (search, redirect)

            tree, article, addr = self._get_article_tree(
                baseurl, didyoumean[0].get('href'))
            search = redirect

        # check if it's a page of search results (rather than an article), and
        # if so, retrieve the first result
        searchresults = tree.xpath('//div[@class="searchresults"]/ul/li/a') or \
            tree.xpath('//article/ul/li/a') # Special case for Wikia (2017-01-27)
        self.log.debug('Wikifetch: got search results %s', searchresults)

        if searchresults:
            redirect = searchresults[0].text_content().strip()
            if self.registryValue('showRedirects', msg.args[0]):
                reply += _('I didn\'t find anything for "%s", but here\'s the '
                           'result for "%s": ') % (search, redirect)
            # Follow the search result and fetch that article. Note: use the original
            # base url to prevent prefixes like "/wiki" from being added twice.
            self.log.debug('Wikifetch: following search result:')
            tree, article, addr = self._get_article_tree(
                None, searchresults[0].get('href'))
            search = redirect
        # otherwise, simply return the title and whether it redirected
        elif self.registryValue('showRedirects', msg.args[0]):
            redirect = re.search(
                '\(%s <a href=[^>]*>([^<]*)</a>\)' % _('Redirected from'),
                article)
            if redirect:
                try:
                    redirect = tree.xpath(
                        '//span[@class="mw-redirectedfrom"]/a')[0]
                    redirect = redirect.text_content().strip()
                    title = tree.xpath('//*[@class="firstHeading"]')
                    title = title[0].text_content().strip()
                    if sys.version_info[0] < 3:
                        if isinstance(title, unicode):
                            title = title.encode('utf-8', 'replace')
                        if isinstance(redirect, unicode):
                            redirect = redirect.encode('utf-8', 'replace')
                    reply += '"%s" (Redirected from "%s"): ' % (title,
                                                                redirect)
                except IndexError:
                    pass
        # extract the address we got it from - most sites have the perm link
        # inside the page itself
        try:
            addr = tree.find(".//div[@class='printfooter']/a").attrib['href']
            addr = re.sub('([&?]|(amp;)?)oldid=\d+$', '', addr)
        except:
            # If any of the above post-processing tricks fail, just ignore
            pass

        # check if it's a disambiguation page
        disambig = tree.xpath('//table[@id="disambigbox"]') or \
            tree.xpath('//table[@id="setindexbox"]') or \
            tree.xpath('//div[contains(@class, "disambig")]')  # Wikia (2017-01-27)
        if disambig:
            reply += format(_('%u is a disambiguation page. '), addr)
            disambig = tree.xpath('//div[@id="bodyContent"]/div/ul/li')

            disambig_results = []
            for item in disambig:
                for link in item.findall('a'):
                    if link.text is not None:
                        # Hackishly bold all <a> tags
                        link.text = "&#x02;%s&#x02;" % link.text
                item = item.text_content().replace('&#x02;', '\x02')
                # Normalize and strip whitespace, to prevent newlines and such
                # from corrupting the display.
                item = utils.str.normalizeWhitespace(item).strip()
                disambig_results.append(item)
            if disambig_results:
                reply += format(_('Possible results include: %L'),
                                disambig_results)

        # Catch talk pages
        elif 'ns-talk' in tree.find("body").attrib.get('class', ''):
            reply += format(_('This article appears to be a talk page: %u'),
                            addr)
        else:
            p = tree.xpath("//div[@id='mw-content-text']/p[1]")
            if len(p) == 0 or 'wiki/Special:Search' in addr:
                if 'wikipedia:wikiproject' in addr.lower():
                    reply += format(
                        _('This page appears to be a WikiProject page, '
                          'but it is too complex for us to parse: %u'), addr)
                else:
                    irc.error(_('Not found, or page malformed.'), Raise=True)
            else:
                p = p[0]
                # Replace <b> tags with IRC-style bold, this has to be
                # done indirectly because unescaped '\x02' is invalid in XML
                for b_tag in p.xpath('//b'):
                    b_tag.text = "&#x02;%s&#x02;" % (b_tag.text or '')
                p = p.text_content()
                p = p.replace('&#x02;', '\x02')
                # Get rid of newlines, etc., that can corrupt the output.
                p = utils.str.normalizeWhitespace(p)
                p = p.strip()
                if sys.version_info[0] < 3:
                    if isinstance(p, unicode):
                        p = p.encode('utf-8', 'replace')
                    if isinstance(reply, unicode):
                        reply = reply.encode('utf-8', 'replace')

                if not p:
                    reply = _('<Page was too complex to parse>')

                reply += format('%s %s %u', p, _('Retrieved from'), addr)
        reply = reply.replace('&amp;', '&')

        # Remove inline citations (text[1][2][3], etc.)
        reply = re.sub('\[\d+\]', '', reply)

        return reply

    @internationalizeDocstring
    @wrap([getopts({'site': 'somethingWithoutSpaces'}), 'text'])
    def wiki(self, irc, msg, args, optlist, search):
        """[--site <site>] <search term>

        Returns the first paragraph of a wiki article. Optionally, a --site
        argument can be given to override the default (usually Wikipedia) -
        try using '--site lyrics.wikia.com' or '--site wiki.archlinux.org'."""
        optlist = dict(optlist)
        baseurl = optlist.get('site') or self.registryValue('url', msg.args[0])
        text = self._wiki(irc, msg, search, baseurl)

        irc.reply(text)

    @internationalizeDocstring
    @wrap([additional('somethingWithoutSpaces')])
    def random(self, irc, msg, args, site):
        """[<site>]

        Returns the first paragraph of a random wiki article. Optionally, the --site
        argument can be given to override the default (usually Wikipedia)."""
        baseurl = site or self.registryValue('url', msg.args[0])
        text = self._wiki(irc, msg, 'Special:Random', baseurl)

        irc.reply(text)
Exemplo n.º 11
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)