Merge branch 'dev'

This commit is contained in:
gpkvt 2022-08-13 21:00:32 +02:00
commit fedcc96610
5 changed files with 397 additions and 294 deletions

View File

@ -2,49 +2,60 @@
All notable changes to this project will be documented in this file. If there is a `Changed` section please read carefully, as this often means that you will need to adapt your `config.yml`, otherwise the bot might fail to start.
## [1.2.2] - 2022-08-13
### Changed 1.2.2
* The message queue is only queried when the Init button is pressed
* Further code optimization
### Fixed 1.2.2
* Minor fixes
## [1.2.1] - 2022-08-13
### Changed
### Changed 1.2.1
* Reworked internal code structure
* Reworked internal code structure
### Fixed
### Fixed 1.2.1
* Publish vote info in chat when reloading config was not working when TTS was disabled
* Casting votes was allowed for broadcaster and mods only
* Publish vote info in chat when reloading config was not working when TTS was disabled
* Casting votes was allowed for broadcaster and mods only
## [1.2.0] - 2022-08-13
### Added
* `!random` feature (see README.md for details)
* `!random` feature (see README.md for details)
### Changed
### Changed 1.2.0
* The vote result will be read out
* The vote result will be read out
### Fixed
### Fixed 1.2.0
* Improved handling of missing config values.
* Improved handling of missing config values
## [1.1.0] - 2022-08-12
### Added
### Added 1.1.0
* `!quickvote` feature (see README.md for details)
* `!ping` command added
* Configoption to start TTS in disabled mode
* OAuth-Token generator
* Webbrowser autostart
* `!quickvote` feature (see README.md for details)
* `!ping` command added
* Configoption to start TTS in disabled mode
* OAuth-Token generator
* Webbrowser autostart
### Changed
### Changed 1.1.0
* You need to review your `config.yml` as there a new config values added.
* The bot replies with a chat message when `!ton` or `!toff` is used
* You need to review your `config.yml` as there a new config values added.
* The bot replies with a chat message when `!ton` or `!toff` is used
### Fixed
### Fixed 1.1.0
* Improved error handling
* Improved error handling
## [1.0.0] - 2022-08-11

View File

@ -34,7 +34,7 @@ By using Javascript for the actual TTS part it's not only very easy to access th
Adapt `config.yml` to your needs. Example:
```
``` lang=yaml
irc:
channel: "#gpkvt"
username: "ttsbot"
@ -82,11 +82,11 @@ whitelist:
##### irc
* `channel`: Channel you want to monitor (e.g. #gpkvt)
* `username`: The bots username (e.g. gpkvt)
* `oauth_token`: The bots OAUTH-Token (e.g. oauth:ohkoace0wooghue8she9xaN0nooSau)
* `server`: Twitch IRC server to be used (default should be fine)
* `clearmsg_timeout`: Time to wait for an moderator to delete a message, before it's added to the TTS queue
* `channel`: Channel you want to monitor (e.g. #gpkvt)
* `username`: The bots username (e.g. gpkvt)
* `oauth_token`: The bots OAUTH-Token (e.g. oauth:ohkoace0wooghue8she9xaN0nooSau)
* `server`: Twitch IRC server to be used (default should be fine)
* `clearmsg_timeout`: Time to wait for an moderator to delete a message, before it's added to the TTS queue
You can generate your `oauth_token` by leaving the value empty when starting `tts.exe/tts.py`. The integrated webserver will then provide an OAuth-Generator. Due to limitations to the `redirect_url` parameter used by twitch, this is only possible if you use Port `8080` or `80` as `http:bind`. If you use a different port, you will need to use another [Twitch OAuth Generator](https://html.duckduckgo.com/html/?q=twitch+oauth+token+generator). The bot will need `chat:edit` and `chat:read` permissions.
@ -94,36 +94,36 @@ Please note that the `oauth_token` is valid for approximately 60 days. If it bec
##### http
* `port`: Internal Webserver Port to listen to (e.g. 8080)
* `bind`: Interface/IP to bind server to (e.g. localhost)
* `port`: Internal Webserver Port to listen to (e.g. 8080)
* `bind`: Interface/IP to bind server to (e.g. localhost)
##### bot
* `start_enabled`: Enable the bot on start? If `False` you need to use `!ton` first to make TTS work.
* `subonly`: If `True` only Subs can use TTS
* `modonly`: If `True` only Mods can use TTS
* `message_length`: Maximum allowed message length for TTS
* `start_enabled`: Enable the bot on start? If `False` you need to use `!ton` first to make TTS work.
* `subonly`: If `True` only Subs can use TTS
* `modonly`: If `True` only Mods can use TTS
* `message_length`: Maximum allowed message length for TTS
##### messages
* `toff`: The bots reply when `!toff` is used.
* `ton`: The bots reply when `!ton` is used.
* `too_long`: The bots reply if message exceeds `message_length`
* `disabled`: The bots reply if TTS is disabled
* `denied`: The bots reply if the user is not allowed to use TTS
* `subonly`: The bots reply if `subonly` is active and the user isn't one.
* `whitelist`: The bots reply if `whitelist` is set and user isn't on the list.
* `ready`: The bots init message
* `says`: Prefix to add between username and message
* `votestart`: Message when a quickvote is started.
* `voteend`: Message if a quickvote ends.
* `votenobody`: Message if quickvote ends, but nobody has voted.
* `voteresult`: Prefix for the result (will be read out)
* `votes`: Suffix to vote count.
* `toff`: The bots reply when `!toff` is used.
* `ton`: The bots reply when `!ton` is used.
* `too_long`: The bots reply if message exceeds `message_length`
* `disabled`: The bots reply if TTS is disabled
* `denied`: The bots reply if the user is not allowed to use TTS
* `subonly`: The bots reply if `subonly` is active and the user isn't one.
* `whitelist`: The bots reply if `whitelist` is set and user isn't on the list.
* `ready`: The bots init message
* `says`: Prefix to add between username and message
* `votestart`: Message when a quickvote is started.
* `voteend`: Message if a quickvote ends.
* `votenobody`: Message if quickvote ends, but nobody has voted.
* `voteresult`: Prefix for the result (will be read out)
* `votes`: Suffix to vote count.
##### log
* `level`: The loglevel, valid values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
* `level`: The loglevel, valid values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
Do not use `DEBUG` in a production environment.
@ -139,7 +139,7 @@ You can add a whitelist section to `config.yml`, a whitelist will override any o
A whitelist looks as follows:
```
``` lang=yaml
whitelist:
- gpkvt
- foo
@ -152,7 +152,7 @@ Please note: Usernames MUST be lowercase.
### Executing program
Execute `tts.exe` (or `tts.py` if you have Python installed), open the TTS webpage in your browser (the URL depends on your `bind` and `port` configuration, usually it's just http://localhost). Click the `Init` button at the button of the TTS webpage (you should hear `Init complete`).
Execute `tts.exe` (or `tts.py` if you have Python installed), open the TTS webpage in your browser (the URL depends on your `bind` and `port` configuration, usually it's just `http://localhost`). Click the `Init` button at the button of the TTS webpage (you should hear `Init complete`).
Connect to the configured Twitch channel and send a message starting with `!tts`. After a few seconds (depending on your `clearmsg_timeout` config), the message should be read.
@ -160,11 +160,11 @@ Connect to the configured Twitch channel and send a message starting with `!tts`
Additional commands (broadcaster and mods only) are:
* `!ping`: Check if bot is alive (the bot should reply: `Pong!`)
* `!toff`: Turn TTS off (will also empty the current TTS queue)
* `!ton`: Turn TTS back on
* `!dtts <username>`: Disable TTS for the given user
* `!ptts <username>`: Allow TTS for the given user
* `!ping`: Check if bot is alive (the bot should reply: `Pong!`)
* `!toff`: Turn TTS off (will also empty the current TTS queue)
* `!ton`: Turn TTS back on
* `!dtts <username>`: Disable TTS for the given user
* `!ptts <username>`: Allow TTS for the given user
### Additional features
@ -180,10 +180,10 @@ The `!random` command will read a random line from a file called `random.txt`. Y
If you prefer to build your own `tts.exe` instead of using the shipped one, you can do as follows:
* Install Python 3
* Install pyinstaller: `pip install pyinstaller`
* Install the required dependencies: `pip install -r requirements.txt -v`
* Create the executeable: `pyinstaller --onefile tts.py`
* Install Python 3
* Install pyinstaller: `pip install pyinstaller`
* Install the required dependencies: `pip install -r requirements.txt -v`
* Create the executeable: `pyinstaller --onefile tts.py`
## Voices
@ -211,9 +211,9 @@ This project is licensed under the GPLv3 License - see [LICENSE](https://gitlab.
### Ideas and Testing
* [GERBrowny and community](https://www.twitch.tv/gerbrowny/) ![](https://static-cdn.jtvnw.net/emoticons/v2/303172270/static/light/1.0)
* [DerZugger and community](https://www.twitch.tv/derzugger/) ![](https://static-cdn.jtvnw.net/emoticons/v2/302400142/static/light/1.0)
* [Timmeh74 and community](https://www.twitch.tv/timmeh74/) ![](https://static-cdn.jtvnw.net/emoticons/v2/300192675/static/light/1.0)
* [GERBrowny and community](https://www.twitch.tv/gerbrowny/) ![Emote](https://static-cdn.jtvnw.net/emoticons/v2/303172270/static/light/1.0)
* [DerZugger and community](https://www.twitch.tv/derzugger/) ![Emote](https://static-cdn.jtvnw.net/emoticons/v2/302400142/static/light/1.0)
* [Timmeh74 and community](https://www.twitch.tv/timmeh74/) ![Emote](https://static-cdn.jtvnw.net/emoticons/v2/300192675/static/light/1.0)
### Libraries

BIN
dist/tts.exe vendored

Binary file not shown.

5
tts.js
View File

@ -34,6 +34,7 @@ document.querySelector("#start").addEventListener("click", () => {
speech.text = "Init complete";
window.speechSynthesis.speak(speech);
$("#start").hide();
init();
});
document.querySelector("#pause").addEventListener("click", () => {
@ -54,7 +55,7 @@ function sleep(ms) {
reload = true;
$(document).ready(function() {
function init() {
setInterval(function(){
if (reload) {
$.ajax({
@ -89,4 +90,4 @@ $(document).ready(function() {
});
}
}, 1000);
});
};

319
tts.py
View File

@ -95,23 +95,22 @@ class IRC:
"""
self.irc.send(bytes("PRIVMSG "+channel+" :"+user+" "+msg+"\r\n", "UTF-8"))
def get_response(self):
"""Get and process response from IRC"""
try:
resp = self.irc.recv(2048).decode("UTF-8")
logging.debug('resp:')
logging.debug(resp)
except socket.timeout:
return False
except Exception: # pylint: disable=broad-except
logging.exception('An unknown error occured while getting a IRC response.')
sys.exit(255)
if resp.find('PING') != -1:
def resp_ping(self):
""" Respond to PING """
logging.debug('PING received')
self.irc.send(bytes('PONG :tmi.twitch.tv\r\n', "UTF-8"))
if resp.find('CLEARMSG') != -1:
def resp_notice(self, resp):
""" Respond to NOTICE """
if 'Login authentication failed' in resp:
try:
raise RuntimeError()
except RuntimeError:
logging.exception('Login failed, please check your credentials and try again.')
sys.exit(251)
def resp_clearmsg(self, resp):
""" Respond to CLEARMSG """
logging.info('CLEARMSG received')
msgid = False
@ -133,25 +132,36 @@ class IRC:
msg_queue_raw = filtered_msg_queue
return True
if resp.find('NOTICE') != -1:
if 'Login authentication failed' in resp:
try:
raise RuntimeError()
except RuntimeError:
logging.exception('Login failed, please check your credentials and try again.')
sys.exit(251)
if resp.find('PRIVMSG') != -1:
def resp_privmsg(self, resp):
""" Respond to PRIVMSG """
logging.debug('PRIVMSG received')
badges = False
subscriber = False
msgid = False
msg = False
msglen = False
user = False
tts = False
tags = self.get_tags(resp)
message = self.get_message(resp)
user = tags['user']
msg = message['message']
msglen = message['length']
logging.debug('Msg: %s', msg)
logging.debug('Msg length: %s', msglen)
logging.debug('Deny List:')
logging.debug(self.tts_denied)
self.priviledged_commands(message, tags)
if msg.startswith('#') and self.quickvote_status is True:
logging.info('Quickvote: Cast detected')
self.pollcount += 1
self.poll[user] = msg.lower()
logging.debug(self.poll)
if msg.startswith('!tts'):
logging.info('!tts command detected')
self.tts_command(message, tags)
def get_tags(self, resp):
""" Strip tags from response """
tags = resp.split(';')
for tag in tags:
@ -167,121 +177,160 @@ class IRC:
user = tag.rsplit('display-name=',1)[1].lower()
logging.debug('Username: %s', user)
msg = resp.rsplit('PRIVMSG #',1)[1]
msg = msg.split(':',1)[1]
msg = msg.replace('\r\n','')
msglen = len(msg)
tags = {}
tags['badges'] = badges
tags['subscriber'] = subscriber
tags['msgid'] = msgid
tags['user'] = user
logging.debug('Msg: %s', msg)
logging.debug('Msg length: %s', msglen)
logging.debug('Deny List:')
logging.debug(self.tts_denied)
return tags
if msg.startswith('#') and self.quickvote_status is True:
logging.info('Quickvote: Cast detected')
self.pollcount += 1
self.poll[user] = msg.lower()
logging.debug(self.poll)
return True
def get_message(self, resp):
""" Process message """
msg = {}
msg['message'] = resp.rsplit('PRIVMSG #',1)[1].split(':',1)[1].replace('\r\n','')
msg['length'] = len(msg['message'])
return msg
def priviledged_commands(self, message, tags):
""" Process priviledged commands """
msg = message['message']
badges = tags['badges']
user = tags['user']
if 'broadcaster' in badges or 'moderator' in badges:
if msg.startswith('!ping'):
logging.debug("Ping check received.")
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), "Pong!")
return True
if msg.startswith('!dtts'):
elif msg.startswith('!dtts'):
logging.debug("!dtts command detected")
self.Commands.dtts(self, msg)
return True
if msg.startswith('!random'):
elif msg.startswith('!random'):
logging.info('!random command detected')
self.Commands.random(self, msg)
return True
if msg.startswith('!quickvote'):
elif msg.startswith('!quickvote'):
logging.info("!quickvote command detected")
self.Commands.quickvote(self, msg)
return True
if msg.startswith('!ptts'):
elif msg.startswith('!ptts'):
logging.debug("!ptts command detected")
self.Commands.ptts(self, msg)
return True
if msg.startswith('!toff'):
elif msg.startswith('!toff'):
logging.info('TTS is now turned off')
msg_queue.clear()
msg_queue_raw.clear()
self.tts_status = False
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOFF'])
return True
if msg.startswith('!ton'):
elif msg.startswith('!ton'):
logging.info('TTS is now turned on')
msg_queue.clear()
msg_queue_raw.clear()
self.tts_status = True
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TON'])
def check_subonly(self, tags):
""" subonly """
if not conf['IRC_SUBONLY']:
return False
subscriber = tags['subscriber']
badges = tags['badges']
user = tags['user']
if subscriber != "0" or 'moderator' in badges or 'broadcaster' in badges:
logging.info('TTS is sub-only and user has allowance')
return False
logging.debug('TTS is sub-only')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['SUBONLY'])
return True
if msg.startswith('!tts'):
logging.debug('!tts command detected')
logging.debug("tts status: %s", self.tts_status)
def check_modonly(self, tags):
""" modonly """
if not self.tts_status:
logging.info('TTS is disabled')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DISABLED'])
if not conf['IRC_MODONLY']:
return False
if msglen > conf['IRC_TTS_LEN']:
logging.info('TTS message is to long')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOO_LONG'])
return False
badges = tags['badges']
user = tags['user']
if conf['IRC_SUBONLY']:
if subscriber != "0" or 'moderator' in badges or 'broadcaster' in badges:
logging.debug('TTS is sub-only and user has allowance')
else:
logging.info('TTS is sub-only')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['SUBONLY'])
return False
if conf['IRC_MODONLY']:
if 'moderator' in badges or 'broadcaster' in badges:
logging.debug('TTS is mod-only and user has allowance')
else:
logging.info('TTS is sub-only')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['MODONLY'])
logging.info('TTS is mod-only and user has allowance')
return False
logging.debug('TTS is mod-only')
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['MODONLY'])
return True
def check_tts_disabled(self, user):
""" Check if TTS is disabled """
if not self.tts_status:
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DISABLED'])
return True
logging.debug('TTS is enabled')
return False
def check_msg_too_long(self, message, user):
""" Check if message is too long """
if message['length'] > conf['IRC_TTS_LEN']:
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOO_LONG'])
return True
logging.debug('Check length: Message is ok')
return False
def check_user_denied(self, user):
""" Check if user is on denied list """
if user in self.tts_denied:
logging.info("%s is not allowed to use TTS", user)
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DENIED'])
return True
logging.debug("%s is allowed to use TTS", user)
return False
def check_whitelist(self, user):
""" Check Whitelist """
if conf['WHITELIST']:
if user not in self.tts_allowed:
logging.info('User is not on whitelist')
logging.info(self.tts_allowed)
logging.debug("tts_allowed: %s", self.tts_allowed)
self.sendmsg(
conf['IRC_CHANNEL'],
"@"+str(user), conf['MESSAGE']['WHITELISTONLY']
)
return False
logging.warning('Nobody is on the whitelist.')
self.sendmsg(
conf['IRC_CHANNEL'],
"@"+str(user), conf['MESSAGE']['WHITELISTONLY']
)
return True
return False
def send_tts_msg(self, message, tags):
""" Send message to TTS queue """
logging.info('Valid TTS message, adding to raw queue')
tts = True
now = datetime.datetime.now()
timestamp = str(time.time_ns())
user = tags['user']
msgid = tags['msgid']
badges = tags['badges']
subscriber = tags['subscriber']
msg = message['message']
msglen = message['length']
msg = msg.replace('!tts','',1)
msg = {
"TTS": tts,
"msg": msg,
@ -291,13 +340,55 @@ class IRC:
"user": user,
"length": msglen,
"queuetime": now,
"timestamp": str(time.time_ns())
"timestamp": timestamp
}
msg_queue_raw.append(msg)
return True
def tts_command(self, message, tags):
""" Process !tts command """
return False
user = tags['user']
if self.check_tts_disabled(user):
logging.info('TTS is disabled')
elif self.check_msg_too_long(message, user):
logging.info('TTS message is too long')
elif self.check_user_denied(user):
logging.info('User is not allowed to use TTS')
elif self.check_subonly(tags):
logging.info('TTS is sub-only')
elif self.check_modonly(tags):
logging.info('TTS is mod-only')
elif self.check_whitelist(user):
logging.info('User is not on whitelist')
else:
logging.info('Sending TTS message to raw_queue')
self.send_tts_msg(message, tags)
def get_response(self):
"""Get and process response from IRC"""
try:
resp = self.irc.recv(2048).decode("UTF-8")
logging.debug('resp:')
logging.debug(resp)
except socket.timeout:
return
except Exception: # pylint: disable=broad-except
logging.exception('An unknown error occured while getting a IRC response.')
sys.exit(255)
if resp.find('PING') != -1:
self.resp_ping()
if resp.find('CLEARMSG') != -1:
self.resp_clearmsg(resp)
if resp.find('NOTICE') != -1:
self.resp_notice(resp)
if resp.find('PRIVMSG') != -1:
self.resp_privmsg(resp)
class Commands():
""" Bot commands """
@ -345,7 +436,7 @@ class IRC:
self.quickvote_status = False
self.poll = {}
return False
return
logging.info("Counting votes")
count = 0
@ -640,7 +731,7 @@ def load_config():
conf['IRC_SUBONLY'] = cfg.get('bot', {}).get('subonly', False)
conf['IRC_MODONLY'] = cfg.get('bot', {}).get('modonly', False)
conf['IRC_TTS_LEN'] = cfg.get('bot', {}).get('message_length', 200)
conf['TTS_STARTENABLED'] = cfg.get('bot', {}).get('start_enabled', False)
conf['TTS_STARTENABLED'] = cfg.get('bot', {}).get('start_enabled', True)
conf['LOG_LEVEL'] = cfg.get('log', {}).get('level', "INFO")
conf['HTTP_PORT'] = cfg.get('http', {}).get('port', 80)
@ -704,9 +795,25 @@ sys.tracebacklimit = 0
if sys.argv[1:]:
if sys.argv[1] == "--version":
print('Simple TTS Bot')
print('Version 1.2.1')
print('Version 1.2.2')
sys.exit(1)
def send_tts_queue():
""" Send messages to TTS """
for raw_msg in msg_queue_raw:
logging.debug('Raw msg: %s', msg_queue_raw)
now = datetime.datetime.now()
if now - raw_msg['queuetime'] > datetime.timedelta(seconds=conf['IRC_CLEARMSG_TIMEOUT']):
logging.debug('clearmsg_timeout reached')
if raw_msg['timestamp'] not in msg_queue:
logging.info('Sending TTS message')
msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']]
logging.debug("msg_queue: %s", msg_queue)
else:
logging.debug('Msg is already in queue')
def main():
"""Main loop"""
@ -734,7 +841,7 @@ def main():
logging.info('Please complete the OAuth process within the next 15 minutes.')
time.sleep(900)
sys.exit(250)
else:
logging.info("Starting IRC bot")
irc = IRC()
@ -762,27 +869,11 @@ def main():
irc.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTESTART'] + " (" + str(irc.votemsg) + ")")
if not irc.tts_status:
logging.debug("TTS is disabled")
if conf['LOG_LEVEL'] == "DEBUG":
time.sleep(1)
continue
logging.debug('Raw message queue:')
logging.debug(msg_queue_raw)
logging.debug('msg_queue_raw: %s', msg_queue_raw)
send_tts_queue()
for raw_msg in msg_queue_raw:
logging.debug('Raw msg:')
logging.debug(msg_queue_raw)
now = datetime.datetime.now()
if now - raw_msg['queuetime'] > datetime.timedelta(seconds=conf['IRC_CLEARMSG_TIMEOUT']):
logging.debug('clearmsg_timeout reached')
if raw_msg['timestamp'] not in msg_queue:
logging.info('Sending TTS message')
msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']]
logging.debug(msg_queue)
else:
logging.debug('Msg is already in queue')
except KeyboardInterrupt:
logging.info('Exiting...')
sys.exit()