diff --git a/irc.py b/irc.py index a445d07..853d241 100644 --- a/irc.py +++ b/irc.py @@ -115,6 +115,8 @@ class PesterIRC(QtCore.QThread): "768": self._keynotset, "769": self._keynopermission, "770": self._metadatasubok, + "903": self._saslsuccess, # We did a SASL!! woo yeah!! (RPL_SASLSUCCESS) + "904": self._saslfail, # oh no,,, cringe,,, (ERR_SASLFAIL) "error": self._error, "join": self._join, "kick": self._kick, @@ -129,6 +131,7 @@ class PesterIRC(QtCore.QThread): "metadata": self._metadata, # Metadata specification "tagmsg": self._tagmsg, # IRCv3 message tags extension "cap": self._cap, # IRCv3 Client Capability Negotiation + "authenticate": self._authenticate, # IRCv3 SASL authentication } def run(self): @@ -190,7 +193,28 @@ class PesterIRC(QtCore.QThread): if self.password: self._send_irc.pass_(self.password) - self._send_irc.nick(self.mainwindow.profile().handle) + + # Negotiate capabilities + self._send_irc.cap("REQ", "message-tags") + self._send_irc.cap( + "REQ", + "draft/metadata-notify-2", # <--- Not required for the unreal5 module. + ) + self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this + self._send_irc.cap("REQ", "twitch.tv/membership") # Twitch silly + + # This should not be here. + profile = self.mainwindow.profile() + # Do SASL!! + if self.mainwindow.userprofile.getAutoIdentify(): + self._send_irc.cap("REQ", "sasl") + self._send_irc.authenticate("PLAIN") + else: + # Without SASL, end caps here. + self._send_irc.cap("END") + + # Send NICK & USER :3 + self._send_irc.nick(profile.handle) self._send_irc.user("pcc31", "pcc31") def _conn_generator(self): @@ -827,13 +851,6 @@ class PesterIRC(QtCore.QThread): ) self.connected.emit() # Alert main thread that we've connected. profile = self.mainwindow.profile() - # Negotiate capabilities - self._send_irc.cap("REQ", "message-tags") - self._send_irc.cap( - "REQ", "draft/metadata-notify-2" - ) # <--- Not required in the unreal5 module implementation - self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this - self._send_irc.cap("REQ", "twitch.tv/membership") # Twitch silly # Get mood mood = profile.mood.value_str() # Moods via metadata @@ -867,8 +884,9 @@ class PesterIRC(QtCore.QThread): See: https://ircv3.net/specs/extensions/capability-negotiation """ PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag) - # if tag == "message-tags": - # if subcommand == "ACK": + if subcommand.casefold() == "nak" and tag.casefold() == "sasl": + # SASL isn't supported, end CAP negotation. + self._send_irc.cap("END") def _umodeis(self, _server, _handle, modes): """Numeric reply 221 RPL_UMODEIS, shows us our user modes.""" @@ -1013,6 +1031,23 @@ class PesterIRC(QtCore.QThread): """ "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key.""" PchumLog.info("_metadatasubok: %s", params) + def _authenticate(self, _, token): + """Handle IRCv3 SASL authneticate command from server.""" + if token == "+": + # Try to send password now + self._send_irc.authenticate( + nick=self.mainwindow.profile().handle, + password=self.mainwindow.userprofile.getNickServPass(), + ) + + def _saslfail(self, *_msg): + """Handle 'RPL_SASLSUCCESS' reply from server, SASL authentication succeeded! woo yeah!!""" + self._send_irc.cap("END") + + def _saslsuccess(self, *_msg): + """Handle 'ERR_SASLFAIL' reply from server, SASL failed somehow.""" + self._send_irc.cap("END") + moodUpdated = QtCore.pyqtSignal(str, Mood) colorUpdated = QtCore.pyqtSignal(str, QtGui.QColor) messageReceived = QtCore.pyqtSignal(str, str) diff --git a/pesterchum.py b/pesterchum.py index 3a66518..9c9e725 100755 --- a/pesterchum.py +++ b/pesterchum.py @@ -2545,10 +2545,11 @@ class PesterWindow(MovingWindow): self.waitingMessages.answerMessage() def doAutoIdentify(self): - if self.userprofile.getAutoIdentify(): - self.sendMessage.emit( - "identify " + self.userprofile.getNickServPass(), "NickServ" - ) + pass + # if self.userprofile.getAutoIdentify(): + # self.sendMessage.emit( + # "identify " + self.userprofile.getNickServPass(), "NickServ" + # ) def doAutoJoins(self): if not self.autoJoinDone: diff --git a/scripts/irc_protocol.py b/scripts/irc_protocol.py index 4c054fd..ce7ed4b 100644 --- a/scripts/irc_protocol.py +++ b/scripts/irc_protocol.py @@ -1,5 +1,6 @@ """IRC-related functions and classes to be imported by irc.py""" import logging +import base64 PchumLog = logging.getLogger("pchumLogger") @@ -41,7 +42,7 @@ class SendIRC: try: PchumLog.debug("Sending: %s", command) - self.socket.sendall(outgoing_bytes) + self.socket.send(outgoing_bytes) except OSError: PchumLog.exception("Error while sending: '%s'", command.strip()) self.socket.close() @@ -157,6 +158,27 @@ class SendIRC: """Send QUIT to terminate connection.""" self._send("QUIT", text=reason) + def authenticate(self, token=None, nick=None, password=None): + """Authenticate command for SASL. + + Send either a token like 'PLAIN' or authenticate with nick and password. + + Reference: https://ircv3.net/irc/#account-authentication-and-registration + """ + if token: + self._send("AUTHENTICATE", text=token) + return + if nick and password: + # Authentication identity 'nick', authorization identity 'nick' and password 'password'. + sasl_string = f"{nick}\x00{nick}\x00{password}" + # Encode to use base64, then decode since 'text' only takes str. + sasl_string_bytes = sasl_string.encode(encoding="utf-8", errors="replace") + sasl_string_base64 = base64.b64encode(sasl_string_bytes).decode( + encoding="utf-8" + ) + # Woo yeah woo yeah + self._send("AUTHENTICATE", text=sasl_string_base64) + def parse_irc_line(line: str): """Retrieves tags, prefix, command, and arguments from an unparsed IRC line."""