mirror of https://gitlab.com/gpvkt/twitchtts.git
Merge branch 'experimental' into 'main'
Experimental See merge request gpvkt/twitchtts!1
This commit is contained in:
commit
9bc6e4b1fa
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,12 +2,26 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.0.0] - 2022-08-11
|
||||
|
||||
Initial Release
|
||||
## [1.1.0] - unreleased
|
||||
|
||||
### Added
|
||||
|
||||
* `!quickvote` feature (see README.md for details)
|
||||
* `!ping` command added
|
||||
|
||||
* Configoption to start TTS in disabled mode
|
||||
* OAuth-Token generator
|
||||
* Webbrowser autostart
|
||||
|
||||
### 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
|
||||
|
||||
* Improved error handling
|
||||
|
||||
## [1.0.0] - 2022-08-11
|
||||
|
||||
Initial Release
|
||||
|
|
22
README.md
22
README.md
|
@ -45,6 +45,7 @@ http:
|
|||
bind: "localhost"
|
||||
|
||||
bot:
|
||||
start_enabled: True
|
||||
subonly: False
|
||||
modonly: False
|
||||
message_length: 200
|
||||
|
@ -59,6 +60,10 @@ messages:
|
|||
whitelist: "Sorry, you are not allowed to use TTS."
|
||||
ready: "TTS bot alpha ready!"
|
||||
says: "says"
|
||||
votestart: "Quickvote started. Send #yourchoice to participate."
|
||||
voteend: "Quickvote ended. The results are:"
|
||||
votenobody: "Nobody casted a vote. :("
|
||||
votes: "Votes"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
|
@ -80,13 +85,18 @@ whitelist:
|
|||
* `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).
|
||||
|
||||
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
|
||||
|
||||
* `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
|
||||
|
@ -102,11 +112,17 @@ whitelist:
|
|||
* `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.
|
||||
* `votes`: Suffix to vote count.
|
||||
|
||||
##### log
|
||||
|
||||
* `level`: The loglevel, valid values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
||||
|
||||
Do not use `DEBUG` in a production environment.
|
||||
|
||||
##### 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).
|
||||
|
@ -146,6 +162,10 @@ Additional commands (broadcaster and mods only) are:
|
|||
* `!dtts <username>`: Disable 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
|
||||
|
||||
If you prefer to build your own `tts.exe` instead of using the shipped one, you can do as follows:
|
||||
|
|
|
@ -10,6 +10,7 @@ http:
|
|||
bind: "localhost"
|
||||
|
||||
bot:
|
||||
start_enabled: True
|
||||
subonly: False
|
||||
modonly: False
|
||||
message_length: 200
|
||||
|
@ -25,6 +26,10 @@ messages:
|
|||
modonly: "Sorry, TTS is a mod-only feature."
|
||||
ready: "TTS bot alpha ready!"
|
||||
says: "says"
|
||||
votestart: "Quickvote started. Send #yourchoice to participate."
|
||||
voteend: "Quickvote ended. The results are:"
|
||||
votenobody: "Nobody casted a vote. :("
|
||||
votes: "Votes"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
|
|
341
tts.py
341
tts.py
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
"""
|
||||
TwitchTTS
|
||||
|
@ -20,32 +21,43 @@
|
|||
"""
|
||||
|
||||
import json
|
||||
import yaml
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import socketserver
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
|
||||
from threading import Thread
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs
|
||||
from collections import Counter
|
||||
|
||||
import yaml
|
||||
|
||||
class IRC:
|
||||
"""IRC bot"""
|
||||
irc = socket.socket()
|
||||
|
||||
def __init__(self):
|
||||
self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.tts_denied = []
|
||||
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:
|
||||
self.tts_allowed = conf['WHITELIST_USER']
|
||||
|
||||
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:
|
||||
self.irc.connect((server, port))
|
||||
except ConnectionResetError:
|
||||
|
@ -55,7 +67,9 @@ class IRC:
|
|||
self.irc.settimeout(1)
|
||||
|
||||
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("CAP REQ :twitch.tv/commands twitch.tv/tags \r\n", "UTF-8"))
|
||||
time.sleep(5)
|
||||
|
@ -63,22 +77,31 @@ class IRC:
|
|||
try:
|
||||
self.irc.send(bytes("JOIN " + channel + "\r\n", "UTF-8"))
|
||||
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)
|
||||
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"))
|
||||
|
||||
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"))
|
||||
|
||||
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:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logging.exception('An unknown error occured while getting a IRC response.')
|
||||
sys.exit(255)
|
||||
|
||||
|
@ -90,7 +113,7 @@ class IRC:
|
|||
logging.info('CLEARMSG received')
|
||||
msgid = False
|
||||
|
||||
global msg_queue_raw
|
||||
global msg_queue_raw # pylint: disable=global-statement,invalid-name
|
||||
filtered_msg_queue = []
|
||||
|
||||
tags = resp.split(';')
|
||||
|
@ -102,7 +125,7 @@ class IRC:
|
|||
|
||||
for msg in list(msg_queue_raw):
|
||||
if msg['msgid'] == msgid:
|
||||
logging.info('Suppressing message '+str(msgid))
|
||||
logging.info('Suppressing message %s', msgid)
|
||||
else:
|
||||
filtered_msg_queue.append(msg)
|
||||
|
||||
|
@ -134,22 +157,21 @@ class IRC:
|
|||
badges = tag.rsplit('badges=',1)[1]
|
||||
if tag.startswith('subscriber='):
|
||||
subscriber = tag.rsplit('subscriber=',1)[1]
|
||||
logging.debug('Subscriber: '+str(subscriber))
|
||||
logging.debug('Subscriber: %s', subscriber)
|
||||
if tag.startswith('id='):
|
||||
msgid = tag.rsplit('id=',1)[1]
|
||||
logging.debug('Message ID: '+str(msgid))
|
||||
logging.debug('Message ID: %s', msgid)
|
||||
if tag.startswith('display-name='):
|
||||
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 = msg.split(':',1)[1]
|
||||
msg = msg.replace('\r\n','')
|
||||
msglen = len(msg)
|
||||
|
||||
logging.debug('Msg:')
|
||||
logging.debug(msg)
|
||||
logging.debug('Msg length: '+str(msglen))
|
||||
logging.debug('Msg: %s', msg)
|
||||
logging.debug('Msg length: %s', msglen)
|
||||
logging.debug('Deny List:')
|
||||
logging.debug(self.tts_denied)
|
||||
|
||||
|
@ -162,17 +184,62 @@ class IRC:
|
|||
logging.debug('Removing "@" from username')
|
||||
user = user.replace('@', '')
|
||||
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)
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
if msg.startswith('!ping'):
|
||||
logging.debug('Ping check received.')
|
||||
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), "Pong!")
|
||||
logging.debug("Ping check received.")
|
||||
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
|
||||
|
||||
|
@ -184,21 +251,27 @@ class IRC:
|
|||
logging.debug('Removing "@" from username')
|
||||
user = user.replace('@', '')
|
||||
|
||||
logging.info("Adding "+str(user)+" to whitelist")
|
||||
logging.info("Adding %s to whitelist", user)
|
||||
self.tts_allowed.append(user)
|
||||
|
||||
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)
|
||||
|
||||
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'):
|
||||
logging.info('TTS is now turned off')
|
||||
msg_queue.clear()
|
||||
msg_queue_raw.clear()
|
||||
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
|
||||
|
||||
|
@ -207,7 +280,7 @@ class IRC:
|
|||
msg_queue.clear()
|
||||
msg_queue_raw.clear()
|
||||
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
|
||||
|
||||
|
@ -216,17 +289,20 @@ class IRC:
|
|||
|
||||
if msglen > conf['IRC_TTS_LEN']:
|
||||
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
|
||||
|
||||
logging.debug("tts status: %s", self.tts_status)
|
||||
logging.debug(conf['TTS_STARTENABLED'])
|
||||
|
||||
if not self.tts_status:
|
||||
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
|
||||
|
||||
if user in self.tts_denied:
|
||||
logging.info(str(user) + " is not allowed to use TTS")
|
||||
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DENIED'])
|
||||
logging.info("%s is not allowed to use TTS", user)
|
||||
self.sendmsg(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['DENIED'])
|
||||
return False
|
||||
|
||||
if conf['IRC_SUBONLY']:
|
||||
|
@ -234,7 +310,7 @@ class IRC:
|
|||
logging.debug('TTS is sub-only and user has allowance')
|
||||
else:
|
||||
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
|
||||
|
||||
if conf['IRC_MODONLY']:
|
||||
|
@ -242,25 +318,41 @@ class IRC:
|
|||
logging.debug('TTS is mod-only and user has allowance')
|
||||
else:
|
||||
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
|
||||
|
||||
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(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
|
||||
else:
|
||||
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
|
||||
|
||||
logging.info('Valid TTS message, adding to raw queue')
|
||||
tts = True
|
||||
now = datetime.datetime.now()
|
||||
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)
|
||||
|
||||
return True
|
||||
|
@ -268,48 +360,52 @@ class IRC:
|
|||
return False
|
||||
|
||||
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
|
||||
|
||||
def do_GET(self):
|
||||
def do_GET(self): # pylint: disable=invalid-name
|
||||
"""Process GET requests"""
|
||||
if self.path == '/':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
with open("tts.html", "rb") as fh:
|
||||
html = fh.read()
|
||||
with open("tts.html", "rb") as file:
|
||||
html = file.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
elif self.path == '/favicon.ico':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'image/x-icon')
|
||||
self.end_headers()
|
||||
with open("favicon.ico", "rb") as fh:
|
||||
icon = fh.read()
|
||||
with open("favicon.ico", "rb") as file:
|
||||
icon = file.read()
|
||||
self.wfile.write(icon)
|
||||
|
||||
elif self.path == '/tts.js':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/javascript')
|
||||
self.end_headers()
|
||||
with open("tts.js", "rb") as fh:
|
||||
html = fh.read()
|
||||
with open("tts.js", "rb") as file:
|
||||
html = file.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
elif self.path == '/jquery.js':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/javascript')
|
||||
self.end_headers()
|
||||
with open("jquery.js", "rb") as fh:
|
||||
html = fh.read()
|
||||
with open("jquery.js", "rb") as file:
|
||||
html = file.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
elif self.path == '/bootstrap.min.css':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/css')
|
||||
self.end_headers()
|
||||
with open("bootstrap.min.css", "rb") as fh:
|
||||
html = fh.read()
|
||||
with open("bootstrap.min.css", "rb") as file:
|
||||
html = file.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
elif self.path.startswith('/tts_queue'):
|
||||
|
@ -325,9 +421,9 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
for key in list(sorted_tts.keys()):
|
||||
if key not in tts_done:
|
||||
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:
|
||||
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)
|
||||
self.wfile.write(bytes(str(tts_json)+"\n", "utf-8"))
|
||||
|
@ -347,6 +443,41 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
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:
|
||||
self.send_response(404)
|
||||
self.send_header('Server', 'TTS')
|
||||
|
@ -357,13 +488,16 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
def http_serve_forever(httpd):
|
||||
httpd.serve_forever()
|
||||
"""httpd loop"""
|
||||
httpd.serve_forever()
|
||||
|
||||
def load_config():
|
||||
"""Loading config variables"""
|
||||
|
||||
logging.info("Loading configfile")
|
||||
|
||||
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)
|
||||
except FileNotFoundError:
|
||||
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:
|
||||
try:
|
||||
logging.debug('Fetching config: '+str(section))
|
||||
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_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['LOG_LEVEL'] = cfg['log']['level'] or "INFO"
|
||||
conf['HTTP_PORT'] = cfg['http']['port'] or 80
|
||||
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']['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 []
|
||||
|
||||
if 'whitelist' in cfg:
|
||||
|
@ -420,7 +560,8 @@ def load_config():
|
|||
if not conf['IRC_USERNAME']:
|
||||
raise ValueError('Please add the bots username to config.yml.')
|
||||
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:'):
|
||||
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
|
||||
|
||||
def main():
|
||||
"""Main loop"""
|
||||
|
||||
global conf # pylint: disable=global-statement,invalid-name
|
||||
|
||||
conf = load_config()
|
||||
lastreload = datetime.datetime.now()
|
||||
logging.getLogger().setLevel(conf['LOG_LEVEL'])
|
||||
if conf['LOG_LEVEL'] == 'DEBUG':
|
||||
sys.tracebacklimit = 5
|
||||
|
||||
|
||||
logging.info("Starting Webserver")
|
||||
httpd = socketserver.TCPServer((conf['HTTP_BIND'], conf['HTTP_PORT']), HTTPserv)
|
||||
httpd.allow_reuse_port = True
|
||||
|
@ -449,50 +594,66 @@ def main():
|
|||
http_thread = Thread(target=http_serve_forever, daemon=True, args=(httpd, ))
|
||||
http_thread.start()
|
||||
|
||||
logging.info("Starting IRC bot")
|
||||
irc = IRC()
|
||||
irc.connect(conf['IRC_SERVER'], 6667, conf['IRC_CHANNEL'], conf['IRC_USERNAME'], conf['IRC_OAUTH_TOKEN'])
|
||||
irc.sendpriv(conf['IRC_CHANNEL'], 'MrDestructoid', conf['MESSAGE']['READY'])
|
||||
if conf['IRC_OAUTH_TOKEN'] == "Invalid":
|
||||
logging.error('No OAuth Token, skipping start of IRC bot.')
|
||||
logging.error('Please open http://%s:%s/token to generate your OAuth-Token.', conf['HTTP_BIND'], conf['HTTP_PORT'])
|
||||
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:
|
||||
if conf['LOG_LEVEL'] == "DEBUG":
|
||||
time.sleep(1)
|
||||
logging.info("Please open your browser and visit: http://%s:%s/", conf['HTTP_BIND'], conf['HTTP_PORT'])
|
||||
url = 'http://'+str(conf['HTTP_BIND'])+':'+str(conf['HTTP_PORT'])
|
||||
webbrowser.open_new_tab(url)
|
||||
|
||||
try:
|
||||
irc.get_response()
|
||||
while True:
|
||||
if conf['LOG_LEVEL'] == "DEBUG":
|
||||
time.sleep(1)
|
||||
|
||||
if not irc.tts_status:
|
||||
logging.debug("TTS is disabled")
|
||||
if conf['LOG_LEVEL'] == "DEBUG":
|
||||
time.sleep(1)
|
||||
continue
|
||||
try:
|
||||
irc.get_response()
|
||||
|
||||
confreload = datetime.datetime.now()
|
||||
if confreload - lastreload > datetime.timedelta(seconds=60):
|
||||
conf = load_config()
|
||||
lastreload = datetime.datetime.now()
|
||||
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)
|
||||
confreload = datetime.datetime.now()
|
||||
if confreload - lastreload > datetime.timedelta(seconds=60):
|
||||
conf = load_config()
|
||||
lastreload = datetime.datetime.now()
|
||||
|
||||
for raw_msg in msg_queue_raw:
|
||||
logging.debug('Raw msg:')
|
||||
if irc.quickvote and irc.votemsg:
|
||||
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)
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
Loading…
Reference in New Issue