3
\$\begingroup\$

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
\$\endgroup\$
1
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\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 as messageHandler 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 Jan 31 at 2:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.