diff --git a/.gitignore b/.gitignore index 1b5ac8b..614de36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config.yml build tts.spec tts.exe +random*.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index e7bef18..3ba5054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ 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.0] - 2022-08-13 + +### Added + + * `!random` feature (see README.md for details) + +### Changed + + * The vote result will be read out + +### Fixed + + * Improved handling of missing config values. + ## [1.1.0] - 2022-08-12 ### Added diff --git a/README.md b/README.md index 3e6a850..129d8a7 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ messages: votestart: "Quickvote started. Send #yourchoice to participate." voteend: "Quickvote ended. The results are:" votenobody: "Nobody casted a vote. :(" + voteresult: "Voting has ended. The result is:" votes: "Votes" log: @@ -117,6 +118,7 @@ Please note that the `oauth_token` is valid for approximately 60 days. If it bec * `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 @@ -166,7 +168,13 @@ Additional commands (broadcaster and mods only) are: ### Additional features -The bot also contains a `!quickvote` feature. If a broadcaster or moderator send the `!quickvote` command a vote will be started (or a already running vote will be ended). After a quickvote has been started your community can casts votes by sending a chat message starting with `#`. You can include a message after `!quickvote` (e.g. `!quickvote Is pizza hawaii any good? #yes/#no`). If you do so, this message will be repeated every 60 seconds, so everyone keeps in mind, that a vote is still active. +#### !quickvote + +The `!quickvote` feature implements a simple vote system. If a broadcaster or moderator send the `!quickvote` command a vote will be started (or a already running vote will be ended). After a quickvote has been started your community can casts votes by sending a chat message starting with `#`. You can include a message after `!quickvote` (e.g. `!quickvote Is pizza hawaii any good? #yes/#no`). If you do so, this message will be repeated every 60 seconds, so everyone keeps in mind, that a vote is still active. + +#### !random + +The `!random` command will read a random line from a file called `random.txt`. You can also use multiple files, if you call `!random foo` the bot fetch the random line from a file called `random_foo.txt` instead of `random.txt`. `!random bar` will use the file `random_bar.txt` and so on. The `!random` command is restricted to the broadcaster and moderators. ## Build diff --git a/config-dist.yml b/config-dist.yml index 60a6801..aa9f22f 100644 --- a/config-dist.yml +++ b/config-dist.yml @@ -29,6 +29,7 @@ messages: # Things the bot can send as chat message votestart: "Quickvote started. Send #yourchoice to participate." voteend: "Quickvote ended. The results are:" votenobody: "Nobody casted a vote. :(" + voteresult: "Voting has ended. The result is:" votes: "Votes" log: diff --git a/tts.py b/tts.py index 9713644..dc33271 100644 --- a/tts.py +++ b/tts.py @@ -20,21 +20,23 @@ along with this program. If not, see . """ -import json -import logging -import socket +import os import sys import time +import json +import socket +import random +import logging import datetime -import socketserver -import urllib.request -import urllib.parse import webbrowser +import socketserver +import urllib.parse +import urllib.request from threading import Thread -from http.server import BaseHTTPRequestHandler -from urllib.parse import parse_qs from collections import Counter +from urllib.parse import parse_qs +from http.server import BaseHTTPRequestHandler import yaml @@ -77,8 +79,8 @@ class IRC: try: self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8")) except ConnectionResetError: - logging.warning('JOIN was refused, will try again in 5 seconds.') - time.sleep(5) + logging.warning('JOIN was refused, will try again in 30 seconds.') + time.sleep(30) logging.warning('Please check your credentials, if this error persists.') self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8")) @@ -198,6 +200,38 @@ class IRC: return True + if msg.startswith('!random'): + logging.info('!random command detected') + + randomfile = msg.replace('!random', '').strip().lower() + if randomfile: + randomfile = "random_"+str(os.path.basename(randomfile))+".txt" + else: + randomfile = "random.txt" + + try: + with open(randomfile,"r", encoding="utf-8") as file: + lines = file.read().splitlines() + random_msg = random.choice(lines) + except FileNotFoundError: + logging.error('%s not found', randomfile) + return False + + raw_msg = { + "TTS": True, + "msg": random_msg, + "badges": True, + "subscriber": True, + "msgid": True, + "user": conf['IRC_USERNAME'], + "length": conf['IRC_TTS_LEN'], + "queuetime": datetime.datetime.now(), + "timestamp": str(time.time_ns()) + } + msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']] + + return True + if msg.startswith('!quickvote'): logging.info("!quickvote command detected") if self.quickvote: @@ -207,6 +241,23 @@ class IRC: logging.info("Nobody voted") self.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTEEND']) self.sendmsg(conf['IRC_CHANNEL'], "*", conf['MESSAGE']['VOTENOBODY']) + + raw_msg = { + "TTS": True, + "msg": conf['MESSAGE']['VOTENOBODY'], + "badges": True, + "subscriber": True, + "msgid": True, + "user": conf['IRC_USERNAME'], + "length": conf['IRC_TTS_LEN'], + "queuetime": datetime.datetime.now(), + "timestamp": str(time.time_ns()) + } + msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']] + + logging.info('The result is: %s', conf['MESSAGE']['VOTENOBODY']) + logging.debug('Votemsg: %s', msg) + self.quickvote = False self.poll = {} return False @@ -218,6 +269,22 @@ class IRC: logging.debug(count) + raw_msg = { + "TTS": True, + "msg": conf['MESSAGE']['VOTERESULT'] +" "+ str(count[0][0].replace('#','')), + "badges": True, + "subscriber": True, + "msgid": True, + "user": conf['IRC_USERNAME'], + "length": conf['IRC_TTS_LEN'], + "queuetime": datetime.datetime.now(), + "timestamp": str(time.time_ns()) + } + msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']] + + logging.info('The result is: %s', conf['MESSAGE']['VOTERESULT'] +" "+ str(count[0])) + logging.debug('Votemsg: %s', msg) + for key, value in count: self.sendmsg( conf['IRC_CHANNEL'], "*", @@ -504,39 +571,40 @@ def load_config(): for section in cfg: try: logging.debug('Fetching config: %s', section) - conf['IRC_CHANNEL'] = cfg['irc']['channel'] - conf['IRC_USERNAME'] = cfg['irc']['username'] - conf['IRC_OAUTH_TOKEN'] = cfg['irc']['oauth_token'] - conf['IRC_SERVER'] = cfg['irc']['server'] or "irc.chat.twitch.tv" - conf['IRC_CLEARMSG_TIMEOUT'] = cfg['irc']['clearmsg_timeout'] or 60 + conf['IRC_CHANNEL'] = cfg.get('irc', {}).get('channel', False) + conf['IRC_USERNAME'] = cfg.get('irc', {}).get('username', False) + conf['IRC_OAUTH_TOKEN'] = cfg.get('irc', {}).get('oauth_token', False) + conf['IRC_SERVER'] = cfg.get('irc', {}).get('server', "irc.chat.twitch.tv") + conf['IRC_CLEARMSG_TIMEOUT'] = cfg.get('irc', {}).get('clearmsg_timeout', 60) - conf['IRC_SUBONLY'] = cfg['bot']['subonly'] or False - conf['IRC_MODONLY'] = cfg['bot']['modonly'] or False - conf['IRC_TTS_LEN'] = cfg['bot']['message_length'] or 200 - conf['TTS_STARTENABLED'] = cfg['bot']['start_enabled'] or False + 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['LOG_LEVEL'] = cfg['log']['level'] or "INFO" - conf['HTTP_PORT'] = cfg['http']['port'] or 80 - conf['HTTP_BIND'] = cfg['http']['bind'] or "localhost" + conf['LOG_LEVEL'] = cfg.get('log', {}).get('level', "INFO") + conf['HTTP_PORT'] = cfg.get('http', {}).get('port', 80) + conf['HTTP_BIND'] = cfg.get('http', {}).get('bind', "localhost") conf['MESSAGE'] = {} - conf['MESSAGE']['TOFF'] = cfg['messages']['toff'] or "TTS is now disabled." - conf['MESSAGE']['TON'] = cfg['messages']['ton'] or "TTS is now active." - conf['MESSAGE']['TOO_LONG'] = cfg['messages']['too_long'] or "Sorry, your message is too long." - conf['MESSAGE']['DISABLED'] = cfg['messages']['disabled'] or "Sorry, TTS is disabled." - conf['MESSAGE']['DENIED'] = cfg['messages']['denied'] or "Sorry, you're not allowed to use TTS." - conf['MESSAGE']['SUBONLY'] = cfg['messages']['subonly'] or "Sorry, TTS is sub-only." - conf['MESSAGE']['MODONLY'] = cfg['messages']['modonly'] or "Sorry, TTS is mod-only." - conf['MESSAGE']['READY'] = cfg['messages']['ready'] or "TTS bot is ready." - conf['MESSAGE']['WHITELISTONLY'] = cfg['messages']['whitelist'] or False - conf['MESSAGE']['SAYS'] = cfg['messages']['says'] or "says" + conf['MESSAGE']['TOFF'] = cfg.get('messages', {}).get('toff', "TTS is now disabled.") + conf['MESSAGE']['TON'] = cfg.get('messages', {}).get('ton', "TTS is now active.") + conf['MESSAGE']['TOO_LONG'] = cfg.get('messages', {}).get('too_long', "Sorry, your message is too long.") + conf['MESSAGE']['DISABLED'] = cfg.get('messages', {}).get('disabled', "Sorry, TTS is disabled.") + conf['MESSAGE']['DENIED'] = cfg.get('messages', {}).get('denied', "Sorry, you're not allowed to use TTS.") + conf['MESSAGE']['SUBONLY'] = cfg.get('messages', {}).get('subonly', "Sorry, TTS is sub-only.") + conf['MESSAGE']['MODONLY'] = cfg.get('messages', {}).get('modonly', "Sorry, TTS is mod-only.") + conf['MESSAGE']['READY'] = cfg.get('messages', {}).get('ready', "TTS bot is ready.") + conf['MESSAGE']['WHITELISTONLY'] = cfg.get('messages', {}).get('whitelist', False) + conf['MESSAGE']['SAYS'] = cfg.get('messages', {}).get('says', "says") - conf['MESSAGE']['VOTESTART'] = cfg['messages']['votestart'] or "Quickvote started. Send #yourchoice to participate." - conf['MESSAGE']['VOTEEND'] = cfg['messages']['voteend'] or "Quickvote ended. The results are:" - conf['MESSAGE']['VOTENOBODY'] = cfg['messages']['votenobody'] or "Nobody casted a vote. :(" - conf['MESSAGE']['VOTES'] = cfg['messages']['votes'] or "Stimmen" + conf['MESSAGE']['VOTESTART'] = cfg.get('messages', {}).get('votestart', "Quickvote started. Send #yourchoice to participate.") + conf['MESSAGE']['VOTEEND'] = cfg.get('messages', {}).get('voteend', "Quickvote ended. The results are:") + conf['MESSAGE']['VOTENOBODY'] = cfg.get('messages', {}).get('votenobody', "Nobody casted a vote. :(") + conf['MESSAGE']['VOTERESULT'] = cfg.get('messages', {}).get('voteresult', "Voting has ended. The result is:") + conf['MESSAGE']['VOTES'] = cfg.get('messages', {}).get('votes', "Stimmen") - conf['USERMAP'] = cfg['usermapping'] or [] + conf['USERMAP'] = cfg.get('usermapping', []) if 'whitelist' in cfg: conf['WHITELIST'] = True