Merge branch 'experimental' into 'main'

Experimental

See merge request gpvkt/twitchtts!1
This commit is contained in:
gpkvt 2022-08-12 16:54:41 +00:00
commit bce550ec16
5 changed files with 294 additions and 94 deletions

View File

@ -2,12 +2,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.0.0] - 2022-08-11 ## [1.1.0] - unreleased
Initial Release
### Added ### Added
* `!quickvote` feature (see README.md for details)
* `!ping` command added
* Configoption to start TTS in disabled mode
* OAuth-Token generator
* Webbrowser autostart
### Changed ### Changed
* 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
* Improved error handling
## [1.0.0] - 2022-08-11
Initial Release

View File

@ -45,6 +45,7 @@ http:
bind: "localhost" bind: "localhost"
bot: bot:
start_enabled: True
subonly: False subonly: False
modonly: False modonly: False
message_length: 200 message_length: 200
@ -59,6 +60,10 @@ messages:
whitelist: "Sorry, you are not allowed to use TTS." whitelist: "Sorry, you are not allowed to use TTS."
ready: "TTS bot alpha ready!" ready: "TTS bot alpha ready!"
says: "says" says: "says"
votestart: "Quickvote started. Send #yourchoice to participate."
voteend: "Quickvote ended. The results are:"
votenobody: "Nobody casted a vote. :("
votes: "Votes"
log: log:
level: "INFO" level: "INFO"
@ -80,13 +85,18 @@ whitelist:
* `server`: Twitch IRC server to be used (default should be fine) * `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 * `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).
Please note that the `oauth_token` is valid for approximately 60 days. If it become invalid the bot will not connect anymore and you will have to renew the token.
##### http ##### http
* `port`: Internal Webserver Port to listen to (e.g. 8080) * `port`: Internal Webserver Port to listen to (e.g. 8080)
* `bind`: Interface/IP to bind server to (e.g. localhost) * `bind`: Interface/IP to bind server to (e.g. localhost)
##### bot ##### 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 * `subonly`: If `True` only Subs can use TTS
* `modonly`: If `True` only Mods can use TTS * `modonly`: If `True` only Mods can use TTS
* `message_length`: Maximum allowed message length for TTS * `message_length`: Maximum allowed message length for TTS
@ -102,11 +112,17 @@ whitelist:
* `whitelist`: The bots reply if `whitelist` is set and user isn't on the list. * `whitelist`: The bots reply if `whitelist` is set and user isn't on the list.
* `ready`: The bots init message * `ready`: The bots init message
* `says`: Prefix to add between username and 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.
* `votes`: Suffix to vote count.
##### log ##### 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.
##### usermapping ##### usermapping
Use this section to define key:value pairs of usernames. The first value is the Twitch username, the second value is how the bot should pronouce the user, when reading the message. This is helpfull if you have regulars with numbers or strangs chars in the name. You can add new/change entries on the fly without restarting the bot (changes took up to 60 seconds). Use this section to define key:value pairs of usernames. The first value is the Twitch username, the second value is how the bot should pronouce the user, when reading the message. This is helpfull if you have regulars with numbers or strangs chars in the name. You can add new/change entries on the fly without restarting the bot (changes took up to 60 seconds).
@ -146,6 +162,10 @@ Additional commands (broadcaster and mods only) are:
* `!dtts <username>`: Disable TTS for the given user * `!dtts <username>`: Disable TTS for the given user
* `!ptts <username>`: Allow TTS for the given user * `!ptts <username>`: Allow TTS for the given user
### 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.
## Build ## Build
If you prefer to build your own `tts.exe` instead of using the shipped one, you can do as follows: If you prefer to build your own `tts.exe` instead of using the shipped one, you can do as follows:

View File

@ -10,6 +10,7 @@ http:
bind: "localhost" bind: "localhost"
bot: bot:
start_enabled: True
subonly: False subonly: False
modonly: False modonly: False
message_length: 200 message_length: 200
@ -25,6 +26,10 @@ messages:
modonly: "Sorry, TTS is a mod-only feature." modonly: "Sorry, TTS is a mod-only feature."
ready: "TTS bot alpha ready!" ready: "TTS bot alpha ready!"
says: "says" says: "says"
votestart: "Quickvote started. Send #yourchoice to participate."
voteend: "Quickvote ended. The results are:"
votenobody: "Nobody casted a vote. :("
votes: "Votes"
log: log:
level: "INFO" level: "INFO"

BIN
dist/tts.exe vendored

Binary file not shown.

341
tts.py
View File

@ -1,5 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=line-too-long
""" """
TwitchTTS TwitchTTS
@ -20,32 +21,43 @@
""" """
import json import json
import yaml
import logging import logging
import socket import socket
import sys import sys
import time import time
import datetime import datetime
import socketserver import socketserver
import urllib.request
import urllib.parse
import webbrowser
from threading import Thread from threading import Thread
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from urllib.parse import parse_qs from urllib.parse import parse_qs
from collections import Counter
import yaml
class IRC: class IRC:
"""IRC bot"""
irc = socket.socket() irc = socket.socket()
def __init__(self): def __init__(self):
self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tts_denied = [] self.tts_denied = []
self.tts_allowed = [] self.tts_allowed = []
self.tts_status = True self.tts_status = conf['TTS_STARTENABLED']
self.quickvote = False
self.votemsg = False
self.poll = {}
self.pollcount = 0
if 'WHITELIST_USER' in conf: if 'WHITELIST_USER' in conf:
self.tts_allowed = conf['WHITELIST_USER'] self.tts_allowed = conf['WHITELIST_USER']
def connect(self, server, port, channel, botnick, botpass): def connect(self, server, port, channel, botnick, botpass):
logging.info("Connecting to: " + server) """Connect to Twitch IRC servers"""
logging.info("Connecting to: %s", server)
try: try:
self.irc.connect((server, port)) self.irc.connect((server, port))
except ConnectionResetError: except ConnectionResetError:
@ -55,7 +67,9 @@ class IRC:
self.irc.settimeout(1) self.irc.settimeout(1)
self.irc.send(bytes("PASS " + botpass + "\r\n", "UTF-8")) self.irc.send(bytes("PASS " + botpass + "\r\n", "UTF-8"))
self.irc.send(bytes("USER " + botnick + " " + botnick +" " + botnick + " :python\r\n", "UTF-8")) self.irc.send(bytes(
"USER " + botnick + " " + botnick +" " + botnick + " :python\r\n", "UTF-8")
)
self.irc.send(bytes("NICK " + botnick + "\r\n", "UTF-8")) self.irc.send(bytes("NICK " + botnick + "\r\n", "UTF-8"))
self.irc.send(bytes("CAP REQ :twitch.tv/commands twitch.tv/tags \r\n", "UTF-8")) self.irc.send(bytes("CAP REQ :twitch.tv/commands twitch.tv/tags \r\n", "UTF-8"))
time.sleep(5) time.sleep(5)
@ -63,22 +77,31 @@ class IRC:
try: try:
self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8")) self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8"))
except ConnectionResetError: except ConnectionResetError:
logging.warn('JOIN was refused, will try again in 5 seconds.') logging.warning('JOIN was refused, will try again in 5 seconds.')
time.sleep(5) time.sleep(5)
logging.warn('Please check your credentials, if this error persists.') logging.warning('Please check your credentials, if this error persists.')
self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8")) self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8"))
def sendpriv(self, channel, user, msg): def sendmsg(self, channel, user, msg):
"""
Send (a public) message to IRC channel
Parameters:
channel (str): Channel to post msg to
user (str): User to address message to
msg (str): Message to send
"""
self.irc.send(bytes("PRIVMSG "+channel+" :"+user+" "+msg+"\r\n", "UTF-8")) self.irc.send(bytes("PRIVMSG "+channel+" :"+user+" "+msg+"\r\n", "UTF-8"))
def get_response(self): def get_response(self):
"""Get and process response from IRC"""
try: try:
resp = self.irc.recv(2048).decode("UTF-8") resp = self.irc.recv(2048).decode("UTF-8")
logging.debug('resp:') logging.debug('resp:')
logging.debug(resp) logging.debug(resp)
except socket.timeout: except socket.timeout:
return False return False
except: except Exception: # pylint: disable=broad-except
logging.exception('An unknown error occured while getting a IRC response.') logging.exception('An unknown error occured while getting a IRC response.')
sys.exit(255) sys.exit(255)
@ -90,7 +113,7 @@ class IRC:
logging.info('CLEARMSG received') logging.info('CLEARMSG received')
msgid = False msgid = False
global msg_queue_raw global msg_queue_raw # pylint: disable=global-statement,invalid-name
filtered_msg_queue = [] filtered_msg_queue = []
tags = resp.split(';') tags = resp.split(';')
@ -102,7 +125,7 @@ class IRC:
for msg in list(msg_queue_raw): for msg in list(msg_queue_raw):
if msg['msgid'] == msgid: if msg['msgid'] == msgid:
logging.info('Suppressing message '+str(msgid)) logging.info('Suppressing message %s', msgid)
else: else:
filtered_msg_queue.append(msg) filtered_msg_queue.append(msg)
@ -134,22 +157,21 @@ class IRC:
badges = tag.rsplit('badges=',1)[1] badges = tag.rsplit('badges=',1)[1]
if tag.startswith('subscriber='): if tag.startswith('subscriber='):
subscriber = tag.rsplit('subscriber=',1)[1] subscriber = tag.rsplit('subscriber=',1)[1]
logging.debug('Subscriber: '+str(subscriber)) logging.debug('Subscriber: %s', subscriber)
if tag.startswith('id='): if tag.startswith('id='):
msgid = tag.rsplit('id=',1)[1] msgid = tag.rsplit('id=',1)[1]
logging.debug('Message ID: '+str(msgid)) logging.debug('Message ID: %s', msgid)
if tag.startswith('display-name='): if tag.startswith('display-name='):
user = tag.rsplit('display-name=',1)[1].lower() user = tag.rsplit('display-name=',1)[1].lower()
logging.debug('Username: '+str(user)) logging.debug('Username: %s', user)
msg = resp.rsplit('PRIVMSG #',1)[1] msg = resp.rsplit('PRIVMSG #',1)[1]
msg = msg.split(':',1)[1] msg = msg.split(':',1)[1]
msg = msg.replace('\r\n','') msg = msg.replace('\r\n','')
msglen = len(msg) msglen = len(msg)
logging.debug('Msg:') logging.debug('Msg: %s', msg)
logging.debug(msg) logging.debug('Msg length: %s', msglen)
logging.debug('Msg length: '+str(msglen))
logging.debug('Deny List:') logging.debug('Deny List:')
logging.debug(self.tts_denied) logging.debug(self.tts_denied)
@ -162,17 +184,62 @@ class IRC:
logging.debug('Removing "@" from username') logging.debug('Removing "@" from username')
user = user.replace('@', '') user = user.replace('@', '')
if user not in self.tts_denied: if user not in self.tts_denied:
logging.info("Adding "+str(user)+" to deny list") logging.info("Adding %s to deny list", user)
self.tts_denied.append(user) self.tts_denied.append(user)
if user in self.tts_allowed: if user in self.tts_allowed:
logging.info("Removing "+str(user)+" from allowed list") logging.info("Removing %s from allowed list", user)
self.tts_allowed.remove(user) self.tts_allowed.remove(user)
return True return True
if msg.startswith('!ping'): if msg.startswith('!ping'):
logging.debug('Ping check received.') logging.debug("Ping check received.")
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), "Pong!") self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), "Pong!")
return True
if msg.startswith('!quickvote'):
logging.info("!quickvote command detected")
if self.quickvote:
logging.debug('Quickvote stopped')
if self.pollcount == 0:
logging.info("Nobody voted")
self.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTEEND'])
self.sendmsg(conf['IRC_CHANNEL'], "*", conf['MESSAGE']['VOTENOBODY'])
self.quickvote = False
self.poll = {}
return False
logging.info("Counting votes")
count = 0
count = Counter(self.poll.values()).most_common(5)
self.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTEEND'])
logging.debug(count)
for key, value in count:
self.sendmsg(
conf['IRC_CHANNEL'], "*",
str(key)+" ("+str(value)+ " "+ conf['MESSAGE']['VOTES'] + ")"
)
self.quickvote = False
self.poll = {}
self.pollcount = 0
return True
else:
logging.debug('Quickvote started')
self.quickvote = True
self.votemsg = resp.split('!quickvote', 1)[1].strip()
if self.votemsg:
self.sendmsg(
conf['IRC_CHANNEL'], "@chat",
conf['MESSAGE']['VOTESTART'] + " (" + str(self.votemsg) + ")"
)
else:
self.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTESTART'])
return True
return True return True
@ -184,21 +251,27 @@ class IRC:
logging.debug('Removing "@" from username') logging.debug('Removing "@" from username')
user = user.replace('@', '') user = user.replace('@', '')
logging.info("Adding "+str(user)+" to whitelist") logging.info("Adding %s to whitelist", user)
self.tts_allowed.append(user) self.tts_allowed.append(user)
if user in self.tts_denied: if user in self.tts_denied:
logging.info("Removing "+str(user)+" from deny list") logging.info("Removing %s from deny list", user)
self.tts_denied.remove(user) self.tts_denied.remove(user)
return True return True
if msg.startswith('#') and self.quickvote is True:
logging.info('Quickvote: Cast detected')
self.pollcount += 1
self.poll[user] = msg.lower()
logging.debug(self.poll)
if msg.startswith('!toff'): if msg.startswith('!toff'):
logging.info('TTS is now turned off') logging.info('TTS is now turned off')
msg_queue.clear() msg_queue.clear()
msg_queue_raw.clear() msg_queue_raw.clear()
self.tts_status = False self.tts_status = False
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOFF']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOFF'])
return True return True
@ -207,7 +280,7 @@ class IRC:
msg_queue.clear() msg_queue.clear()
msg_queue_raw.clear() msg_queue_raw.clear()
self.tts_status = True self.tts_status = True
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TON']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TON'])
return True return True
@ -216,17 +289,20 @@ class IRC:
if msglen > conf['IRC_TTS_LEN']: if msglen > conf['IRC_TTS_LEN']:
logging.info('TTS message is to long') logging.info('TTS message is to long')
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOO_LONG']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['TOO_LONG'])
return False return False
logging.debug("tts status: %s", self.tts_status)
logging.debug(conf['TTS_STARTENABLED'])
if not self.tts_status: if not self.tts_status:
logging.info('TTS is disabled') logging.info('TTS is disabled')
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DISABLED']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DISABLED'])
return False return False
if user in self.tts_denied: if user in self.tts_denied:
logging.info(str(user) + " is not allowed to use TTS") logging.info("%s is not allowed to use TTS", user)
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DENIED']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DENIED'])
return False return False
if conf['IRC_SUBONLY']: if conf['IRC_SUBONLY']:
@ -234,7 +310,7 @@ class IRC:
logging.debug('TTS is sub-only and user has allowance') logging.debug('TTS is sub-only and user has allowance')
else: else:
logging.info('TTS is sub-only') logging.info('TTS is sub-only')
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['SUBONLY']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['SUBONLY'])
return False return False
if conf['IRC_MODONLY']: if conf['IRC_MODONLY']:
@ -242,25 +318,41 @@ class IRC:
logging.debug('TTS is mod-only and user has allowance') logging.debug('TTS is mod-only and user has allowance')
else: else:
logging.info('TTS is sub-only') logging.info('TTS is sub-only')
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['MODONLY']) self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['MODONLY'])
return False return False
if conf['WHITELIST']: if conf['WHITELIST']:
if not user in self.tts_allowed: if user not in self.tts_allowed:
logging.info('User is not on whitelist') logging.info('User is not on whitelist')
logging.info(self.tts_allowed) logging.info(self.tts_allowed)
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['WHITELISTONLY']) self.sendmsg(
conf['IRC_CHANNEL'],
"@"+str(user), conf['MESSAGE']['WHITELISTONLY']
)
return False return False
else: else:
logging.info('Nobody is on the whitelist.') logging.info('Nobody is on the whitelist.')
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['WHITELISTONLY']) self.sendmsg(
conf['IRC_CHANNEL'],
"@"+str(user), conf['MESSAGE']['WHITELISTONLY']
)
return False return False
logging.info('Valid TTS message, adding to raw queue') logging.info('Valid TTS message, adding to raw queue')
tts = True tts = True
now = datetime.datetime.now() now = datetime.datetime.now()
msg = msg.replace('!tts','',1) msg = msg.replace('!tts','',1)
msg = {"TTS": tts, "msg": msg, "badges": badges, "subscriber": subscriber, "msgid": msgid, "user": user, "length": msglen, "queuetime": now, "timestamp": str(time.time_ns())} msg = {
"TTS": tts,
"msg": msg,
"badges": badges,
"subscriber": subscriber,
"msgid": msgid,
"user": user,
"length": msglen,
"queuetime": now,
"timestamp": str(time.time_ns())
}
msg_queue_raw.append(msg) msg_queue_raw.append(msg)
return True return True
@ -268,48 +360,52 @@ class IRC:
return False return False
class HTTPserv(BaseHTTPRequestHandler): class HTTPserv(BaseHTTPRequestHandler):
def log_message(self, format, *args): """Simple HTTP Server"""
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Suppress HTTP log messages"""
return return
def do_GET(self): def do_GET(self): # pylint: disable=invalid-name
"""Process GET requests"""
if self.path == '/': if self.path == '/':
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/html') self.send_header('Content-type', 'text/html')
self.end_headers() self.end_headers()
with open("tts.html", "rb") as fh: with open("tts.html", "rb") as file:
html = fh.read() html = file.read()
self.wfile.write(html) self.wfile.write(html)
elif self.path == '/favicon.ico': elif self.path == '/favicon.ico':
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'image/x-icon') self.send_header('Content-type', 'image/x-icon')
self.end_headers() self.end_headers()
with open("favicon.ico", "rb") as fh: with open("favicon.ico", "rb") as file:
icon = fh.read() icon = file.read()
self.wfile.write(icon) self.wfile.write(icon)
elif self.path == '/tts.js': elif self.path == '/tts.js':
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/javascript') self.send_header('Content-type', 'text/javascript')
self.end_headers() self.end_headers()
with open("tts.js", "rb") as fh: with open("tts.js", "rb") as file:
html = fh.read() html = file.read()
self.wfile.write(html) self.wfile.write(html)
elif self.path == '/jquery.js': elif self.path == '/jquery.js':
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/javascript') self.send_header('Content-type', 'text/javascript')
self.end_headers() self.end_headers()
with open("jquery.js", "rb") as fh: with open("jquery.js", "rb") as file:
html = fh.read() html = file.read()
self.wfile.write(html) self.wfile.write(html)
elif self.path == '/bootstrap.min.css': elif self.path == '/bootstrap.min.css':
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/css') self.send_header('Content-type', 'text/css')
self.end_headers() self.end_headers()
with open("bootstrap.min.css", "rb") as fh: with open("bootstrap.min.css", "rb") as file:
html = fh.read() html = file.read()
self.wfile.write(html) self.wfile.write(html)
elif self.path.startswith('/tts_queue'): elif self.path.startswith('/tts_queue'):
@ -325,9 +421,9 @@ class HTTPserv(BaseHTTPRequestHandler):
for key in list(sorted_tts.keys()): for key in list(sorted_tts.keys()):
if key not in tts_done: if key not in tts_done:
if msg_queue[key][0].lower() in usermap: if msg_queue[key][0].lower() in usermap:
tts = {key: usermap[msg_queue[key][0].lower()]+" "+str(conf['MESSAGE']['SAYS'])+":"+msg_queue[key][1]} tts = {str(key): str(usermap[msg_queue[key][0].lower()]) + " " + str(conf['MESSAGE']['SAYS']) + ":" + str(msg_queue[key][1])}
else: else:
tts = {key: msg_queue[key][0]+" "+str(conf['MESSAGE']['SAYS'])+":"+msg_queue[key][1]} tts = {str(key): str(msg_queue[key][0]) + " " + str(conf['MESSAGE']['SAYS']) + ":" + str(msg_queue[key][1])}
tts_json = json.dumps(tts) tts_json = json.dumps(tts)
self.wfile.write(bytes(str(tts_json)+"\n", "utf-8")) self.wfile.write(bytes(str(tts_json)+"\n", "utf-8"))
@ -347,6 +443,41 @@ class HTTPserv(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(bytes("Internal Server error\n", "utf-8")) self.wfile.write(bytes("Internal Server error\n", "utf-8"))
elif self.path.startswith('/token') and conf['IRC_OAUTH_TOKEN'] == "Invalid":
data = {}
data['client_id'] = "ebo548vs6tq54c9zlrgin2yfzzlrrs"
data['response_type'] = "token"
data['scope'] = "chat:edit chat:read"
if conf['HTTP_PORT'] == 80:
data['redirect_uri'] = "http://localhost/token"
elif conf['HTTP_PORT'] == 8080:
data['redirect_uri'] = "http://localhost:8080/token"
elif conf['HTTP_PORT'] == 3000:
data['redirect_uri'] = "http://localhost:3000/token"
else:
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(bytes("You can only use this function if HTTP_PORT is 80, 8080 or 3000. Please change your port, or use https://www.21x9.org/twitch instead.\n", "utf-8"))
return False
try:
url_values = urllib.parse.urlencode(data)
url = "https://id.twitch.tv/oauth2/authorize"
full_url = url + "?" + url_values
data = urllib.request.urlopen(full_url)
if data:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(bytes("<html><head><title>OAuth Token Generator</title></head><body onload=\"displayCode();\"><div id=\"code\"><a href=\""+str(data.geturl())+"\">Click to start the OAuth process.</a></div><script>function displayCode() { var url = window.location.href; var test = url.indexOf(\"access_token\");if (test != -1) { token = url.substring(42,72); document.getElementById(\"code\").innerHTML = \"<p>oauth:\" + token + \"</p><p>Copy the token into your config.yml and restart the bot.</p>\";}}</script></body></html>\n", "utf-8"))
else:
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(bytes("Could not get OAuth-URL from Twitch\n", "utf-8"))
except Exception: # pylint: disable=broad-except
logging.error('Could not fetch OAuth-URL from Twitch.')
else: else:
self.send_response(404) self.send_response(404)
self.send_header('Server', 'TTS') self.send_header('Server', 'TTS')
@ -357,13 +488,16 @@ class HTTPserv(BaseHTTPRequestHandler):
return return
def http_serve_forever(httpd): def http_serve_forever(httpd):
httpd.serve_forever() """httpd loop"""
httpd.serve_forever()
def load_config(): def load_config():
"""Loading config variables"""
logging.info("Loading configfile") logging.info("Loading configfile")
try: try:
with open("config.yml", "r") as ymlfile: with open("config.yml", "r", encoding="UTF-8") as ymlfile:
cfg = yaml.load(ymlfile, Loader=yaml.Loader) cfg = yaml.load(ymlfile, Loader=yaml.Loader)
except FileNotFoundError: except FileNotFoundError:
logging.fatal('Your config file is missing, please copy config-dist.yml to config.yml and review your settings.') logging.fatal('Your config file is missing, please copy config-dist.yml to config.yml and review your settings.')
@ -371,17 +505,18 @@ def load_config():
for section in cfg: for section in cfg:
try: try:
logging.debug('Fetching config: '+str(section)) logging.debug('Fetching config: %s', section)
conf['IRC_CHANNEL'] = cfg['irc']['channel'] conf['IRC_CHANNEL'] = cfg['irc']['channel']
conf['IRC_USERNAME'] = cfg['irc']['username'] conf['IRC_USERNAME'] = cfg['irc']['username']
conf['IRC_OAUTH_TOKEN'] = cfg['irc']['oauth_token'] conf['IRC_OAUTH_TOKEN'] = cfg['irc']['oauth_token']
conf['IRC_SERVER'] = cfg['irc']['server'] or "irc.chat.twitch.tv" conf['IRC_SERVER'] = cfg['irc']['server'] or "irc.chat.twitch.tv"
conf['IRC_CLEARMSG_TIMEOUT'] = cfg['irc']['clearmsg_timeout'] or 60 conf['IRC_CLEARMSG_TIMEOUT'] = cfg['irc']['clearmsg_timeout'] or 60
conf['IRC_SUBONLY'] = cfg['bot']['subonly'] or False conf['IRC_SUBONLY'] = cfg['bot']['subonly'] or False
conf['IRC_MODONLY'] = cfg['bot']['modonly'] or False conf['IRC_MODONLY'] = cfg['bot']['modonly'] or False
conf['IRC_TTS_LEN'] = cfg['bot']['message_length'] or 200 conf['IRC_TTS_LEN'] = cfg['bot']['message_length'] or 200
conf['TTS_STARTENABLED'] = cfg['bot']['start_enabled'] or False
conf['LOG_LEVEL'] = cfg['log']['level'] or "INFO" conf['LOG_LEVEL'] = cfg['log']['level'] or "INFO"
conf['HTTP_PORT'] = cfg['http']['port'] or 80 conf['HTTP_PORT'] = cfg['http']['port'] or 80
conf['HTTP_BIND'] = cfg['http']['bind'] or "localhost" conf['HTTP_BIND'] = cfg['http']['bind'] or "localhost"
@ -398,6 +533,11 @@ def load_config():
conf['MESSAGE']['WHITELISTONLY'] = cfg['messages']['whitelist'] or False conf['MESSAGE']['WHITELISTONLY'] = cfg['messages']['whitelist'] or False
conf['MESSAGE']['SAYS'] = cfg['messages']['says'] or "says" conf['MESSAGE']['SAYS'] = cfg['messages']['says'] or "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['USERMAP'] = cfg['usermapping'] or [] conf['USERMAP'] = cfg['usermapping'] or []
if 'whitelist' in cfg: if 'whitelist' in cfg:
@ -420,7 +560,8 @@ def load_config():
if not conf['IRC_USERNAME']: if not conf['IRC_USERNAME']:
raise ValueError('Please add the bots username to config.yml.') raise ValueError('Please add the bots username to config.yml.')
if not conf['IRC_OAUTH_TOKEN']: if not conf['IRC_OAUTH_TOKEN']:
raise ValueError('Please add the bots oauth-token to config.yml.') conf['IRC_OAUTH_TOKEN'] = "Invalid"
return conf
if not conf['IRC_OAUTH_TOKEN'].startswith('oauth:'): if not conf['IRC_OAUTH_TOKEN'].startswith('oauth:'):
raise ValueError('Your oauth-token is invalid, it has to start with: "oauth:"') raise ValueError('Your oauth-token is invalid, it has to start with: "oauth:"')
@ -435,12 +576,16 @@ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(module)s %(thread
sys.tracebacklimit = 0 sys.tracebacklimit = 0
def main(): def main():
"""Main loop"""
global conf # pylint: disable=global-statement,invalid-name
conf = load_config() conf = load_config()
lastreload = datetime.datetime.now() lastreload = datetime.datetime.now()
logging.getLogger().setLevel(conf['LOG_LEVEL']) logging.getLogger().setLevel(conf['LOG_LEVEL'])
if conf['LOG_LEVEL'] == 'DEBUG': if conf['LOG_LEVEL'] == 'DEBUG':
sys.tracebacklimit = 5 sys.tracebacklimit = 5
logging.info("Starting Webserver") logging.info("Starting Webserver")
httpd = socketserver.TCPServer((conf['HTTP_BIND'], conf['HTTP_PORT']), HTTPserv) httpd = socketserver.TCPServer((conf['HTTP_BIND'], conf['HTTP_PORT']), HTTPserv)
httpd.allow_reuse_port = True httpd.allow_reuse_port = True
@ -449,50 +594,66 @@ def main():
http_thread = Thread(target=http_serve_forever, daemon=True, args=(httpd, )) http_thread = Thread(target=http_serve_forever, daemon=True, args=(httpd, ))
http_thread.start() http_thread.start()
logging.info("Starting IRC bot") if conf['IRC_OAUTH_TOKEN'] == "Invalid":
irc = IRC() logging.error('No OAuth Token, skipping start of IRC bot.')
irc.connect(conf['IRC_SERVER'], 6667, conf['IRC_CHANNEL'], conf['IRC_USERNAME'], conf['IRC_OAUTH_TOKEN']) logging.error('Please open http://%s:%s/token to generate your OAuth-Token.', conf['HTTP_BIND'], conf['HTTP_PORT'])
irc.sendpriv(conf['IRC_CHANNEL'], 'MrDestructoid', conf['MESSAGE']['READY']) url = 'http://'+str(conf['HTTP_BIND'])+':'+str(conf['HTTP_PORT'])+'/token'
webbrowser.open_new_tab(url)
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()
logging.info("Please open your browser and visit: http://"+str(conf['HTTP_BIND']+":"+str(conf['HTTP_PORT'])+"/")) irc.connect(conf['IRC_SERVER'], 6667, conf['IRC_CHANNEL'], conf['IRC_USERNAME'], conf['IRC_OAUTH_TOKEN'])
irc.sendmsg(conf['IRC_CHANNEL'], 'MrDestructoid', conf['MESSAGE']['READY'])
while True: logging.info("Please open your browser and visit: http://%s:%s/", conf['HTTP_BIND'], conf['HTTP_PORT'])
if conf['LOG_LEVEL'] == "DEBUG": url = 'http://'+str(conf['HTTP_BIND'])+':'+str(conf['HTTP_PORT'])
time.sleep(1) webbrowser.open_new_tab(url)
try: while True:
irc.get_response() if conf['LOG_LEVEL'] == "DEBUG":
time.sleep(1)
if not irc.tts_status: try:
logging.debug("TTS is disabled") irc.get_response()
if conf['LOG_LEVEL'] == "DEBUG":
time.sleep(1)
continue
confreload = datetime.datetime.now() if not irc.tts_status:
if confreload - lastreload > datetime.timedelta(seconds=60): logging.debug("TTS is disabled")
conf = load_config() if conf['LOG_LEVEL'] == "DEBUG":
lastreload = datetime.datetime.now() time.sleep(1)
continue
logging.debug('Raw message queue:') confreload = datetime.datetime.now()
logging.debug(msg_queue_raw) if confreload - lastreload > datetime.timedelta(seconds=60):
conf = load_config()
lastreload = datetime.datetime.now()
for raw_msg in msg_queue_raw: if irc.quickvote and irc.votemsg:
logging.debug('Raw msg:') logging.info('Quickvote is active')
irc.sendmsg(conf['IRC_CHANNEL'], "@chat", conf['MESSAGE']['VOTESTART'] + " (" + str(irc.votemsg) + ")")
logging.debug('Raw message queue:')
logging.debug(msg_queue_raw) logging.debug(msg_queue_raw)
now = datetime.datetime.now() for raw_msg in msg_queue_raw:
if now - raw_msg['queuetime'] > datetime.timedelta(seconds=conf['IRC_CLEARMSG_TIMEOUT']): logging.debug('Raw msg:')
logging.debug('clearmsg_timeout reached') logging.debug(msg_queue_raw)
if raw_msg['timestamp'] not in msg_queue:
logging.info('Sending TTS message') now = datetime.datetime.now()
msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']] if now - raw_msg['queuetime'] > datetime.timedelta(seconds=conf['IRC_CLEARMSG_TIMEOUT']):
logging.debug(msg_queue) logging.debug('clearmsg_timeout reached')
else: if raw_msg['timestamp'] not in msg_queue:
logging.debug('Msg is already in queue') logging.info('Sending TTS message')
except KeyboardInterrupt: msg_queue[raw_msg['timestamp']] = [raw_msg['user'], raw_msg['msg']]
logging.info('Exiting...') logging.debug(msg_queue)
sys.exit() else:
logging.debug('Msg is already in queue')
except KeyboardInterrupt:
logging.info('Exiting...')
sys.exit()
if __name__ == "__main__": if __name__ == "__main__":
main() main()