IRC rewrite continued

- Make functions for handling incoming IRC commands private where possible.
 - Add a few checks for input validation
 - Rewrite CTCP handling.
This commit is contained in:
Dpeta 2023-02-12 00:36:36 +01:00
parent fbe8f48d63
commit 1d4d1dbab6
No known key found for this signature in database
GPG key ID: 51227517CEA0030C
5 changed files with 389 additions and 295 deletions

551
irc.py
View file

@ -28,12 +28,10 @@ the license notice included with oyoyo source files is indented here:
# THE SOFTWARE. # THE SOFTWARE.
""" """
import sys
import socket import socket
import random import random
import datetime
import logging import logging
from ssl import SSLEOFError, SSLCertVerificationError from ssl import SSLCertVerificationError
try: try:
from PyQt6 import QtCore, QtGui from PyQt6 import QtCore, QtGui
@ -47,6 +45,7 @@ from generic import PesterList
from version import _pcVersion from version import _pcVersion
from scripts.irc_protocol import SendIRC, parse_irc_line from scripts.irc_protocol import SendIRC, parse_irc_line
from scripts.ssl_context import get_ssl_context from scripts.ssl_context import get_ssl_context
from scripts.input_validation import is_valid_mood, is_valid_rgb_color
PchumLog = logging.getLogger("pchumLogger") PchumLog = logging.getLogger("pchumLogger")
SERVICES = [ SERVICES = [
@ -63,100 +62,128 @@ SERVICES = [
class PesterIRC(QtCore.QThread): class PesterIRC(QtCore.QThread):
"""Class for making a thread that manages the connection to server.""" """Class for making a thread that manages the connection to server."""
def __init__(self, config, window, verify_hostname=True): def __init__(self, window, server: str, port: int, ssl: bool, verify_hostname=True):
QtCore.QThread.__init__(self) QtCore.QThread.__init__(self)
self.mainwindow = window self.mainwindow = window
self.config = config
self.unresponsive = False
self.registeredIRC = False
self.verify_hostname = verify_hostname
self.metadata_supported = False
self.stopIRC = None
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.host = self.config.server() self.server = server # Server to connect to.
self.port = self.config.port() self.port = port # Port on server to connect to.
self.ssl = self.config.ssl() self.ssl = ssl # Whether to connect over SSL/TLS.
self._end = False self.verify_hostname = (
verify_hostname # Whether to verify server hostname. (SSL-only)
)
self.send_irc = SendIRC() self._send_irc = SendIRC()
self.conn = None
self.unresponsive = False
self.registeredIRC = False
self.metadata_supported = False
self.stopIRC = None
self._conn = None
self._end = False # Set to True when ending connection.
self.joined = False self.joined = False
self.channelnames = {} self.channelnames = {}
self.channel_list = [] self.channel_list = []
self.channel_field = None self.channel_field = None
self.commands = { self.commands = {
"001": self.welcome, "001": self._welcome,
"005": self.featurelist, "005": self._featurelist,
"221": self.umodeis, "221": self._umodeis,
"321": self.liststart, "321": self._liststart,
"322": self.list, "322": self._list,
"323": self.listend, "323": self._listend,
"324": self.channelmodeis, "324": self._channelmodeis,
"353": self.namreply, "353": self._namreply,
"366": self.endofnames, "366": self._endofnames,
"432": self.erroneusnickname, "432": self._erroneusnickname,
"433": self.nicknameinuse, "433": self._nicknameinuse,
"436": self.nickcollision, "436": self._nickcollision,
"448": self.forbiddenchannel, # non-standard "448": self._forbiddenchannel, # non-standard
"473": self.inviteonlychan, "473": self._inviteonlychan,
"761": self.keyvalue, # 7XX is ircv3 deprecated metadata spec "761": self._keyvalue, # 7XX is ircv3 deprecated metadata spec
"766": self.nomatchingkey, "766": self._nomatchingkey,
"768": self.keynotset, "768": self._keynotset,
"769": self.keynopermission, "769": self._keynopermission,
"770": self.metadatasubok, "770": self._metadatasubok,
"error": self.error, "error": self._error,
"join": self.join, "join": self._join,
"kick": self.kick, "kick": self._kick,
"mode": self.mode, "mode": self._mode,
"part": self.part, "part": self._part,
"ping": self.ping, "ping": self._ping,
"privmsg": self.privmsg, "privmsg": self._privmsg,
"notice": self.notice, "notice": self._notice,
"quit": self.quit, "quit": self._quit,
"invite": self.invite, "invite": self._invite,
"nick": self.nick, # We can get svsnicked "nick": self._nick, # We can get svsnicked
"metadata": self.metadata, # Metadata specification "metadata": self._metadata, # Metadata specification
"tagmsg": self.tagmsg, # IRCv3 message tags extension "tagmsg": self._tagmsg, # IRCv3 message tags extension
"cap": self.cap, # IRCv3 Client Capability Negotiation "cap": self._cap, # IRCv3 Client Capability Negotiation
} }
def connect(self, verify_hostname=True): def run(self):
"""Initiates the connection to the server set in self.host:self.port """Implements the main loop for the thread.
This function reimplements QThread::run() and is ran after self.irc.start()
is called on the main thread. Returning from this method ends the thread."""
try:
self.IRCConnect()
except OSError as se:
self.stopIRC = se
return
while True:
res = True
try:
PchumLog.debug("updateIRC()")
self.mainwindow.sincerecv = 0
res = self.updateIRC()
except socket.timeout as se:
PchumLog.debug("timeout in thread %s", self)
self._close()
self.stopIRC = "{}, {}".format(type(se), se)
return
except (OSError, IndexError, ValueError) as se:
self.stopIRC = "{}, {}".format(type(se), se)
PchumLog.debug("Socket error, exiting thread.")
return
else:
if not res:
PchumLog.debug("False Yield: %s, returning", res)
return
def _connect(self, verify_hostname=True):
"""Initiates the connection to the server set in self.server:self.port
self.ssl decides whether the connection uses ssl. self.ssl decides whether the connection uses ssl.
Certificate validation when using SSL/TLS may be disabled by Certificate validation when using SSL/TLS may be disabled by
passing the 'verify_hostname' parameter. The user is asked if they passing the 'verify_hostname' parameter. The user is asked if they
want to disable it if this functions raises a certificate validation error, want to disable it if this functions raises a certificate validation error,
in which case the function may be called again with 'verify_hostname'.""" in which case the function may be called again with 'verify_hostname'."""
PchumLog.info("Connecting to %s:%s", self.host, self.port) PchumLog.info("Connecting to %s:%s", self.server, self.port)
# Open connection # Open connection
plaintext_socket = socket.create_connection((self.host, self.port)) plaintext_socket = socket.create_connection((self.server, self.port))
if self.ssl: if self.ssl:
# Upgrade connection to use SSL/TLS if enabled # Upgrade connection to use SSL/TLS if enabled
context = get_ssl_context() context = get_ssl_context()
context.check_hostname = verify_hostname context.check_hostname = verify_hostname
self.socket = context.wrap_socket( self.socket = context.wrap_socket(
plaintext_socket, server_hostname=self.host plaintext_socket, server_hostname=self.server
) )
else: else:
# SSL/TLS is disabled, connection is plaintext # SSL/TLS is disabled, connection is plaintext
self.socket = plaintext_socket self.socket = plaintext_socket
self.socket.settimeout(90) self.socket.settimeout(90)
self.send_irc.socket = self.socket self._send_irc.socket = self.socket
self.send_irc.nick(self.mainwindow.profile().handle) self._send_irc.nick(self.mainwindow.profile().handle)
self.send_irc.user("pcc31", "pcc31") self._send_irc.user("pcc31", "pcc31")
# if self.connect_cb:
# self.connect_cb(self)
def conn_generator(self): def _conn_generator(self):
"""Returns a generator object.""" """Returns a generator object."""
try: try:
buffer = b"" buffer = b""
@ -184,39 +211,33 @@ class PesterIRC(QtCore.QThread):
if command: if command:
# Only need tags with tagmsg # Only need tags with tagmsg
if command.casefold() == "tagmsg": if command.casefold() == "tagmsg":
self.run_command(command, prefix, tags, *args) self._run_command(command, prefix, tags, *args)
else: else:
self.run_command(command, prefix, *args) self._run_command(command, prefix, *args)
yield True yield True
except socket.timeout as se: except OSError as socket_exception:
PchumLog.debug("passing timeout") PchumLog.warning(
raise se "OSError raised in _conn, closing socket. (%s)", socket_exception
except (OSError, SSLEOFError) as se: )
PchumLog.warning("Problem: %s", se) self._close()
if self.socket: raise socket_exception
PchumLog.info("Error: closing socket.") except Exception as exception:
self.socket.close() PchumLog.exception("Non-socket exception in _conn.")
raise se raise exception
except Exception as e:
PchumLog.exception("Non-socket exception in conn_generator().")
raise e
else: else:
PchumLog.debug("Ending conn() while loop, end is %s.", self._end) PchumLog.debug("Ending _conn while loop, end is %s.", self._end)
if self.socket: self._close()
PchumLog.info("Finished: closing socket.")
self.socket.close()
yield False yield False
def run_command(self, command, *args): def _run_command(self, command, *args):
"""Finds and runs a command if it has a matching function in the self.command dict.""" """Finds and runs a command if it has a matching function in the self.commands dict."""
PchumLog.debug("run_command %s(%s)", command, args) PchumLog.debug("_run_command %s(%s)", command, args)
if command in self.commands: if command in self.commands:
command_function = self.commands[command] command_function = self.commands[command]
else: else:
PchumLog.warning("No matching function for command: %s(%s)", command, args) PchumLog.debug("No matching function for command: %s(%s)", command, args)
return return
try: try:
command_function(*args) command_function(*args)
except TypeError: except TypeError:
@ -226,10 +247,10 @@ class PesterIRC(QtCore.QThread):
except Exception: except Exception:
PchumLog.exception("Exception while parsing command.") PchumLog.exception("Exception while parsing command.")
def close(self): def _close(self):
"""Kill the socket 'with extreme prejudice'.""" """Kill the socket 'with extreme prejudice'."""
if self.socket: if self.socket:
PchumLog.info("close() was called, shutting down socket.") PchumLog.info("_close() was called, shutting down socket.")
self._end = True self._end = True
try: try:
self.socket.shutdown(socket.SHUT_RDWR) self.socket.shutdown(socket.SHUT_RDWR)
@ -241,40 +262,14 @@ class PesterIRC(QtCore.QThread):
PchumLog.info("Error while closing socket, already broken? %s", e) PchumLog.info("Error while closing socket, already broken? %s", e)
def IRCConnect(self): def IRCConnect(self):
"""Try to connect and signal for connect-anyway prompt on cert fail."""
try: try:
self.connect(self.verify_hostname) self._connect(self.verify_hostname)
except SSLCertVerificationError as e: except SSLCertVerificationError as e:
# Ask if users wants to connect anyway # Ask if users wants to connect anyway
self.askToConnect.emit(e) self.askToConnect.emit(e)
raise e raise e
self.conn = self.conn_generator() self._conn = self._conn_generator()
def run(self):
"""Connect and run update loop."""
try:
self.IRCConnect()
except OSError as se:
self.stopIRC = se
return
while True:
res = True
try:
PchumLog.debug("updateIRC()")
self.mainwindow.sincerecv = 0
res = self.updateIRC()
except socket.timeout as se:
PchumLog.debug("timeout in thread %s", self)
self.close()
self.stopIRC = "{}, {}".format(type(se), se)
return
except (OSError, IndexError, ValueError) as se:
self.stopIRC = "{}, {}".format(type(se), se)
PchumLog.debug("Socket error, exiting thread.")
return
else:
if not res:
PchumLog.debug("False Yield: %s, returning", res)
return
def setConnectionBroken(self): def setConnectionBroken(self):
"""Called when the connection is broken.""" """Called when the connection is broken."""
@ -285,18 +280,14 @@ class PesterIRC(QtCore.QThread):
def updateIRC(self): def updateIRC(self):
"""Get a silly scrunkler from the generator!!""" """Get a silly scrunkler from the generator!!"""
try: try:
res = next(self.conn) res = next(self._conn)
except socket.timeout as se: except socket.timeout as se:
if self.registeredIRC: if self.registeredIRC:
return True return True
else: else:
raise se raise se
except OSError as se:
raise se
except (OSError, ValueError, IndexError) as se:
raise se
except StopIteration: except StopIteration:
self.conn = self.conn_generator() self._conn = self.conn_generator()
return True return True
else: else:
return res return res
@ -310,7 +301,7 @@ class PesterIRC(QtCore.QThread):
# Metadata # Metadata
for chum in chums: for chum in chums:
try: try:
self.send_irc.metadata(chum.handle, "get", "mood") self._send_irc.metadata(chum.handle, "get", "mood")
except OSError as e: except OSError as e:
PchumLog.warning(e) PchumLog.warning(e)
self.setConnectionBroken() self.setConnectionBroken()
@ -323,7 +314,7 @@ class PesterIRC(QtCore.QThread):
for chum in chums: for chum in chums:
if len(chumglub + chum.handle) >= 350: if len(chumglub + chum.handle) >= 350:
try: try:
self.send_irc.privmsg("#pesterchum", chumglub) self._send_irc.privmsg("#pesterchum", chumglub)
except OSError as e: except OSError as e:
PchumLog.warning(e) PchumLog.warning(e)
self.setConnectionBroken() self.setConnectionBroken()
@ -333,7 +324,7 @@ class PesterIRC(QtCore.QThread):
chumglub += chum.handle chumglub += chum.handle
if chumglub != "GETMOOD ": if chumglub != "GETMOOD ":
try: try:
self.send_irc.privmsg("#pesterchum", chumglub) self._send_irc.privmsg("#pesterchum", chumglub)
except OSError as e: except OSError as e:
PchumLog.warning(e) PchumLog.warning(e)
self.setConnectionBroken() self.setConnectionBroken()
@ -344,7 +335,7 @@ class PesterIRC(QtCore.QThread):
@QtCore.pyqtSlot(str, str) @QtCore.pyqtSlot(str, str)
def sendNotice(self, text, handle): def sendNotice(self, text, handle):
self.send_irc.notice(handle, text) self._send_irc.notice(handle, text)
@QtCore.pyqtSlot(str, str) @QtCore.pyqtSlot(str, str)
def sendMessage(self, text, handle): def sendMessage(self, text, handle):
@ -391,25 +382,25 @@ class PesterIRC(QtCore.QThread):
textl = splittext(textl) textl = splittext(textl)
for t in textl: for t in textl:
self.send_irc.privmsg(handle, t) self._send_irc.privmsg(handle, t)
@QtCore.pyqtSlot(str, str) @QtCore.pyqtSlot(str, str)
def sendCTCP(self, handle, text): def sendCTCP(self, handle, text):
self.send_irc.ctcp(handle, text) self._send_irc.ctcp(handle, text)
@QtCore.pyqtSlot(str, bool) @QtCore.pyqtSlot(str, bool)
def startConvo(self, handle, initiated): def startConvo(self, handle, initiated):
self.send_irc.privmsg(handle, f"COLOR >{self.mainwindow.profile().colorcmd()}") self._send_irc.privmsg(handle, f"COLOR >{self.mainwindow.profile().colorcmd()}")
if initiated: if initiated:
self.send_irc.privmsg(handle, "PESTERCHUM:BEGIN") self._send_irc.privmsg(handle, "PESTERCHUM:BEGIN")
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def endConvo(self, handle): def endConvo(self, handle):
self.send_irc.privmsg(handle, "PESTERCHUM:CEASE") self._send_irc.privmsg(handle, "PESTERCHUM:CEASE")
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def updateProfile(self): def updateProfile(self):
self.send_irc.nick(self.mainwindow.profile().handle) self._send_irc.nick(self.mainwindow.profile().handle)
self.mainwindow.closeConversations(True) self.mainwindow.closeConversations(True)
self.mainwindow.doAutoIdentify() self.mainwindow.doAutoIdentify()
self.mainwindow.autoJoinDone = False self.mainwindow.autoJoinDone = False
@ -418,68 +409,68 @@ class PesterIRC(QtCore.QThread):
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def updateMood(self): def updateMood(self):
mood = str(self.mainwindow.profile().mood.value()) mood = self.mainwindow.profile().mood.value_str()
# Moods via metadata # Moods via metadata
self.send_irc.metadata("*", "set", "mood", mood) self._send_irc.metadata("*", "set", "mood", mood)
# Backwards compatibility # Backwards compatibility
self.send_irc.privmsg("#pesterchum", f"MOOD >{mood}") self._send_irc.privmsg("#pesterchum", f"MOOD >{mood}")
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def updateColor(self): def updateColor(self):
# Update color metadata field # Update color metadata field
color = self.mainwindow.profile().color color = self.mainwindow.profile().color
self.send_irc.metadata("*", "set", "color", str(color.name())) self._send_irc.metadata("*", "set", "color", str(color.name()))
# Send color messages # Send color messages
for convo in list(self.mainwindow.convos.keys()): for convo in list(self.mainwindow.convos.keys()):
self.send_irc.privmsg( self._send_irc.privmsg(
convo, convo,
f"COLOR >{self.mainwindow.profile().colorcmd()}", f"COLOR >{self.mainwindow.profile().colorcmd()}",
) )
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def blockedChum(self, handle): def blockedChum(self, handle):
self.send_irc.privmsg(handle, "PESTERCHUM:BLOCK") self._send_irc.privmsg(handle, "PESTERCHUM:BLOCK")
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def unblockedChum(self, handle): def unblockedChum(self, handle):
self.send_irc.privmsg(handle, "PESTERCHUM:UNBLOCK") self._send_irc.privmsg(handle, "PESTERCHUM:UNBLOCK")
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def requestNames(self, channel): def requestNames(self, channel):
self.send_irc.names(channel) self._send_irc.names(channel)
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def requestChannelList(self): def requestChannelList(self):
self.send_irc.list() self._send_irc.list()
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def joinChannel(self, channel): def joinChannel(self, channel):
self.send_irc.join(channel) self._send_irc.join(channel)
self.send_irc.mode(channel) self._send_irc.mode(channel)
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def leftChannel(self, channel): def leftChannel(self, channel):
self.send_irc.part(channel) self._send_irc.part(channel)
@QtCore.pyqtSlot(str, str, str) @QtCore.pyqtSlot(str, str, str)
def kickUser(self, channel, user, reason=""): def kickUser(self, channel, user, reason=""):
self.send_irc.kick(channel, user, reason) self._send_irc.kick(channel, user, reason)
@QtCore.pyqtSlot(str, str, str) @QtCore.pyqtSlot(str, str, str)
def setChannelMode(self, channel, mode, command): def setChannelMode(self, channel, mode, command):
self.send_irc.mode(channel, mode, command) self._send_irc.mode(channel, mode, command)
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def channelNames(self, channel): def channelNames(self, channel):
self.send_irc.names(channel) self._send_irc.names(channel)
@QtCore.pyqtSlot(str, str) @QtCore.pyqtSlot(str, str)
def inviteChum(self, handle, channel): def inviteChum(self, handle, channel):
self.send_irc.invite(handle, channel) self._send_irc.invite(handle, channel)
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def pingServer(self): def pingServer(self):
self.send_irc.ping("B33") self._send_irc.ping("B33")
@QtCore.pyqtSlot(bool) @QtCore.pyqtSlot(bool)
def setAway(self, away=True): def setAway(self, away=True):
@ -490,18 +481,18 @@ class PesterIRC(QtCore.QThread):
@QtCore.pyqtSlot(str, str) @QtCore.pyqtSlot(str, str)
def killSomeQuirks(self, channel, handle): def killSomeQuirks(self, channel, handle):
self.send_irc.ctcp(channel, "NOQUIRKS", handle) self._send_irc.ctcp(channel, "NOQUIRKS", handle)
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def disconnectIRC(self): def disconnectIRC(self):
self.send_irc.quit(f"{_pcVersion} <3") self._send_irc.quit(f"{_pcVersion} <3")
self._end = True self._end = True
self.close() self._close()
def notice(self, nick, chan, msg): def _notice(self, nick, chan, msg):
"""Standard IRC 'NOTICE' message, primarily used for automated replies from services.""" """Standard IRC 'NOTICE' message, primarily used for automated replies from services."""
handle = nick[0 : nick.find("!")] handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "NOTICE {} :{}"'.format(handle, msg)) PchumLog.info('---> recv "NOTICE %s :%s"', handle, msg)
if ( if (
handle == "ChanServ" handle == "ChanServ"
and chan == self.mainwindow.profile().handle and chan == self.mainwindow.profile().handle
@ -511,23 +502,29 @@ class PesterIRC(QtCore.QThread):
else: else:
self.noticeReceived.emit(handle, msg) self.noticeReceived.emit(handle, msg)
def metadata(self, _target, nick, key, _visibility, value): def _metadata(self, _target, nick, key, _visibility, value):
"""METADATA DRAFT metadata message from server. """METADATA DRAFT metadata message from server.
The format of the METADATA server notication is: The format of the METADATA server notication is:
METADATA <Target> <Key> <Visibility> <Value> METADATA <Target> <Key> <Visibility> <Value>
""" """
if key.casefold() == "mood": if key.casefold() == "mood":
try: if is_valid_mood(value[0]):
mood = Mood(int(value)) mood = Mood(int(value[0]))
else:
PchumLog.warning(
"Mood index '%s' from '%s' is not valid.", value[0], nick
)
mood = Mood(0)
self.moodUpdated.emit(nick, mood) self.moodUpdated.emit(nick, mood)
except ValueError:
PchumLog.warning("Invalid mood value, %s, %s", nick, mood)
elif key.casefold() == "color": elif key.casefold() == "color":
color = QtGui.QColor(value) # Invalid color becomes rgb 0,0,0 if QtGui.QColor.isValidColorName(value):
color = QtGui.QColor.fromString(value)
else:
color = QtGui.QColor(0, 0, 0)
self.colorUpdated.emit(nick, color) self.colorUpdated.emit(nick, color)
def tagmsg(self, prefix, tags, *args): def _tagmsg(self, prefix, tags, *args):
"""IRCv3 'TAGMSG' message/command, contains a tag without a command. """IRCv3 'TAGMSG' message/command, contains a tag without a command.
For reference see: For reference see:
@ -556,115 +553,117 @@ class PesterIRC(QtCore.QThread):
]: ]:
# Process like it's a PESTERCHUM: PRIVMSG # Process like it's a PESTERCHUM: PRIVMSG
msg = "PESTERCHUM:" + value msg = "PESTERCHUM:" + value
self.privmsg(prefix, args[0], msg) self._privmsg(prefix, args[0], msg)
elif value.startswith("COLOR>"): elif value.startswith("COLOR>"):
# Process like it's a COLOR >0,0,0 PRIVMSG # Process like it's a COLOR >0,0,0 PRIVMSG
msg = value.replace(">", " >") msg = value.replace(">", " >")
self.privmsg(prefix, args[0], msg) self._privmsg(prefix, args[0], msg)
elif value.startswith("TIME>"): elif value.startswith("TIME>"):
# Process like it's a PESTERCHUM:TIME> PRIVMSG # Process like it's a PESTERCHUM:TIME> PRIVMSG
msg = "PESTERCHUM:" + value msg = "PESTERCHUM:" + value
self.privmsg(prefix, args[0], msg) self._privmsg(prefix, args[0], msg)
else: else:
# Invalid syntax # Invalid syntax
PchumLog.warning("TAGMSG with invalid syntax.") PchumLog.warning("TAGMSG with invalid syntax.")
def ping(self, _prefix, token): def _ping(self, _prefix, token):
"""'PING' command from server, we respond with PONG and a matching token.""" """'PING' command from server, we respond with PONG and a matching token."""
self.send_irc.pong(token) self._send_irc.pong(token)
def error(self, *params): def _error(self, *params):
"""'ERROR' message from server, the server is terminating our connection.""" """'ERROR' message from server, the server is terminating our connection."""
self.stopIRC = " ".join(params).strip() self.stopIRC = " ".join(params).strip()
self.disconnectIRC() self.disconnectIRC()
def privmsg(self, nick, chan, msg): def __ctcp(self, nick: str, chan: str, msg: str):
"""'PRIVMSG' message from server, the standard message.""" """Client-to-client protocol handling.
Called by _privmsg. CTCP messages are PRIVMSG messages wrapped in '\x01' characters.
"""
msg = msg.strip("\x01") # We already know this is a CTCP message.
handle = nick[0 : nick.find("!")] handle = nick[0 : nick.find("!")]
if not msg: # Length 0
return
# CTCP
# ACTION, IRC /me (The CTCP kind) # ACTION, IRC /me (The CTCP kind)
if msg[0:8] == "\x01ACTION ": if msg.startswith("ACTION "):
msg = "/me" + msg[7:-1] self._privmsg(nick, chan, f"/me {msg[7:]}")
# CTCPs that don't need to be shown # VERSION, return version.
elif msg[0] == "\x01": elif msg.startswith("VERSION"):
PchumLog.info('---> recv "CTCP %s :%s"', handle, msg[1:-1]) self._send_irc.ctcp_reply(handle, "VERSION", f"Pesterchum {_pcVersion}")
# VERSION, return version
if msg[1:-1].startswith("VERSION"):
self.send_irc.ctcp(handle, "VERSION", "Pesterchum {_pcVersion}")
# CLIENTINFO, return supported CTCP commands. # CLIENTINFO, return supported CTCP commands.
elif msg[1:-1].startswith("CLIENTINFO"): elif msg.startswith("CLIENTINFO"):
self.send_irc.ctcp( self._send_irc.ctcp_reply(
handle, handle,
"CLIENTINFO", "CLIENTINFO",
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS GETMOOD", "ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS",
) )
# PING, return pong # PING, return pong.
elif msg[1:-1].startswith("PING"): elif msg.startswith("PING"):
if len(msg[1:-1].split("PING ")) > 1: self._send_irc.ctcp_reply(handle, "PING", msg[4:])
self.send_irc.ctcp(handle, "PING", msg[1:-1].split("PING ")[1]) # SOURCE, return source code link.
else: elif msg.startswith("SOURCE"):
self.send_irc.ctcp(handle, "PING") self._send_irc.ctcp_reply(
# SOURCE, return source
elif msg[1:-1].startswith("SOURCE"):
self.send_irc.ctcp(
handle, handle,
"SOURCE", "SOURCE",
"https://github.com/Dpeta/pesterchum-alt-servers", "https://github.com/Dpeta/pesterchum-alt-servers",
) )
# ??? # ???
elif msg[1:-1].startswith("NOQUIRKS") and chan[0] == "#": elif msg.startswith("NOQUIRKS") and chan[0] == "#":
op = nick[0 : nick.find("!")] op = nick[0 : nick.find("!")]
self.quirkDisable.emit(chan, msg[10:-1], op) self.quirkDisable.emit(chan, msg[9:], op)
# GETMOOD via CTCP
elif msg[1:-1].startswith("GETMOOD"): def _privmsg(self, nick: str, chan: str, msg: str):
# GETMOOD via CTCP """'PRIVMSG' message from server, the standard message."""
# Maybe we can do moods like this in the future... if not msg: # Length 0
mymood = self.mainwindow.profile().mood.value() return
self.send_irc.ctcp(handle, f"MOOD >{mymood}") handle = nick[0 : nick.find("!")]
# Backwards compatibility chan = (
self.send_irc.privmsg(f"#pesterchum", f"MOOD >{mymood}") chan.lower()
) # Channel capitalization not guarenteed, casefold() too aggressive.
# CTCP, indicated by a message wrapped in '\x01' characters.
# Only checking for the first character is recommended by the protocol.
if msg[0] == "\x01":
self.__ctcp(nick, chan, msg)
return return
if chan != "#pesterchum": if chan.startswith("#"):
# We don't need anywhere near that much spam. # PRIVMSG to chnnale
PchumLog.info('---> recv "PRIVMSG %s :%s"', handle, msg)
if chan == "#pesterchum": if chan == "#pesterchum":
# follow instructions # follow instructions
if msg[0:6] == "MOOD >": if msg.startswith("MOOD >"):
try: if is_valid_mood(msg[6:]):
mood = Mood(int(msg[6:])) mood = Mood(int(msg[6:]))
except ValueError: else:
PchumLog.warning(
"Mood index '%s' from '%s' is not valid.", msg[6:], handle
)
mood = Mood(0) mood = Mood(0)
self.moodUpdated.emit(handle, mood) self.moodUpdated.emit(handle, mood)
elif msg[0:7] == "GETMOOD": elif msg.startswith("GETMOOD"):
mychumhandle = self.mainwindow.profile().handle mychumhandle = self.mainwindow.profile().handle
mymood = self.mainwindow.profile().mood.value() if mychumhandle in msg:
if msg.find(mychumhandle, 8) != -1: mymood = self.mainwindow.profile().mood.value_str()
self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood)) self._send_irc.privmsg("#pesterchum", f"MOOD >{mymood}")
elif chan[0] == "#": else:
if msg[0:16] == "PESTERCHUM:TIME>": if msg.startswith("PESTERCHUM:TIME>"):
self.timeCommand.emit(chan, handle, msg[16:]) self.timeCommand.emit(chan, handle, msg[16:])
else: else:
self.memoReceived.emit(chan, handle, msg) self.memoReceived.emit(chan, handle, msg)
else: else:
# Normal PRIVMSG messages (the normal kind!!) # Direct person-to-person PRIVMSG messages
if msg[0:7] == "COLOR >": if msg.startswith("COLOR >"):
if is_valid_rgb_color(msg[7:]):
colors = msg[7:].split(",") colors = msg[7:].split(",")
try:
colors = [int(d) for d in colors] colors = [int(d) for d in colors]
except ValueError as e:
PchumLog.warning(e)
colors = [0, 0, 0]
PchumLog.debug("colors: %s", colors)
color = QtGui.QColor(*colors) color = QtGui.QColor(*colors)
elif QtGui.QColor.isValidColorName(msg[7:]):
color = QtGui.QColor.fromString(msg[7:])
else:
color = QtGui.QColor(0, 0, 0)
self.colorUpdated.emit(handle, color) self.colorUpdated.emit(handle, color)
else: else:
self.messageReceived.emit(handle, msg) self.messageReceived.emit(handle, msg)
def quit(self, nick, reason): def _quit(self, nick, reason):
"""QUIT message from server, a client has quit the server.""" """QUIT message from server, a client has quit the server."""
handle = nick[0 : nick.find("!")] handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "QUIT %s: %s"', handle, reason) PchumLog.info('---> recv "QUIT %s: %s"', handle, reason)
@ -678,13 +677,13 @@ class PesterIRC(QtCore.QThread):
self.userPresentUpdate.emit(handle, "", "quit") self.userPresentUpdate.emit(handle, "", "quit")
self.moodUpdated.emit(handle, Mood("offline")) self.moodUpdated.emit(handle, Mood("offline"))
def kick(self, opnick, channel, handle, reason): def _kick(self, opnick, channel, handle, reason):
"""'KICK' message from server, someone got kicked from a channel.""" """'KICK' message from server, someone got kicked from a channel."""
op = opnick[0 : opnick.find("!")] op = opnick[0 : opnick.find("!")]
self.userPresentUpdate.emit(handle, channel, f"kick:{op}:{reason}") self.userPresentUpdate.emit(handle, channel, f"kick:{op}:{reason}")
# ok i shouldnt be overloading that but am lazy # ok i shouldnt be overloading that but am lazy
def part(self, nick, channel, _reason="nanchos"): def _part(self, nick, channel, _reason="nanchos"):
"""'PART' message from server, someone left a channel.""" """'PART' message from server, someone left a channel."""
handle = nick[0 : nick.find("!")] handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "PART %s: %s"', handle, channel) PchumLog.info('---> recv "PART %s: %s"', handle, channel)
@ -692,7 +691,7 @@ class PesterIRC(QtCore.QThread):
if channel == "#pesterchum": if channel == "#pesterchum":
self.moodUpdated.emit(handle, Mood("offline")) self.moodUpdated.emit(handle, Mood("offline"))
def join(self, nick, channel): def _join(self, nick, channel):
"""'JOIN' message from server, someone joined a channel.""" """'JOIN' message from server, someone joined a channel."""
handle = nick[0 : nick.find("!")] handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "JOIN %s: %s"', handle, channel) PchumLog.info('---> recv "JOIN %s: %s"', handle, channel)
@ -702,7 +701,7 @@ class PesterIRC(QtCore.QThread):
self.mainwindow.randhandler.setRunning(True) self.mainwindow.randhandler.setRunning(True)
self.moodUpdated.emit(handle, Mood("chummy")) self.moodUpdated.emit(handle, Mood("chummy"))
def mode(self, op, channel, mode, *handles): def _mode(self, op, channel, mode, *handles):
"""'MODE' message from server, a user or a channel's mode changed.""" """'MODE' message from server, a user or a channel's mode changed."""
PchumLog.debug( PchumLog.debug(
"mode(op=%s, channel=%s, mode=%s, handles=%s)", op, channel, mode, handles "mode(op=%s, channel=%s, mode=%s, handles=%s)", op, channel, mode, handles
@ -752,14 +751,14 @@ class PesterIRC(QtCore.QThread):
except IndexError as e: except IndexError as e:
PchumLog.exception("modeSetIndexError: %s", e) PchumLog.exception("modeSetIndexError: %s", e)
def invite(self, sender, _you, channel): def _invite(self, sender, _you, channel):
"""'INVITE' message from server, someone invited us to a channel. """'INVITE' message from server, someone invited us to a channel.
Pizza party everyone invited!!!""" Pizza party everyone invited!!!"""
handle = sender.split("!")[0] handle = sender.split("!")[0]
self.inviteReceived.emit(handle, channel) self.inviteReceived.emit(handle, channel)
def nick(self, oldnick, newnick, _hopcount=0): def _nick(self, oldnick, newnick, _hopcount=0):
"""'NICK' message from server, signifies a nick change. """'NICK' message from server, signifies a nick change.
Is send when our or someone else's nick got changed willingly or unwillingly.""" Is send when our or someone else's nick got changed willingly or unwillingly."""
@ -783,44 +782,46 @@ class PesterIRC(QtCore.QThread):
elif newnick == self.mainwindow.randhandler.randNick: elif newnick == self.mainwindow.randhandler.randNick:
self.mainwindow.randhandler.setRunning(True) self.mainwindow.randhandler.setRunning(True)
def welcome(self, _server, _nick, _msg): def _welcome(self, _server, _nick, _msg):
"""Numeric reply 001 RPL_WELCOME, send when we've connected to the server.""" """Numeric reply 001 RPL_WELCOME, send when we've connected to the server."""
self.registeredIRC = True # Registered as in, the server has accepted our nick & user. self.registeredIRC = (
True # Registered as in, the server has accepted our nick & user.
)
self.connected.emit() # Alert main thread that we've connected. self.connected.emit() # Alert main thread that we've connected.
profile = self.mainwindow.profile() profile = self.mainwindow.profile()
if not self.mainwindow.config.lowBandwidth(): if not self.mainwindow.config.lowBandwidth():
# Negotiate capabilities # Negotiate capabilities
self.send_irc.cap("REQ", "message-tags") self._send_irc.cap("REQ", "message-tags")
self.send_irc.cap( self._send_irc.cap(
self, "REQ", "draft/metadata-notify-2" "REQ", "draft/metadata-notify-2"
) # <--- Not required in the unreal5 module implementation ) # <--- Not required in the unreal5 module implementation
self.send_irc.cap( self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this
self, "REQ", "pesterchum-tag" self._send_irc.join("#pesterchum")
) # <--- Currently not using this # Get mood
self.send_irc.join("#pesterchum") mood = profile.mood.value_str()
# Moods via metadata # Moods via metadata
self.send_irc.metadata("*", "sub", "mood") self._send_irc.metadata("*", "sub", "mood")
self.send_irc.metadata("*", "set", "mood", str(profile.mood.value)) self._send_irc.metadata("*", "set", "mood", mood)
# Color via metadata # Color via metadata
self.send_irc.metadata("*", "sub", "color") self._send_irc.metadata("*", "sub", "color")
self.send_irc.metadata("*", "set", "color", profile.color.name()) self._send_irc.metadata("*", "set", "color", profile.color.name())
# Backwards compatible moods # Backwards compatible moods
self.send_irc.privmsg("#pesterchum", f"MOOD >{profile.mymood}") self._send_irc.privmsg("#pesterchum", f"MOOD >{mood}")
def featurelist(self, _target, _handle, *params): def _featurelist(self, _target, _handle, *params):
"""Numerical reply 005 RPL_ISUPPORT to communicate supported server features. """Numerical reply 005 RPL_ISUPPORT to communicate supported server features.
Not in the original specification. Not in the original specification.
Metadata support could be confirmed via CAP ACK/CAP NEK. Metadata support could be confirmed via CAP ACK/CAP NEK.
""" """
features = params[:-1] features = params[:-1]
PchumLog.info("Server featurelist: %s", features) PchumLog.info("Server _featurelist: %s", features)
for feature in features: for feature in features:
if feature.casefold().startswith("metadata"): if feature.casefold().startswith("metadata"):
PchumLog.info("Server supports metadata.") PchumLog.info("Server supports metadata.")
self.metadata_supported = True self.metadata_supported = True
def cap(self, server, nick, subcommand, tag): def _cap(self, server, nick, subcommand, tag):
"""IRCv3 capabilities command from server. """IRCv3 capabilities command from server.
See: https://ircv3.net/specs/extensions/capability-negotiation See: https://ircv3.net/specs/extensions/capability-negotiation
@ -829,18 +830,18 @@ class PesterIRC(QtCore.QThread):
# if tag == "message-tags": # if tag == "message-tags":
# if subcommand == "ACK": # if subcommand == "ACK":
def umodeis(self, _server, _handle, modes): def _umodeis(self, _server, _handle, modes):
"""Numeric reply 221 RPL_UMODEIS, shows us our user modes.""" """Numeric reply 221 RPL_UMODEIS, shows us our user modes."""
self.mainwindow.modes = modes self.mainwindow.modes = modes
def liststart(self, _server, _handle, *info): def _liststart(self, _server, _handle, *info):
"""Numeric reply 321 RPL_LISTSTART, start of list of channels.""" """Numeric reply 321 RPL_LISTSTART, start of list of channels."""
self.channel_list = [] self.channel_list = []
info = list(info) info = list(info)
self.channel_field = info.index("Channel") # dunno if this is protocol self.channel_field = info.index("Channel") # dunno if this is protocol
PchumLog.info('---> recv "CHANNELS: %s ', self.channel_field) PchumLog.info('---> recv "CHANNELS: %s ', self.channel_field)
def list(self, _server, _handle, *info): def _list(self, _server, _handle, *info):
"""Numeric reply 322 RPL_LIST, returns part of the list of channels.""" """Numeric reply 322 RPL_LIST, returns part of the list of channels."""
channel = info[self.channel_field] channel = info[self.channel_field]
usercount = info[1] usercount = info[1]
@ -848,17 +849,17 @@ class PesterIRC(QtCore.QThread):
self.channel_list.append((channel, usercount)) self.channel_list.append((channel, usercount))
PchumLog.info('---> recv "CHANNELS: %s ', channel) PchumLog.info('---> recv "CHANNELS: %s ', channel)
def listend(self, _server, _handle, _msg): def _listend(self, _server, _handle, _msg):
"""Numeric reply 323 RPL_LISTEND, end of a series of LIST replies.""" """Numeric reply 323 RPL_LISTEND, end of a series of LIST replies."""
PchumLog.info('---> recv "CHANNELS END"') PchumLog.info('---> recv "CHANNELS END"')
self.channelListReceived.emit(PesterList(self.channel_list)) self.channelListReceived.emit(PesterList(self.channel_list))
self.channel_list = [] self.channel_list = []
def channelmodeis(self, _server, _handle, channel, modes, _mode_params=""): def _channelmodeis(self, _server, _handle, channel, modes, _mode_params=""):
"""Numeric reply 324 RPL_CHANNELMODEIS, gives channel modes.""" """Numeric reply 324 RPL_CHANNELMODEIS, gives channel modes."""
self.modesUpdated.emit(channel, modes) self.modesUpdated.emit(channel, modes)
def namreply(self, _server, _nick, _op, channel, names): def _namreply(self, _server, _nick, _op, channel, names):
"""Numeric reply 353 RPL_NAMREPLY, part of a NAMES list of members, usually of a channel.""" """Numeric reply 353 RPL_NAMREPLY, part of a NAMES list of members, usually of a channel."""
namelist = names.split(" ") namelist = names.split(" ")
PchumLog.info('---> recv "NAMES %s: %s names"', channel, len(namelist)) PchumLog.info('---> recv "NAMES %s: %s names"', channel, len(namelist))
@ -868,7 +869,7 @@ class PesterIRC(QtCore.QThread):
self.channelnames[channel] = [] self.channelnames[channel] = []
self.channelnames[channel].extend(namelist) self.channelnames[channel].extend(namelist)
def endofnames(self, _server, _nick, channel, _msg): def _endofnames(self, _server, _nick, channel, _msg):
"""Numeric reply 366 RPL_ENDOFNAMES, end of NAMES list of members, usually of a channel.""" """Numeric reply 366 RPL_ENDOFNAMES, end of NAMES list of members, usually of a channel."""
try: try:
namelist = self.channelnames[channel] namelist = self.channelnames[channel]
@ -892,22 +893,22 @@ class PesterIRC(QtCore.QThread):
lesschums.append(chum) lesschums.append(chum)
self.getMood(*lesschums) self.getMood(*lesschums)
def cannotsendtochan(self, _server, _handle, channel, msg): def _cannotsendtochan(self, _server, _handle, channel, msg):
"""Numeric reply 404 ERR_CANNOTSENDTOCHAN, we aren't in the channel or don't have voice.""" """Numeric reply 404 ERR_CANNOTSENDTOCHAN, we aren't in the channel or don't have voice."""
self.cannotSendToChan.emit(channel, msg) self.cannotSendToChan.emit(channel, msg)
def erroneusnickname(self, *args): def _erroneusnickname(self, *args):
"""Numeric reply 432 ERR_ERRONEUSNICKNAME, we have a forbidden or protocol-breaking nick.""" """Numeric reply 432 ERR_ERRONEUSNICKNAME, we have a forbidden or protocol-breaking nick."""
# Server is not allowing us to connect. # Server is not allowing us to connect.
reason = "Handle is not allowed on this server.\n" + " ".join(args) reason = "Handle is not allowed on this server.\n" + " ".join(args)
self.stopIRC = reason.strip() self.stopIRC = reason.strip()
self.disconnectIRC() self.disconnectIRC()
def nicknameinuse(self, _server, _cmd, nick, _msg): def _nicknameinuse(self, _server, _cmd, nick, _msg):
"""Numerical reply 433 ERR_NICKNAMEINUSE, raised when changing nick to nick in use.""" """Numerical reply 433 ERR_NICKNAMEINUSE, raised when changing nick to nick in use."""
self._reset_nick(nick) self._reset_nick(nick)
def nickcollision(self, _server, _cmd, nick, _msg): def _nickcollision(self, _server, _cmd, nick, _msg):
"""Numerical reply 436 ERR_NICKCOLLISION, raised during connect when nick is in use.""" """Numerical reply 436 ERR_NICKCOLLISION, raised during connect when nick is in use."""
self._reset_nick(nick) self._reset_nick(nick)
@ -917,55 +918,61 @@ class PesterIRC(QtCore.QThread):
random.random() * 9999 random.random() * 9999
) # Random int in range 1000 <---> 9999 ) # Random int in range 1000 <---> 9999
newnick = f"pesterClient{random_number}" newnick = f"pesterClient{random_number}"
self.send_irc.nick(newnick) self._send_irc.nick(newnick)
self.nickCollision.emit(oldnick, newnick) self.nickCollision.emit(oldnick, newnick)
def forbiddenchannel(self, _server, handle, channel, msg): def _forbiddenchannel(self, _server, handle, channel, msg):
"""Numeric reply 448 'forbiddenchannel' reply, channel is forbidden. """Numeric reply 448 'forbiddenchannel' reply, channel is forbidden.
Not in the specification but used by UnrealIRCd.""" Not in the specification but used by UnrealIRCd."""
self.signal_forbiddenchannel.emit(channel, msg) self.signal_forbiddenchannel.emit(channel, msg)
self.userPresentUpdate.emit(handle, channel, "left") self.userPresentUpdate.emit(handle, channel, "left")
def inviteonlychan(self, _server, _handle, channel, _msg): def _inviteonlychan(self, _server, _handle, channel, _msg):
"""Numeric reply 473 ERR_INVITEONLYCHAN, can't join channel (+i).""" """Numeric reply 473 ERR_INVITEONLYCHAN, can't join channel (+i)."""
self.chanInviteOnly.emit(channel) self.chanInviteOnly.emit(channel)
def keyvalue(self, _target, _handle_us, handle_owner, key, _visibility, *value): def _keyvalue(self, _target, _handle_us, handle_owner, key, _visibility, *value):
"""METADATA DRAFT numeric reply 761 RPL_KEYVALUE, we received the value of a key. """METADATA DRAFT numeric reply 761 RPL_KEYVALUE, we received the value of a key.
The format of the METADATA server notication is: The format of the METADATA server notication is:
METADATA <Target> <Key> <Visibility> <Value> METADATA <Target> <Key> <Visibility> <Value>
""" """
if key == "mood": if key.casefold() == "mood":
if is_valid_mood(value[0]):
mood = Mood(int(value[0])) mood = Mood(int(value[0]))
else:
PchumLog.warning(
"Mood index '%s' from '%s' is not valid.", value[0], handle_owner
)
mood = Mood(0)
self.moodUpdated.emit(handle_owner, mood) self.moodUpdated.emit(handle_owner, mood)
def nomatchingkey(self, _target, _our_handle, failed_handle, _key, *_error): def _nomatchingkey(self, _target, _our_handle, failed_handle, _key, *_error):
"""METADATA DRAFT numeric reply 766 ERR_NOMATCHINGKEY, no matching key.""" """METADATA DRAFT numeric reply 766 ERR_NOMATCHINGKEY, no matching key."""
PchumLog.info("nomatchingkey: %s", failed_handle) PchumLog.info("_nomatchingkey: %s", failed_handle)
# No point in GETMOOD-ing services # No point in GETMOOD-ing services
# Fallback to the normal GETMOOD method if getting mood via metadata fails. # Fallback to the normal GETMOOD method if getting mood via metadata fails.
if failed_handle.casefold() not in SERVICES: if failed_handle.casefold() not in SERVICES:
self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
def keynotset(self, _target, _our_handle, failed_handle, _key, *_error): def _keynotset(self, _target, _our_handle, failed_handle, _key, *_error):
"""METADATA DRAFT numeric reply 768 ERR_KEYNOTSET, key isn't set.""" """METADATA DRAFT numeric reply 768 ERR_KEYNOTSET, key isn't set."""
PchumLog.info("nomatchingkey: %s", failed_handle) PchumLog.info("_keynotset: %s", failed_handle)
# Fallback to the normal GETMOOD method if getting mood via metadata fails. # Fallback to the normal GETMOOD method if getting mood via metadata fails.
if failed_handle.casefold() not in SERVICES: if failed_handle.casefold() not in SERVICES:
self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
def keynopermission(self, _target, _our_handle, failed_handle, _key, *_error): def _keynopermission(self, _target, _our_handle, failed_handle, _key, *_error):
"""METADATA DRAFT numeric reply 769 ERR_KEYNOPERMISSION, no permission for key.""" """METADATA DRAFT numeric reply 769 ERR_KEYNOPERMISSION, no permission for key."""
PchumLog.info("nomatchingkey: %s", failed_handle) PchumLog.info("_keynopermission: %s", failed_handle)
# Fallback to the normal GETMOOD method if getting mood via metadata fails. # Fallback to the normal GETMOOD method if getting mood via metadata fails.
if failed_handle.casefold() not in SERVICES: if failed_handle.casefold() not in SERVICES:
self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
def metadatasubok(self, *params): def _metadatasubok(self, *params):
""" "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key.""" """ "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key."""
PchumLog.info("metadatasubok: %s", params) PchumLog.info("_metadatasubok: %s", params)
moodUpdated = QtCore.pyqtSignal("QString", Mood) moodUpdated = QtCore.pyqtSignal("QString", Mood)
colorUpdated = QtCore.pyqtSignal("QString", QtGui.QColor) colorUpdated = QtCore.pyqtSignal("QString", QtGui.QColor)

View file

@ -61,11 +61,15 @@ class Mood:
} }
def __init__(self, mood): def __init__(self, mood):
if type(mood) is int: if isinstance(mood, int):
self.mood = mood self.mood = mood
else: else:
self.mood = self.moods.index(mood) self.mood = self.moods.index(mood)
def value_str(self):
"""Return mood index as str."""
return str(self.mood)
def value(self): def value(self):
return self.mood return self.mood

View file

@ -4320,7 +4320,12 @@ class MainProgram(QtCore.QObject):
self.attempts = 0 self.attempts = 0
# but it's at least better than the way it was before. # but it's at least better than the way it was before.
self.irc = PesterIRC(self.widget.config, self.widget) self.irc = PesterIRC(
self.widget,
self.widget.config.server(),
self.widget.config.port(),
self.widget.config.ssl(),
)
self.connectWidgets(self.irc, self.widget) self.connectWidgets(self.irc, self.widget)
self.widget.passIRC( self.widget.passIRC(

View file

@ -0,0 +1,71 @@
"""Provides functions for validating input from the server and other clients."""
# import re
# _color_rgb = re.compile(r"^\d{1,3},\d{1,3},\d{1,3}$")
# _color_hex = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
def is_valid_mood(value: str):
"""Returns True if an unparsed value (str) is a valid mood index."""
if value in [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
]:
return True
return False
def is_valid_rgb_color(value: str):
"""Returns True if an unparsed value (str) is a valid rgb color as "r,g,b".
Yeah you could do this via re but this is faster."""
if not isinstance(value, str):
return False
if 4 > len(value) > 11:
return False
components = value.split(",")
if len(components) != 3:
return False
for component in components:
if not component.isnumeric():
return False
if int(component) > 255:
return False
return True
"""
def is_valid_rgb_color(value: str):
"Returns True if an unparsed value (str) is a valid rgb color."
if re.search(_color_rgb, value):
return True
return False
def is_valid_hex_color(value: str):
"Returns True if an unparsed value (str) is a valid hex color."
if re.search(_color_hex, value):
return True
return False
"""

View file

@ -90,6 +90,13 @@ class SendIRC:
).strip() # Extra spaces break protocol, so strip. ).strip() # Extra spaces break protocol, so strip.
self.privmsg(target, f"\x01{outgoing_ctcp}\x01") self.privmsg(target, f"\x01{outgoing_ctcp}\x01")
def ctcp_reply(self, target, command, msg=""):
"""Send Client-to-Client Protocol reply message, responding to a CTCP message."""
outgoing_ctcp = " ".join(
[command, msg]
).strip() # Extra spaces break protocol, so strip.
self.notice(target, f"\x01{outgoing_ctcp}\x01")
def metadata(self, target, subcommand, *params): def metadata(self, target, subcommand, *params):
"""Send Metadata command to get or set metadata. """Send Metadata command to get or set metadata.