Add support for receiving commands from draft/multiline batches.

This commit is contained in:
Valentin Lorentz 2021-03-04 21:30:48 +01:00
parent 975a9101f4
commit 4aca6e3d5a
3 changed files with 172 additions and 3 deletions

View File

@ -44,7 +44,6 @@ import supybot.i18n as i18n
import supybot.utils as utils
import supybot.world as world
import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.irclib as irclib
import supybot.plugin as plugin
import supybot.plugins as plugins
@ -54,6 +53,7 @@ import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as callbacks
from supybot.commands import additional, getopts, optional, wrap
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Owner')
@ -226,11 +226,103 @@ class Owner(callbacks.Plugin):
def setFloodQueueTimeout(self, *args, **kwargs):
self.commands.timeout = conf.supybot.abuse.flood.interval()
def doBatch(self, irc, msg):
if not conf.supybot.protocols.irc.experimentalExtensions():
return
batch = msg.tagged('batch') # Always not-None on a BATCH message
if msg.args[0].startswith('+'):
# Start of a batch, we're not interested yet.
return
if batch.type != 'draft/multiline':
# This is not a multiline batch, also not interested.
return
assert msg.args[0].startswith("-"), (
"BATCH's first argument should start with either - or +, but "
"it is %s."
) % msg.args[0]
# End of multiline batch. It may be a long command.
payloads = []
first_privmsg = None
for message in batch.messages:
if message.command != "PRIVMSG":
# We're only interested in PRIVMSGs for the payloads.
# (eg. exclude NOTICE)
continue
elif not payloads:
# This is the first PRIVMSG of the batch
first_privmsg = message
payloads.append(message.args[1])
elif 'draft/multiline-concat' in message.server_tags:
# This message is not a new line, but the continuation
# of the previous one.
payloads.append(message.args[1])
else:
# New line; stop here. We're not processing extra lines
# either as the rest of the command or as new commands.
# This may change in the future.
break
payload = ''.join(payloads)
if not payload:
self.log.error(
'Got empty multiline payload. This is a bug, please '
'report it along with logs.'
)
return
assert first_privmsg, "This shouldn't be None unless payload is empty"
# Let's build a synthetic message from the various parts of the
# batch, to look like the multiline batch was a single (large)
# PRIVMSG:
# * copy the tags and server tags of the 'BATCH +' command,
# * copy the prefix and channel of any of the PRIVMSGs
# inside the batch
# * create a new args[1]
target = first_privmsg.args[0]
synthetic_msg = ircmsgs.IrcMsg(
msg=batch.messages[0], # tags, server_tags, time
prefix=first_privmsg.prefix,
command='PRIVMSG',
args=(target, payload)
)
self._doPrivmsgs(irc, synthetic_msg)
def doPrivmsg(self, irc, msg):
if conf.supybot.protocols.irc.experimentalExtensions():
if 'batch' in msg.server_tags \
and any(batch.type =='draft/multiline'
for batch in irc.state.getParentBatches(msg)):
# We will handle the message in doBatch when the entire batch ends.
return
self._doPrivmsgs(irc, msg)
def _doPrivmsgs(self, irc, msg):
"""If the given message is a command, triggers Limnoria's
command-dispatching for that command.
Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can
potentially be an artificial message synthesized in doBatch
from a multiline batch.
Usually, a command is a single message, so ``payload=msg.params[0]``
However, when ``msg`` is part of a multiline message, the payload
is the concatenation of multiple messages.
See <https://ircv3.net/specs/extensions/multiline>.
"""
assert self is irc.callbacks[0], \
'Owner isn\'t first callback: %r' % irc.callbacks
if ircmsgs.isCtcp(msg):
return
s = callbacks.addressed(irc, msg)
if s:
ignored = ircdb.checkIgnored(msg.prefix)

View File

@ -118,4 +118,73 @@ class OwnerTestCase(PluginTestCase):
self.assertError('defaultplugin foobar owner')
class CommandsTestCase(PluginTestCase):
plugins = ('Owner', 'Utilities')
def testSimpleCommand(self):
self.irc.feedMsg(
ircmsgs.privmsg(self.irc.nick, 'echo foo $nick!$user@$host', self.prefix))
response = self.irc.takeMsg()
self.assertEqual(response.args, (self.nick, 'foo ' + self.prefix))
def testMultilineCommandDisabled(self):
self._sendBatch()
# response to 'echo '
self.assertRegexp('', '(echo <text>)')
# response to 'foo '
self.assertResponse('', 'Error: "foo" is not a valid command.')
# response to '$prefix'
self.assertResponse(
'', 'Error: "$nick!$user@$host" is not a valid command.')
# response to 'echo nope'
self.assertResponse('', 'nope')
def testMultilineCommand(self):
with conf.supybot.protocols.irc.experimentalExtensions.context(True):
self._sendBatch()
response = self.irc.takeMsg()
self.assertEqual(response.args, (self.nick, 'foo ' + self.prefix))
response = self.irc.takeMsg()
self.assertIsNone(response, 'Should not respond to second line')
def _sendBatch(self):
self.irc.feedMsg(ircmsgs.IrcMsg(
command='BATCH',
args=('+123', 'draft/multiline', self.irc.nick)))
# one line
self.irc.feedMsg(ircmsgs.IrcMsg(
server_tags={'batch': '123'},
prefix=self.prefix,
command='PRIVMSG',
args=(self.irc.nick, 'echo ')))
self.irc.feedMsg(ircmsgs.IrcMsg(
server_tags={'batch': '123', 'draft/multiline-concat': None},
prefix=self.prefix,
command='PRIVMSG',
args=(self.irc.nick, 'foo ')))
self.irc.feedMsg(ircmsgs.IrcMsg(
server_tags={'batch': '123', 'draft/multiline-concat': None},
prefix=self.prefix,
command='PRIVMSG',
args=(self.irc.nick, '$nick!$user@$host')))
# an other line
self.irc.feedMsg(ircmsgs.IrcMsg(
server_tags={'batch': '123'},
prefix=self.prefix,
command='PRIVMSG',
args=(self.irc.nick, 'echo nope')))
self.irc.feedMsg(ircmsgs.IrcMsg(
command='BATCH',
args=('-123',)))
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1588,6 +1588,10 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
To check if a capability was negotiated, use `irc.state.capabilities_ack`.
"""
REQUEST_EXPERIMENTAL_CAPABILITIES = set(['draft/multiline'])
"""Like REQUEST_CAPABILITIES, but these capabilities are only requested
if supybot.protocols.irc.experimentalExtensions is enabled."""
def _queueConnectMessages(self):
if self.zombie:
self.driver.die()
@ -1940,9 +1944,12 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
# Normally at this point, self.state.capabilities_ack should be
# empty; but let's just make sure we're not requesting the same
# caps twice for no reason.
want_capabilities = self.REQUEST_CAPABILITIES
if conf.supybot.protocols.irc.experimentalExtensions():
want_capabilities |= self.REQUEST_EXPERIMENTAL_CAPABILITIES
new_caps = (
set(self.state.capabilities_ls) &
self.REQUEST_CAPABILITIES -
want_capabilities -
self.state.capabilities_ack)
# NOTE: Capabilities are requested in alphabetic order, because
# sets are unordered, and their "order" is nondeterministic.
@ -2138,7 +2145,8 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
if not self.afterConnect:
self.triedNicks.add(self.nick)
newNick = self._getNextNick()
assert newNick != self.nick
assert newNick != self.nick, \
(self.nick, self.alternateNicks, self.triedNicks)
log.info('Got %s: %s %s. Trying %s.',
msg.command, self.nick, problem, newNick)
self.sendMsg(ircmsgs.nick(newNick))