From 1c6c1cb16a9aa203758dea4016c2da7d22e8acb9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 25 Jan 2021 21:57:12 +0100 Subject: [PATCH] Services: Add initial implementation of the @register and @verify commands. Using this early draft specification: https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495 --- plugins/Services/plugin.py | 181 ++++++++++++++++++++++++++++++++++++- plugins/Services/test.py | 115 +++++++++++++++++++++++ 2 files changed, 295 insertions(+), 1 deletion(-) diff --git a/plugins/Services/plugin.py b/plugins/Services/plugin.py index 743c625ad..b96fcde2a 100644 --- a/plugins/Services/plugin.py +++ b/plugins/Services/plugin.py @@ -52,6 +52,12 @@ class Services(callbacks.Plugin): configuration variables to match the NickServ and ChanServ nicks on your network. Other commands such as identify, op, etc. should not be necessary if the bot is properly configured.""" + + # 10 minutes ought to be more than enough for the server to reply. + # Holds the (irc, nick) of the caller so we can notify them when the + # command either succeeds or fails. + _register = utils.structures.ExpiringDict(600) + def __init__(self, irc): self.__parent = super(Services, self) self.__parent.__init__(irc) @@ -578,7 +584,180 @@ class Services(callbacks.Plugin): else: irc.reply(_('I\'m not currently configured for any nicks.')) nicks = wrap(nicks, [('checkCapability', 'admin')]) -Services = internationalizeDocstring(Services) + + + def _checkCanRegister(self, irc, otherIrc): + if not conf.supybot.protocols.irc.experimentalExtensions(): + irc.error( + _("Experimental IRC extensions are not enabled for this bot."), + Raise=True + ) + + if "draft/register" not in otherIrc.state.capabilities_ls: + irc.error( + _("This network does not support draft/register."), + Raise=True + ) + + if "labeled-response" not in otherIrc.state.capabilities_ls: + irc.error( + _("This network does not support labeled-response."), + Raise=True + ) + + if otherIrc.sasl_authenticated: + irc.error( + _("This bot is already authenticated on the network."), + Raise=True + ) + + def register(self, irc, msg, args, otherIrc, password, email): + """[] [] + + Uses the experimental REGISTER command to create an account for the bot + on the , using the and the if provided. + Some networks may require the email. + You may need to use the 'network verify' command afterward to confirm + your email address.""" + # Using this early draft specification: + # https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495 + self._checkCanRegister(irc, otherIrc) + + cap_values = (otherIrc.state.capabilities_ls["draft/register"] or "").split(",") + if "email-required" in cap_values and email is None: + irc.error( + _("This network requires an email address to register."), + Raise=True + ) + + label = ircutils.makeLabel() + self._register[label] = (irc, msg.nick) + otherIrc.queueMsg(ircmsgs.IrcMsg( + server_tags={"label": label}, + command="REGISTER", + args=[email or "*", password], + )) + register = wrap(register, ["owner", "private", "networkIrc", "something", optional("email")]) + + def verify(self, irc, msg, args, otherIrc, account, code): + """[] + + If the requires a verification code, you need to call this + command with the code the server gave you to finish the + registration.""" + self._checkCanRegister(irc, otherIrc) + + label = ircutils.makeLabel() + self._register[label] = (irc, msg.nick) + otherIrc.queueMsg(ircmsgs.IrcMsg( + server_tags={"label": label}, + command="VERIFY", + args=[account, code] + )) + verify = wrap(verify, [ + "owner", "private", "networkIrc", "somethingWithoutSpaces", "something" + ]) + + def _replyToRegister(self, irc, msg, command, reply): + if not conf.supybot.protocols.irc.experimentalExtensions: + self.log.warning( + "Got unexpected '%s' on %s, this should not " + "happen unless supybot.protocols.irc.experimentalExtensions " + "is enabled", + command, irc.network + ) + return + + label = msg.server_tags["label"] + if label not in self._register: + self.log.warning( + "Got '%s' on %s, but I don't remember using " + "REGISTER/VERIFY. " + "This may be caused by high latency from the server.", + command, irc.network + ) + return + + (initialIrc, initialNick) = self._register[label] + initialIrc.reply(reply) + + def doFailRegister(self, irc, msg): + self._replyToRegister( + irc, msg, "FAIL %s" % msg.args[0], + format( + "Failed to register on %s; the server said: %s (%s)", + irc.network, msg.args[1], msg.args[-1] + ) + ) + doFailVerify = doFailRegister + + # This should not be called, but you never know. + def doWarnRegister(self, irc, msg): + self._replyToRegister( + irc, msg, "WARN %s" % msg.args[0], + format( + "Registration warning from %s: %s (%s)", + irc.network, msg.args[1], msg.args[-1] + ) + ) + doWarnVerify = doWarnRegister + + # This should not be called, but you never know. + def doNoteRegister(self, irc, msg): + self._replyToRegister( + irc, msg, "NOTE %s" % msg.args[0], + format( + "Registration note from %s: %s (%s)", + irc.network, msg.args[1], msg.args[-1] + ) + ) + doNoteVerify = doNoteRegister + + def doRegister(self, irc, msg): + (subcommand, account, message) = msg.args + if subcommand == "SUCCESS": + self._replyToRegister( + irc, msg, "REGISTER SUCCESS", + format( + "Registration of account %s on %s succeeded: %s", + account, irc.network, message + ) + ) + elif subcommand == "VERIFICATION_REQUIRED": + self._replyToRegister( + irc, msg, "REGISTER VERIFICATION_REQUIRED", + format( + "Registration of %s on %s requires verification to complete: %s", + account, irc.network, message + ) + ) + else: + self._replyToRegister( + irc, msg, "REGISTER %s" % subcommand, + format( + "Unknown reply while registering %s on %s: %s %s", + account, irc.network, subcommand, message + ) + ) + + def doVerify(self, irc, msg): + (subcommand, account, message) = msg.args + if subcommand == "SUCCESS": + self._replyToRegister( + irc, msg, "VERIFY SUCCESS", + format( + "Verification of account %s on %s succeeded: %s", + account, irc.network, message + ) + ) + else: + self._replyToRegister( + irc, msg, "VERIFY %s" % subcommand, + format( + "Unknown reply while registering %s on %s: %s %s", + account, irc.network, subcommand, message + ) + ) Class = Services diff --git a/plugins/Services/test.py b/plugins/Services/test.py index 9bb7ee689..d629395ad 100644 --- a/plugins/Services/test.py +++ b/plugins/Services/test.py @@ -28,6 +28,8 @@ ### from supybot.test import * +import supybot.conf as conf +from supybot.ircmsgs import IrcMsg class ServicesTestCase(PluginTestCase): plugins = ('Services', 'Config') @@ -75,6 +77,119 @@ class ServicesTestCase(PluginTestCase): self.assertTrue(m.args[0] == 'NickServ') self.assertTrue(m.args[1].lower() == 'identify bar2') + def testRegisterNoExperimentalExtensions(self): + self.assertRegexp( + "register p4ssw0rd", "error: Experimental IRC extensions") + + self.irc.feedMsg(IrcMsg( + command="FAIL", args=["REGISTER", "BLAH", "message"])) + self.assertIsNone(self.irc.takeMsg()) + + self.irc.feedMsg(IrcMsg( + command="REGISTER", args=["SUCCESS", "account", "msg"])) + self.assertIsNone(self.irc.takeMsg()) + + self.irc.feedMsg(IrcMsg( + command="REGISTER", + args=["VERIFICATION_REQUIRED", "account", "msg"])) + self.assertIsNone(self.irc.takeMsg()) + + +class ExperimentalServicesTestCase(PluginTestCase): + plugins = ["Services"] + timeout = 0.1 + + def setUp(self): + super().setUp() + conf.supybot.protocols.irc.experimentalExtensions.setValue(True) + self._initialCaps = self.irc.state.capabilities_ls.copy() + self.irc.state.capabilities_ls["draft/register"] = None + self.irc.state.capabilities_ls["labeled-response"] = None + + def tearDown(self): + self.irc.state.capabilities_ls = self._initialCaps + conf.supybot.protocols.irc.experimentalExtensions.setValue(False) + super().tearDown() + + def testRegisterSupportError(self): + old_caps = self.irc.state.capabilities_ls.copy() + try: + del self.irc.state.capabilities_ls["labeled-response"] + self.assertRegexp( + "register p4ssw0rd", + "error: This network does not support labeled-response.") + + del self.irc.state.capabilities_ls["draft/register"] + self.assertRegexp( + "register p4ssw0rd", + "error: This network does not support draft/register.") + finally: + self.irc.state.capabilities_ls = old_caps + + def testRegisterRequireEmail(self): + old_caps = self.irc.state.capabilities_ls.copy() + try: + self.irc.state.capabilities_ls["draft/register"] = "email-required" + self.assertRegexp( + "register p4ssw0rd", + "error: This network requires an email address to register.") + finally: + self.irc.state.capabilities_ls = old_caps + + def testRegisterSuccess(self): + m = self.getMsg("register p4ssw0rd") + label = m.server_tags.pop("label") + self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"])) + self.irc.feedMsg(IrcMsg( + server_tags={"label": label}, + command="REGISTER", + args=["SUCCESS", "accountname", "welcome!"] + )) + self.assertResponse( + "", + "Registration of account accountname on test succeeded: welcome!") + + def testRegisterSuccessEmail(self): + m = self.getMsg("register p4ssw0rd foo@example.org") + label = m.server_tags.pop("label") + self.assertEqual(m, IrcMsg( + command="REGISTER", args=["foo@example.org", "p4ssw0rd"])) + self.irc.feedMsg(IrcMsg( + server_tags={"label": label}, + command="REGISTER", + args=["SUCCESS", "accountname", "welcome!"] + )) + self.assertResponse( + "", + "Registration of account accountname on test succeeded: welcome!") + + def testRegisterVerify(self): + m = self.getMsg("register p4ssw0rd") + label = m.server_tags.pop("label") + self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "p4ssw0rd"])) + self.irc.feedMsg(IrcMsg( + server_tags={"label": label}, + command="REGISTER", + args=["VERIFICATION_REQUIRED", "accountname", "check your emails"] + )) + self.assertResponse( + "", + "Registration of accountname on test requires verification " + "to complete: check your emails") + + m = self.getMsg("verify accountname c0de") + label = m.server_tags.pop("label") + self.assertEqual(m, IrcMsg( + command="VERIFY", args=["accountname", "c0de"])) + self.irc.feedMsg(IrcMsg( + server_tags={"label": label}, + command="VERIFY", + args=["SUCCESS", "accountname", "welcome!"] + )) + self.assertResponse( + "", + "Verification of account accountname on test succeeded: welcome!") + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: