Anonymous: Add @react command

Gated behind supybot.protocols.irc.experimentalExtensions, as usual.

Spec: https://ircv3.net/specs/client-tags/react
This commit is contained in:
Valentin Lorentz 2021-03-18 20:20:05 +01:00
parent d60cc5c92a
commit ac0d7952a7
2 changed files with 121 additions and 0 deletions

View File

@ -1,6 +1,7 @@
### ###
# Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2005, Daniel DiPaolo
# Copyright (c) 2014, James McCoy # Copyright (c) 2014, James McCoy
# Copyright (c) 2021, Valentin Lorentz
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -28,6 +29,9 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
### ###
import functools
import supybot.conf as conf
import supybot.ircdb as ircdb import supybot.ircdb as ircdb
import supybot.utils as utils import supybot.utils as utils
from supybot.commands import * from supybot.commands import *
@ -113,6 +117,69 @@ class Anonymous(callbacks.Plugin):
text, channel, msg.prefix) text, channel, msg.prefix)
irc.reply(text, action=True, to=channel) irc.reply(text, action=True, to=channel)
do = wrap(do, ['inChannel', 'text']) do = wrap(do, ['inChannel', 'text'])
def react(self, irc, msg, args, channel, reaction, nick):
"""<channel> <reaction> <nick>
Sends the <reaction> to <nick>'s last message.
<reaction> 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) Anonymous = internationalizeDocstring(Anonymous)
Class = Anonymous Class = Anonymous

View File

@ -1,6 +1,7 @@
### ###
# Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2005, Daniel DiPaolo
# Copyright (c) 2014, James McCoy # Copyright (c) 2014, James McCoy
# Copyright (c) 2021, Valentin Lorentz
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -28,6 +29,7 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
### ###
import supybot.conf as conf
from supybot.test import * from supybot.test import *
class AnonymousTestCase(ChannelPluginTestCase): class AnonymousTestCase(ChannelPluginTestCase):
@ -56,5 +58,57 @@ class AnonymousTestCase(ChannelPluginTestCase):
self.assertEqual(m.args, ircmsgs.action(self.channel, self.assertEqual(m.args, ircmsgs.action(self.channel,
'loves you!').args) '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: # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: