<commit_before>"""
Author: NekoGamiYuki
Version: 0.1.6

Description:
A simple twitch API. Current version will be rather basic, with the main ability
being that it is capable of reading twitch chat.

Examples:
Example program is located in the 'Examples' folder.
"""

# TODO: Add logging capabilities (maybe)
# TODO: Update all print/string statements to use .format instead of %
# TODO: Finish adding docstrings and commenting out code
# TODO: Make get_info check for CAP NACK.
# TODO: Make manage_irc capable of basic IRC if tags aren't ACK'd
# TODO: With new regex I can check if the bot is a mod! I could create a 'me'
#       class and check channels I have joined for my status within them.
# TODO: If desired, I could put all of this into a class. That would make this
#       module capable of managing multiple bots. Though I find that unnecessary,
#       but cool.
# TODO: Add local time to User class.
# TODO: twitchnotifty is the user that notifies everyone of new subscribers
# TODO: Need to implement the select module as soon as possible~!
# TODO: Consider merging SpicyTwitchIRC and SpicyBotAPI into SpicyTwitch.bot
#       and Spicytwitch.irc respectively, as a single package.
# TODO: Either turn this into a class or create a class using this module. This
#       will allow for the use of multiple users, which will be useful for certain
#       applications that might want to allow multi-user applications

# TODO: Rewrite returns of all functions to take advantage of raising exceptions
# I want to make it so that I raise exceptions instead of just returning False.
# Things like not giving me a username when connecting are VERY serious, and
# therefore require an exception, a complete stop, rather than a return value.

# NOTE: Turns out you can actually PING twitch... The same way I send a PONG,
# but replace it with PING. They reply with a PONG. This is what I was doing
# accidentally this entire time...

# Imported Modules--------------------------------------------------------------
import warnings
import logging
from logging import NullHandler
import socket
import time
import re

# Global Variables--------------------------------------------------------------
_SOCK = None
_TWITCH = "irc.chat.twitch.tv"  # Compliant with twitch's new server
_PORT = 6667  # Not using twitch's SSL capable server's

# Connection information
_CONNECTION_PROTOCOL = ''  # Do not set this yourself! Use connect() instead
_RECONNECT = False
is_connected = False

# Configuration variables for _send_info()
_commands_sent = 0
_send_time = 0
_RATE = 100  # No more than 100 commands sent every 30 seconds

# User information (for reconnecting)
_username = ''
_oauth = ''  # Good idea? It's the only way I know of keeping it around...

# Twitch chat information
channels = {}
notification = {}  # Holds a single notification from twitch (channel_name, message)
user = None  # Once initialized by get_info() it contains a single users info

# Logging
irc_logger = logging.getLogger(__name__)
irc_logger.addHandler(NullHandler())
logging.basicConfig(format='[%(asctime)s] [%(levelname)s] [%(module)s] (%(funcName)s): %(message)s', datefmt='%Y/%m/%d %I:%M:%S %p')

# Regular Expressions-----------------------------------------------------------
# TODO: do a re.match for each of these to test which parser to send the information to?
# TODO: Maybe add r"$" to the end of each of these?
# For capturing ACK/NAK from capability requests.
cap_regex = re.compile(r"^:tmi\.twitch\.tv CAP \* ([A-Z]+) :twitch\.tv/(\w+)")
# Gets badges, a number and space (unknown why it does that...),
# extra information, COMMAND, channel, and message if available.
tags_regex = re.compile(r"^@((\w|\W)+):(\w+!\w+@\w+\.)?tmi\.twitch\.tv ([A-Z]+) #(\w+)[ ]?:?([\S\s]+)?")
# Gets username, extra information, channel, and user's message.
irc_chat_regex = re.compile(r"^:(\w+)!(\w+@\w+\.tmi\.twitch\.tv) ([A-Z]+) #(\w+) :?([\S\s]+)?")
# Gets channel and string of all usernames.
# Although this says "start", I think twitch sends this for each list of users. So it can
# send multiple of these for the same channel until the list is done. I can use name_end for
# logging purposes as it really won't be useful for anything else.
names_start_regex = re.compile(r"^:\w+\.tmi\.twitch\.tv 353 \w+ = #(\w+) :([\S\s]+)")
# Gets channel
names_end_regex = re.compile(r"^:\w+\.tmi\.twitch\.tv 366 \w+ #(\w+) :([\S\s]+)")
# Gets channel and username
# NOTE: Sometimes is incomplete on channel name! Maybe do a check to see if the
#       partial channel name fits inside only ONE of the current channels? If more than
#       than one match, maybe discard? Or throw into an "unknown" bin.
join_part_regex = re.compile(r"^:(\w+)!(\w+@\w+\.tmi\.twitch\.tv) ([A-Z]+) #(\w+)")
mod_regex = re.compile(r"^:jtv ([A-Z]+) #(\w+) ([-+])o (\w+)")


# Classes-----------------------------------------------------------------------
# TODO: Add followers_only check.
class Channel(object):
    """
    Contains information about a single channel. Such as their viewers, the
    message count (since you joined), whether they're in slow mode, etc...

    Information contained in class:
    name: Name of the channel
    moderators: Moderators of the channel, updated when a mod chats
    banned_users: All banned users since you joined, and any reasons for the ban
    timed_out_users: All users that have been timed out, and the seconds/reason.
    viewers: A list of all viewers.
    language: If a channel designated a language, it goes here.
    slow_mode: Whether the channel has slow mode on
    slow_time: The time for the slow mode cooldown
    r9k_mode: Whether the channel has r9kbeta on
    subscribers_only: Whether the channel has subscriber-mode on
    followers_only: Whether the channel has followers-mode on.
    hosting: Whether the channel is hosting
    hosted_channel: What channel is being hosted

    """

    def __init__(self, name: str):
        if name:
            self.name = name
        else:
            raise ValueError("Channel objects must have a non-empty name.")

        # How many messages were chatted since you joined
        self.message_count = 0
        self.moderators = []
        self.banned_users = {}
        self.timed_out_users = {}
        self.viewers = []
        self.language = ''
        self.slow_mode = False
        self.slow_time = 0
        self.r9k_mode = False
        self.subscribers_only = False
        self.followers_only = False
        self.hosting = False
        self.hosted_channel = ''


# TODO: If useful for future, add emote url and function for getting the image.
#       function should cache the image locally for multiple uses.
class Emote(object):
    """
    Contains information relating to an emote. The Name, ID, and position in
    a string are provided.

    Information you can get from this class:
    name: Name of the emote
    id: Id of the emote, for use in other parts of the twitch API (not this one)
    position: Start and End position of the emote in the user's message

    """

    def __init__(self, name: str, id_number: int, start: int, end: int):
        self.name = name
        self.id = id_number
        self.position = {"start": start, "end": end}


# TODO: Add purge capability
# TODO: Add unban and untimeout capabilities.
# TODO: Add reason to timeout()
class User(object):
    """
    Largest class in the API. Contains as much information about a user as given
    by Twitch.

    You can get their:
    name: Username
    message: What they just said in a channel (that you are joined to)
    chatted_from: What channel they just spoke from
    is_broadcaster: Whether they're a broadcaster
    is_mod: Whether they're a moderator
    is_sub: Whether they're a subscriber
    is_turbo: Whether they're a turbo subscriber
    user_type: This can be Admin, Staff, Mod, and anything else twitch adds
    color: The color of the user
    emotes: A collection of Emote objects, each relating to an emote the user
            used in chat.
    command: For bots, it's the first word in their message.

    Functions:
    ban(): Ban a user
    timeout(): Timeout a user
    purge(): Purge a user
    send_message(): Sends/Directs a message to a user in chat
    """

    # TODO: Consider renaming "extra" to something more specific.
    def __init__(self, extracted_tag_data: dict, channel_chatted_from: str,
                 chat_message: str, extra: str, filler_message="UNASSIGNED"):
        filler = filler_message  # For when we can't assign a value
        if not extracted_tag_data:
            self.name = filler
            self.message = filler
            self.chatted_from = filler
            self.is_broadcaster = False
            self.is_mod = False
            self.is_sub = False
            self.is_turbo = False
            self.user_type = filler
            self.color = filler
            self.emotes = []
            self.command = filler
        else:
            try:
                # Basic user information
                self.name = extracted_tag_data["display-name"]
                if not self.name:
                    self.name = extra.split('!')[0]

                self.message = chat_message
                # Where the user last chatted from
                self.chatted_from = channel_chatted_from

                # Role information
                if self.name.lower() == self.chatted_from:
                    self.is_broadcaster = True
                else:
                    self.is_broadcaster = False

                # ??? Double conversion because bool would see '0' as true
                self.is_mod = bool(int(extracted_tag_data["mod"]))
                self.is_sub = bool(int(extracted_tag_data["subscriber"]))
                self.is_turbo = bool(int(extracted_tag_data["turbo"]))

                # The user's role
                if extracted_tag_data["user-type"] is ' ':
                    self.user_type = filler
                else:
                    self.user_type = extracted_tag_data["user-type"] # Admin, Staff, Mod, etc...

                # Styling information
                if not extracted_tag_data["color"]:
                    self.color = filler
                else:
                    self.color = extracted_tag_data["color"]

                # NOTE: Twitch actually shows the months/bits for the badge a user has. I just
                #       saw someone talked that had "subscriber/6" and "bits/10000", then checked
                #       twitch chat and it showed them with a 6 month sub badge and a cheer 10000
                #       badge!
                self.badges = {}
                if extracted_tag_data["badges"]:
                    for badge in extracted_tag_data["badges"].split(','):
                        data = badge.split('/')
                        # TODO: Convert data[1] to int?
                        self.badges[data[0]] = data[1]

                # Emotes!
                self.emotes = []
                if extracted_tag_data["emotes"]:
                    for info in extracted_tag_data["emotes"].split('/'):
                        for position in info.split(":")[1].split(','):
                            start = int(position.split('-')[0])
                            end = int(position.split('-')[1]) + 1
                            name = self.message[start:end]
                            id = int(info.split(":")[0])
                            self.emotes.append(Emote(name,
                                                      id,
                                                      start,  # Start position
                                                      end))  # end position

                # Bot information (Useful for bots) It's the first word of their
                # chat message.
                self.command = self.message.split()[0]
            # TODO: Maybe change this?
            except (KeyError, TypeError) as error_information:
                error_filler = ("ERROR: Please check  user.error if this "
                                "issue persists.")
                self.name = error_filler
                self.message = error_filler
                self.message_history = []
                self.chatted_from = error_filler
                self.is_broadcaster = False
                self.is_mod = False
                self.is_sub = False
                self.is_turbo = False
                self.user_type = error_filler
                self.color = error_filler
                self.emotes = []
                self.command = error_filler
                self.error = error_information

    # NOTE: Would "reply" be a better name?
    def send_message(self, message: str, append_symbol=True) -> bool:
        """
        Send a message to this user. This is for chatting, not twitch messaging
        nor whispering. It pops up in chat as "@user <message>"

        Args:
            message: What you would like to tell the user
            append_symbol: Appends the '@' symbol at the start of the username

        Returns:
            True: When it succeeds in sending a message
            False: If it fails to send the message.

        """
        if not message:
            return False

        if append_symbol:
            formatted_message = "{}{} {}".format('@', self.name, message)
        else:
            formatted_message = "{} {}".format(self.name, message)

        if not chat(formatted_message, self.chatted_from):
            return False
        else:
            return True

    def purge(self, reason='') -> bool:
        """
        Purges (times out for 1 second) this user.

        Args:
            reason: The reason for the purging of this user

        Returns:
            True: When it succeeds to send the purge command
            False: When it fails to send the purge command

        """
        if self.timeout(1, reason):
            return True
        else:
            return False

    def timeout(self, seconds=600, reason='') -> bool:
        """
        Times out this user.

        Args:
            seconds: The duration of the timeout in seconds. Default 600.
            reason: The reason for timing out this user.

        Returns:
            True: When it succeeds in sending the timeout command to twitch
            False When it fails at sending the timeout command to twitch

        """
        # Doesn't matter if the reason is blank!
        if chat("/timeout {} {} {}".format(self.name, seconds, reason), self.chatted_from):
            return True
        else:
            return False

    def ban(self, reason='') -> bool:
        """
        Permanently Bans this user. Can be given a reason, which shows up in
        chat as a twitch notification.

        Args:
            reason: The reason for banning the user

        Returns:
            True: If it succeeds at sending the Ban command to twitch.
            False: if it fails at sending the Ban command to twitch.

        """
        # Doesn't matter if the reason is blank!
        if chat("/ban {} {}".format(self.name, reason), self.chatted_from):
            return True
        else:
            return False


# Parsing-----------------------------------------------------------------------
# TODO: Create functions for managing each command. Use _manage_tags to determine
#       which function to use.
# TODO: Manage GLOBALUSERSTATE, use to create a "me" variable that contains our
#       user's information.
def _manage_tags(input_data: str):
    """
    Manages most tags given by Twitch. Specifically, it manages PRIVMSG, NOTICE,
    ROOMSTATE, and CLEARCHAT tags. Updates the corresponding variables, such as
    updating the "user" variable when a new PRIVMSG is received (A single chat
    line).

    Updates the following variables:
    user: Is updated with the latest information from a single line of chat
    channels: Updates any information relating to the channel
    notification: Places the latest notification from chat, along with the
                  channel it came from
    Args:
        input_data: The data, from Twitch, that will be parsed.

    """

    if input_data:
        # At times twitch sends some broken/weird data, the one that caused
        # this one to break was '@user.tmi.twitch.tv PART #channel', since
        # JOIN/PARTS aren't meant to start with @, this was unexpected and
        # broke the parsing by being sent to the wrong parser.
        try:
            irc_logger.debug("Twitch Data: {}".format(input_data))
            twitch_data = tags_regex.findall(input_data)[0]
            irc_logger.debug("Parsed result: {}".format(twitch_data))  # DEBUG!!!
            extracted_tag_data = {}
            irc_logger.debug("Extracting data from tags.")
            for data in twitch_data[0].split(';'):
                extracted_tag_data[data.split('=')[0]] = data.split('=')[1]
            irc_logger.debug("Extracted tag data: {}".format(extracted_tag_data))
        except IndexError:
            warnings.warn("Received unexpected/broken data in _manage_tags(). Will not attempt to parse")
            return  # We will not try to parse broken/strange data.

        # TODO: Consider whether I should do USERSTATE or GLOBALUSERSTATE since
        # PRIVMSG has the same tags.
        global user
        global channels
        global notification

        # Time to parse the tags!
        if twitch_data[-3] == "PRIVMSG":  # Chat lines
            # Increment channel message count
            if twitch_data[-2] in channels:
                channels[twitch_data[-2]].message_count += 1

            # Update user variable
            irc_logger.info("Updating user variable.")
            user = User(extracted_tag_data, twitch_data[-2], twitch_data[-1], twitch_data[-4])

            # Sometimes this breaks, specifically when a user calls leave_channel()
            # as that deletes the channel from the channels dictionary.
            try:
                # Since we get to see if a user is a mod here, we add them to their
                # respective channel's moderator list if they're not already there.
                moderator_list = channels[user.chatted_from].moderators
                if user.is_mod and user.name.lower() not in moderator_list:
                    irc_logger.info("Adding {} to {}'s mod list.".format(user.name, user.chatted_from))
                    channels[user.chatted_from].moderators.append(user.name)
                elif not user.is_mod and user.name.lower() in moderator_list:
                    irc_logger.info("Removing {} from {}'s mod list.".format(user.name, user.chatted_from))
                    channels[user.chatted_from].moderators.remove(user.name)

                # We can also add them to the list of viewers if they weren't
                # already there.
                viewers = channels[user.chatted_from].viewers
                if user.name and user.name.lower() not in viewers:
                    irc_logger.info("Adding {} to {}'s viewer list.".format(user.name.lower(), user.chatted_from))
                    channels[user.chatted_from].viewers.append(user.name.lower())
            except KeyError:
                irc_logger.error("Attempted to add user to a channel list that no longer exists. This is likely a "
                                 "result from leaving a channel while the module parsed a user's chat message. Nothing "
                                 "can be done about it, it should not affect your program.")
                pass

        elif twitch_data[-3] == "NOTICE":  # Twitch NOTICE tag management
            affected_channel = twitch_data[-2]
            message_id = twitch_data[0].split('=')[1].strip()

            notification["channel_name"] = affected_channel
            notification["message"] = twitch_data[-1]

            if "slow" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking slow mode as on for {}.".format(affected_channel))
                    channels[affected_channel].slow = True
                else:
                    irc_logger.info("Marking slow mode as off for {}.".format(affected_channel))
                    channels[affected_channel].slow = False
            elif "subs" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking subscriber mode as on for {}.".format(affected_channel))
                    channels[affected_channel].subscriber = True
                else:
                    irc_logger.info("Marking subscriber mode as off for {}.".format(affected_channel))
                    channels[affected_channel].subscriber = False
            elif "r9k" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking r9k mode as on for {}.".format(affected_channel))
                    channels[affected_channel].r9k = True
                else:
                    irc_logger.info("Marking r9k mode as off for {}.".format(affected_channel))
                    channels[affected_channel].r9k = False

            # TODO: NEED TO CHECK IF THIS STILL WORKS!
            elif "host" in message_id:
                if "on" in message_id:
                    hosted_channel = re.findall(r"Now hosting (\w+).", notification["message"])[0]
                    irc_logger.info("Marking host mode as on for {}.".format(affected_channel))
                    channels[affected_channel].hosting = True
                    irc_logger.info("Setting {} as hosted channel for {}.".format(
                        hosted_channel, affected_channel
                    ))
                    channels[affected_channel].hosted_channel = hosted_channel
                else:
                    irc_logger.info("Marking host mode as off and resetting hosted channel")
                    channels[affected_channel].hosting = False
                    channels[affected_channel].hosted_channel = ''

        elif twitch_data[-3] == "ROOMSTATE":
            affected_channel = twitch_data[-2]

            # TODO: Check for "followers-only"
            irc_logger.info("Getting channel ROOMSTATE (Basic information for when you first enter a channel).")
            if "broadcaster-lang" in extracted_tag_data.keys():
                if extracted_tag_data["broadcaster-lang"]:
                    irc_logger.info("Setting {}'s channel language as {}.".format(
                        affected_channel, extracted_tag_data["broadcaster-lang"]
                    ))
                    channels[affected_channel].language = extracted_tag_data["broadcaster-lang"]
                else:
                    irc_logger.info("Channel {} has not set its language. This is ok.".format(affected_channel))
                    channels[affected_channel].language = ''
            elif "slow" in extracted_tag_data.keys():
                if extracted_tag_data["slow"] == '0':
                    irc_logger.info("Marking slow mode as off for {}.".format(affected_channel))
                    channels[affected_channel].slow_mode = False
                    channels[affected_channel].slow_time = 0
                    irc_logger.info("Slow time is set to 0 for {}.".format(affected_channel))

                else:
                    channels[affected_channel].slow_mode = True
                    channels[affected_channel].slow_time = int(extracted_tag_data["slow"])
                    irc_logger.info("Slow time is set to {} for {}.".format(
                        extracted_tag_data["slow"], affected_channel
                    ))
            elif "subs-only" in extracted_tag_data.keys():
                if '0' in extracted_tag_data["subs-only"]:
                    irc_logger.info("Marking subscriber mode as off for {}.".format(affected_channel))
                    channels[affected_channel].subscribers_only = False
                else:
                    irc_logger.info("Marking subscriber mode as on for {}.".format(affected_channel))
                    channels[affected_channel].subscribers_only = True
            elif "followers-only" in extracted_tag_data.keys():<commit_msg>Made log for unset channel language into a warning<commit_after>"""
Author: NekoGamiYuki
Version: 0.1.6

Description:
A simple twitch API. Current version will be rather basic, with the main ability
being that it is capable of reading twitch chat.

Examples:
Example program is located in the 'Examples' folder.
"""

# TODO: Add logging capabilities (maybe)
# TODO: Update all print/string statements to use .format instead of %
# TODO: Finish adding docstrings and commenting out code
# TODO: Make get_info check for CAP NACK.
# TODO: Make manage_irc capable of basic IRC if tags aren't ACK'd
# TODO: With new regex I can check if the bot is a mod! I could create a 'me'
#       class and check channels I have joined for my status within them.
# TODO: If desired, I could put all of this into a class. That would make this
#       module capable of managing multiple bots. Though I find that unnecessary,
#       but cool.
# TODO: Add local time to User class.
# TODO: twitchnotifty is the user that notifies everyone of new subscribers
# TODO: Need to implement the select module as soon as possible~!
# TODO: Consider merging SpicyTwitchIRC and SpicyBotAPI into SpicyTwitch.bot
#       and Spicytwitch.irc respectively, as a single package.
# TODO: Either turn this into a class or create a class using this module. This
#       will allow for the use of multiple users, which will be useful for certain
#       applications that might want to allow multi-user applications

# TODO: Rewrite returns of all functions to take advantage of raising exceptions
# I want to make it so that I raise exceptions instead of just returning False.
# Things like not giving me a username when connecting are VERY serious, and
# therefore require an exception, a complete stop, rather than a return value.

# NOTE: Turns out you can actually PING twitch... The same way I send a PONG,
# but replace it with PING. They reply with a PONG. This is what I was doing
# accidentally this entire time...

# Imported Modules--------------------------------------------------------------
import warnings
import logging
from logging import NullHandler
import socket
import time
import re

# Global Variables--------------------------------------------------------------
_SOCK = None
_TWITCH = "irc.chat.twitch.tv"  # Compliant with twitch's new server
_PORT = 6667  # Not using twitch's SSL capable server's

# Connection information
_CONNECTION_PROTOCOL = ''  # Do not set this yourself! Use connect() instead
_RECONNECT = False
is_connected = False

# Configuration variables for _send_info()
_commands_sent = 0
_send_time = 0
_RATE = 100  # No more than 100 commands sent every 30 seconds

# User information (for reconnecting)
_username = ''
_oauth = ''  # Good idea? It's the only way I know of keeping it around...

# Twitch chat information
channels = {}
notification = {}  # Holds a single notification from twitch (channel_name, message)
user = None  # Once initialized by get_info() it contains a single users info

# Logging
irc_logger = logging.getLogger(__name__)
irc_logger.addHandler(NullHandler())
logging.basicConfig(format='[%(asctime)s] [%(levelname)s] [%(module)s] (%(funcName)s): %(message)s', datefmt='%Y/%m/%d %I:%M:%S %p')

# Regular Expressions-----------------------------------------------------------
# TODO: do a re.match for each of these to test which parser to send the information to?
# TODO: Maybe add r"$" to the end of each of these?
# For capturing ACK/NAK from capability requests.
cap_regex = re.compile(r"^:tmi\.twitch\.tv CAP \* ([A-Z]+) :twitch\.tv/(\w+)")
# Gets badges, a number and space (unknown why it does that...),
# extra information, COMMAND, channel, and message if available.
tags_regex = re.compile(r"^@((\w|\W)+):(\w+!\w+@\w+\.)?tmi\.twitch\.tv ([A-Z]+) #(\w+)[ ]?:?([\S\s]+)?")
# Gets username, extra information, channel, and user's message.
irc_chat_regex = re.compile(r"^:(\w+)!(\w+@\w+\.tmi\.twitch\.tv) ([A-Z]+) #(\w+) :?([\S\s]+)?")
# Gets channel and string of all usernames.
# Although this says "start", I think twitch sends this for each list of users. So it can
# send multiple of these for the same channel until the list is done. I can use name_end for
# logging purposes as it really won't be useful for anything else.
names_start_regex = re.compile(r"^:\w+\.tmi\.twitch\.tv 353 \w+ = #(\w+) :([\S\s]+)")
# Gets channel
names_end_regex = re.compile(r"^:\w+\.tmi\.twitch\.tv 366 \w+ #(\w+) :([\S\s]+)")
# Gets channel and username
# NOTE: Sometimes is incomplete on channel name! Maybe do a check to see if the
#       partial channel name fits inside only ONE of the current channels? If more than
#       than one match, maybe discard? Or throw into an "unknown" bin.
join_part_regex = re.compile(r"^:(\w+)!(\w+@\w+\.tmi\.twitch\.tv) ([A-Z]+) #(\w+)")
mod_regex = re.compile(r"^:jtv ([A-Z]+) #(\w+) ([-+])o (\w+)")


# Classes-----------------------------------------------------------------------
# TODO: Add followers_only check.
class Channel(object):
    """
    Contains information about a single channel. Such as their viewers, the
    message count (since you joined), whether they're in slow mode, etc...

    Information contained in class:
    name: Name of the channel
    moderators: Moderators of the channel, updated when a mod chats
    banned_users: All banned users since you joined, and any reasons for the ban
    timed_out_users: All users that have been timed out, and the seconds/reason.
    viewers: A list of all viewers.
    language: If a channel designated a language, it goes here.
    slow_mode: Whether the channel has slow mode on
    slow_time: The time for the slow mode cooldown
    r9k_mode: Whether the channel has r9kbeta on
    subscribers_only: Whether the channel has subscriber-mode on
    followers_only: Whether the channel has followers-mode on.
    hosting: Whether the channel is hosting
    hosted_channel: What channel is being hosted

    """

    def __init__(self, name: str):
        if name:
            self.name = name
        else:
            raise ValueError("Channel objects must have a non-empty name.")

        # How many messages were chatted since you joined
        self.message_count = 0
        self.moderators = []
        self.banned_users = {}
        self.timed_out_users = {}
        self.viewers = []
        self.language = ''
        self.slow_mode = False
        self.slow_time = 0
        self.r9k_mode = False
        self.subscribers_only = False
        self.followers_only = False
        self.hosting = False
        self.hosted_channel = ''


# TODO: If useful for future, add emote url and function for getting the image.
#       function should cache the image locally for multiple uses.
class Emote(object):
    """
    Contains information relating to an emote. The Name, ID, and position in
    a string are provided.

    Information you can get from this class:
    name: Name of the emote
    id: Id of the emote, for use in other parts of the twitch API (not this one)
    position: Start and End position of the emote in the user's message

    """

    def __init__(self, name: str, id_number: int, start: int, end: int):
        self.name = name
        self.id = id_number
        self.position = {"start": start, "end": end}


# TODO: Add purge capability
# TODO: Add unban and untimeout capabilities.
# TODO: Add reason to timeout()
class User(object):
    """
    Largest class in the API. Contains as much information about a user as given
    by Twitch.

    You can get their:
    name: Username
    message: What they just said in a channel (that you are joined to)
    chatted_from: What channel they just spoke from
    is_broadcaster: Whether they're a broadcaster
    is_mod: Whether they're a moderator
    is_sub: Whether they're a subscriber
    is_turbo: Whether they're a turbo subscriber
    user_type: This can be Admin, Staff, Mod, and anything else twitch adds
    color: The color of the user
    emotes: A collection of Emote objects, each relating to an emote the user
            used in chat.
    command: For bots, it's the first word in their message.

    Functions:
    ban(): Ban a user
    timeout(): Timeout a user
    purge(): Purge a user
    send_message(): Sends/Directs a message to a user in chat
    """

    # TODO: Consider renaming "extra" to something more specific.
    def __init__(self, extracted_tag_data: dict, channel_chatted_from: str,
                 chat_message: str, extra: str, filler_message="UNASSIGNED"):
        filler = filler_message  # For when we can't assign a value
        if not extracted_tag_data:
            self.name = filler
            self.message = filler
            self.chatted_from = filler
            self.is_broadcaster = False
            self.is_mod = False
            self.is_sub = False
            self.is_turbo = False
            self.user_type = filler
            self.color = filler
            self.emotes = []
            self.command = filler
        else:
            try:
                # Basic user information
                self.name = extracted_tag_data["display-name"]
                if not self.name:
                    self.name = extra.split('!')[0]

                self.message = chat_message
                # Where the user last chatted from
                self.chatted_from = channel_chatted_from

                # Role information
                if self.name.lower() == self.chatted_from:
                    self.is_broadcaster = True
                else:
                    self.is_broadcaster = False

                # ??? Double conversion because bool would see '0' as true
                self.is_mod = bool(int(extracted_tag_data["mod"]))
                self.is_sub = bool(int(extracted_tag_data["subscriber"]))
                self.is_turbo = bool(int(extracted_tag_data["turbo"]))

                # The user's role
                if extracted_tag_data["user-type"] is ' ':
                    self.user_type = filler
                else:
                    self.user_type = extracted_tag_data["user-type"] # Admin, Staff, Mod, etc...

                # Styling information
                if not extracted_tag_data["color"]:
                    self.color = filler
                else:
                    self.color = extracted_tag_data["color"]

                # NOTE: Twitch actually shows the months/bits for the badge a user has. I just
                #       saw someone talked that had "subscriber/6" and "bits/10000", then checked
                #       twitch chat and it showed them with a 6 month sub badge and a cheer 10000
                #       badge!
                self.badges = {}
                if extracted_tag_data["badges"]:
                    for badge in extracted_tag_data["badges"].split(','):
                        data = badge.split('/')
                        # TODO: Convert data[1] to int?
                        self.badges[data[0]] = data[1]

                # Emotes!
                self.emotes = []
                if extracted_tag_data["emotes"]:
                    for info in extracted_tag_data["emotes"].split('/'):
                        for position in info.split(":")[1].split(','):
                            start = int(position.split('-')[0])
                            end = int(position.split('-')[1]) + 1
                            name = self.message[start:end]
                            id = int(info.split(":")[0])
                            self.emotes.append(Emote(name,
                                                      id,
                                                      start,  # Start position
                                                      end))  # end position

                # Bot information (Useful for bots) It's the first word of their
                # chat message.
                self.command = self.message.split()[0]
            # TODO: Maybe change this?
            except (KeyError, TypeError) as error_information:
                error_filler = ("ERROR: Please check  user.error if this "
                                "issue persists.")
                self.name = error_filler
                self.message = error_filler
                self.message_history = []
                self.chatted_from = error_filler
                self.is_broadcaster = False
                self.is_mod = False
                self.is_sub = False
                self.is_turbo = False
                self.user_type = error_filler
                self.color = error_filler
                self.emotes = []
                self.command = error_filler
                self.error = error_information

    # NOTE: Would "reply" be a better name?
    def send_message(self, message: str, append_symbol=True) -> bool:
        """
        Send a message to this user. This is for chatting, not twitch messaging
        nor whispering. It pops up in chat as "@user <message>"

        Args:
            message: What you would like to tell the user
            append_symbol: Appends the '@' symbol at the start of the username

        Returns:
            True: When it succeeds in sending a message
            False: If it fails to send the message.

        """
        if not message:
            return False

        if append_symbol:
            formatted_message = "{}{} {}".format('@', self.name, message)
        else:
            formatted_message = "{} {}".format(self.name, message)

        if not chat(formatted_message, self.chatted_from):
            return False
        else:
            return True

    def purge(self, reason='') -> bool:
        """
        Purges (times out for 1 second) this user.

        Args:
            reason: The reason for the purging of this user

        Returns:
            True: When it succeeds to send the purge command
            False: When it fails to send the purge command

        """
        if self.timeout(1, reason):
            return True
        else:
            return False

    def timeout(self, seconds=600, reason='') -> bool:
        """
        Times out this user.

        Args:
            seconds: The duration of the timeout in seconds. Default 600.
            reason: The reason for timing out this user.

        Returns:
            True: When it succeeds in sending the timeout command to twitch
            False When it fails at sending the timeout command to twitch

        """
        # Doesn't matter if the reason is blank!
        if chat("/timeout {} {} {}".format(self.name, seconds, reason), self.chatted_from):
            return True
        else:
            return False

    def ban(self, reason='') -> bool:
        """
        Permanently Bans this user. Can be given a reason, which shows up in
        chat as a twitch notification.

        Args:
            reason: The reason for banning the user

        Returns:
            True: If it succeeds at sending the Ban command to twitch.
            False: if it fails at sending the Ban command to twitch.

        """
        # Doesn't matter if the reason is blank!
        if chat("/ban {} {}".format(self.name, reason), self.chatted_from):
            return True
        else:
            return False


# Parsing-----------------------------------------------------------------------
# TODO: Create functions for managing each command. Use _manage_tags to determine
#       which function to use.
# TODO: Manage GLOBALUSERSTATE, use to create a "me" variable that contains our
#       user's information.
def _manage_tags(input_data: str):
    """
    Manages most tags given by Twitch. Specifically, it manages PRIVMSG, NOTICE,
    ROOMSTATE, and CLEARCHAT tags. Updates the corresponding variables, such as
    updating the "user" variable when a new PRIVMSG is received (A single chat
    line).

    Updates the following variables:
    user: Is updated with the latest information from a single line of chat
    channels: Updates any information relating to the channel
    notification: Places the latest notification from chat, along with the
                  channel it came from
    Args:
        input_data: The data, from Twitch, that will be parsed.

    """

    if input_data:
        # At times twitch sends some broken/weird data, the one that caused
        # this one to break was '@user.tmi.twitch.tv PART #channel', since
        # JOIN/PARTS aren't meant to start with @, this was unexpected and
        # broke the parsing by being sent to the wrong parser.
        try:
            irc_logger.debug("Twitch Data: {}".format(input_data))
            twitch_data = tags_regex.findall(input_data)[0]
            irc_logger.debug("Parsed result: {}".format(twitch_data))  # DEBUG!!!
            extracted_tag_data = {}
            irc_logger.debug("Extracting data from tags.")
            for data in twitch_data[0].split(';'):
                extracted_tag_data[data.split('=')[0]] = data.split('=')[1]
            irc_logger.debug("Extracted tag data: {}".format(extracted_tag_data))
        except IndexError:
            warnings.warn("Received unexpected/broken data in _manage_tags(). Will not attempt to parse")
            return  # We will not try to parse broken/strange data.

        # TODO: Consider whether I should do USERSTATE or GLOBALUSERSTATE since
        # PRIVMSG has the same tags.
        global user
        global channels
        global notification

        # Time to parse the tags!
        if twitch_data[-3] == "PRIVMSG":  # Chat lines
            # Increment channel message count
            if twitch_data[-2] in channels:
                channels[twitch_data[-2]].message_count += 1

            # Update user variable
            irc_logger.info("Updating user variable.")
            user = User(extracted_tag_data, twitch_data[-2], twitch_data[-1], twitch_data[-4])

            # Sometimes this breaks, specifically when a user calls leave_channel()
            # as that deletes the channel from the channels dictionary.
            try:
                # Since we get to see if a user is a mod here, we add them to their
                # respective channel's moderator list if they're not already there.
                moderator_list = channels[user.chatted_from].moderators
                if user.is_mod and user.name.lower() not in moderator_list:
                    irc_logger.info("Adding {} to {}'s mod list.".format(user.name, user.chatted_from))
                    channels[user.chatted_from].moderators.append(user.name)
                elif not user.is_mod and user.name.lower() in moderator_list:
                    irc_logger.info("Removing {} from {}'s mod list.".format(user.name, user.chatted_from))
                    channels[user.chatted_from].moderators.remove(user.name)

                # We can also add them to the list of viewers if they weren't
                # already there.
                viewers = channels[user.chatted_from].viewers
                if user.name and user.name.lower() not in viewers:
                    irc_logger.info("Adding {} to {}'s viewer list.".format(user.name.lower(), user.chatted_from))
                    channels[user.chatted_from].viewers.append(user.name.lower())
            except KeyError:
                irc_logger.error("Attempted to add user to a channel list that no longer exists. This is likely a "
                                 "result from leaving a channel while the module parsed a user's chat message. Nothing "
                                 "can be done about it, it should not affect your program.")
                pass

        elif twitch_data[-3] == "NOTICE":  # Twitch NOTICE tag management
            affected_channel = twitch_data[-2]
            message_id = twitch_data[0].split('=')[1].strip()

            notification["channel_name"] = affected_channel
            notification["message"] = twitch_data[-1]

            if "slow" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking slow mode as on for {}.".format(affected_channel))
                    channels[affected_channel].slow = True
                else:
                    irc_logger.info("Marking slow mode as off for {}.".format(affected_channel))
                    channels[affected_channel].slow = False
            elif "subs" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking subscriber mode as on for {}.".format(affected_channel))
                    channels[affected_channel].subscriber = True
                else:
                    irc_logger.info("Marking subscriber mode as off for {}.".format(affected_channel))
                    channels[affected_channel].subscriber = False
            elif "r9k" in message_id:
                if "on" in message_id:
                    irc_logger.info("Marking r9k mode as on for {}.".format(affected_channel))
                    channels[affected_channel].r9k = True
                else:
                    irc_logger.info("Marking r9k mode as off for {}.".format(affected_channel))
                    channels[affected_channel].r9k = False

            # TODO: NEED TO CHECK IF THIS STILL WORKS!
            elif "host" in message_id:
                if "on" in message_id:
                    hosted_channel = re.findall(r"Now hosting (\w+).", notification["message"])[0]
                    irc_logger.info("Marking host mode as on for {}.".format(affected_channel))
                    channels[affected_channel].hosting = True
                    irc_logger.info("Setting {} as hosted channel for {}.".format(
                        hosted_channel, affected_channel
                    ))
                    channels[affected_channel].hosted_channel = hosted_channel
                else:
                    irc_logger.info("Marking host mode as off and resetting hosted channel")
                    channels[affected_channel].hosting = False
                    channels[affected_channel].hosted_channel = ''

        elif twitch_data[-3] == "ROOMSTATE":
            affected_channel = twitch_data[-2]

            # TODO: Check for "followers-only"
            irc_logger.info("Getting channel ROOMSTATE (Basic information for when you first enter a channel).")
            if "broadcaster-lang" in extracted_tag_data.keys():
                if extracted_tag_data["broadcaster-lang"]:
                    irc_logger.info("Setting {}'s channel language as {}.".format(
                        affected_channel, extracted_tag_data["broadcaster-lang"]
                    ))
                    channels[affected_channel].language = extracted_tag_data["broadcaster-lang"]
                else:
                    irc_logger.warning("Channel {} has not set its language. This is ok in most cases.".format(affected_channel))
                    channels[affected_channel].language = ''
            elif "slow" in extracted_tag_data.keys():
                if extracted_tag_data["slow"] == '0':
                    irc_logger.info("Marking slow mode as off for {}.".format(affected_channel))
                    channels[affected_channel].slow_mode = False
                    channels[affected_channel].slow_time = 0
                    irc_logger.info("Slow time is set to 0 for {}.".format(affected_channel))

                else:
                    channels[affected_channel].slow_mode = True
                    channels[affected_channel].slow_time = int(extracted_tag_data["slow"])
                    irc_logger.info("Slow time is set to {} for {}.".format(
                        extracted_tag_data["slow"], affected_channel
                    ))
            elif "subs-only" in extracted_tag_data.keys():
                if '0' in extracted_tag_data["subs-only"]:
                    irc_logger.info("Marking subscriber mode as off for {}.".format(affected_channel))
                    channels[affected_channel].subscribers_only = False
                else:
                    irc_logger.info("Marking subscriber mode as on for {}.".format(affected_channel))
                    channels[affected_channel].subscribers_only = True
            elif "followers-only" in extracted_tag_data.keys():
{'fraction_non_alphanumeric': 0.10093295904887714, 'fraction_numerical': 0.003591479524438573, 'mean_word_length': 1.978910477127398, 'pattern_counts': {'":': 14, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '39272', 'n_tokens_mistral': 13313, 'n_tokens_neox': 12360, 'n_words': 4580}