Exemplo n.º 1
0
def test_get_sendable_message_optional():
    text, excess = tools.get_sendable_message('aaaa', 3)
    assert text == 'aaa'
    assert excess == 'a'

    text, excess = tools.get_sendable_message('aaa bbb', 3)
    assert text == 'aaa'
    assert excess == 'bbb'

    text, excess = tools.get_sendable_message('aa bb cc', 3)
    assert text == 'aa'
    assert excess == 'bb cc'
Exemplo n.º 2
0
def test_get_sendable_message_optional():
    text, excess = tools.get_sendable_message('aaaa', 3)
    assert text == 'aaa'
    assert excess == 'a'

    text, excess = tools.get_sendable_message('aaa bbb', 3)
    assert text == 'aaa'
    assert excess == 'bbb'

    text, excess = tools.get_sendable_message('aa bb cc', 3)
    assert text == 'aa'
    assert excess == 'bb cc'
Exemplo n.º 3
0
def test_get_sendable_message_bigger_multibyte_whitespace():
    """Tests that the logic doesn't break for multi-word strings with emoji.

    Testing multibyte characters without whitespace is fine, but there's an
    alternate code path to exercise.
    """
    text = (
        'Egg 🍳 and bacon; 🐷 egg, 🍳 sausage 🌭 and bacon; 🥓 egg 🐣 and spam; '
        'egg, 🍳 bacon 🥓 and spam, egg, 🍳 bacon, 🥓 sausage 🌭 and spam; spam, '
        'bacon, 🐖 sausage 🌭 and spam; spam, egg, 🍳 spam, spam, bacon 🐖 and '
        'spam; spam, spam, spam, egg 🥚🍳 and spam; spam, spam, spam, spam, spam, '
        'spam, baked beans, 🍛 spam, spam, spam and spam; lobster 🦞 thermidor aux '
        'crevettes with a mornay sauce garnished with truffle paté, 👨😏 brandy'
        'and a fried 🍤 egg 🥚🍳 on 🔛 top 🎩 and spam')

    first, second = tools.get_sendable_message(text)
    expected_first = (
        'Egg 🍳 and bacon; 🐷 egg, 🍳 sausage 🌭 and bacon; 🥓 egg 🐣 and spam; '
        'egg, 🍳 bacon 🥓 and spam, egg, 🍳 bacon, 🥓 sausage 🌭 and spam; spam, '
        'bacon, 🐖 sausage 🌭 and spam; spam, egg, 🍳 spam, spam, bacon 🐖 and '
        'spam; spam, spam, spam, egg 🥚🍳 and spam; spam, spam, spam, spam, spam, '
        'spam, baked beans, 🍛 spam, spam, spam and spam; lobster 🦞 thermidor aux'
    )
    expected_second = (
        'crevettes with a mornay sauce garnished with truffle paté, 👨😏 brandy'
        'and a fried 🍤 egg 🥚🍳 on 🔛 top 🎩 and spam')

    assert first == expected_first
    assert second == expected_second
Exemplo n.º 4
0
def test_get_sendable_message_excess_space_limit():
    # aaa...aaa bbb...bbb
    initial = ' '.join(['a' * 400, 'b' * 200])
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'b' * 200
Exemplo n.º 5
0
def test_get_sendable_message_excess_bigger():
    # aaa...aaa bbb...bbb
    initial = ' '.join(['a' * 401, 'b' * 1000])
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'a ' + 'b' * 1000
Exemplo n.º 6
0
def test_get_sendable_message_excess_bigger():
    # aaa...aaa bbb...bbb
    initial = ' '.join(['a' * 401, 'b' * 1000])
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'a ' + 'b' * 1000
Exemplo n.º 7
0
def test_get_sendable_message_excess_space_limit():
    # aaa...aaa bbb...bbb
    initial = ' '.join(['a' * 400, 'b' * 200])
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'b' * 200
Exemplo n.º 8
0
def test_get_sendable_message_four_bytes():
    text, excess = tools.get_sendable_message('𡃤𡃤𡃤𡃤', 8)
    assert text == '𡃤𡃤'
    assert excess == '𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤𡃤𡃤𡃤', 9)
    assert text == '𡃤𡃤'
    assert excess == '𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤𡃤𡃤𡃤', 10)
    assert text == '𡃤𡃤'
    assert excess == '𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤𡃤𡃤𡃤', 11)
    assert text == '𡃤𡃤'
    assert excess == '𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤 𡃤𡃤𡃤', 8)
    assert text == '𡃤'
    assert excess == '𡃤𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤𡃤 𡃤𡃤', 8)
    assert text == '𡃤𡃤'
    assert excess == '𡃤𡃤'

    text, excess = tools.get_sendable_message('𡃤𡃤𡃤 𡃤', 8)
    assert text == '𡃤𡃤'
    assert excess == '𡃤 𡃤'
Exemplo n.º 9
0
def test_get_sendable_message_two_bytes():
    text, excess = tools.get_sendable_message('αααα', 4)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('αααα', 5)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('α ααα', 4)
    assert text == 'α'
    assert excess == 'ααα'

    text, excess = tools.get_sendable_message('αα αα', 4)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('ααα α', 4)
    assert text == 'αα'
    assert excess == 'α α'
Exemplo n.º 10
0
def test_get_sendable_message_two_bytes():
    text, excess = tools.get_sendable_message('αααα', 4)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('αααα', 5)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('α ααα', 4)
    assert text == 'α'
    assert excess == 'ααα'

    text, excess = tools.get_sendable_message('αα αα', 4)
    assert text == 'αα'
    assert excess == 'αα'

    text, excess = tools.get_sendable_message('ααα α', 4)
    assert text == 'αα'
    assert excess == 'α α'
Exemplo n.º 11
0
def gettld(bot, trigger):
    """Show information about the given Top Level Domain."""
    tld = trigger.group(2)
    if not tld:
        bot.reply("You must provide a top-level domain to search.")
        return  # Stop if no tld argument is provided
    tld = tld.strip('.').lower()

    if not bot.memory['tld_list_cache']:
        _update_tld_data(bot, 'list')
    tld_list = bot.memory['tld_list_cache']

    if not any([
        name in tld_list
        for name
        in [tld, idna.ToASCII(tld).decode('utf-8')]
    ]):
        bot.reply(
            "The top-level domain '{}' is not in IANA's list of valid TLDs."
            .format(tld))
        return

    if not bot.memory['tld_data_cache']:
        _update_tld_data(bot, 'data')
    tld_data = bot.memory['tld_data_cache']

    record = tld_data.get(tld, None)
    if not record:
        bot.say(
            "The top-level domain '{}' exists, "
            "but no details about it could be found."
            .format(tld)
        )
        return

    # Get the current order of available data fields
    fields = list(record.keys())
    # This trick moves matching keys to the end of the list
    fields.sort(key=lambda s: s.startswith('Notes') or s.startswith('Comments'))

    items = []
    for field in fields:
        value = record[field]
        if value:
            items.append('{}: {}'.format(field, value))

    message = ' | '.join(items)
    usable, excess = tools.get_sendable_message(message)
    if excess:
        message = usable + ' […]'

    bot.say(message)
Exemplo n.º 12
0
def display(bot, term, data, num=1):
    try:
        definition = data['list'][num - 1]['definition']
    except IndexError:
        bot.reply("Requested definition does not exist. Try a lower number.")
        return

    # This guesswork nonsense can be replaced with `bot.say()`'s `trailing`
    # parameter when dropping support for Sopel <7.1
    msg, excess = get_sendable_message(
        '[urban] {term} - {definition}'.format(term=term, definition=definition),
        400,
    )
    if excess:
        msg += ' […]'

    bot.say(msg)
Exemplo n.º 13
0
def test_get_sendable_message_three_bytes():
    text, excess = tools.get_sendable_message('अअअअ', 6)
    assert text == 'अअ'
    assert excess == 'अअ'

    text, excess = tools.get_sendable_message('अअअअ', 7)
    assert text == 'अअ'
    assert excess == 'अअ'

    text, excess = tools.get_sendable_message('अअअअ', 8)
    assert text == 'अअ'
    assert excess == 'अअ'

    text, excess = tools.get_sendable_message('अ अअअ', 6)
    assert text == 'अ'
    assert excess == 'अअअ'

    text, excess = tools.get_sendable_message('अअ अअ', 6)
    assert text == 'अअ'
    assert excess == 'अअ'

    text, excess = tools.get_sendable_message('अअअ अ', 6)
    assert text == 'अअ'
    assert excess == 'अ अ'
Exemplo n.º 14
0
    def say(
        self,
        text: str,
        recipient: str,
        max_messages: int = 1,
        truncation: str = '',
        trailing: str = '',
    ) -> None:
        """Send a ``PRIVMSG`` to a user or channel.

        :param text: the text to send
        :param recipient: the message recipient
        :param max_messages: split ``text`` into at most this many messages
                             if it is too long to fit in one (optional)
        :param truncation: string to append if ``text`` is too long to fit in
                           a single message, or into the last message if
                           ``max_messages`` is greater than 1 (optional)
        :param trailing: string to append after ``text`` and (if used)
                         ``truncation`` (optional)

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.

        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages. The split is made at the last space character
        before the "safe length" (which is calculated based on the bot's
        nickname and hostmask), or exactly at the "safe length" if no such
        space character exists.

        If the ``text`` is too long to fit into the specified number of messages
        using the above splitting, the final message will contain the entire
        remainder, which may be truncated by the server. You can specify
        ``truncation`` to tell Sopel how it should indicate that the remaining
        ``text`` was cut off. Note that the ``truncation`` parameter must
        include leading whitespace if you desire any between it and the
        truncated text.

        The ``trailing`` parameter is *always* appended to ``text``, after the
        point where ``truncation`` would be inserted if necessary. It's useful
        for making sure e.g. a link is always included, even if the summary your
        plugin fetches is too long to fit.

        Here are some examples of how the ``truncation`` and ``trailing``
        parameters work, using an artificially low maximum line length::

            # bot.say() outputs <text> + <truncation?> + <trailing>
            #                   always     if needed       always

            bot.say(
                '"This is a short quote.',
                truncation=' […]',
                trailing='"')
            # Sopel says: "This is a short quote."

            bot.say(
                '"This quote is very long and will not fit on a line.',
                truncation=' […]',
                trailing='"')
            # Sopel says: "This quote is very long […]"

            bot.say(
                # note the " included at the end this time
                '"This quote is very long and will not fit on a line."',
                truncation=' […]')
            # Sopel says: "This quote is very long […]
            # The ending " goes missing

        .. versionadded:: 7.1

            The ``truncation`` and ``trailing`` parameters.

        """
        if self.backend is None:
            raise RuntimeError(ERR_BACKEND_NOT_INITIALIZED)

        excess = ''
        safe_length = self.safe_text_length(recipient)

        if not isinstance(text, str):
            # Make sure we are dealing with a Unicode string
            text = text.decode('utf-8')

        if max_messages > 1 or truncation or trailing:
            if max_messages == 1 and trailing:
                safe_length -= len(trailing.encode('utf-8'))
            text, excess = tools.get_sendable_message(text, safe_length)

        if max_messages == 1:
            if excess and truncation:
                # only append `truncation` if this is the last message AND it's still too long
                safe_length -= len(truncation.encode('utf-8'))
                text, excess = tools.get_sendable_message(text, safe_length)
                text += truncation

            # ALWAYS append `trailing`;
            # its length is included when determining if truncation happened above
            text += trailing

        flood_max_wait = self.settings.core.flood_max_wait
        flood_burst_lines = self.settings.core.flood_burst_lines
        flood_refill_rate = self.settings.core.flood_refill_rate
        flood_empty_wait = self.settings.core.flood_empty_wait

        flood_text_length = self.settings.core.flood_text_length
        flood_penalty_ratio = self.settings.core.flood_penalty_ratio

        with self.sending:
            recipient_id = self.make_identifier(recipient)
            recipient_stack = self.stack.setdefault(
                recipient_id, {
                    'messages': [],
                    'flood_left': flood_burst_lines,
                })

            if recipient_stack['messages']:
                elapsed = time.time() - recipient_stack['messages'][-1][0]
            else:
                # Default to a high enough value that we won't care.
                # Five minutes should be enough not to matter anywhere below.
                elapsed = 300

            # If flood bucket is empty, refill the appropriate number of lines
            # based on how long it's been since our last message to recipient
            if not recipient_stack['flood_left']:
                recipient_stack['flood_left'] = min(
                    flood_burst_lines,
                    int(elapsed) * flood_refill_rate)

            # If it's too soon to send another message, wait
            if not recipient_stack['flood_left']:
                penalty = 0

                if flood_penalty_ratio > 0:
                    penalty_ratio = flood_text_length * flood_penalty_ratio
                    text_length_overflow = float(
                        max(0,
                            len(text) - flood_text_length))
                    penalty = text_length_overflow / penalty_ratio

                # Maximum wait time is 2 sec by default
                initial_wait_time = flood_empty_wait + penalty
                wait = min(initial_wait_time, flood_max_wait)
                if elapsed < wait:
                    sleep_time = wait - elapsed
                    LOGGER.debug(
                        'Flood protection wait time: %.3fs; '
                        'elapsed time: %.3fs; '
                        'initial wait time (limited to %.3fs): %.3fs '
                        '(including %.3fs of penalty).',
                        sleep_time,
                        elapsed,
                        flood_max_wait,
                        initial_wait_time,
                        penalty,
                    )
                    time.sleep(sleep_time)

            # Loop detection
            messages = [m[1] for m in recipient_stack['messages'][-8:]]

            # If what we're about to send repeated at least 5 times in the last
            # two minutes, replace it with '...'
            if messages.count(text) >= 5 and elapsed < 120:
                text = '...'
                if messages.count('...') >= 3:
                    # If we've already said '...' 3 times, discard message
                    return

            self.backend.send_privmsg(recipient, text)
            recipient_stack['flood_left'] = max(
                0, recipient_stack['flood_left'] - 1)
            recipient_stack['messages'].append((time.time(), safe(text)))
            recipient_stack['messages'] = recipient_stack['messages'][-10:]

        # Now that we've sent the first part, we need to send the rest if
        # requested. Doing so recursively seems simpler than iteratively.
        if max_messages > 1 and excess:
            self.say(excess, recipient, max_messages - 1, truncation, trailing)
Exemplo n.º 15
0
def output_user(bot, trigger, sn):
    client = get_client(bot)
    response, content = client.request(
        'https://api.twitter.com/1.1/users/show.json?screen_name={}'.format(
            sn))
    if response['status'] != '200':
        logger.error('%s error reaching the twitter API for screen name %s',
                     response['status'], sn)

    user = json.loads(content.decode('utf-8'))
    if user.get('errors', []):
        msg = "Twitter returned an error"
        try:
            error = user['errors'][0]
        except IndexError:
            error = {}
        try:
            msg = msg + ': ' + error['message']
            if msg[-1] != '.':
                msg = msg + '.'  # some texts end with a period, but not all... thanks, Twitter
        except KeyError:
            msg = msg + '. :( Maybe that user doesn\'t exist?'
        bot.say(msg)
        logger.debug(
            'Screen name {sn} returned error code {code}: "{message}"'.format(
                sn=sn,
                code=error.get('code', '-1'),
                message=error.get('message', '(unknown description)')))
        return

    if user.get('url', None):
        url = user['entities']['url']['urls'][0][
            'expanded_url']  # Twitter c'mon, this is absurd
    else:
        url = ''

    joined = datetime.strptime(user['created_at'], '%a %b %d %H:%M:%S %z %Y')
    tz = tools.time.get_timezone(bot.db, bot.config, None, trigger.nick,
                                 trigger.sender)
    joined = tools.time.format_time(bot.db, bot.config, tz, trigger.nick,
                                    trigger.sender, joined)

    bio = user.get('description', '')
    if bio:
        for link in user['entities']['description'][
                'urls']:  # bloody t.co everywhere
            bio = bio.replace(link['url'], link['expanded_url'])
        bio = tools.web.decode(bio)

    message = (
        '[Twitter] {user[name]} (@{user[screen_name]}){verified}{protected}{location}{url}'
        ' | {user[friends_count]:,} friends, {user[followers_count]:,} followers'
        ' | {user[statuses_count]:,} tweets, {user[favourites_count]:,} ♥s'
        ' | Joined: {joined}{bio}').format(
            user=user,
            verified=(' ✔️' if user['verified'] else ''),
            protected=(' 🔒' if user['protected'] else ''),
            location=(' | ' +
                      user['location'] if user.get('location', None) else ''),
            url=(' | ' + url if url else ''),
            joined=format_time(bot, trigger, user['created_at']),
            bio=(' | ' + bio if bio else ''))

    # It's unlikely to happen, but theoretically we *might* need to truncate the message if enough
    # of the field values are ridiculously long. Best to be safe.
    message, excess = tools.get_sendable_message(message)
    if excess:
        message += ' […]'
    bot.say(message)
Exemplo n.º 16
0
def test_get_sendable_message_excess():
    initial = 'a' * 401
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'a'
Exemplo n.º 17
0
def test_get_sendable_message_limit():
    initial = 'a' * 400
    text, excess = tools.get_sendable_message(initial)

    assert text == initial
    assert excess == ''
Exemplo n.º 18
0
    def say(self, text, recipient, max_messages=1):
        """Send ``text`` as a PRIVMSG to ``recipient``.

        In the context of a triggered callable, the ``recipient`` defaults to
        the channel (or nickname, if a private message) from which the message
        was received.

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.
        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages, each no more than 400 bytes. The split is made at
        the last space character before the 400th byte, or at the 400th byte if
        no such space exists. If the ``text`` is too long to fit into the
        specified number of messages using the above splitting, the final
        message will contain the entire remainder, which may be truncated by
        the server.
        """
        excess = ''
        if not isinstance(text, unicode):
            # Make sure we are dealing with unicode string
            text = text.decode('utf-8')

        if max_messages > 1:
            # Manage multi-line only when needed
            text, excess = tools.get_sendable_message(text)

        try:
            self.sending.acquire()

            # No messages within the last 3 seconds? Go ahead!
            # Otherwise, wait so it's been at least 0.8 seconds + penalty

            recipient_id = Identifier(recipient)

            if recipient_id not in self.stack:
                self.stack[recipient_id] = []
            elif self.stack[recipient_id]:
                elapsed = time.time() - self.stack[recipient_id][-1][0]
                if elapsed < 3:
                    penalty = float(max(0, len(text) - 40)) / 70
                    wait = 0.8 + penalty
                    if elapsed < wait:
                        time.sleep(wait - elapsed)

                # Loop detection
                messages = [m[1] for m in self.stack[recipient_id][-8:]]

                # If what we about to send repeated at least 5 times in the
                # last 2 minutes, replace with '...'
                if messages.count(text) >= 5 and elapsed < 120:
                    text = '...'
                    if messages.count('...') >= 3:
                        # If we said '...' 3 times, discard message
                        return

            self.write(('PRIVMSG', recipient), text)
            self.stack[recipient_id].append((time.time(), self.safe(text)))
            self.stack[recipient_id] = self.stack[recipient_id][-10:]
        finally:
            self.sending.release()
        # Now that we've sent the first part, we need to send the rest. Doing
        # this recursively seems easier to me than iteratively
        if excess:
            self.msg(recipient, excess, max_messages - 1)
Exemplo n.º 19
0
def test_get_sendable_message_default():
    initial = 'aaaa'
    text, excess = tools.get_sendable_message(initial)

    assert text == initial
    assert excess == ''
Exemplo n.º 20
0
def test_get_sendable_message_excess():
    initial = 'a' * 401
    text, excess = tools.get_sendable_message(initial)

    assert text == 'a' * 400
    assert excess == 'a'
Exemplo n.º 21
0
def test_get_sendable_message_limit():
    initial = 'a' * 400
    text, excess = tools.get_sendable_message(initial)

    assert text == initial
    assert excess == ''
Exemplo n.º 22
0
    def say(self, text, recipient, max_messages=1):
        """Send ``text`` as a PRIVMSG to ``recipient``.

        In the context of a triggered callable, the ``recipient`` defaults to
        the channel (or nickname, if a private message) from which the message
        was received.

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.
        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages, each no more than 400 bytes. The split is made at
        the last space character before the 400th byte, or at the 400th byte if
        no such space exists. If the ``text`` is too long to fit into the
        specified number of messages using the above splitting, the final
        message will contain the entire remainder, which may be truncated by
        the server.
        """
        excess = ''
        if not isinstance(text, unicode):
            # Make sure we are dealing with unicode string
            text = text.decode('utf-8')

        if max_messages > 1:
            # Manage multi-line only when needed
            text, excess = tools.get_sendable_message(text)

        try:
            self.sending.acquire()

            # No messages within the last 3 seconds? Go ahead!
            # Otherwise, wait so it's been at least 0.8 seconds + penalty

            recipient_id = Identifier(recipient)

            if recipient_id not in self.stack:
                self.stack[recipient_id] = []
            elif self.stack[recipient_id]:
                elapsed = time.time() - self.stack[recipient_id][-1][0]
                if elapsed < 3:
                    penalty = float(max(0, len(text) - 40)) / 70
                    wait = min(0.8 + penalty,
                               2)  # Never wait more than 2 seconds
                    if elapsed < wait and self.config.core.message_throttle:
                        time.sleep(wait - elapsed)

                # Loop detection
                messages = [m[1] for m in self.stack[recipient_id][-8:]]

                # If what we about to send repeated at least 5 times in the
                # last 2 minutes, replace with '...'
                if messages.count(
                        text
                ) >= 5 and elapsed < 120 and self.config.core.message_loop_detection:
                    text = '...'
                    if messages.count('...') >= 3:
                        # If we said '...' 3 times, discard message
                        return

            self.write(('PRIVMSG', recipient), text)
            self.stack[recipient_id].append((time.time(), self.safe(text)))
            self.stack[recipient_id] = self.stack[recipient_id][-10:]
        finally:
            self.sending.release()
        # Now that we've sent the first part, we need to send the rest. Doing
        # this recursively seems easier to me than iteratively
        if excess:
            self.say(excess, max_messages - 1, recipient)
Exemplo n.º 23
0
def test_get_sendable_message_default():
    initial = 'aaaa'
    text, excess = tools.get_sendable_message(initial)

    assert text == initial
    assert excess == ''
Exemplo n.º 24
0
    def say(self, text, recipient, max_messages=1, trailing=''):
        """Send a PRIVMSG to a user or channel.

        :param str text: the text to send
        :param str recipient: the message recipient
        :param int max_messages: split ``text`` into at most this many messages
                                 if it is too long to fit in one (optional)
        :param str trailing: text to append if ``text`` is too long to fit in
                             a single message, or into the last message if
                             ``max_messages`` is greater than 1 (optional)

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.

        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages. The split is made at the last space character
        before the "safe length" (which is calculated based on the bot's
        nickname and hostmask), or exactly at the "safe length" if no such
        space character exists.

        If the ``text`` is too long to fit into the specified number of
        messages using the above splitting, the final message will contain the
        entire remainder, which may be truncated by the server.

        The ``trailing`` parameter allows gracefully terminating a ``text``
        that is too long to fit in the specified number of messages. The final
        message (or only message, if ``max_messages`` is left at the default
        value of 1) will be truncated slightly to fit the ``trailing`` string.
        Note that the ``trailing`` parameter must include leading whitespace
        if you desire any between it and the truncated text.
        """
        excess = ''
        if not isinstance(text, unicode):
            # Make sure we are dealing with a Unicode string
            text = text.decode('utf-8')

        if max_messages > 1 or trailing:
            # Handle message splitting/truncation only if needed
            try:
                hostmask_length = len(self.hostmask)
            except KeyError:
                # calculate maximum possible length, given current nick/username
                hostmask_length = (
                    len(self.nick)  # own nick length
                    + 1  # (! separator)
                    + 1  # (for the optional ~ in user)
                    +
                    min(  # own ident length, capped to ISUPPORT or RFC maximum
                        len(self.user), getattr(self.isupport, 'USERLEN', 9)) +
                    1  # (@ separator)
                    + 63  # <hostname> has a maximum length of 63 characters.
                )
            safe_length = (
                512  # maximum IRC line length in bytes, per RFC
                - 1  # leading colon
                -
                hostmask_length  # calculated/maximum length of own hostmask prefix
                - 1  # space between prefix & command
                - 7  # PRIVMSG command
                - 1  # space before recipient
                - len(recipient.encode(
                    'utf-8'))  # target channel/nick (can contain Unicode)
                - 2  # space after recipient, colon before text
                - 2  # trailing CRLF
            )
            text, excess = tools.get_sendable_message(text, safe_length)

        if max_messages == 1 and excess and trailing:
            # only append `trailing` if this is the last message AND it's still too long
            safe_length -= len(trailing.encode('utf-8'))
            text, excess = tools.get_sendable_message(text, safe_length)
            text += trailing

        flood_max_wait = self.settings.core.flood_max_wait
        flood_burst_lines = self.settings.core.flood_burst_lines
        flood_refill_rate = self.settings.core.flood_refill_rate
        flood_empty_wait = self.settings.core.flood_empty_wait

        flood_text_length = self.settings.core.flood_text_length
        flood_penalty_ratio = self.settings.core.flood_penalty_ratio

        with self.sending:
            recipient_id = tools.Identifier(recipient)
            recipient_stack = self.stack.setdefault(
                recipient_id, {
                    'messages': [],
                    'flood_left': flood_burst_lines,
                })

            if recipient_stack['messages']:
                elapsed = time.time() - recipient_stack['messages'][-1][0]
            else:
                # Default to a high enough value that we won't care.
                # Five minutes should be enough not to matter anywhere below.
                elapsed = 300

            # If flood bucket is empty, refill the appropriate number of lines
            # based on how long it's been since our last message to recipient
            if not recipient_stack['flood_left']:
                recipient_stack['flood_left'] = min(
                    flood_burst_lines,
                    int(elapsed) * flood_refill_rate)

            # If it's too soon to send another message, wait
            if not recipient_stack['flood_left']:
                penalty = 0

                if flood_penalty_ratio > 0:
                    penalty_ratio = flood_text_length * flood_penalty_ratio
                    text_length_overflow = float(
                        max(0,
                            len(text) - flood_text_length))
                    penalty = text_length_overflow / penalty_ratio

                # Maximum wait time is 2 sec by default
                initial_wait_time = flood_empty_wait + penalty
                wait = min(initial_wait_time, flood_max_wait)
                if elapsed < wait:
                    sleep_time = wait - elapsed
                    LOGGER.debug(
                        'Flood protection wait time: %.3fs; '
                        'elapsed time: %.3fs; '
                        'initial wait time (limited to %.3fs): %.3fs '
                        '(including %.3fs of penalty).',
                        sleep_time,
                        elapsed,
                        flood_max_wait,
                        initial_wait_time,
                        penalty,
                    )
                    time.sleep(sleep_time)

            # Loop detection
            messages = [m[1] for m in recipient_stack['messages'][-8:]]

            # If what we're about to send repeated at least 5 times in the last
            # two minutes, replace it with '...'
            if messages.count(text) >= 5 and elapsed < 120:
                text = '...'
                if messages.count('...') >= 3:
                    # If we've already said '...' 3 times, discard message
                    return

            self.backend.send_privmsg(recipient, text)
            recipient_stack['flood_left'] = max(
                0, recipient_stack['flood_left'] - 1)
            recipient_stack['messages'].append((time.time(), safe(text)))
            recipient_stack['messages'] = recipient_stack['messages'][-10:]

        # Now that we've sent the first part, we need to send the rest if
        # requested. Doing so recursively seems simpler than iteratively.
        if max_messages > 1 and excess:
            self.say(excess, recipient, max_messages - 1, trailing)
Exemplo n.º 25
0
Arquivo: bot.py Projeto: nsnw/sopel
    def say(self, text, recipient, max_messages=1):
        """Send a PRIVMSG to a user or channel.

        :param str text: the text to send
        :param str recipient: the message recipient
        :param int max_messages: the maximum number of messages to break the
                                 text into

        In the context of a triggered callable, the ``recipient`` defaults to
        the channel (or nickname, if a private message) from which the message
        was received.

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.
        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages, each no more than 400 bytes. The split is made at
        the last space character before the 400th byte, or at the 400th byte if
        no such space exists. If the ``text`` is too long to fit into the
        specified number of messages using the above splitting, the final
        message will contain the entire remainder, which may be truncated by
        the server.
        """
        excess = ''
        if not isinstance(text, unicode):
            # Make sure we are dealing with unicode string
            text = text.decode('utf-8')

        if max_messages > 1:
            # Manage multi-line only when needed
            text, excess = tools.get_sendable_message(text)

        try:
            self.sending.acquire()

            recipient_id = Identifier(recipient)
            recipient_stack = self.stack.setdefault(
                recipient_id, {
                    'messages': [],
                    'flood_left': self.config.core.flood_burst_lines,
                })

            if recipient_stack['messages']:
                elapsed = time.time() - recipient_stack['messages'][-1][0]
            else:
                # Default to a high enough value that we won't care.
                # Five minutes should be enough not to matter anywhere below.
                elapsed = 300

            # If flood bucket is empty, refill the appropriate number of lines
            # based on how long it's been since our last message to recipient
            if not recipient_stack['flood_left']:
                recipient_stack['flood_left'] = min(
                    self.config.core.flood_burst_lines,
                    int(elapsed) * self.config.core.flood_refill_rate)

            # If it's too soon to send another message, wait
            if not recipient_stack['flood_left']:
                penalty = float(max(0, len(text) - 50)) / 70
                wait = min(self.config.core.flood_empty_wait + penalty,
                           2)  # Maximum wait time is 2 sec
                if elapsed < wait:
                    time.sleep(wait - elapsed)

            # Loop detection
            messages = [m[1] for m in recipient_stack['messages'][-8:]]

            # If what we're about to send repeated at least 5 times in the last
            # two minutes, replace it with '...'
            if messages.count(text) >= 5 and elapsed < 120:
                text = '...'
                if messages.count('...') >= 3:
                    # If we've already said '...' 3 times, discard message
                    return

            self.write(('PRIVMSG', recipient), text)
            recipient_stack['flood_left'] = max(
                0, recipient_stack['flood_left'] - 1)
            recipient_stack['messages'].append((time.time(), self.safe(text)))
            recipient_stack['messages'] = recipient_stack['messages'][-10:]
        finally:
            self.sending.release()
        # Now that we've sent the first part, we need to send the rest. Doing
        # this recursively seems easier to me than iteratively
        if excess:
            self.say(excess, max_messages - 1, recipient)
Exemplo n.º 26
0
    def say(self, text, recipient, max_messages=1):
        """Send a PRIVMSG to a user or channel.

        :param str text: the text to send
        :param str recipient: the message recipient
        :param int max_messages: split ``text`` into at most this many messages
                                 if it is too long to fit in one (optional)

        By default, this will attempt to send the entire ``text`` in one
        message. If the text is too long for the server, it may be truncated.

        If ``max_messages`` is given, the ``text`` will be split into at most
        that many messages, each no more than 400 bytes. The split is made at
        the last space character before the 400th byte, or at the 400th byte
        if no such space exists.

        If the ``text`` is too long to fit into the specified number of
        messages using the above splitting, the final message will contain the
        entire remainder, which may be truncated by the server.
        """
        excess = ''
        if not isinstance(text, unicode):
            # Make sure we are dealing with a Unicode string
            text = text.decode('utf-8')

        if max_messages > 1:
            # Manage multi-line only when needed
            text, excess = tools.get_sendable_message(text)

        flood_max_wait = self.settings.core.flood_max_wait
        flood_burst_lines = self.settings.core.flood_burst_lines
        flood_refill_rate = self.settings.core.flood_refill_rate
        flood_empty_wait = self.settings.core.flood_empty_wait

        flood_text_length = self.settings.core.flood_text_length
        flood_penalty_ratio = self.settings.core.flood_penalty_ratio

        with self.sending:
            recipient_id = tools.Identifier(recipient)
            recipient_stack = self.stack.setdefault(
                recipient_id, {
                    'messages': [],
                    'flood_left': flood_burst_lines,
                })

            if recipient_stack['messages']:
                elapsed = time.time() - recipient_stack['messages'][-1][0]
            else:
                # Default to a high enough value that we won't care.
                # Five minutes should be enough not to matter anywhere below.
                elapsed = 300

            # If flood bucket is empty, refill the appropriate number of lines
            # based on how long it's been since our last message to recipient
            if not recipient_stack['flood_left']:
                recipient_stack['flood_left'] = min(
                    flood_burst_lines,
                    int(elapsed) * flood_refill_rate)

            # If it's too soon to send another message, wait
            if not recipient_stack['flood_left']:
                penalty = 0

                if flood_penalty_ratio > 0:
                    penalty_ratio = flood_text_length * flood_penalty_ratio
                    text_length_overflow = float(
                        max(0,
                            len(text) - flood_text_length))
                    penalty = text_length_overflow / penalty_ratio

                # Maximum wait time is 2 sec by default
                initial_wait_time = flood_empty_wait + penalty
                wait = min(initial_wait_time, flood_max_wait)
                if elapsed < wait:
                    sleep_time = wait - elapsed
                    LOGGER.debug(
                        'Flood protection wait time: %.3fs; '
                        'elapsed time: %.3fs; '
                        'initial wait time (limited to %.3fs): %.3fs '
                        '(including %.3fs of penalty).',
                        sleep_time,
                        elapsed,
                        flood_max_wait,
                        initial_wait_time,
                        penalty,
                    )
                    time.sleep(sleep_time)

            # Loop detection
            messages = [m[1] for m in recipient_stack['messages'][-8:]]

            # If what we're about to send repeated at least 5 times in the last
            # two minutes, replace it with '...'
            if messages.count(text) >= 5 and elapsed < 120:
                text = '...'
                if messages.count('...') >= 3:
                    # If we've already said '...' 3 times, discard message
                    return

            self.backend.send_privmsg(recipient, text)
            recipient_stack['flood_left'] = max(
                0, recipient_stack['flood_left'] - 1)
            recipient_stack['messages'].append((time.time(), safe(text)))
            recipient_stack['messages'] = recipient_stack['messages'][-10:]

        # Now that we've sent the first part, we need to send the rest. Doing
        # this recursively seems easier to me than iteratively
        if excess:
            self.say(excess, recipient, max_messages - 1)