I am looking for a better way to minimize nested if statements in my MessageHandler.handleMessage
method. I am looking to adhere to SRP and a function should do one thing and one thing well!
Any input would be greatly appreciated.
class MessageHandler():
""" MessageHandler - Handles twitch IRC messages and emits events based on Commands and msgid tags """
def __init__(self):
self.COMMANDS: COMMANDS = COMMANDS()
def handleMessage(self, IrcMessage: str)->tuple:
""" MessageHandler.handleMessage - breaks down data string from irc server returns tuple (event: str, message: Message) or
returns tuple of (None, None)
:param IrcMessage: a irc recieved message for server
:type IrcMessage: str
:raises TypeError: IrcMessage needs to be of type str
:return: a tuple with event string and Message data type populasted with all parsed message data
:rtype: tuple
"""
try:
if not isinstance(IrcMessage, str):
raise TypeError("MessageHandler.handleMessage requires input of type str")
message = self._parse(IrcMessage)
# Populate message values
message.channel: str = message.params[0] if len(message.params) > 0 else None
message.text: str = message.params[1] if len(message.params) > 1 else None
message.id: str = message.tags.get("msg-id")
message.raw: str = IrcMessage
message.username: str = message.tags.get("display-name")
# Parse badges and emotes
message.tags = self._badges(self._emotes(message.tags))
# Transform IRCv3 Tags
message = self._TransformIRCv3Tags(message)
except AttributeError:
return(None, None)
# Handle message with prefix "tmi.twitch.tv"
if message.prefix == self.COMMANDS.TMI_TWITCH_TV:
# Handle command bot Username
if message.command == self.COMMANDS.USERNAME:
botUsername = message.params[0]
# Chatroom NOTICE check msgid tag
elif message.command == self.COMMANDS.NOTICE:
if message.id in self.COMMANDS.MESSAGEIDS.__dict__.values():
return (self.COMMANDS.NOTICE, message)
else:
if message.raw.replace(":tmi.twitch.tv NOTICE * :",'') in ("Login unsuccessful", "Login authentication failed", "Error logging in", "Invalid NICK"):
return (self.COMMANDS.LOGIN_UNSUCCESSFUL, \
message.raw.replace(":tmi.twitch.tv NOTICE * :",''))
else:
return (message.command, message)
# Handle message with prefix jtv ??????? unsure it is still required
elif message.prefix == "jtv":
print(message.params)#still testing
else:
if message.command == self.COMMANDS.MESSAGE:
message.username: str = message.prefix[:message.prefix.find("!")]
return (self.COMMANDS.MESSAGE, message)
elif message.command == self.COMMANDS.WHISPER:
return (self.COMMANDS.WHISPER, message)
elif message.command == self.COMMANDS.NAMES:
return (self.COMMANDS.NAMES, message)
return (None, None) # invalid message
@staticmethod
def _TransformIRCv3Tags(message: Message)->Message:
""" MessageHandler._TransformIRCv3Tags reformats message tags
:param message: message object
:type message: Message
:return: message with updated tags
:rtype: Message
"""
if message.tags:
for key in message.tags:
if key not in ("emote-sets", "ban-duration", "bits"):
if isinstance(message.tags[key], bool):
message.tags[key] = None
elif message.tags[key] in ('0', '1'):
message.tags[key] = bool(int(message.tags[key]))
return message
@staticmethod
def _badges(tags: dict)->dict:
""" MessageHandler._badges - Parse tags['badges'] from str to dict and update tags['badges']
:param tags: tags from parsed IRC message
:type event: dict
:return: tags
:rtype: dict
"""
if ("badges" in tags and isinstance(tags.get("badges"), str)):
badges = tags.get("badges").split(",")
tags["badges"]: dict = {}
for badge in badges:
key, value = badge.split("/")
if value is None:
return tags
tags["badges"][key] = value
return tags
@staticmethod
def _emotes(tags: dict)->dict:
""" MessageHandler._emotes - Parse tags['emotes'] from str to list and update tags['emotes']
:param tags: tags from parsed IRC message
:type event: dict
:return: tags
:rtype: dict
"""
if ("emotes" in tags and isinstance(tags.get("emotes"), str)):
emotes: dict = {}
emoticons = tags.get("emotes").split("/")
for emoticon in emoticons:
key, value = emoticon.split(":")
if value is None:
return tags
emotes[key] = value.split(",")
tags["emotes"] = emotes
return tags
@staticmethod
def _parse(data: str)->Message:
""" MessageHandler._parse - Parses IRC messages to Message type
:param data: string from IRC server
:type event: str
:return: message
:rtype: Message
"""
message = Message()
if not isinstance(data, str):
raise TypeError("MessageHandler._parse requires input of type str")
position: int = 0
nextspace: int = 0
if len(data) < 1:
return None
if data.startswith("@"):
nextspace = data.find(" ")
if nextspace == -1:
return None # invalid message form
tags = data[1:nextspace].split(";")
for tag in tags:
key, value = tag.split("=")
message.tags[key] = value or True
position = nextspace + 1
while data[position] == " ":
position += 1
if data[position] == ":":
nextspace = data.find(" ", position)
if nextspace == -1:
return None # invalid message form
message.prefix = data[position + 1:nextspace]
position = nextspace + 1
while data[position] == " ":
position += 1
nextspace = data.find(" ", position)
if nextspace == -1:
if len(data) > position:
message.command = data[position:]
return message
return None # invalid message form
message.command = data[position:nextspace]
position = nextspace + 1
while data[position] == " ":
position += 1
dataLen = len(data)
while position < dataLen:
nextspace = data.find(" ", position)
if data[position] == ":":
message.params.append(data[position + 1:])
break
if nextspace != -1:
message.params.append(data[position:nextspace])
position = nextspace + 1
while data[position] == " ":
position += 1
continue
if nextspace == -1:
message.params.append(data[position:])
break
return message
1 Answer 1
I don't know how are twitch messages formes, but here is just a quick thought after a first look at your code: regex might be helpful, especially for _parse method. It seems that there are known delimiters. You will be able to capture all relevant data in a single match, and for example drop all of your space skipping loops.
You might want to take your _parse method outside of your handler class, and create a specific MessageParser class. Parsing the raw message is different from handling parsed message.
In the handleMessage method, there's no return for the case of a "tmi.twitch.tv" when message.command == self.COMMANDS.USERNAME, so it ends up to return(None, None). Is it wanted? All other cases return something.
Concerning the nested if, by now I don't see a lot of options... However, you can use intermediate variables to have only one exit point, instead of all the returns. This will greatly help debugging by having, for example, only one breakpoint.
My 2 cents.
-
\$\begingroup\$ twitch messages use IRC formats, and I do like the idea of a single return statement and will have to edit that section. the username is just who you logged in as so no event necessary. Having the
_parse
method in the same module asmessageHandler
my thought was they would need to change at the same time which makes them bound together. i will also try out the regex method too ,thanks for you input \$\endgroup\$Eli Reid– Eli Reid2020年01月31日 02:07:45 +00:00Commented Jan 31, 2020 at 2:07