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.utils as utils
import supybot.world as world import supybot.world as world
import supybot.ircdb as ircdb import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.irclib as irclib import supybot.irclib as irclib
import supybot.plugin as plugin import supybot.plugin as plugin
import supybot.plugins as plugins import supybot.plugins as plugins
@ -54,6 +53,7 @@ import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.registry as registry import supybot.registry as registry
import supybot.callbacks as callbacks import supybot.callbacks as callbacks
from supybot.commands import additional, getopts, optional, wrap
from supybot.i18n import PluginInternationalization, internationalizeDocstring from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Owner') _ = PluginInternationalization('Owner')
@ -226,11 +226,103 @@ class Owner(callbacks.Plugin):
def setFloodQueueTimeout(self, *args, **kwargs): def setFloodQueueTimeout(self, *args, **kwargs):
self.commands.timeout = conf.supybot.abuse.flood.interval() 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): 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], \ assert self is irc.callbacks[0], \
'Owner isn\'t first callback: %r' % irc.callbacks 'Owner isn\'t first callback: %r' % irc.callbacks
if ircmsgs.isCtcp(msg): if ircmsgs.isCtcp(msg):
return return
s = callbacks.addressed(irc, msg) s = callbacks.addressed(irc, msg)
if s: if s:
ignored = ircdb.checkIgnored(msg.prefix) ignored = ircdb.checkIgnored(msg.prefix)

View File

@ -118,4 +118,73 @@ class OwnerTestCase(PluginTestCase):
self.assertError('defaultplugin foobar owner') 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: # 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`. 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): def _queueConnectMessages(self):
if self.zombie: if self.zombie:
self.driver.die() self.driver.die()
@ -1940,9 +1944,12 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
# Normally at this point, self.state.capabilities_ack should be # Normally at this point, self.state.capabilities_ack should be
# empty; but let's just make sure we're not requesting the same # empty; but let's just make sure we're not requesting the same
# caps twice for no reason. # caps twice for no reason.
want_capabilities = self.REQUEST_CAPABILITIES
if conf.supybot.protocols.irc.experimentalExtensions():
want_capabilities |= self.REQUEST_EXPERIMENTAL_CAPABILITIES
new_caps = ( new_caps = (
set(self.state.capabilities_ls) & set(self.state.capabilities_ls) &
self.REQUEST_CAPABILITIES - want_capabilities -
self.state.capabilities_ack) self.state.capabilities_ack)
# NOTE: Capabilities are requested in alphabetic order, because # NOTE: Capabilities are requested in alphabetic order, because
# sets are unordered, and their "order" is nondeterministic. # sets are unordered, and their "order" is nondeterministic.
@ -2138,7 +2145,8 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
if not self.afterConnect: if not self.afterConnect:
self.triedNicks.add(self.nick) self.triedNicks.add(self.nick)
newNick = self._getNextNick() 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.', log.info('Got %s: %s %s. Trying %s.',
msg.command, self.nick, problem, newNick) msg.command, self.nick, problem, newNick)
self.sendMsg(ircmsgs.nick(newNick)) self.sendMsg(ircmsgs.nick(newNick))