pesterchum/irc.py

1197 lines
44 KiB
Python
Raw Normal View History

import logging
2011-02-15 12:10:57 -05:00
import socket
import random
import time
import ssl
2023-01-31 19:12:43 -05:00
import sys
import select
import datetime
import traceback
2022-08-19 07:12:58 -04:00
try:
from PyQt6 import QtCore, QtGui
except ImportError:
print("PyQt5 fallback (irc.py)")
from PyQt5 import QtCore, QtGui
from mood import Mood
from dataobjs import PesterProfile
from generic import PesterList
2011-05-26 03:40:30 -04:00
from version import _pcVersion
2023-01-31 19:12:43 -05:00
from oyoyo import services
2023-02-03 14:39:16 -05:00
from oyoyo.ircevents import numeric_events
2023-01-31 19:12:43 -05:00
import scripts.irc.outgoing
PchumLog = logging.getLogger("pchumLogger")
SERVICES = [
"nickserv",
"chanserv",
"memoserv",
"operserv",
"helpserv",
"hostserv",
"botserv",
]
2023-01-31 19:12:43 -05:00
class CommandError(Exception):
def __init__(self, cmd):
self.cmd = cmd
class NoSuchCommandError(CommandError):
def __str__(self):
return 'No such command "%s"' % ".".join(self.cmd)
class ProtectedCommandError(CommandError):
def __str__(self):
return 'Command "%s" is protected' % ".".join(self.cmd)
# Python 3
QString = str
2021-03-23 17:36:43 -04:00
2023-01-31 19:12:43 -05:00
try:
import certifi
except ImportError:
if sys.platform == "darwin":
# Certifi is required to validate certificates on MacOS with pyinstaller builds.
PchumLog.warning(
"Failed to import certifi, which is recommended on MacOS. "
"Pesterchum might not be able to validate certificates unless "
"Python's root certs are installed."
)
else:
PchumLog.info(
"Failed to import certifi, Pesterchum will not be able to validate "
"certificates if the system-provided root certificates are invalid."
)
class IRCClientError(Exception):
pass
2011-02-19 18:06:54 -05:00
class PesterIRC(QtCore.QThread):
def __init__(self, config, window, verify_hostname=True):
2011-02-19 18:06:54 -05:00
QtCore.QThread.__init__(self)
self.mainwindow = window
2011-02-13 21:01:58 -05:00
self.config = config
2022-08-14 14:44:28 -04:00
self.unresponsive = False
2011-02-21 14:07:59 -05:00
self.registeredIRC = False
self.verify_hostname = verify_hostname
self.metadata_supported = False
2011-02-21 14:07:59 -05:00
self.stopIRC = None
2011-09-28 19:16:01 -04:00
self.NickServ = services.NickServ()
self.ChanServ = services.ChanServ()
2023-01-31 19:12:43 -05:00
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.host = self.config.server()
self.port = self.config.port()
self.ssl = self.config.ssl()
self.timeout = 120
self.blocking = True
self._end = False
2023-02-03 16:46:48 -05:00
2023-02-03 14:39:16 -05:00
self.send_irc = scripts.irc.outgoing.SendIRC()
2023-01-31 19:12:43 -05:00
def get_ssl_context(self):
"""Returns an SSL context for connecting over SSL/TLS.
Loads the certifi root certificate bundle if the certifi module is less
than a year old or if the system certificate store is empty.
The cert store on Windows also seems to have issues, so it's better
to use the certifi provided bundle assuming it's a recent version.
On MacOS the system cert store is usually empty, as Python does not use
the system provided ones, instead relying on a bundle installed with the
python installer."""
default_context = ssl.create_default_context()
if "certifi" not in sys.modules:
return default_context
# Get age of certifi module
certifi_date = datetime.datetime.strptime(certifi.__version__, "%Y.%m.%d")
current_date = datetime.datetime.now()
certifi_age = current_date - certifi_date
empty_cert_store = (
list(default_context.cert_store_stats().values()).count(0) == 3
)
2023-01-31 19:12:43 -05:00
# 31557600 seconds is approximately 1 year
if empty_cert_store or certifi_age.total_seconds() <= 31557600:
PchumLog.info(
"Using SSL/TLS context with certifi-provided root certificates."
)
return ssl.create_default_context(cafile=certifi.where())
PchumLog.info("Using SSL/TLS context with system-provided root certificates.")
return default_context
def connect(self, verify_hostname=True):
"""initiates the connection to the server set in self.host:self.port
self.ssl decides whether the connection uses ssl.
Certificate validation when using SSL/TLS may be disabled by
passing the 'verify_hostname' parameter. The user is asked if they
want to disable it if this functions raises a certificate validation error,
in which case the function may be called again with 'verify_hostname'."""
PchumLog.info("connecting to {}:{}".format(self.host, self.port))
# Open connection
plaintext_socket = socket.create_connection((self.host, self.port))
if self.ssl:
# Upgrade connection to use SSL/TLS if enabled
context = self.get_ssl_context()
context.check_hostname = verify_hostname
self.socket = context.wrap_socket(
plaintext_socket, server_hostname=self.host
)
else:
# SSL/TLS is disabled, connection is plaintext
self.socket = plaintext_socket
self.send_irc.socket = self.socket
# setblocking is a shorthand for timeout,
# we shouldn't use both.
if self.timeout:
self.socket.settimeout(self.timeout)
elif not self.blocking:
self.socket.setblocking(False)
elif self.blocking:
self.socket.setblocking(True)
2023-02-03 14:39:16 -05:00
self.send_irc.nick(self.mainwindow.profile().handle)
2023-01-31 19:12:43 -05:00
self.send_irc.user("pcc31", "pcc31")
# if self.connect_cb:
# self.connect_cb(self)
2023-02-03 14:39:16 -05:00
def conn_generator(self):
2023-01-31 19:12:43 -05:00
"""returns a generator object."""
try:
2023-01-31 19:12:43 -05:00
buffer = b""
while not self._end:
2023-02-03 15:23:43 -05:00
data = []
2023-01-31 19:12:43 -05:00
try:
2023-02-03 14:39:16 -05:00
buffer += self.socket.recv(1024)
2023-01-31 19:12:43 -05:00
except OSError as e:
PchumLog.warning("conn exception {} in {}".format(e, self))
if self._end:
break
2023-02-03 14:39:16 -05:00
raise e
2023-01-31 19:12:43 -05:00
else:
if self._end:
break
2023-02-03 14:39:16 -05:00
if not buffer and self.blocking:
2023-01-31 19:12:43 -05:00
PchumLog.debug("len(buffer) = 0")
raise OSError("Connection closed")
2023-02-03 14:39:16 -05:00
data, buffer = self.parse_buffer(buffer)
2023-02-03 15:23:43 -05:00
print(f"data: {data}")
2023-02-03 14:39:16 -05:00
for line in data:
tags, prefix, command, args = self.parse_irc_line(line)
2023-01-31 19:12:43 -05:00
# print(tags, prefix, command, args)
try:
# Only need tags with tagmsg
2023-02-03 14:39:16 -05:00
if command.casefold() == "tagmsg":
2023-01-31 19:12:43 -05:00
self.run_command(command, prefix, tags, *args)
else:
self.run_command(command, prefix, *args)
except CommandError as e:
2023-02-03 14:39:16 -05:00
PchumLog.warning(f"CommandError: {e}")
2023-01-31 19:12:43 -05:00
yield True
except socket.timeout as se:
PchumLog.debug("passing timeout")
raise se
except (OSError, ssl.SSLEOFError) as se:
PchumLog.debug("problem: %s" % (str(se)))
if self.socket:
PchumLog.info("error: closing socket")
self.socket.close()
raise se
except Exception as e:
2023-02-03 14:39:16 -05:00
PchumLog.exception("Non-socket related exception in conn_generator().")
2023-01-31 19:12:43 -05:00
raise e
else:
PchumLog.debug("ending while, end is %s" % self._end)
if self.socket:
PchumLog.info("finished: closing socket")
self.socket.close()
yield False
2023-02-03 14:39:16 -05:00
def parse_buffer(self, buffer):
"""Parse lines from bytes buffer, returns lines and emptied buffer."""
try:
decoded_buffer = buffer.decode(encoding="utf-8")
except UnicodeDecodeError as exception:
PchumLog.warning(f"Failed to decode with utf-8, falling back to latin-1.")
try:
decoded_buffer = buffer.decode(encoding="latin-1")
except ValueError as exception:
PchumLog.warning("latin-1 failed too xd")
return "", buffer # throw it back in the cooker
data = decoded_buffer.split("\r\n")
if data[-1]:
# Last entry has incomplete data, add back to buffer
2023-02-03 15:23:43 -05:00
buffer = data[-1].encode(encoding="utf-8")
2023-02-03 14:39:16 -05:00
return data[:-1], buffer
def parse_irc_line(self, line: str):
parts = line.split(" ")
tags = None
prefix = None
if parts[0].startswith(":"):
prefix = parts[0][1:]
command = parts[1]
args = parts[2:]
elif parts[0].startswith("@"):
tags = parts[0] # IRCv3 message tag
prefix = parts[1][1:]
command = parts[2]
args = parts[3:]
else:
command = parts[0]
args = parts[1:]
if command.isdigit():
try:
command = numeric_events[command]
except KeyError:
2023-02-03 15:23:43 -05:00
PchumLog.info("Server send unknown numeric event %s.", command)
2023-02-03 14:39:16 -05:00
command = command.lower()
# If ':' is present the subsequent args are one parameter.
fused_args = []
for idx, arg in enumerate(args):
if arg.startswith(":"):
final_param = ' '.join(args[idx:])
fused_args.append(final_param[1:])
break
else:
fused_args.append(arg)
return (tags, prefix, command, fused_args)
2023-01-31 19:12:43 -05:00
def close(self):
# with extreme prejudice
if self.socket:
PchumLog.info("shutdown socket")
# print("shutdown socket")
self._end = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
except OSError as e:
PchumLog.debug(
"Error while shutting down socket, already broken? %s" % str(e)
)
try:
self.socket.close()
except OSError as e:
PchumLog.debug(
"Error while closing socket, already broken? %s" % str(e)
)
def IRCConnect(self):
try:
self.connect(self.verify_hostname)
except ssl.SSLCertVerificationError as e:
# Ask if users wants to connect anyway
self.askToConnect.emit(e)
raise e
2023-02-03 14:39:16 -05:00
self.conn = self.conn_generator()
2011-02-19 18:06:54 -05:00
def run(self):
2011-02-21 14:07:59 -05:00
try:
self.IRCConnect()
2022-06-12 22:03:39 -04:00
except OSError as se:
2011-02-21 14:07:59 -05:00
self.stopIRC = se
return
2022-06-12 22:03:39 -04:00
while True:
2011-02-21 14:07:59 -05:00
res = True
2011-02-19 18:06:54 -05:00
try:
PchumLog.debug("updateIRC()")
2022-08-14 14:44:28 -04:00
self.mainwindow.sincerecv = 0
2011-02-19 21:38:06 -05:00
res = self.updateIRC()
2021-03-23 17:36:43 -04:00
except socket.timeout as se:
PchumLog.debug("timeout in thread %s" % (self))
2023-01-31 19:12:43 -05:00
self.close()
self.stopIRC = "{}, {}".format(type(se), se)
2011-02-19 21:38:06 -05:00
return
2022-06-16 14:25:24 -04:00
except (OSError, IndexError, ValueError) as se:
self.stopIRC = "{}, {}".format(type(se), se)
PchumLog.debug("socket error, exiting thread")
2011-02-19 18:06:54 -05:00
return
2011-02-19 21:38:06 -05:00
else:
if not res:
PchumLog.debug("false Yield: %s, returning" % res)
2011-02-19 21:38:06 -05:00
return
2011-02-18 03:17:13 -05:00
def setConnected(self):
2011-02-18 21:02:54 -05:00
self.registeredIRC = True
2011-02-18 03:17:13 -05:00
self.connected.emit()
2011-02-19 18:06:54 -05:00
def setConnectionBroken(self):
2022-03-18 05:55:01 -04:00
PchumLog.critical("setconnection broken")
self.disconnectIRC()
# self.brokenConnection = True # Unused
2011-02-19 18:06:54 -05:00
@QtCore.pyqtSlot()
def updateIRC(self):
try:
2021-03-23 17:36:43 -04:00
res = next(self.conn)
except socket.timeout as se:
2011-02-19 18:06:54 -05:00
if self.registeredIRC:
2011-02-19 21:38:06 -05:00
return True
2011-02-19 18:06:54 -05:00
else:
raise se
except OSError as se:
2011-02-19 18:06:54 -05:00
raise se
except (OSError, ValueError, IndexError) as se:
2022-06-12 22:03:39 -04:00
raise se
2011-02-19 18:06:54 -05:00
except StopIteration:
2023-02-03 14:39:16 -05:00
self.conn = self.conn_generator()
2011-02-19 21:38:06 -05:00
return True
else:
return res
2011-02-19 18:06:54 -05:00
@QtCore.pyqtSlot(PesterProfile)
def getMood(self, *chums):
2023-02-03 14:39:16 -05:00
"""Get mood via metadata if supported"""
# Get via metadata or via legacy method
2023-02-03 16:46:48 -05:00
if self.metadata_supported:
2023-02-03 14:39:16 -05:00
# Metadata
for chum in chums:
try:
self.send_irc.metadata(chum.handle, "get", "mood")
except OSError as e:
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
2023-02-03 14:39:16 -05:00
else:
# Legacy
PchumLog.warning(
"Server doesn't seem to support metadata, using legacy GETMOOD."
)
chumglub = "GETMOOD "
for chum in chums:
if len(chumglub + chum.handle) >= 350:
try:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", chumglub)
2023-02-03 14:39:16 -05:00
except OSError as e:
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
2023-02-03 14:39:16 -05:00
chumglub = "GETMOOD "
# No point in GETMOOD-ing services
if chum.handle.casefold() not in SERVICES:
chumglub += chum.handle
if chumglub != "GETMOOD ":
try:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", chumglub)
2023-02-03 14:39:16 -05:00
except OSError as e:
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
@QtCore.pyqtSlot(PesterList)
def getMoods(self, chums):
2023-02-03 16:46:48 -05:00
self.getMood(*chums)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, QString)
def sendNotice(self, text, handle):
2023-02-03 16:46:48 -05:00
self.send_irc.notice(handle, text)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, QString)
def sendMessage(self, text, handle):
2023-02-03 16:46:48 -05:00
h = str(handle)
textl = [str(text)]
def splittext(l):
if len(l[0]) > 450:
space = l[0].rfind(" ", 0, 430)
if space == -1:
space = 450
elif l[0][space + 1 : space + 5] == "</c>":
space = space + 4
a = l[0][0 : space + 1]
b = l[0][space + 1 :]
if a.count("<c") > a.count("</c>"):
# oh god ctags will break!! D=
hanging = []
usedends = []
c = a.rfind("<c")
while c != -1:
d = a.find("</c>", c)
while d in usedends:
d = a.find("</c>", d + 1)
if d != -1:
usedends.append(d)
else:
f = a.find(">", c) + 1
hanging.append(a[c:f])
c = a.rfind("<c", 0, c)
# end all ctags in first part
for _ in range(a.count("<c") - a.count("</c>")):
a = a + "</c>"
# start them up again in the second part
for c in hanging:
b = c + b
if len(b) > 0:
return [a] + splittext([b])
2011-02-13 20:32:02 -05:00
else:
2023-02-03 16:46:48 -05:00
return [a]
else:
return l
2023-02-03 16:46:48 -05:00
textl = splittext(textl)
try:
for t in textl:
self.send_irc.privmsg(h, t)
except OSError as e:
PchumLog.warning(e)
self.setConnectionBroken()
@QtCore.pyqtSlot(
QString,
QString,
)
2022-06-02 17:15:45 -04:00
def sendCTCP(self, handle, text):
2023-02-03 16:46:48 -05:00
self.send_irc.ctcp(handle, text)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, bool)
def startConvo(self, handle, initiated):
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg(handle, "COLOR >%s" % (self.mainwindow.profile().colorcmd()))
if initiated:
self.send_irc.privmsg(handle, "PESTERCHUM:BEGIN")
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def endConvo(self, handle):
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg(handle, "PESTERCHUM:CEASE")
@QtCore.pyqtSlot()
def updateProfile(self):
2023-02-03 16:46:48 -05:00
me = self.mainwindow.profile()
handle = me.handle
self.send_irc.nick(handle)
self.mainwindow.closeConversations(True)
self.mainwindow.doAutoIdentify()
self.mainwindow.autoJoinDone = False
self.mainwindow.doAutoJoins()
self.updateMood()
@QtCore.pyqtSlot()
def updateMood(self):
2023-02-03 16:46:48 -05:00
me = self.mainwindow.profile()
# Moods via metadata
self.send_irc.metadata("*", "set", "mood", str(me.mood.value()))
# Backwards compatibility
self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (me.mood.value()))
@QtCore.pyqtSlot()
def updateColor(self):
2023-02-03 16:46:48 -05:00
# Update color metadata field
color = self.mainwindow.profile().color
self.send_irc.metadata("*", "set", "color", str(color.name()))
# Send color messages
for h in list(self.mainwindow.convos.keys()):
self.send_irc.privmsg(
h,
"COLOR >%s" % (self.mainwindow.profile().colorcmd()),
)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def blockedChum(self, handle):
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg(handle, "PESTERCHUM:BLOCK")
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def unblockedChum(self, handle):
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg(handle, "PESTERCHUM:UNBLOCK")
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def requestNames(self, channel):
2023-02-03 16:46:48 -05:00
self.send_irc.names(channel)
@QtCore.pyqtSlot()
def requestChannelList(self):
2023-02-03 16:46:48 -05:00
self.send_irc.list()
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def joinChannel(self, channel):
2023-02-03 16:46:48 -05:00
self.send_irc.join(channel)
self.send_irc.mode(channel)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def leftChannel(self, channel):
2023-02-03 16:46:48 -05:00
self.send_irc.part(channel)
self.joined = False
2023-02-03 16:46:48 -05:00
@QtCore.pyqtSlot(QString, QString, QString)
def kickUser(self, channel, user, reason=""):
self.send_irc.kick(channel, user, reason)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, QString, QString)
2011-02-06 19:50:21 -05:00
def setChannelMode(self, channel, mode, command):
2023-01-31 19:12:43 -05:00
self.send_irc.mode(channel, mode, command)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString)
def channelNames(self, channel):
2023-02-03 16:46:48 -05:00
self.send_irc.names(channel)
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, QString)
def inviteChum(self, handle, channel):
2023-02-03 16:46:48 -05:00
self.send_irc.invite(handle, channel)
@QtCore.pyqtSlot()
def pingServer(self):
2023-02-03 16:46:48 -05:00
self.send_irc.ping("B33")
2011-06-28 19:26:13 -04:00
@QtCore.pyqtSlot(bool)
def setAway(self, away=True):
2023-02-03 16:46:48 -05:00
if away:
self.away("Idle")
else:
self.away()
2011-06-28 19:26:13 -04:00
2021-03-23 17:36:43 -04:00
@QtCore.pyqtSlot(QString, QString)
2011-06-29 13:19:22 -04:00
def killSomeQuirks(self, channel, handle):
2023-02-03 16:46:48 -05:00
self.send_irc.ctcp(channel, "NOQUIRKS", handle)
@QtCore.pyqtSlot()
def disconnectIRC(self):
2023-02-03 16:46:48 -05:00
self.send_irc.quit(f"{_pcVersion} <3")
self._end = True
self.close()
def notice(self, nick, chan, msg):
handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "NOTICE {} :{}"'.format(handle, msg))
if (
handle == "ChanServ"
2023-02-03 16:46:48 -05:00
and chan == self.mainwindow.profile().handle
and msg[0:2] == "[#"
):
2023-02-03 16:46:48 -05:00
self.memoReceived.emit(msg[1 : msg.index("]")], handle, msg)
else:
2023-02-03 16:46:48 -05:00
self.noticeReceived.emit(handle, msg)
2022-06-04 19:08:38 -04:00
def metadata(self, target, nick, key, visibility, value):
# The format of the METADATA server notication is:
# METADATA <Target> <Key> <Visibility> <Value>
2022-09-01 20:05:46 -04:00
if key.lower() == "mood":
try:
mood = Mood(int(value))
2023-02-03 16:46:48 -05:00
self.moodUpdated.emit(nick, mood)
2022-09-01 20:05:46 -04:00
except ValueError:
PchumLog.warning("Invalid mood value, {}, {}".format(nick, mood))
2022-09-01 20:05:46 -04:00
elif key.lower() == "color":
color = QtGui.QColor(value) # Invalid color becomes rgb 0,0,0
2023-02-03 16:46:48 -05:00
self.colorUpdated.emit(nick, color)
def tagmsg(self, prefix, tags, *args):
PchumLog.info("TAGMSG: {} {} {}".format(prefix, tags, str(args)))
message_tags = tags[1:].split(";")
for m in message_tags:
if m.startswith("+pesterchum"):
# Pesterchum tag
try:
key, value = m.split("=")
except ValueError:
return
PchumLog.info("Pesterchum tag: {}={}".format(key, value))
2022-06-06 15:10:31 -04:00
# PESTERCHUM: syntax check
if (
(value == "BEGIN")
2022-06-06 15:10:31 -04:00
or (value == "BLOCK")
2022-06-07 18:19:56 -04:00
or (value == "CEASE")
or (value == "BLOCK")
or (value == "BLOCKED")
or (value == "UNBLOCK")
or (value == "IDLE")
or (value == "ME")
):
2022-06-06 15:10:31 -04:00
# Process like it's a PESTERCHUM: PRIVMSG
msg = "PESTERCHUM:" + value
self.privmsg(prefix, args[0], msg)
elif value.startswith("COLOR>"):
# Process like it's a COLOR >0,0,0 PRIVMSG
msg = value.replace(">", " >")
2022-06-06 15:10:31 -04:00
self.privmsg(prefix, args[0], msg)
elif value.startswith("TIME>"):
# Process like it's a PESTERCHUM:TIME> PRIVMSG
msg = "PESTERCHUM:" + value
self.privmsg(prefix, args[0], msg)
else:
# Invalid syntax
PchumLog.warning("TAGMSG with invalid syntax.")
2022-06-12 22:03:39 -04:00
def error(self, *params):
# Server is ending connection.
reason = ""
2022-06-12 22:03:39 -04:00
for x in params:
2023-02-03 16:46:48 -05:00
if x:
reason += x + " "
2023-02-03 16:46:48 -05:00
self.stopIRC = reason.strip()
self.disconnectIRC()
def privmsg(self, nick, chan, msg):
handle = nick[0 : nick.find("!")]
2023-02-03 16:46:48 -05:00
if not msg: # Length 0
2011-02-07 13:40:05 -05:00
return
2022-06-02 17:15:45 -04:00
# CTCP
# ACTION, IRC /me (The CTCP kind)
if msg[0:8] == "\x01ACTION ":
msg = "/me" + msg[7:-1]
2022-06-02 17:15:45 -04:00
# CTCPs that don't need to be shown
elif msg[0] == "\x01":
PchumLog.info('---> recv "CTCP {} :{}"'.format(handle, msg[1:-1]))
2022-06-02 17:15:45 -04:00
# VERSION, return version
if msg[1:-1].startswith("VERSION"):
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(handle, "VERSION", "Pesterchum %s" % (_pcVersion)
)
# CLIENTINFO, return supported CTCP commands.
2022-06-02 17:15:45 -04:00
elif msg[1:-1].startswith("CLIENTINFO"):
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(
handle,
"CLIENTINFO",
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS GETMOOD",
)
2022-06-02 17:15:45 -04:00
# PING, return pong
elif msg[1:-1].startswith("PING"):
if len(msg[1:-1].split("PING ")) > 1:
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(handle, "PING", msg[1:-1].split("PING ")[1]
)
2022-06-02 17:15:45 -04:00
else:
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(handle, "PING")
2022-06-02 17:15:45 -04:00
# SOURCE, return source
elif msg[1:-1].startswith("SOURCE"):
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(
handle,
"SOURCE",
"https://github.com/Dpeta/pesterchum-alt-servers",
)
2022-06-02 17:15:45 -04:00
# ???
2011-06-29 13:19:22 -04:00
elif msg[1:-1].startswith("NOQUIRKS") and chan[0] == "#":
op = nick[0 : nick.find("!")]
2023-02-03 16:46:48 -05:00
self.quirkDisable.emit(chan, msg[10:-1], op)
2022-06-02 17:15:45 -04:00
# GETMOOD via CTCP
elif msg[1:-1].startswith("GETMOOD"):
# GETMOOD via CTCP
# Maybe we can do moods like this in the future...
mymood = self.mainwindow.profile().mood.value()
2023-02-03 14:39:16 -05:00
self.send_irc.ctcp(handle, "MOOD >%d" % (mymood))
2022-06-02 17:15:45 -04:00
# Backwards compatibility
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood))
return
2016-12-11 04:01:48 -05:00
2016-12-10 20:55:42 -05:00
if chan != "#pesterchum":
# We don't need anywhere near that much spam.
PchumLog.info('---> recv "PRIVMSG {} :{}"'.format(handle, msg))
2016-12-11 04:01:48 -05:00
if chan == "#pesterchum":
# follow instructions
if msg[0:6] == "MOOD >":
try:
mood = Mood(int(msg[6:]))
except ValueError:
mood = Mood(0)
2023-02-03 16:46:48 -05:00
self.moodUpdated.emit(handle, mood)
elif msg[0:7] == "GETMOOD":
mychumhandle = self.mainwindow.profile().handle
mymood = self.mainwindow.profile().mood.value()
if msg.find(mychumhandle, 8) != -1:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood))
elif chan[0] == "#":
if msg[0:16] == "PESTERCHUM:TIME>":
2023-02-03 16:46:48 -05:00
self.timeCommand.emit(chan, handle, msg[16:])
else:
2023-02-03 16:46:48 -05:00
self.memoReceived.emit(chan, handle, msg)
else:
# private message
# silently ignore messages to yourself.
if handle == self.mainwindow.profile().handle:
return
if msg[0:7] == "COLOR >":
colors = msg[7:].split(",")
try:
colors = [int(d) for d in colors]
except ValueError as e:
PchumLog.warning(e)
colors = [0, 0, 0]
PchumLog.debug("colors: " + str(colors))
color = QtGui.QColor(*colors)
2023-02-03 16:46:48 -05:00
self.colorUpdated.emit(handle, color)
else:
2023-02-03 16:46:48 -05:00
self.messageReceived.emit(handle, msg)
2011-02-15 12:10:57 -05:00
def welcome(self, server, nick, msg):
2023-02-03 16:46:48 -05:00
self.setConnected()
# mychumhandle = self.mainwindow.profile().handle
2011-02-15 12:10:57 -05:00
mymood = self.mainwindow.profile().mood.value()
2022-09-01 20:05:46 -04:00
color = self.mainwindow.profile().color
2011-09-13 00:03:05 -04:00
if not self.mainwindow.config.lowBandwidth():
2022-09-01 20:05:46 -04:00
# Negotiate capabilities
2023-01-31 19:12:43 -05:00
self.send_irc.cap("REQ", "message-tags")
self.send_irc.cap(
self, "REQ", "draft/metadata-notify-2"
) # <--- Not required in the unreal5 module implementation
2023-01-31 19:12:43 -05:00
self.send_irc.cap(
self, "REQ", "pesterchum-tag"
) # <--- Currently not using this
time.sleep(0.413 + 0.097) # <--- somehow, this actually helps.
2023-01-31 19:12:43 -05:00
self.send_irc.join("#pesterchum")
2022-06-04 19:08:38 -04:00
# Moods via metadata
2023-01-31 19:12:43 -05:00
self.send_irc.metadata("*", "sub", "mood")
self.send_irc.metadata("*", "set", "mood", str(mymood))
2022-09-01 20:05:46 -04:00
# Color via metadata
2023-01-31 19:12:43 -05:00
self.send_irc.metadata("*", "sub", "color")
self.send_irc.metadata("*", "set", "color", str(color.name()))
2022-06-04 19:08:38 -04:00
# Backwards compatible moods
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood))
2022-06-12 22:03:39 -04:00
def erroneusnickname(self, *args):
# Server is not allowing us to connect.
reason = "Handle is not allowed on this server.\n"
for x in args:
if (x != None) and (x != ""):
reason += x + " "
2023-02-03 16:46:48 -05:00
self.stopIRC = reason.strip()
self.disconnectIRC()
2022-06-12 22:03:39 -04:00
2022-06-04 19:08:38 -04:00
def keyvalue(self, target, handle_us, handle_owner, key, visibility, *value):
# The format of the METADATA server notication is:
# METADATA <Target> <Key> <Visibility> <Value>
if key == "mood":
mood = Mood(int(value[0]))
2023-02-03 16:46:48 -05:00
self.moodUpdated.emit(handle_owner, mood)
2022-06-04 19:08:38 -04:00
def metadatasubok(self, *params):
2023-02-03 16:46:48 -05:00
PchumLog.info("metadatasubok: %s", params)
2022-06-04 19:08:38 -04:00
def nomatchingkey(self, target, our_handle, failed_handle, key, *error):
# Try to get moods the old way if metadata fails.
PchumLog.info("nomatchingkey: " + failed_handle)
# No point in GETMOOD-ing services
if failed_handle.casefold() not in SERVICES:
try:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
except OSError as e:
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
2022-06-06 17:03:12 -04:00
def keynotset(self, target, our_handle, failed_handle, key, *error):
# Try to get moods the old way if metadata fails.
PchumLog.info("nomatchingkey: " + failed_handle)
chumglub = "GETMOOD "
try:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", chumglub + failed_handle)
except OSError as e:
2022-06-06 17:03:12 -04:00
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
2022-06-06 17:03:12 -04:00
def keynopermission(self, target, our_handle, failed_handle, key, *error):
# Try to get moods the old way if metadata fails.
PchumLog.info("nomatchingkey: " + failed_handle)
chumglub = "GETMOOD "
try:
2023-02-03 16:46:48 -05:00
self.send_irc.privmsg("#pesterchum", chumglub + failed_handle)
except OSError as e:
2022-06-04 19:08:38 -04:00
PchumLog.warning(e)
2023-02-03 16:46:48 -05:00
self.setConnectionBroken()
2022-06-04 19:28:00 -04:00
def featurelist(self, target, handle, *params):
# Better to do this via CAP ACK/CAP NEK
2022-06-04 19:28:00 -04:00
# RPL_ISUPPORT
features = params[:-1]
PchumLog.info("Server featurelist: " + str(features))
for x in features:
if x.upper().startswith("METADATA"):
PchumLog.info("Server supports metadata.")
2023-02-03 16:46:48 -05:00
self.metadata_supported = True
def cap(self, server, nick, subcommand, tag):
PchumLog.info("CAP {} {} {} {}".format(server, nick, subcommand, tag))
# if tag == "message-tags":
# if subcommand == "ACK":
2011-02-15 12:10:57 -05:00
def nicknameinuse(self, server, cmd, nick, msg):
newnick = "pesterClient%d" % (random.randint(100, 999))
2023-01-31 19:12:43 -05:00
self.send_irc.nick(newnick)
2023-02-03 16:46:48 -05:00
self.nickCollision.emit(nick, newnick)
def nickcollision(self, server, cmd, nick, msg):
newnick = "pesterClient%d" % (random.randint(100, 999))
2023-01-31 19:12:43 -05:00
self.send_irc.nick(newnick)
2023-02-03 16:46:48 -05:00
self.nickCollision.emit(nick, newnick)
2011-02-15 12:10:57 -05:00
def quit(self, nick, reason):
handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "QUIT {}: {}"'.format(handle, reason))
2023-02-03 16:46:48 -05:00
if handle == self.mainwindow.randhandler.randNick:
self.mainwindow.randhandler.setRunning(False)
server = self.mainwindow.config.server()
baseserver = server[server.rfind(".", 0, server.rfind(".")) :]
2011-07-17 04:58:19 -04:00
if reason.count(baseserver) == 2:
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(handle, "", "netsplit")
2011-07-17 04:58:19 -04:00
else:
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(handle, "", "quit")
self.moodUpdated.emit(handle, Mood("offline"))
2011-06-13 16:37:07 -04:00
def kick(self, opnick, channel, handle, reason):
op = opnick[0 : opnick.find("!")]
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(
handle, channel, "kick:{}:{}".format(op, reason)
)
2011-02-06 01:02:39 -05:00
# ok i shouldnt be overloading that but am lazy
2011-02-15 12:10:57 -05:00
def part(self, nick, channel, reason="nanchos"):
handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "PART {}: {}"'.format(handle, channel))
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(handle, channel, "left")
if channel == "#pesterchum":
2023-02-03 16:46:48 -05:00
self.moodUpdated.emit(handle, Mood("offline"))
2011-02-15 12:10:57 -05:00
def join(self, nick, channel):
handle = nick[0 : nick.find("!")]
PchumLog.info('---> recv "JOIN {}: {}"'.format(handle, channel))
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(handle, channel, "join")
if channel == "#pesterchum":
2023-02-03 16:46:48 -05:00
if handle == self.mainwindow.randhandler.randNick:
self.mainwindow.randhandler.setRunning(True)
self.moodUpdated.emit(handle, Mood("chummy"))
def mode(self, op, channel, mode, *handles):
PchumLog.debug("op=" + str(op))
PchumLog.debug("channel=" + str(channel))
PchumLog.debug("mode=" + str(mode))
PchumLog.debug("*handles=" + str(handles))
if len(handles) <= 0:
handles = [""]
opnick = op[0 : op.find("!")]
PchumLog.debug("opnick=" + opnick)
# Channel section
# Okay so, as I understand it channel modes will always be applied to a channel even if the commands also sets a mode to a user.
# So "MODE #channel +ro handleHandle" will set +r to channel #channel as well as set +o to handleHandle
# Therefore the bellow method causes a crash if both user and channel mode are being set in one command.
2023-02-03 16:46:48 -05:00
# if op == channel or channel == self.mainwindow.profile().handle:
# modes = list(self.mainwindow.modes)
# if modes and modes[0] == "+": modes = modes[1:]
# if mode[0] == "+":
# for m in mode[1:]:
# if m not in modes:
# modes.extend(m)
# elif mode[0] == "-":
# for i in mode[1:]:
# try:
# modes.remove(i)
# except ValueError:
# pass
# modes.sort()
2023-02-03 16:46:48 -05:00
# self.mainwindow.modes = "+" + "".join(modes)
# EXPIRIMENTAL FIX
2021-08-24 11:32:47 -04:00
# No clue how stable this is but since it doesn't seem to cause a crash it's probably an improvement.
# This might be clunky with non-unrealircd IRC servers
channel_mode = ""
unrealircd_channel_modes = [
"c",
"C",
"d",
"f",
"G",
"H",
"i",
"k",
"K",
"L",
"l",
"m",
"M",
"N",
"n",
"O",
"P",
"p",
"Q",
"R",
"r",
"s",
"S",
"T",
"t",
"V",
"z",
"Z",
]
if any(md in mode for md in unrealircd_channel_modes):
PchumLog.debug("Channel mode in string.")
2023-02-03 16:46:48 -05:00
modes = list(self.mainwindow.modes)
for md in unrealircd_channel_modes:
if mode.find(md) != -1: # -1 means not found
PchumLog.debug("md=" + md)
if mode[0] == "+":
modes.extend(md)
channel_mode = "+" + md
elif mode[0] == "-":
try:
modes.remove(md)
channel_mode = "-" + md
except ValueError:
PchumLog.warning(
"Can't remove channel mode that isn't set."
)
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(
"", channel, channel_mode + ":%s" % (op)
)
PchumLog.debug("pre-mode=" + str(mode))
mode = mode.replace(md, "")
PchumLog.debug("post-mode=" + str(mode))
modes.sort()
2023-02-03 16:46:48 -05:00
self.mainwindow.modes = "+" + "".join(modes)
modes = []
cur = "+"
for l in mode:
if l in ["+", "-"]:
cur = l
else:
modes.append("{}{}".format(cur, l))
PchumLog.debug("handles=" + str(handles))
PchumLog.debug("enumerate(modes) = " + str(list(enumerate(modes))))
for (i, m) in enumerate(modes):
# Server-set usermodes don't need to be passed.
if (handles == [""]) & (
("x" in m) | ("z" in m) | ("o" in m) | ("x" in m)
) != True:
2022-03-17 00:36:14 -04:00
try:
2023-02-03 16:46:48 -05:00
self.userPresentUpdate.emit(
handles[i], channel, m + ":%s" % (op)
)
2022-03-17 00:36:14 -04:00
except IndexError as e:
PchumLog.exception("modeSetIndexError: %s" % e)
# print("i = " + i)
# print("m = " + m)
2023-02-03 16:46:48 -05:00
# self.userPresentUpdate.emit(handles[i], channel, m+":%s" % (op))
# self.userPresentUpdate.emit(handles[i], channel, m+":%s" % (op))
# Passing an empty handle here might cause a crash.
# except IndexError:
2023-02-03 16:46:48 -05:00
# self.userPresentUpdate.emit("", channel, m+":%s" % (op))
def nick(self, oldnick, newnick, hopcount=0):
PchumLog.info("{}, {}".format(oldnick, newnick))
2022-08-17 05:24:50 -04:00
# svsnick
if oldnick == self.mainwindow.profile().handle:
# Server changed our handle, svsnick?
2023-02-03 16:46:48 -05:00
self.getSvsnickedOn.emit(oldnick, newnick)
2022-08-17 05:24:50 -04:00
# etc.
oldhandle = oldnick[0 : oldnick.find("!")]
if (oldhandle == self.mainwindow.profile().handle) or (
newnick == self.mainwindow.profile().handle
):
# print('hewwo')
2023-02-03 16:46:48 -05:00
self.myHandleChanged.emit(newnick)
newchum = PesterProfile(newnick, chumdb=self.mainwindow.chumdb)
2023-02-03 16:46:48 -05:00
self.moodUpdated.emit(oldhandle, Mood("offline"))
self.userPresentUpdate.emit(
"{}:{}".format(oldhandle, newnick), "", "nick"
)
if newnick in self.mainwindow.chumList.chums:
self.getMood(newchum)
2023-02-03 16:46:48 -05:00
if oldhandle == self.mainwindow.randhandler.randNick:
self.mainwindow.randhandler.setRunning(False)
elif newnick == self.mainwindow.randhandler.randNick:
self.mainwindow.randhandler.setRunning(True)
2011-02-15 12:10:57 -05:00
def namreply(self, server, nick, op, channel, names):
namelist = names.split(" ")
PchumLog.info('---> recv "NAMES %s: %d names"' % (channel, len(namelist)))
if not hasattr(self, "channelnames"):
2011-02-15 12:10:57 -05:00
self.channelnames = {}
if channel not in self.channelnames:
2011-02-15 12:10:57 -05:00
self.channelnames[channel] = []
self.channelnames[channel].extend(namelist)
# def ison(self, server, nick, nicks):
# nicklist = nicks.split(" ")
# getglub = "GETMOOD "
# PchumLog.info("---> recv \"ISON :%s\"" % nicks)
# for nick_it in nicklist:
2023-02-03 16:46:48 -05:00
# self.moodUpdated.emit(nick_it, Mood(0))
# if nick_it in self.mainwindow.namesdb["#pesterchum"]:
# getglub += nick_it
# if getglub != "GETMOOD ":
2023-02-03 16:46:48 -05:00
# self.send_irc.privmsg("#pesterchum", getglub)
2011-02-15 12:10:57 -05:00
def endofnames(self, server, nick, channel, msg):
2022-08-17 05:24:50 -04:00
try:
namelist = self.channelnames[channel]
except KeyError:
# EON seems to return with wrong capitalization sometimes?
for cn in self.channelnames.keys():
if channel.lower() == cn.lower():
channel = cn
namelist = self.channelnames[channel]
2011-02-15 12:10:57 -05:00
pl = PesterList(namelist)
del self.channelnames[channel]
2023-02-03 16:46:48 -05:00
self.namesReceived.emit(channel, pl)
if channel == "#pesterchum" and (
not hasattr(self, "joined") or not self.joined
):
self.joined = True
2023-02-03 16:46:48 -05:00
self.mainwindow.randhandler.setRunning(
self.mainwindow.randhandler.randNick in namelist
)
chums = self.mainwindow.chumList.chums
# self.isOn(*chums)
lesschums = []
for c in chums:
chandle = c.handle
if chandle in namelist:
lesschums.append(c)
self.getMood(*lesschums)
2011-02-15 12:10:57 -05:00
def liststart(self, server, handle, *info):
self.channel_list = []
info = list(info)
self.channel_field = info.index("Channel") # dunno if this is protocol
PchumLog.info('---> recv "CHANNELS: %s ' % (self.channel_field))
2011-02-15 12:10:57 -05:00
def list(self, server, handle, *info):
channel = info[self.channel_field]
2011-03-31 17:57:30 -04:00
usercount = info[1]
2011-02-15 12:10:57 -05:00
if channel not in self.channel_list and channel != "#pesterchum":
2011-03-31 17:57:30 -04:00
self.channel_list.append((channel, usercount))
PchumLog.info('---> recv "CHANNELS: %s ' % (channel))
2011-02-15 12:10:57 -05:00
def listend(self, server, handle, msg):
pl = PesterList(self.channel_list)
PchumLog.info('---> recv "CHANNELS END"')
2023-02-03 16:46:48 -05:00
self.channelListReceived.emit(pl)
2011-02-15 12:10:57 -05:00
self.channel_list = []
def umodeis(self, server, handle, modes):
2023-02-03 16:46:48 -05:00
self.mainwindow.modes = modes
def invite(self, sender, you, channel):
handle = sender.split("!")[0]
2023-02-03 16:46:48 -05:00
self.inviteReceived.emit(handle, channel)
def inviteonlychan(self, server, handle, channel, msg):
2023-02-03 16:46:48 -05:00
self.chanInviteOnly.emit(channel)
# channelmodeis can have six arguments.
2022-03-17 00:41:42 -04:00
def channelmodeis(self, server, handle, channel, modes, mode_params=""):
2023-02-03 16:46:48 -05:00
self.modesUpdated.emit(channel, modes)
def cannotsendtochan(self, server, handle, channel, msg):
2023-02-03 16:46:48 -05:00
self.cannotSendToChan.emit(channel, msg)
2011-06-20 19:18:47 -04:00
def toomanypeeps(self, *stuff):
2023-02-03 16:46:48 -05:00
self.tooManyPeeps.emit()
# def badchanmask(channel, *args):
2022-06-30 04:15:06 -04:00
# # Channel name is not valid.
# msg = ' '.join(args)
2023-02-03 16:46:48 -05:00
# self.forbiddenchannel.emit(channel, msg)
def forbiddenchannel(self, server, handle, channel, msg):
# Channel is forbidden.
2023-02-03 16:46:48 -05:00
self.forbiddenchannel.emit(channel, msg)
self.userPresentUpdate.emit(handle, channel, "left")
2023-01-31 19:12:43 -05:00
def ping(self, prefix, token):
"""Respond to server PING with PONG."""
self.send_irc.pong(token)
2023-01-31 19:12:43 -05:00
def get(self, in_command_parts):
PchumLog.debug("in_command_parts: %s" % in_command_parts)
""" finds a command
commands may be dotted. each command part is checked that it does
not start with and underscore and does not have an attribute
"protected". if either of these is true, ProtectedCommandError
is raised.
its possible to pass both "command.sub.func" and
["command", "sub", "func"].
"""
if isinstance(in_command_parts, (str, bytes)):
in_command_parts = in_command_parts.split(".")
command_parts = in_command_parts[:]
p = self
while command_parts:
cmd = command_parts.pop(0)
if cmd.startswith("_"):
raise ProtectedCommandError(in_command_parts)
try:
f = getattr(p, cmd)
except AttributeError:
raise NoSuchCommandError(in_command_parts)
if hasattr(f, "protected"):
raise ProtectedCommandError(in_command_parts)
#if isinstance(f, self) and command_parts:
if command_parts:
return f.get(command_parts)
p = f
return f
def run_command(self, command, *args):
"""finds and runs a command"""
2023-02-03 14:39:16 -05:00
PchumLog.debug("processCommand {}({})".format(command, args))
2023-01-31 19:12:43 -05:00
try:
f = self.get(command)
except NoSuchCommandError as e:
PchumLog.info(e)
self.__unhandled__(command, *args)
return
PchumLog.debug("f %s" % f)
try:
f(*args)
except TypeError as e:
PchumLog.info(
"Failed to pass command, did the server pass an unsupported paramater? "
+ str(e)
)
except Exception as e:
# logging.info("Failed to pass command, %s" % str(e))
PchumLog.exception("Failed to pass command")
def __unhandled__(self, cmd, *args):
"""The default handler for commands. Override this method to
apply custom behavior (example, printing) unhandled commands.
"""
PchumLog.debug("unhandled command {}({})".format(cmd, args))
moodUpdated = QtCore.pyqtSignal("QString", Mood)
colorUpdated = QtCore.pyqtSignal("QString", QtGui.QColor)
messageReceived = QtCore.pyqtSignal("QString", "QString")
memoReceived = QtCore.pyqtSignal("QString", "QString", "QString")
noticeReceived = QtCore.pyqtSignal("QString", "QString")
inviteReceived = QtCore.pyqtSignal("QString", "QString")
timeCommand = QtCore.pyqtSignal("QString", "QString", "QString")
namesReceived = QtCore.pyqtSignal("QString", PesterList)
channelListReceived = QtCore.pyqtSignal(PesterList)
nickCollision = QtCore.pyqtSignal("QString", "QString")
getSvsnickedOn = QtCore.pyqtSignal("QString", "QString")
myHandleChanged = QtCore.pyqtSignal("QString")
chanInviteOnly = QtCore.pyqtSignal("QString")
modesUpdated = QtCore.pyqtSignal("QString", "QString")
connected = QtCore.pyqtSignal()
askToConnect = QtCore.pyqtSignal(Exception)
userPresentUpdate = QtCore.pyqtSignal("QString", "QString", "QString")
cannotSendToChan = QtCore.pyqtSignal("QString", "QString")
tooManyPeeps = QtCore.pyqtSignal()
quirkDisable = QtCore.pyqtSignal("QString", "QString", "QString")
forbiddenchannel = QtCore.pyqtSignal("QString", "QString")