diff --git a/bots.py b/bots.py deleted file mode 100644 index cc3ef55..0000000 --- a/bots.py +++ /dev/null @@ -1 +0,0 @@ -botList = ["Nightbot", "StreamElements", "Moobot", "praxis_bot"] \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..e4899e4 --- /dev/null +++ b/config.py @@ -0,0 +1,83 @@ +from enum import Enum + + +class Speaker(Enum): + GOOGLE_TEXT_TO_SPEECH = 1 + STREAMLABS_API = 2 + + +class FileNameStrategy(Enum): + TIME_BASED = 1 + CONTENT_BASED = 2 + + +class PollyVoices(Enum): + Aditi = "Aditi" + Amy = "Amy" + Astrid = "Astrid" + Bianca = "Bianca" + Brian = "Brian" + Camila = "Camila" + Carla = "Carla" + Carmen = "Carmen" + Celine = "Celine" + Chantal = "Chantal" + Conchita = "Conchita" + Cristiano = "Cristiano" + Dora = "Dora" + Emma = "Emma" + Enrique = "Enrique" + Ewa = "Ewa" + Filiz = "Filiz" + Geraint = "Geraint" + Giorgio = "Giorgio" + Gwyneth = "Gwyneth" + Hans = "Hans" + Ines = "Ines" + Ivy = "Ivy" + Jacek = "Jacek" + Jan = "Jan" + Joanna = "Joanna" + Joey = "Joey" + Justin = "Justin" + Karl = "Karl" + Kendra = "Kendra" + Kimberly = "Kimberly" + Lea = "Lea" + Liv = "Liv" + Lotte = "Lotte" + Lucia = "Lucia" + Lupe = "Lupe" + Mads = "Mads" + Maja = "Maja" + Marlene = "Marlene" + Mathieu = "Mathieu" + Matthew = "Matthew" + Maxim = "Maxim" + Mia = "Mia" + Miguel = "Miguel" + Mizuki = "Mizuki" + Naja = "Naja" + Nicole = "Nicole" + Penelope = "Penelope" + Raveena = "Raveena" + Ricardo = "Ricardo" + Ruben = "Ruben" + Russell = "Russell" + Salli = "Salli" + Seoyeon = "Seoyeon" + Takumi = "Takumi" + Tatyana = "Tatyana" + Vicki = "Vicki" + Vitoria = "Vitoria" + Zeina = "Zeina" + Zhiyu = "Zhiyu" + + +botList = ("Nightbot", "StreamElements", "Moobot", "praxis_bot") + +slurList = ("fag", "faggot", "niga", "nigga", "nigger", "retard", "tard", "rtard", "coon") + +currentSpeaker = Speaker.STREAMLABS_API +fileNameStrategy = FileNameStrategy.CONTENT_BASED +streamlabsVoice = PollyVoices.Justin diff --git a/db.py b/db.py index c028711..253afb1 100644 --- a/db.py +++ b/db.py @@ -1,10 +1,9 @@ -import mysql.connector -import os import db_cred as db_credentials import pandas as pd from sqlalchemy import create_engine + class db_module(): def __init__(self): super().__init__() @@ -15,27 +14,26 @@ class db_module(): self.engine = create_engine(db_credentials.engine_url) print("Engine Created") - def create_table(self, tableName:str = ""): + def create_table(self, tableName: str = ""): pass - def does_table_exist(self, tableName:str = ""): + def does_table_exist(self, tableName: str = ""): pass - def delete_table(self, tableName:str = ""): + def delete_table(self, tableName: str = ""): pass - - #This was a old function used prior to the creation of this class. I need to remake it. - def get_data_old(self, tableName:str = "", key:str = ""): + + # This was a old function used prior to the creation of this class. I need to remake it. + def get_data_old(self, tableName: str = "", key: str = ""): table = '_channel_commands' table = tableName - - - df = pd.read_sql_query('SELECT * FROM '+ table, engine) + + df = pd.read_sql_query('SELECT * FROM ' + table, engine) stmt = "trigger == '" + key + "'" temp = df.query(stmt) result = temp.get("response") - #print(result) + # print(result) i = len(temp.index.values) if i == 1: @@ -45,20 +43,19 @@ class db_module(): output = "$$None$$" return output - def get_data(self, tableName:str = "", key:str = ""): + def get_data(self, tableName: str = "", key: str = ""): pass - def insert_data(self, tableName:str = "", param:str = ""): + def insert_data(self, tableName: str = "", param: str = ""): pass - def edit_data(self, tableName:str = "", key:str = "", param:str = ""): + def edit_data(self, tableName: str = "", key: str = "", param: str = ""): pass - def delete_data(self, tableName:str = "", key:str = ""): + def delete_data(self, tableName: str = "", key: str = ""): pass - if __name__ == "__main__": - db_connection = db_module() - db_connection.setup_engine() \ No newline at end of file + db_connection = db_module() + db_connection.setup_engine() diff --git a/db_cred.py b/db_cred.py index 382ad5b..13fc233 100644 --- a/db_cred.py +++ b/db_cred.py @@ -1 +1 @@ -engine_url = "mysql+mysqlconnector://" \ No newline at end of file +engine_url = "mysql+mysqlconnector://" diff --git a/hotkey_script.py b/hotkey_script.py index 560dd3c..eebe3fa 100644 --- a/hotkey_script.py +++ b/hotkey_script.py @@ -1,6 +1,7 @@ import pygetwindow as gw from pynput.keyboard import Key, Controller import time + keyboard = Controller() diff --git a/main.py b/main.py index 373f60c..173bb9f 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,5 @@ -# Install these: -# pip install mysql-connector-python -# pip install pynput -# pip install twitch-python -# pip install SQLAlchemy -# pip install pandas -# pip install numpy -# pip install gTTS -# pip install playsound +# I moved all the requirements into requirements.txt. +# you can install everything with pip install -r requirements.txt while you're in the directory import sys import time @@ -17,6 +10,7 @@ import utilities_script as utility twitch_chat: twitch_script_class.Twitch_Module + def main(): print("Connecting to Channels...") @@ -24,9 +18,8 @@ def main(): twitch_chat = twitch_script_class.Twitch_Module() twitch_chat.join_channel("thecuriousnerd") - #twitch_chat.send_message("activated") + # twitch_chat.send_message("activated") if __name__ == "__main__": main() - \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d052841 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +mysql-connector-python +pynput +twitch-python +SQLAlchemy +pandas +numpy +gTTS +playsound \ No newline at end of file diff --git a/slurs.py b/slurs.py deleted file mode 100644 index 4743cbd..0000000 --- a/slurs.py +++ /dev/null @@ -1 +0,0 @@ -slurList = ["fag", "faggot", "niga", "nigga", "nigger", "retard", "tard", "rtard", "coon"] diff --git a/tests/test_tts.py b/tests/test_tts.py new file mode 100644 index 0000000..faf992e --- /dev/null +++ b/tests/test_tts.py @@ -0,0 +1,12 @@ +import unittest + +import tts + +class TTSTest(unittest.TestCase): + + def test_file_name(self): + tts.create_file_name("test", "mp3") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_twitch_script_class.py b/tests/test_twitch_script_class.py new file mode 100644 index 0000000..63a6f52 --- /dev/null +++ b/tests/test_twitch_script_class.py @@ -0,0 +1,35 @@ +import unittest +import twitch_script_class +import twitch + + +testValidUrls = ['https://shady.ru', 'http://stolencards.zn', 'https://i.imgur.com/FL6slHd.jpg'] +testInvalidUrls = ['this is just a sentence. With a period', 'gotta have some other stuff', 'bad punctuation.does not produces false positives'] + + +class TwitchBotTest(unittest.TestCase): + def setUp(self): + self.bot = twitch_script_class.Twitch_Module() + + def test_find_url(self): + bot = self.bot + for link in testInvalidUrls: + msg = twitch.chat.Message("", "", link) + t = bot.contains_url(msg) + assert not t + + for link in testValidUrls: + msg = twitch.chat.Message("", "", link) + t = bot.contains_url(msg) + assert t + + def test_find_slur(self): + nonSlurMessage = twitch.chat.Message("", "", "hey look, a normal sentence") + slurMessage = twitch.chat.Message("", "", "fag is a hateful word that shouldn't be used anymore") + assert not self.bot.contains_slur(nonSlurMessage) + assert self.bot.contains_slur(slurMessage) + + +if __name__ == '__main__': + unittest.main() + diff --git a/tts.py b/tts.py index af4b554..3223b76 100644 --- a/tts.py +++ b/tts.py @@ -1,35 +1,109 @@ -from gtts import gTTS +import datetime +import hashlib import os -import datetime - +import requests +from gtts import gTTS from playsound import playsound +import config -def tts(inputText:str, *args): +streamLabsUrl = "https://streamlabs.com/polly/speak" - destPath = os.getcwd() + "\\tts\\" - time = datetime.datetime.now() - fileName:str = time.strftime("%m-%d-%Y_%H-%M-%S") + "_tts.mp3" +def tts(inputText: str, *args): + outpath = create_speech_file(inputText) + playsound(outpath) - if len(args) == 1: - fileName = args[0] + "_tts.mp3" - tts = gTTS(text=inputText, lang='en') - tts.save(destPath + fileName) +def create_speech_gtts(input_text: str): + """ + Will create a sound file for the provided text by using gTTS + :param input_text: any reasonable english text + :return: returns the path of the file for the sound + """ + path = os.path.join(get_tts_dir(), create_file_name(input_text, "mp3")) + if not os.path.exists(path): + sound_digest = gTTS(text=input_text, lang='en') + sound_digest.save(path) + return path - playsound(destPath + fileName) - #os.system(filename) +def create_speech_streamlabs(text: str): + """ + Will create a sound file for the provided text by querying and downloading a file from streamlabs + :param text: any reasonable english text + :return: returns the path of the file for the sound + """ + path = os.path.join(get_tts_dir(), create_file_name(text, "ogg")) + if not os.path.exists(path): + body = {"voice": config.streamlabsVoice.value, "text": text} + resp = requests.post(streamLabsUrl, data=body).json() + sound_file_url = resp["speak_url"] + if sound_file_url is not None: + sound_bytes = requests.get(sound_file_url, stream=True) + f = open(path, "+wb") + f.write(sound_bytes.content) + f.close() + return path + + +speechCreationFunctions = { # this is a mapping of the Speaker enum to function pointers + config.Speaker.STREAMLABS_API: create_speech_streamlabs, + config.Speaker.GOOGLE_TEXT_TO_SPEECH: create_speech_gtts +} + + +def create_speech_file(text: str): + """ + Helper function that will create a sound file for the provided text. This will use the configuration in config.py + to use TTS engines and name the file + :param text: the text you would like to turn into a sound file + :return: returns the path of the sound file + """ + text_creation_function = speechCreationFunctions.get(config.currentSpeaker) + output_path = text_creation_function(text) + return output_path + + +def create_file_name(text: str, ext: str): + """ + :param text: the content of the message. using the CONTENT_BASED FileNameStrategy, this will (ostensibly) produce a + unique file name based on the content of the message. Two messages of equal content will produce the same name + :param ext: the desired file extension i.e. mp3, ogg, wav, etc... + :return: returns the formatted filename i.e. 01-01-20_01-01-01_tts.mp3 + """ + if config.fileNameStrategy == config.FileNameStrategy.CONTENT_BASED: + unique_id = hashlib.md5(bytes(text, 'utf-8')).hexdigest() + return "%s_tts.%s" % (unique_id, ext) + + elif config.fileNameStrategy == config.FileNameStrategy.TIME_BASED: + time = datetime.datetime.now() + return "%s_tts.%s" % (time.strftime("%m-%d-%Y_%H-%M-%S"), ext) + + else: + return "unconfigured_tts.%s" % ext + def play_speech(fileName): - destPath = os.getcwd() + "\\tts\\" + destPath = get_tts_dir() playsound(destPath + fileName) + +def get_tts_dir(): + """ + Checks for the tts directory, and will create it if it does not exist + :return: the relative file path of the tts dir + """ + dir = os.path.join(os.getcwd(), "tts") # this is platform-agnostic + if not os.path.exists(dir): + os.mkdir(dir) + return dir + + if __name__ == "__main__": print("Enter Text: ") textInput = str(input()) - print("Custom FileName? y/n: ") + print("Custom FileName? y/n: ") bool_string = str(input()) if bool_string == "y": @@ -37,4 +111,4 @@ if __name__ == "__main__": fileName = str(input()) tts(textInput, fileName) else: - tts(textInput) \ No newline at end of file + tts(textInput) diff --git a/twitch_cred.py b/twitch_cred.py index 07d0800..e5dd8d4 100644 --- a/twitch_cred.py +++ b/twitch_cred.py @@ -1,3 +1,5 @@ +# So I'm a little conflicted here. My nit-picky self says that this should be a class you have to instantiate rather +# than static variables... I'll leave this alone for now, but you may wish to refactor this in future username = "" helix = "" diff --git a/twitch_script_class.py b/twitch_script_class.py index 9a4eeda..fe6cd5f 100644 --- a/twitch_script_class.py +++ b/twitch_script_class.py @@ -1,22 +1,26 @@ import random +import re + import twitch import twitch.chat + +import config as config +import db +import tts import twitch_cred as twitch_credentials -import bots as botList -import slurs as slurList -import tts - -import db class Twitch_Module(): def __init__(self): super().__init__() self.chat: twitch.Chat - self.tts_enabled:bool = False - self.tts_whitelist_enabled:bool = False - self.links_allowed:bool = True - self.whitelisted_users:list = ["thecuriousnerd", "theredpoint", "lakotor"] + self.tts_enabled: bool = False + self.tts_whitelist_enabled: bool = False + self.links_allowed: bool = True + self.whitelisted_users: list = ["thecuriousnerd", "theredpoint", "lakotor"] + # don't freak out, this is *merely* a regex for matching urls that will hit just about everything + self._urlMatcher = re.compile( + "(https?:(/{1,3}|[a-z0-9%])|[a-z0-9.-]+[.](com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))") def join_channel(self, channel_name): channel_name = "#" + channel_name @@ -28,35 +32,32 @@ class Twitch_Module(): helix=twitch.Helix(twitch_credentials.helix, use_cache=True) ) self.chat.subscribe(self.twitch_chat) - - print("Connected to Channel: ", channel_name) - + print("Connected to Channel: ", channel_name) def leave_channel(self): - print("Leaving Channel",self.chat.channel) + print("Leaving Channel", self.chat.channel) self.chat.irc.leave_channel(self.chat.channel) self.chat.irc.socket.close() def send_message(self, message): - self.chat.send(message) + self.chat.send(message) def send_whisper(self, user, message): pass - #This reacts to messages + # This reacts to messages def twitch_chat(self, message: twitch.chat.Message) -> None: - print("[#"+ message.channel + "](" + message.sender + ")> " + message.text) + print("[#" + message.channel + "](" + message.sender + ")> " + message.text) if message.channel == "thecuriousnerd": - if self.isSenderBot(message) == False: + if not self.isSenderBot(message): if message.sender.lower() == "thecuriousnerd": self.eval_commands(message) self.tts_message(message) - def eval_commands(self, message: twitch.chat.Message): - containsURL:bool = self.contains_url(message) + containsURL: bool = self.contains_url(message) if message.text.startswith('!tts start'): print("tts activated on #" + message.channel) @@ -67,11 +68,11 @@ class Twitch_Module(): print("tts deactivated on #" + message.channel) self.send_message("tts deactivated") self.tts_enabled = True - + if message.text.startswith('!test'): print("!test Detected") message.chat.send("test acknowledged") - #message.chat.send(f'@{message.user().display_name}, you have {message.user().view_count} views.') + # message.chat.send(f'@{message.user().display_name}, you have {message.user().view_count} views.') if message.text.startswith('!roll'): try: @@ -79,82 +80,73 @@ class Twitch_Module(): except Exception: self.send_message("{something went wrong}") print("{something went wrong}") - def tts_message(self, message: twitch.chat.Message): - if self.contains_slur(message) == False: - if self.tts_enabled == True: - if message.text.startswith('!') == False: - if message.sender.lower() == message.channel: - tts.tts(message.sender + " says, " + message.text) - else: - #tts.tts(message.sender + " says, " + message.text) - tts.tts(message.sender + " says, " + message.text, message.channel + " user msg") + if not self.contains_slur(message): + if self.tts_enabled: + if not message.text.startswith('!'): + text_to_say: str = "%s says, %s" % (message.sender, message.text) + channel_text = "%s user msg" % message.channel + + if message.sender.lower() == message.channel: + tts.tts(text_to_say) + else: + # tts.tts(message.sender + " says, " + message.text) + tts.tts(text_to_say, channel_text) def contains_url(self, message: twitch.chat.Message): - containsURL:bool = False - if message.text.lower().find("http") != -1: - containsURL = True - if message.text.lower().find("https") != -1: - containsURL = True - if message.text.lower().find(".com") != -1: - containsURL = True - if message.text.lower().find(".net") != -1: - containsURL = True - if message.text.lower().find(".org") != -1: - containsURL = True - if message.text.lower().find(".tv") != -1: - containsURL = True - if message.text.lower().find(".io") != -1: - containsURL = True - if containsURL == True: - print("<{ link detected! }> " + " [#"+ message.channel + "](" + message.sender + ") sent a link in chat") + containsURL = re.search(self._urlMatcher, message.text.lower()) is not None + if containsURL: + print("<{ link detected! }> " + " [#" + message.channel + "](" + message.sender + ") sent a link in chat") return containsURL - - #Checks if Sender is bot. + + # Checks if Sender is bot. def isSenderBot(self, message: twitch.chat.Message): isBot = False - for bot in botList.botList: + for bot in config.botList: if message.sender.lower() == bot.lower(): isBot = True - print("<{ bot detected! }> " + " [#"+ message.channel + "](" + message.sender + ") is a bot") + print("<{ bot detected! }> " + " [#" + message.channel + "](" + message.sender + ") is a bot") return isBot - #Checks for basic slurs. + # Checks for basic slurs. def contains_slur(self, message: twitch.chat.Message): - containsSlur:bool = False + containsSlur: bool = False parsedMessage = message.text.split(" ") for word in parsedMessage: - for slur in slurList.slurList: + for slur in config.slurList: if word.lower() == slur: containsSlur = True + break # we want to immediately escape if we found a slur + if containsSlur: + break - if containsSlur == True: - print("<{ slur detected! }> " + " [#"+ message.channel + "](" + message.sender + ") used a slur in chat") + if containsSlur: + print("<{ slur detected! }> " + " [#" + message.channel + "](" + message.sender + ") used a slur in chat") return containsSlur - #Rolls Dice. + # Rolls Dice. def dice_roll(self, message: twitch.chat.Message): - diceRoll:str = "" + diceRoll: str = "" self.send_message("Rolling Dice...") print("Rolling Dice...") temp_preParsedMessage = message.text.split("+") tempParsedMessage = temp_preParsedMessage[0].split(" ") - temp_dice_stmt:str = tempParsedMessage[1] + temp_dice_stmt: str = tempParsedMessage[1] parsedMessage = temp_dice_stmt.lower().split("d") - loopBool:bool = False + loopBool: bool = False if parsedMessage[0] != "": loopBool = True if loopBool == True: if int(parsedMessage[0]) == 1: loopBool = False - #If roll is in xdx+x format + # If roll is in xdx+x format if loopBool == True: - rolls:list = [] + rolls: list = [] for x in range(int(parsedMessage[0])): rolls.append(random.randint(1, int(parsedMessage[1]))) @@ -162,18 +154,20 @@ class Twitch_Module(): for roll in rolls: rollTotal = rollTotal + roll diceRoll = diceRoll + str(roll) + ", " - diceRoll = diceRoll[:-2] #This removes the last two characters in the string + diceRoll = diceRoll[:-2] # This removes the last two characters in the string if len(temp_preParsedMessage) == 2: - diceRoll = diceRoll + " + " + temp_preParsedMessage[1] + " = " + str(rollTotal + int(temp_preParsedMessage[1])) + diceRoll = diceRoll + " + " + temp_preParsedMessage[1] + " = " + str( + rollTotal + int(temp_preParsedMessage[1])) else: diceRoll = diceRoll + " = " + str(rollTotal) - #If roll is in dx+x format + # If roll is in dx+x format if loopBool == False: - roll:int = random.randint(1, int(parsedMessage[1])) - + roll: int = random.randint(1, int(parsedMessage[1])) + if len(temp_preParsedMessage) == 2: - diceRoll = str(roll) + " + " + temp_preParsedMessage[1] + " = " + str(roll + int(temp_preParsedMessage[1])) + diceRoll = str(roll) + " + " + temp_preParsedMessage[1] + " = " + str( + roll + int(temp_preParsedMessage[1])) else: diceRoll = str(roll) @@ -182,8 +176,8 @@ class Twitch_Module(): self.send_message(diceRoll) -#This is a old function used prior to the creation of the Twitch_Module class above. -#I need to make a new one for the class. +# This is a old function used prior to the creation of the Twitch_Module class above. +# I need to make a new one for the class. def main_chat_commands_check(channel, sender, text): response = db.basic_command_trigger(channel, sender, text) if response == "$$None$$": @@ -193,7 +187,6 @@ def main_chat_commands_check(channel, sender, text): print(response) - if __name__ == "__main__": testChat = Twitch_Module() - testChat.join_channel("thecuriousnerd") \ No newline at end of file + testChat.join_channel("thecuriousnerd") diff --git a/utilities_script.py b/utilities_script.py index bbedaf0..dc1f6b6 100644 --- a/utilities_script.py +++ b/utilities_script.py @@ -1,4 +1,3 @@ import os -clearScreen = lambda : os.system('cls' if os.name == 'nt' else 'clear') - +clearScreen = lambda: os.system('cls' if os.name == 'nt' else 'clear')