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:
parent
fbe8f48d63
commit
1d4d1dbab6
5 changed files with 389 additions and 295 deletions
551
irc.py
551
irc.py
|
@ -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)
|
||||||
|
|
6
mood.py
6
mood.py
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
71
scripts/input_validation.py
Normal file
71
scripts/input_validation.py
Normal 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
|
||||||
|
"""
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue