Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

irc: more ways for bot.say() to help with overlength messages #2050

Merged
merged 5 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1209,25 +1209,32 @@ def __getattr__(self, attr):
def __setattr__(self, attr, value):
return setattr(self._bot, attr, value)

def say(self, message, destination=None, max_messages=1, trailing=''):
def say(self, message, destination=None, max_messages=1, truncation='', trailing=''):
"""Override ``Sopel.say`` to use trigger source by default.

:param str message: message to say
:param str destination: channel or nickname; defaults to
:attr:`trigger.sender <sopel.trigger.Trigger.sender>`
:param int max_messages: split ``text`` into at most this many messages
if it is too long to fit in one (optional)
:param int max_messages: split ``message`` into at most this many
messages if it is too long to fit into one
line (optional)
:param str truncation: string to indicate that the ``message`` was
truncated (optional)
:param str trailing: string that should always appear at the end of
``message`` (optional)

The ``destination`` will default to the channel in which the
trigger happened (or nickname, if received in a private message).

.. seealso::

:meth:`sopel.bot.Sopel.say`
For more details about the optional arguments to this wrapper
method, consult the documentation for :meth:`sopel.bot.Sopel.say`.

"""
if destination is None:
destination = self._trigger.sender
self._bot.say(self._out_pfx + message, destination, max_messages, trailing)
self._bot.say(self._out_pfx + message, destination, max_messages, truncation, trailing)

def action(self, message, destination=None):
"""Override ``Sopel.action`` to use trigger source by default.
Expand Down
79 changes: 58 additions & 21 deletions sopel/irc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,16 +536,18 @@ def reply(self, text, dest, reply_to, notice=False):
else:
self.say(text, dest)

def say(self, text, recipient, max_messages=1, trailing=''):
def say(self, text, recipient, max_messages=1, truncation='', 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)
:param str 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 str 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.
Expand All @@ -556,28 +558,55 @@ def say(self, text, recipient, max_messages=1, trailing=''):
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.
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 ``trailing`` parameter.
The ``truncation`` and ``trailing`` parameters.

"""
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:
if max_messages > 1 or truncation or trailing:
# Handle message splitting/truncation only if needed
try:
hostmask_length = len(self.hostmask)
Expand All @@ -604,12 +633,20 @@ def say(self, text, recipient, max_messages=1, trailing=''):
- 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'))
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
Expand Down Expand Up @@ -688,4 +725,4 @@ def say(self, text, recipient, max_messages=1, trailing=''):
# 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)
self.say(excess, recipient, max_messages - 1, truncation, trailing)
4 changes: 2 additions & 2 deletions sopel/modules/reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def comment_info(bot, trigger, match):
author=author, points=c.score, points_text=points_text,
posted=posted, comment=' '.join(lines))

bot.say(message, trailing=' […]')
bot.say(message, truncation=' […]')


def subreddit_info(bot, trigger, match, commanded=False):
Expand Down Expand Up @@ -309,7 +309,7 @@ def subreddit_info(bot, trigger, match, commanded=False):
link=link, nsfw=nsfw, subscribers='{:,}'.format(s.subscribers),
created=created, public_description=s.public_description)

bot.say(message, trailing=' […]')
bot.say(message, truncation=' […]')


def redditor_info(bot, trigger, match, commanded=False):
Expand Down
2 changes: 1 addition & 1 deletion sopel/modules/tld.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def gettld(bot, trigger):

message = ' | '.join(items)

bot.say(message, trailing=' […]')
bot.say(message, truncation=' […]')


@plugin.command('tldcache')
Expand Down
4 changes: 2 additions & 2 deletions sopel/modules/wikipedia.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def say_snippet(bot, trigger, server, query, show_url=True):
if show_url:
trailing += ' | ' + url

bot.say(msg, trailing=' […]' + trailing)
bot.say(msg, truncation=' […]', trailing=trailing)


def mw_snippet(server, query):
Expand Down Expand Up @@ -200,7 +200,7 @@ def say_section(bot, trigger, server, query, section):
return

msg = '{} - {} | "{}"'.format(page_name, section.replace('_', ' '), snippet)
bot.say(msg, trailing=' […]"')
bot.say(msg, truncation=' […]"')


def mw_section(server, query, section):
Expand Down
4 changes: 2 additions & 2 deletions sopel/modules/wiktionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def wiktionary(bot, trigger):
if len(result) < 300:
result = format(word, definitions, 5)

bot.say(result, trailing=' […]')
bot.say(result, truncation=' […]')


@plugin.command('ety')
Expand All @@ -144,4 +144,4 @@ def wiktionary_ety(bot, trigger):

result = "{}: {}".format(word, etymology)

bot.say(result, trailing=' […]')
bot.say(result, truncation=' […]')
47 changes: 36 additions & 11 deletions test/test_irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,55 +267,80 @@ def test_say_long_extra_multi_message_multibyte_recipient(bot):
)


def test_say_long_trailing_fit(bot):
def test_say_long_truncation_fit(bot):
"""Test optional truncation indicator with message that fits in one line."""
text = 'a' * (512 - prefix_length(bot) - len('PRIVMSG #sopel :\r\n') - 3)
bot.say(text + 'ttt', '#sopel', trailing='...')
bot.say(text + 'ttt', '#sopel', truncation='...')

assert bot.backend.message_sent == rawlist(
'PRIVMSG #sopel :%s' % text + 'ttt', # nothing is truncated or replaced
)


def test_say_long_trailing_extra(bot):
def test_say_long_truncation_extra(bot):
"""Test optional truncation indicator with message that is too long."""
text = 'a' * (512 - prefix_length(bot) - len('PRIVMSG #sopel :\r\n') - 3)
bot.say(text + 'ttt' + 'b', '#sopel', trailing='...')
bot.say(text + 'ttt' + 'b', '#sopel', truncation='...')

assert bot.backend.message_sent == rawlist(
# 'b' is truncated; 'ttt' is replaced by `trailing`
# 'b' is truncated; 'ttt' is replaced by `truncation`
'PRIVMSG #sopel :%s' % text + '...',
)


def test_say_long_trailing_extra_multi_message(bot):
def test_say_long_truncation_extra_multi_message(bot):
"""Test optional truncation indicator with message splitting."""
msg1 = 'a' * (512 - prefix_length(bot) - len('PRIVMSG #sopel :\r\n'))
msg2 = msg1[:-3] + 'ttt'
bot.say(msg1 + msg2 + 'b', '#sopel', max_messages=2, trailing='...')
bot.say(msg1 + msg2 + 'b', '#sopel', max_messages=2, truncation='...')

assert bot.backend.message_sent == rawlist(
# split as expected
'PRIVMSG #sopel :%s' % msg1,
# 'b' is truncated; 'ttt' is replaced by `trailing`
# 'b' is truncated; 'ttt' is replaced by `truncation`
'PRIVMSG #sopel :%s' % msg2.replace('ttt', '...'),
)


def test_say_long_trailing_extra_multi_message_multibyte(bot):
def test_say_long_truncation_extra_multi_message_multibyte(bot):
"""Test optional truncation indicator with message splitting."""
msg1 = 'a' * (512 - prefix_length(bot) - len('PRIVMSG #sopel :\r\n'))
msg2 = msg1[:-3] + 'ttt'
bot.say(msg1 + msg2 + 'b', '#sopel', max_messages=2, trailing='…')
bot.say(msg1 + msg2 + 'b', '#sopel', max_messages=2, truncation='…')

assert bot.backend.message_sent == rawlist(
# split as expected
'PRIVMSG #sopel :%s' % msg1,
# 'b' is truncated; 'ttt' is replaced by `trailing`
# 'b' is truncated; 'ttt' is replaced by `truncation`
'PRIVMSG #sopel :%s' % msg2.replace('ttt', '…'),
)


def test_say_trailing(bot):
"""Test optional trailing string."""
text = '"This is a test quote.'
bot.say(text, '#sopel', trailing='"')

assert bot.backend.message_sent == rawlist(
# combined
'PRIVMSG #sopel :%s' % text + '"'
)


def test_say_long_truncation_trailing(bot):
"""Test optional truncation indicator AND trailing string together."""
msg1 = 'a' * (512 - prefix_length(bot) - len('PRIVMSG #sopel :\r\n'))
msg2 = msg1[:-4] + 'bbbc'
bot.say(msg1 + msg2 + 'd', '#sopel', max_messages=2, truncation='…', trailing='q')

assert bot.backend.message_sent == rawlist(
# split as expected
'PRIVMSG #sopel :%s' % msg1,
# 'd' is truncated; 'bbb' is replaced by `truncation`; 'c' is replaced by `trailing`
'PRIVMSG #sopel :%s' % msg2.replace('bbb', '…').replace('c', 'q')
)


def test_say_no_repeat_protection(bot):
# five is fine
bot.say('hello', '#sopel')
Expand Down