From ac0d7952a7494508edfe3aa321e2298f6423fde9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 18 Mar 2021 20:20:05 +0100 Subject: [PATCH] Anonymous: Add @react command Gated behind supybot.protocols.irc.experimentalExtensions, as usual. Spec: https://ircv3.net/specs/client-tags/react --- plugins/Anonymous/plugin.py | 67 +++++++++++++++++++++++++++++++++++++ plugins/Anonymous/test.py | 54 ++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/plugins/Anonymous/plugin.py b/plugins/Anonymous/plugin.py index 76b379a65..6c00daa21 100644 --- a/plugins/Anonymous/plugin.py +++ b/plugins/Anonymous/plugin.py @@ -1,6 +1,7 @@ ### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2014, James McCoy +# Copyright (c) 2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,6 +29,9 @@ # POSSIBILITY OF SUCH DAMAGE. ### +import functools + +import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * @@ -113,6 +117,69 @@ class Anonymous(callbacks.Plugin): text, channel, msg.prefix) irc.reply(text, action=True, to=channel) do = wrap(do, ['inChannel', 'text']) + + def react(self, irc, msg, args, channel, reaction, nick): + """ + + Sends the to 's last message. + is typically a smiley or an emoji. + + This may not be supported on the current network, as this + command depends on IRCv3 features. + This is also not supported if + supybot.protocols.irc.experimentalExtensions disabled + (don't enable it unless you know what you are doing). + """ + self._preCheck(irc, msg, channel, 'react') + + if not conf.supybot.protocols.irc.experimentalExtensions(): + irc.error(_('Unable to react, ' + 'supybot.protocols.irc.experimentalExtensions is ' + 'disabled.'), Raise=True) + + if not 'message-tags' in irc.state.capabilities_ack: + irc.error(_('Unable to react, the network does not support ' + 'message-tags.'), Raise=True) + + if irc.state.getClientTagDenied('draft/reply') \ + or irc.state.getClientTagDenied('draft/react'): + irc.error(_('Unable to react, the network does not allow ' + 'draft/reply and/or draft/react.'), Raise=True) + + iterable = filter(functools.partial(self._validLastMsg, irc), + reversed(irc.state.history)) + for react_to_msg in iterable: + if react_to_msg.nick == nick: + break + else: + irc.error(_('I couldn\'t find a message from %s in ' + 'my history of %s messages.') + % (nick, len(irc.state.history)), + Raise=True) + + react_to_msgid = react_to_msg.server_tags.get('msgid') + + if not react_to_msgid: + irc.error(_('Unable to react, %s\'s last message does not have ' + 'a message id.') % nick, Raise=True) + + self.log.info('Reacting with %q in %s due to %s.', + reaction, channel, msg.prefix) + + reaction_msg = ircmsgs.IrcMsg(command='TAGMSG', args=(channel,), + server_tags={'+draft/reply': react_to_msgid, + '+draft/react': reaction}) + + irc.queueMsg(reaction_msg) + react = wrap( + react, ['inChannel', 'somethingWithoutSpaces', 'nickInChannel']) + + def _validLastMsg(self, irc, msg): + return msg.prefix and \ + msg.command == 'PRIVMSG' and \ + msg.channel + + Anonymous = internationalizeDocstring(Anonymous) Class = Anonymous diff --git a/plugins/Anonymous/test.py b/plugins/Anonymous/test.py index 222352ed5..3ac915e8f 100644 --- a/plugins/Anonymous/test.py +++ b/plugins/Anonymous/test.py @@ -1,6 +1,7 @@ ### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2014, James McCoy +# Copyright (c) 2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,6 +29,7 @@ # POSSIBILITY OF SUCH DAMAGE. ### +import supybot.conf as conf from supybot.test import * class AnonymousTestCase(ChannelPluginTestCase): @@ -56,5 +58,57 @@ class AnonymousTestCase(ChannelPluginTestCase): self.assertEqual(m.args, ircmsgs.action(self.channel, 'loves you!').args) + def testReact(self): + with self.subTest('nick not in channel'): + self.assertRegexp('anonymous react :) blah', + 'blah is not in %s' % self.channel) + + self.irc.feedMsg(ircmsgs.IrcMsg( + ':blah!foo@example JOIN %s' % self.channel)) + + with self.subTest('require registration'): + self.assertRegexp('anonymous react :) blah', + 'must be registered') + self.assertIsNone(self.irc.takeMsg()) + + with conf.supybot.plugins.Anonymous.requireRegistration.context(False): + with self.subTest('experimental extensions disabled'): + self.assertRegexp('anonymous react :) blah', + 'protocols.irc.experimentalExtensions is disabled') + self.assertIsNone(self.irc.takeMsg()) + + with conf.supybot.plugins.Anonymous.requireRegistration.context(False), \ + conf.supybot.protocols.irc.experimentalExtensions.context(True): + with self.subTest('server support missing'): + self.assertRegexp('anonymous react :) blah', + 'network does not support message-tags') + self.assertIsNone(self.irc.takeMsg()) + + self.irc.state.capabilities_ack.add('message-tags') + + with self.subTest('no message from the target'): + self.assertRegexp('anonymous react :) blah', + 'couldn\'t find a message') + self.assertIsNone(self.irc.takeMsg()) + + self.irc.feedMsg(ircmsgs.IrcMsg( + ':blah!foo@example PRIVMSG %s :hello' % self.channel)) + + with self.subTest('original message not tagged with msgid'): + self.assertRegexp('anonymous react :) blah', + 'not have a message id') + self.assertIsNone(self.irc.takeMsg()) + + self.irc.feedMsg(ircmsgs.IrcMsg( + '@msgid=123 :blah!foo@example PRIVMSG %s :hello' + % self.channel)) + + # Works + with self.subTest('canonical working case'): + m = self.getMsg('anonymous react :) blah') + self.assertEqual(m, ircmsgs.IrcMsg( + '@+draft/reply=123;+draft/react=:) TAGMSG %s' + % self.channel)) + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: