diff --git a/plugins/Owner/plugin.py b/plugins/Owner/plugin.py index c163e8f9c..33cf9ef60 100644 --- a/plugins/Owner/plugin.py +++ b/plugins/Owner/plugin.py @@ -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 . + """ 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) diff --git a/plugins/Owner/test.py b/plugins/Owner/test.py index 9bfe111bf..2f725d5f4 100644 --- a/plugins/Owner/test.py +++ b/plugins/Owner/test.py @@ -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 )') + + # 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: diff --git a/src/irclib.py b/src/irclib.py index 80bb99aef..a5bcd2702 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -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))