diff --git a/irc.py b/irc.py index 68b5f8e..4d66483 100644 --- a/irc.py +++ b/irc.py @@ -1,12 +1,18 @@ -import logging -import socket -import random -import time -import ssl +"""Provides classes and functions for communicating with an IRC server. + +References for the IRC protocol: + - Original IRC protocol specification: https://www.rfc-editor.org/rfc/rfc1459 + - Updated IRC Client specification: https://www.rfc-editor.org/rfc/rfc2812 + - Modern IRC client protocol writeup: https://modern.ircdocs.horse + - IRCv3 protocol additions: https://ircv3.net/irc/ + - Draft of metadata specification: https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237 +""" import sys -import select +import socket +import ssl +import random import datetime -import traceback +import logging try: from PyQt6 import QtCore, QtGui @@ -90,11 +96,16 @@ class PesterIRC(QtCore.QThread): self.send_irc = SendIRC() self.conn = None + self.joined = False + self.channelnames = {} + self.channel_list = [] + self.channel_field = None self.commands = { "001": self.welcome, "005": self.featurelist, + "221": self.umodeis, "321": self.liststart, "322": self.list, "323": self.listend, @@ -216,14 +227,12 @@ class PesterIRC(QtCore.QThread): for line in split_buffer: line = line.decode(encoding="utf-8", errors="replace") tags, prefix, command, args = self.parse_irc_line(line) - try: + if command: # Only need tags with tagmsg if command.casefold() == "tagmsg": self.run_command(command, prefix, tags, *args) else: self.run_command(command, prefix, *args) - except CommandError as e: - PchumLog.warning(f"CommandError: {e}") yield True except socket.timeout as se: @@ -463,9 +472,7 @@ class PesterIRC(QtCore.QThread): @QtCore.pyqtSlot(QString, bool) def startConvo(self, handle, initiated): - self.send_irc.privmsg( - handle, "COLOR >%s" % (self.mainwindow.profile().colorcmd()) - ) + self.send_irc.privmsg(handle, f"COLOR >{self.mainwindow.profile().colorcmd()}") if initiated: self.send_irc.privmsg(handle, "PESTERCHUM:BEGIN") @@ -490,7 +497,7 @@ class PesterIRC(QtCore.QThread): # 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())) + self.send_irc.privmsg("#pesterchum", f"MOOD >{me.mood.value()}") @QtCore.pyqtSlot() def updateColor(self): @@ -567,6 +574,7 @@ class PesterIRC(QtCore.QThread): self.close() def notice(self, nick, chan, msg): + """Standard IRC 'NOTICE' message, primarily used for automated replies from services.""" handle = nick[0 : nick.find("!")] PchumLog.info('---> recv "NOTICE {} :{}"'.format(handle, msg)) if ( @@ -579,40 +587,39 @@ class PesterIRC(QtCore.QThread): self.noticeReceived.emit(handle, msg) def metadata(self, target, nick, key, visibility, value): - # The format of the METADATA server notication is: - # METADATA + """METADATA DRAFT metadata message from server. + + The format of the METADATA server notication is: + METADATA + """ if key.lower() == "mood": try: mood = Mood(int(value)) self.moodUpdated.emit(nick, mood) except ValueError: - PchumLog.warning("Invalid mood value, {}, {}".format(nick, mood)) + PchumLog.warning("Invalid mood value, %s, %s", nick, mood) elif key.lower() == "color": color = QtGui.QColor(value) # Invalid color becomes rgb 0,0,0 self.colorUpdated.emit(nick, color) def tagmsg(self, prefix, tags, *args): - PchumLog.info("TAGMSG: {} {} {}".format(prefix, tags, str(args))) + """IRCv3 'TAGMSG' message/command, contains a tag without a command. + + For reference see: + https://ircv3.net/specs/extensions/message-tags.html#the-tagmsg-tag-only-message + """ + PchumLog.info("TAGMSG: %s %s %s", prefix, tags, args) message_tags = tags[1:].split(";") - for m in message_tags: - if m.startswith("+pesterchum"): + for tag in message_tags: + if tag.startswith("+pesterchum"): # Pesterchum tag try: - key, value = m.split("=") + key, value = tag.split("=") except ValueError: return - PchumLog.info("Pesterchum tag: {}={}".format(key, value)) + PchumLog.info("Pesterchum tag: %s=%s", key, value) # PESTERCHUM: syntax check - if ( - (value == "BEGIN") - or (value == "BLOCK") - or (value == "CEASE") - or (value == "BLOCK") - or (value == "BLOCKED") - or (value == "UNBLOCK") - or (value == "IDLE") - or (value == "ME") - ): + if value in ["BEGIN", "BLOCK", "CEASE", "BLOCK", "BLOCKED", "UNBLOCK", "IDLE", "ME"]: # Process like it's a PESTERCHUM: PRIVMSG msg = "PESTERCHUM:" + value self.privmsg(prefix, args[0], msg) @@ -628,20 +635,20 @@ class PesterIRC(QtCore.QThread): # Invalid syntax PchumLog.warning("TAGMSG with invalid syntax.") + def ping(self, _prefix, token): + """'PING' command from server, we respond with PONG and a matching token.""" + self.send_irc.pong(token) + def error(self, *params): - # Server is ending connection. - reason = "" - for x in params: - if x: - reason += x + " " - self.stopIRC = reason.strip() + """'ERROR' message from server, the server is terminating our connection.""" + self.stopIRC = " ".join(params).strip() self.disconnectIRC() def privmsg(self, nick, chan, msg): + """'PRIVMSG' message from server, the standard message.""" handle = nick[0 : nick.find("!")] if not msg: # Length 0 return - # CTCP # ACTION, IRC /me (The CTCP kind) if msg[0:8] == "\x01ACTION ": @@ -726,96 +733,10 @@ class PesterIRC(QtCore.QThread): else: self.messageReceived.emit(handle, msg) - def welcome(self, server, nick, msg): - self.setConnected() - # mychumhandle = self.mainwindow.profile().handle - mymood = self.mainwindow.profile().mood.value() - color = self.mainwindow.profile().color - if not self.mainwindow.config.lowBandwidth(): - # Negotiate capabilities - self.send_irc.cap("REQ", "message-tags") - self.send_irc.cap( - self, "REQ", "draft/metadata-notify-2" - ) # <--- Not required in the unreal5 module implementation - self.send_irc.cap( - self, "REQ", "pesterchum-tag" - ) # <--- Currently not using this - time.sleep(0.413 + 0.097) # <--- somehow, this actually helps. - self.send_irc.join("#pesterchum") - # Moods via metadata - self.send_irc.metadata("*", "sub", "mood") - self.send_irc.metadata("*", "set", "mood", str(mymood)) - # Color via metadata - self.send_irc.metadata("*", "sub", "color") - self.send_irc.metadata("*", "set", "color", str(color.name())) - # Backwards compatible moods - self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood)) - - def erroneusnickname(self, *args): - """RFC 432""" - # Server is not allowing us to connect. - reason = "Handle is not allowed on this server.\n" - for x in args: - if x: - reason += x + " " - self.stopIRC = reason.strip() - self.disconnectIRC() - - def keyvalue(self, target, handle_us, handle_owner, key, visibility, *value): - # The format of the METADATA server notication is: - # METADATA - if key == "mood": - mood = Mood(int(value[0])) - self.moodUpdated.emit(handle_owner, mood) - - def metadatasubok(self, *params): - PchumLog.info("metadatasubok: %s", params) - - 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: - self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") - - def keynotset(self, target, our_handle, failed_handle, key, *error): - # Try to get moods the old way if metadata fails. - PchumLog.info("nomatchingkey: %s", failed_handle) - self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") - - def keynopermission(self, target, our_handle, failed_handle, key, *error): - # Try to get moods the old way if metadata fails. - PchumLog.info("nomatchingkey: %s", failed_handle) - self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") - - def featurelist(self, target, handle, *params): - # Better to do this via CAP ACK/CAP NEK - # 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.") - 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": - - def nicknameinuse(self, server, cmd, nick, msg): - newnick = "pesterClient%d" % (random.randint(100, 999)) - self.send_irc.nick(newnick) - self.nickCollision.emit(nick, newnick) - - def nickcollision(self, server, cmd, nick, msg): - newnick = "pesterClient%d" % (random.randint(100, 999)) - self.send_irc.nick(newnick) - self.nickCollision.emit(nick, newnick) - def quit(self, nick, reason): + """QUIT message from server, a client has quit the server.""" handle = nick[0 : nick.find("!")] - PchumLog.info('---> recv "QUIT {}: {}"'.format(handle, reason)) + PchumLog.info('---> recv "QUIT %s: %s"', handle, reason) if handle == self.mainwindow.randhandler.randNick: self.mainwindow.randhandler.setRunning(False) server = self.mainwindow.config.server() @@ -827,11 +748,13 @@ class PesterIRC(QtCore.QThread): self.moodUpdated.emit(handle, Mood("offline")) def kick(self, opnick, channel, handle, reason): + """'KICK' message from server, someone got kicked from a channel.""" op = opnick[0 : opnick.find("!")] - self.userPresentUpdate.emit(handle, channel, "kick:{}:{}".format(op, reason)) + self.userPresentUpdate.emit(handle, channel, f"kick:{op}:{reason}") # 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.""" handle = nick[0 : nick.find("!")] PchumLog.info('---> recv "PART {}: {}"'.format(handle, channel)) self.userPresentUpdate.emit(handle, channel, "left") @@ -839,6 +762,7 @@ class PesterIRC(QtCore.QThread): self.moodUpdated.emit(handle, Mood("offline")) def join(self, nick, channel): + """'JOIN' message from server, someone joined a channel.""" handle = nick[0 : nick.find("!")] PchumLog.info('---> recv "JOIN {}: {}"'.format(handle, channel)) self.userPresentUpdate.emit(handle, channel, "join") @@ -848,15 +772,13 @@ class PesterIRC(QtCore.QThread): self.moodUpdated.emit(handle, Mood("chummy")) def mode(self, op, channel, mode, *handles): + """'MODE' message from server, a user or a channel's mode changed.""" PchumLog.debug( - "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 ) - if not handles: handles = [""] - opnick = op[0 : op.find("!")] - PchumLog.debug("opnick=%s", opnick) - + # opnick = op[0 : op.find("!")] # Channel section # This might be clunky with non-unrealircd IRC servers channel_mode = "" @@ -866,7 +788,6 @@ class PesterIRC(QtCore.QThread): 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 @@ -879,7 +800,7 @@ class PesterIRC(QtCore.QThread): "Can't remove channel mode that isn't set." ) self.userPresentUpdate.emit( - "", channel, channel_mode + ":%s" % (op) + "", channel, f"{channel_mode}:{op}" ) PchumLog.debug("pre-mode=%s", mode) mode = mode.replace(md, "") @@ -893,28 +814,27 @@ class PesterIRC(QtCore.QThread): if l in ["+", "-"]: cur = l else: - modes.append("{}{}".format(cur, l)) - PchumLog.debug("handles=%s", handles) - PchumLog.debug("enumerate(modes) = " + str(list(enumerate(modes)))) + modes.append(f"{cur}{l}") 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: + if handles == [""] and not ("x" in m or "z" in m or "o" in m or "x" in m): try: - self.userPresentUpdate.emit(handles[i], channel, m + ":%s" % (op)) + self.userPresentUpdate.emit(handles[i], channel, m + f":{op}") except IndexError as e: - PchumLog.exception("modeSetIndexError: %s" % e) - # print("i = " + i) - # print("m = " + m) - # 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: - # self.userPresentUpdate.emit("", channel, m+":%s" % (op)) + PchumLog.exception("modeSetIndexError: %s", e) - def nick(self, oldnick, newnick, hopcount=0): - PchumLog.info("{}, {}".format(oldnick, newnick)) + def invite(self, sender, _you, channel): + """'INVITE' message from server, someone invited us to a channel. + + Pizza party everyone invited!!!""" + handle = sender.split("!")[0] + self.inviteReceived.emit(handle, channel) + + def nick(self, oldnick, newnick, _hopcount=0): + """'NICK' message from server, signifies a nick change. + + Is send when our or someone else's nick got changed willingly or unwillingly.""" + PchumLog.info(oldnick, newnick) # svsnick if oldnick == self.mainwindow.profile().handle: # Server changed our handle, svsnick? @@ -922,14 +842,11 @@ class PesterIRC(QtCore.QThread): # etc. oldhandle = oldnick[0 : oldnick.find("!")] - if (oldhandle == self.mainwindow.profile().handle) or ( - newnick == self.mainwindow.profile().handle - ): - # print('hewwo') + if self.mainwindow.profile().handle in [newnick, oldhandle]: self.myHandleChanged.emit(newnick) newchum = PesterProfile(newnick, chumdb=self.mainwindow.chumdb) self.moodUpdated.emit(oldhandle, Mood("offline")) - self.userPresentUpdate.emit("{}:{}".format(oldhandle, newnick), "", "nick") + self.userPresentUpdate.emit(f"{oldhandle}:{newnick}", "", "nick") if newnick in self.mainwindow.chumList.chums: self.getMood(newchum) if oldhandle == self.mainwindow.randhandler.randNick: @@ -937,134 +854,202 @@ class PesterIRC(QtCore.QThread): elif newnick == self.mainwindow.randhandler.randNick: self.mainwindow.randhandler.setRunning(True) - def namreply(self, server, nick, op, channel, names): + def welcome(self, server, nick, msg): + """Numeric reply 001 RPL_WELCOME, send when we've connected to the server.""" + self.setConnected() + # mychumhandle = self.mainwindow.profile().handle + mymood = self.mainwindow.profile().mood.value() + color = self.mainwindow.profile().color + if not self.mainwindow.config.lowBandwidth(): + # Negotiate capabilities + self.send_irc.cap("REQ", "message-tags") + self.send_irc.cap( + self, "REQ", "draft/metadata-notify-2" + ) # <--- Not required in the unreal5 module implementation + self.send_irc.cap( + self, "REQ", "pesterchum-tag" + ) # <--- Currently not using this + #time.sleep(0.413 + 0.097) # <--- somehow, this actually helps. + self.send_irc.join("#pesterchum") + # Moods via metadata + self.send_irc.metadata("*", "sub", "mood") + self.send_irc.metadata("*", "set", "mood", str(mymood)) + # Color via metadata + self.send_irc.metadata("*", "sub", "color") + self.send_irc.metadata("*", "set", "color", str(color.name())) + # Backwards compatible moods + self.send_irc.privmsg("#pesterchum", "MOOD >%d" % (mymood)) + + def featurelist(self, _target, _handle, *params): + """Numerical reply 005 RPL_ISUPPORT to communicate supported server features. + + Not in the original specification. + Metadata support could be confirmed via CAP ACK/CAP NEK. + """ + features = params[:-1] + PchumLog.info("Server featurelist: %s", features) + for x in features: + if x.upper().startswith("METADATA"): + PchumLog.info("Server supports metadata.") + self.metadata_supported = True + + def cap(self, server, nick, subcommand, tag): + """IRCv3 capabilities command from server. + + See: https://ircv3.net/specs/extensions/capability-negotiation + """ + PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag) + # if tag == "message-tags": + # if subcommand == "ACK": + + def umodeis(self, _server, _handle, modes): + """Numeric reply 221 RPL_UMODEIS, shows us our user modes.""" + self.mainwindow.modes = modes + + def liststart(self, _server, _handle, *info): + """Numeric reply 321 RPL_LISTSTART, start of list of channels.""" + 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) + + def list(self, _server, _handle, *info): + """Numeric reply 322 RPL_LIST, returns part of the list of channels.""" + channel = info[self.channel_field] + usercount = info[1] + if channel not in self.channel_list and channel != "#pesterchum": + self.channel_list.append((channel, usercount)) + PchumLog.info('---> recv "CHANNELS: %s ', channel) + + def listend(self, _server, _handle, _msg): + """Numeric reply 323 RPL_LISTEND, end of a series of LIST replies.""" + PchumLog.info('---> recv "CHANNELS END"') + self.channelListReceived.emit(PesterList(self.channel_list)) + self.channel_list = [] + + def channelmodeis(self, _server, _handle, channel, modes, _mode_params=""): + """Numeric reply 324 RPL_CHANNELMODEIS, gives channel modes.""" + self.modesUpdated.emit(channel, modes) + + def namreply(self, _server, _nick, _op, channel, names): + """Numeric reply 353 RPL_NAMREPLY, part of a NAMES list of members, usually of a channel.""" namelist = names.split(" ") - PchumLog.info('---> recv "NAMES %s: %d names"' % (channel, len(namelist))) + PchumLog.info('---> recv "NAMES %s: %s names"', channel, len(namelist)) if not hasattr(self, "channelnames"): self.channelnames = {} if channel not in self.channelnames: 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: - # self.moodUpdated.emit(nick_it, Mood(0)) - # if nick_it in self.mainwindow.namesdb["#pesterchum"]: - # getglub += nick_it - # if getglub != "GETMOOD ": - # self.send_irc.privmsg("#pesterchum", getglub) - - 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.""" 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 + for channel_name in self.channelnames: + if channel.casefold() == channel_name.casefold(): + channel = channel_name namelist = self.channelnames[channel] - pl = PesterList(namelist) del self.channelnames[channel] - self.namesReceived.emit(channel, pl) + self.namesReceived.emit(channel, PesterList(namelist)) if channel == "#pesterchum" and not self.joined: self.joined = True 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) + for chum in chums: + if chum.handle in namelist: + lesschums.append(chum) self.getMood(*lesschums) - 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)) - - def list(self, server, handle, *info): - channel = info[self.channel_field] - usercount = info[1] - if channel not in self.channel_list and channel != "#pesterchum": - self.channel_list.append((channel, usercount)) - PchumLog.info('---> recv "CHANNELS: %s ' % (channel)) - - def listend(self, server, handle, msg): - """End of a LIST response, assume channel list is complete.""" - pl = PesterList(self.channel_list) - PchumLog.info('---> recv "CHANNELS END"') - self.channelListReceived.emit(pl) - self.channel_list = [] - - def umodeis(self, server, handle, modes): - self.mainwindow.modes = modes - - def invite(self, sender, you, channel): - handle = sender.split("!")[0] - self.inviteReceived.emit(handle, channel) - - def inviteonlychan(self, server, handle, channel, msg): - self.chanInviteOnly.emit(channel) - - # channelmodeis can have six arguments. - def channelmodeis(self, server, handle, channel, modes, mode_params=""): - self.modesUpdated.emit(channel, modes) - - 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.""" self.cannotSendToChan.emit(channel, msg) - def toomanypeeps(self, *stuff): - self.tooManyPeeps.emit() + def erroneusnickname(self, *args): + """Numeric reply 432 ERR_ERRONEUSNICKNAME, we have a forbidden or protocol-breaking nick.""" + # Server is not allowing us to connect. + reason = "Handle is not allowed on this server.\n" + " ".join(args) + self.stopIRC = reason.strip() + self.disconnectIRC() - # def badchanmask(channel, *args): - # # Channel name is not valid. - # msg = ' '.join(args) - # self.forbiddenchannel.emit(channel, msg) + def nicknameinuse(self, _server, _cmd, nick, _msg): + """Numerical reply 433 ERR_NICKNAMEINUSE, raised when changing nick to nick in use.""" + newnick = "pesterClient%d" % (random.randint(100, 999)) + self.send_irc.nick(newnick) + self.nickCollision.emit(nick, newnick) - def forbiddenchannel(self, server, handle, channel, msg): - # Channel is forbidden. + def nickcollision(self, _server, _cmd, nick, _msg): + """Numerical reply 436 ERR_NICKCOLLISION, raised during connect when nick is in use.""" + newnick = "pesterClient%d" % (random.randint(100, 999)) + self.send_irc.nick(newnick) + self.nickCollision.emit(nick, newnick) + + def forbiddenchannel(self, _server, handle, channel, msg): + """Numeric reply 448 'forbiddenchannel' reply, channel is forbidden. + + Not in the specification but used by UnrealIRCd.""" self.signal_forbiddenchannel.emit(channel, msg) self.userPresentUpdate.emit(handle, channel, "left") - def ping(self, prefix, token): - """Respond to server PING with PONG.""" - self.send_irc.pong(token) + def inviteonlychan(self, _server, _handle, channel, _msg): + """Numeric reply 473 ERR_INVITEONLYCHAN, can't join channel (+i).""" + self.chanInviteOnly.emit(channel) + + 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. + + The format of the METADATA server notication is: + METADATA + """ + if key == "mood": + mood = Mood(int(value[0])) + self.moodUpdated.emit(handle_owner, mood) + + def nomatchingkey(self, _target, _our_handle, failed_handle, _key, *_error): + """METADATA DRAFT numeric reply 766 ERR_NOMATCHINGKEY, no matching key.""" + PchumLog.info("nomatchingkey: %s", failed_handle) + # No point in GETMOOD-ing services + # Fallback to the normal GETMOOD method if getting mood via metadata fails. + if failed_handle.casefold() not in SERVICES: + self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") + + def keynotset(self, _target, _our_handle, failed_handle, _key, *_error): + """METADATA DRAFT numeric reply 768 ERR_KEYNOTSET, key isn't set.""" + PchumLog.info("nomatchingkey: %s", failed_handle) + # Fallback to the normal GETMOOD method if getting mood via metadata fails. + if failed_handle.casefold() not in SERVICES: + self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") + + def keynopermission(self, _target, _our_handle, failed_handle, _key, *_error): + """METADATA DRAFT numeric reply 769 ERR_KEYNOPERMISSION, no permission for key.""" + PchumLog.info("nomatchingkey: %s", failed_handle) + # Fallback to the normal GETMOOD method if getting mood via metadata fails. + if failed_handle.casefold() not in SERVICES: + self.send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}") + + def metadatasubok(self, *params): + """"METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key.""" + PchumLog.info("metadatasubok: %s", params) def run_command(self, command, *args): - """finds and runs a command""" - PchumLog.debug("processCommand {}({})".format(command, args)) + """Finds and runs a command.""" + PchumLog.debug("run_command %s(%s)", command, args) try: - f = self.commands[command] - except KeyError as e: - PchumLog.info(e) - self.__unhandled__(command, *args) + command_function = self.commands[command] + except KeyError: + PchumLog.warning("No matching function for command: %s(%s)", 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)) + command_function(*args) + except TypeError: + PchumLog.exception("Failed to pass command, did the server pass an unsupported paramater?") + except Exception: + PchumLog.exception("Exception while parsing command.") moodUpdated = QtCore.pyqtSignal("QString", Mood) colorUpdated = QtCore.pyqtSignal("QString", QtGui.QColor) @@ -1084,13 +1069,11 @@ class PesterIRC(QtCore.QThread): askToConnect = QtCore.pyqtSignal(Exception) userPresentUpdate = QtCore.pyqtSignal("QString", "QString", "QString") cannotSendToChan = QtCore.pyqtSignal("QString", "QString") - tooManyPeeps = QtCore.pyqtSignal() quirkDisable = QtCore.pyqtSignal("QString", "QString", "QString") signal_forbiddenchannel = QtCore.pyqtSignal("QString", "QString") class SendIRC: """Provides functions for outgoing IRC commands.""" - def __init__(self): self.socket = None # INET socket connected with server. diff --git a/pesterchum.py b/pesterchum.py index ec835af..98fc0ae 100755 --- a/pesterchum.py +++ b/pesterchum.py @@ -4363,80 +4363,6 @@ class MainProgram(QtCore.QObject): def trayMessageClick(self): self.widget.config.set("traymsg", False) - widget2irc = [ - ("sendMessage(QString, QString)", "sendMessage(QString, QString)"), - ("sendNotice(QString, QString)", "sendNotice(QString, QString)"), - ("sendCTCP(QString, QString)", "sendCTCP(QString, QString)"), - ("newConvoStarted(QString, bool)", "startConvo(QString, bool)"), - ("convoClosed(QString)", "endConvo(QString)"), - ("profileChanged()", "updateProfile()"), - ("moodRequest(PyQt_PyObject)", "getMood(PyQt_PyObject)"), - ("moodsRequest(PyQt_PyObject)", "getMoods(PyQt_PyObject)"), - ("moodUpdated()", "updateMood()"), - ("mycolorUpdated()", "updateColor()"), - ("blockedChum(QString)", "blockedChum(QString)"), - ("unblockedChum(QString)", "unblockedChum(QString)"), - ("requestNames(QString)", "requestNames(QString)"), - ("requestChannelList()", "requestChannelList()"), - ("joinChannel(QString)", "joinChannel(QString)"), - ("leftChannel(QString)", "leftChannel(QString)"), - ("kickUser(QString, QString, QString)", "kickUser(QString, QString, QString)"), - ( - "setChannelMode(QString, QString, QString)", - "setChannelMode(QString, QString, QString)", - ), - ("channelNames(QString)", "channelNames(QString)"), - ("inviteChum(QString, QString)", "inviteChum(QString, QString)"), - ("pingServer()", "pingServer()"), - ("setAway(bool)", "setAway(bool)"), - ("killSomeQuirks(QString, QString)", "killSomeQuirks(QString, QString)"), - ("disconnectIRC()", "disconnectIRC()"), - ] - # IRC --> Main window - irc2widget = [ - ("connected()", "connected()"), - ( - "moodUpdated(QString, PyQt_PyObject)", - "updateMoodSlot(QString, PyQt_PyObject)", - ), - ( - "colorUpdated(QString, QtGui.QColor)", - "updateColorSlot(QString, QtGui.QColor)", - ), - ("messageReceived(QString, QString)", "deliverMessage(QString, QString)"), - ( - "memoReceived(QString, QString, QString)", - "deliverMemo(QString, QString, QString)", - ), - ("noticeReceived(QString, QString)", "deliverNotice(QString, QString)"), - ("inviteReceived(QString, QString)", "deliverInvite(QString, QString)"), - ("nickCollision(QString, QString)", "nickCollision(QString, QString)"), - ("getSvsnickedOn(QString, QString)", "getSvsnickedOn(QString, QString)"), - ("myHandleChanged(QString)", "myHandleChanged(QString)"), - ( - "namesReceived(QString, PyQt_PyObject)", - "updateNames(QString, PyQt_PyObject)", - ), - ( - "userPresentUpdate(QString, QString, QString)", - "userPresentUpdate(QString, QString, QString)", - ), - ("channelListReceived(PyQt_PyObject)", "updateChannelList(PyQt_PyObject)"), - ( - "timeCommand(QString, QString, QString)", - "timeCommand(QString, QString, QString)", - ), - ("chanInviteOnly(QString)", "chanInviteOnly(QString)"), - ("modesUpdated(QString, QString)", "modesUpdated(QString, QString)"), - ("cannotSendToChan(QString, QString)", "cannotSendToChan(QString, QString)"), - ("tooManyPeeps()", "tooManyPeeps()"), - ( - "quirkDisable(QString, QString, QString)", - "quirkDisable(QString, QString, QString)", - ), - ("signal_forbiddenchannel(QString)", "forbiddenchannel(QString)"), - ] - def ircQtConnections(self, irc, widget): # IRC --> Main window return ( @@ -4484,7 +4410,6 @@ class MainProgram(QtCore.QObject): (irc.modesUpdated, widget.modesUpdated), (irc.cannotSendToChan, widget.cannotSendToChan), (irc.signal_forbiddenchannel, widget.forbiddenchannel), - (irc.tooManyPeeps, widget.tooManyPeeps), (irc.quirkDisable, widget.quirkDisable), )