2022-08-10 20:58:51 +02:00
#!/usr/bin/python3
# -*- coding: utf-8 -*-
2022-08-12 18:54:40 +02:00
# pylint: disable=line-too-long
2022-08-10 20:58:51 +02:00
2022-08-11 13:32:23 +02:00
"""
TwitchTTS
Copyright ( C ) 2022 gpkvt
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program . If not , see < http : / / www . gnu . org / licenses / > .
"""
2022-08-13 09:19:26 +02:00
import os
2022-08-10 20:58:51 +02:00
import sys
import time
2022-08-13 09:19:26 +02:00
import json
import socket
import random
import logging
2022-08-10 20:58:51 +02:00
import datetime
2022-08-13 09:19:26 +02:00
import webbrowser
2022-08-10 20:58:51 +02:00
import socketserver
2022-08-12 18:54:40 +02:00
import urllib . parse
2022-08-13 09:19:26 +02:00
import urllib . request
2022-08-10 20:58:51 +02:00
2022-08-11 01:27:27 +02:00
from threading import Thread
2022-08-12 18:54:40 +02:00
from collections import Counter
2022-08-13 09:19:26 +02:00
from urllib . parse import parse_qs
2022-08-14 11:41:05 +02:00
from urllib . error import HTTPError
2022-08-13 09:19:26 +02:00
from http . server import BaseHTTPRequestHandler
2022-08-12 18:54:40 +02:00
import yaml
2022-08-10 20:58:51 +02:00
class IRC :
2022-08-14 11:41:05 +02:00
""" IRC bot """
2022-08-10 20:58:51 +02:00
irc = socket . socket ( )
def __init__ ( self ) :
self . irc = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
self . tts_denied = [ ]
2022-08-11 12:14:58 +02:00
self . tts_allowed = [ ]
2022-08-12 18:54:40 +02:00
self . tts_status = conf [ ' TTS_STARTENABLED ' ]
2022-08-13 14:34:46 +02:00
self . quickvote_status = False
2022-08-12 18:54:40 +02:00
self . votemsg = False
self . poll = { }
self . pollcount = 0
2022-08-11 12:14:58 +02:00
2022-08-10 22:25:15 +02:00
if ' WHITELIST_USER ' in conf :
2022-08-10 20:58:51 +02:00
self . tts_allowed = conf [ ' WHITELIST_USER ' ]
def connect ( self , server , port , channel , botnick , botpass ) :
2022-08-14 11:41:05 +02:00
""" Connect to Twitch IRC servers """
2022-08-12 18:54:40 +02:00
logging . info ( " Connecting to: %s " , server )
2022-08-16 22:45:36 +02:00
logging . info ( ' Waiting... ' )
2022-08-10 20:58:51 +02:00
try :
self . irc . connect ( ( server , port ) )
except ConnectionResetError :
2022-08-11 09:46:07 +02:00
logging . fatal ( ' Twitch refused to connect, please check your settings and try again. ' )
2022-08-10 20:58:51 +02:00
sys . exit ( 252 )
self . irc . settimeout ( 1 )
self . irc . send ( bytes ( " PASS " + botpass + " \r \n " , " UTF-8 " ) )
2022-08-12 18:54:40 +02:00
self . irc . send ( bytes (
" USER " + botnick + " " + botnick + " " + botnick + " :python \r \n " , " UTF-8 " )
)
2022-08-10 20:58:51 +02:00
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 )
try :
self . irc . send ( bytes ( " JOIN " + channel + " \r \n " , " UTF-8 " ) )
except ConnectionResetError :
2022-08-13 09:19:26 +02:00
logging . warning ( ' JOIN was refused, will try again in 30 seconds. ' )
time . sleep ( 30 )
2022-08-12 18:54:40 +02:00
logging . warning ( ' Please check your credentials, if this error persists. ' )
2022-08-10 20:58:51 +02:00
self . irc . send ( bytes ( " JOIN " + channel + " \r \n " , " UTF-8 " ) )
2022-08-12 18:54:40 +02:00
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
"""
2022-08-10 20:58:51 +02:00
self . irc . send ( bytes ( " PRIVMSG " + channel + " : " + user + " " + msg + " \r \n " , " UTF-8 " ) )
2022-08-13 19:35:02 +02:00
def resp_ping ( self ) :
""" Respond to PING """
logging . debug ( ' PING received ' )
self . irc . send ( bytes ( ' PONG :tmi.twitch.tv \r \n ' , " UTF-8 " ) )
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
global msg_queue_raw # pylint: disable=global-statement,invalid-name
filtered_msg_queue = [ ]
tags = resp . split ( ' ; ' )
for tag in tags :
if " target-msg-id= " in tag :
msgid = tag . rsplit ( ' target-msg-id= ' , 1 ) [ 1 ]
logging . debug ( ' Trying to suppress message ' )
logging . debug ( msgid )
for msg in list ( msg_queue_raw ) :
if msg [ ' msgid ' ] == msgid :
logging . info ( ' Suppressing message %s ' , msgid )
else :
filtered_msg_queue . append ( msg )
msg_queue_raw = filtered_msg_queue
def resp_privmsg ( self , resp ) :
""" Respond to PRIVMSG """
logging . debug ( ' PRIVMSG received ' )
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 ( )
2022-08-14 11:41:05 +02:00
logging . debug ( " poll: %s " , self . poll )
2022-08-13 19:35:02 +02:00
if msg . startswith ( ' !tts ' ) :
2022-08-13 20:05:29 +02:00
logging . info ( ' !tts command detected ' )
2022-08-14 11:41:05 +02:00
self . Commands . tts ( self , message , tags )
2022-08-13 19:35:02 +02:00
def get_tags ( self , resp ) :
""" Strip tags from response """
tags = resp . split ( ' ; ' )
for tag in tags :
if tag . startswith ( ' badges= ' ) :
badges = tag . rsplit ( ' badges= ' , 1 ) [ 1 ]
if tag . startswith ( ' subscriber= ' ) :
subscriber = tag . rsplit ( ' subscriber= ' , 1 ) [ 1 ]
logging . debug ( ' Subscriber: %s ' , subscriber )
if tag . startswith ( ' id= ' ) :
msgid = tag . rsplit ( ' id= ' , 1 ) [ 1 ]
logging . debug ( ' Message ID: %s ' , msgid )
if tag . startswith ( ' display-name= ' ) :
user = tag . rsplit ( ' display-name= ' , 1 ) [ 1 ] . lower ( )
logging . debug ( ' Username: %s ' , user )
tags = { }
tags [ ' badges ' ] = badges
tags [ ' subscriber ' ] = subscriber
tags [ ' msgid ' ] = msgid
tags [ ' user ' ] = user
return tags
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! " )
elif msg . startswith ( ' !dtts ' ) :
logging . debug ( " !dtts command detected " )
self . Commands . dtts ( self , msg )
elif msg . startswith ( ' !random ' ) :
logging . info ( ' !random command detected ' )
self . Commands . random ( self , msg )
elif msg . startswith ( ' !quickvote ' ) :
logging . info ( " !quickvote command detected " )
self . Commands . quickvote ( self , msg )
elif msg . startswith ( ' !ptts ' ) :
logging . debug ( " !ptts command detected " )
self . Commands . ptts ( self , msg )
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 ' ] )
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 """
2022-08-13 20:43:00 +02:00
if not conf [ ' IRC_SUBONLY ' ] :
return False
2022-08-13 19:35:02 +02:00
subscriber = tags [ ' subscriber ' ]
badges = tags [ ' badges ' ]
user = tags [ ' user ' ]
if subscriber != " 0 " or ' moderator ' in badges or ' broadcaster ' in badges :
2022-08-13 20:43:00 +02:00
logging . info ( ' TTS is sub-only and user has allowance ' )
2022-08-13 19:35:02 +02:00
return False
2022-08-13 20:43:00 +02:00
logging . debug ( ' TTS is sub-only ' )
2022-08-13 19:35:02 +02:00
self . sendmsg ( conf [ ' IRC_CHANNEL ' ] , " @ " + str ( user ) , conf [ ' MESSAGE ' ] [ ' SUBONLY ' ] )
return True
def check_modonly ( self , tags ) :
""" modonly """
2022-08-13 20:43:00 +02:00
if not conf [ ' IRC_MODONLY ' ] :
return False
2022-08-13 19:35:02 +02:00
badges = tags [ ' badges ' ]
user = tags [ ' user ' ]
if ' moderator ' in badges or ' broadcaster ' in badges :
2022-08-13 20:43:00 +02:00
logging . info ( ' TTS is mod-only and user has allowance ' )
2022-08-13 19:35:02 +02:00
return False
2022-08-13 20:43:00 +02:00
logging . debug ( ' TTS is mod-only ' )
2022-08-13 19:35:02 +02:00
self . sendmsg ( conf [ ' IRC_CHANNEL ' ] , " @ " + str ( user ) , conf [ ' MESSAGE ' ] [ ' MODONLY ' ] )
return True
2022-08-13 20:05:29 +02:00
def check_tts_disabled ( self , user ) :
""" Check if TTS is disabled """
2022-08-13 19:35:02 +02:00
if not self . tts_status :
self . sendmsg ( conf [ ' IRC_CHANNEL ' ] , " @ " + str ( user ) , conf [ ' MESSAGE ' ] [ ' DISABLED ' ] )
2022-08-13 20:05:29 +02:00
return True
2022-08-13 19:35:02 +02:00
logging . debug ( ' TTS is enabled ' )
2022-08-13 20:05:29 +02:00
return False
2022-08-13 19:35:02 +02:00
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
2022-08-13 20:43:00 +02:00
logging . debug ( ' Check length: Message is ok ' )
2022-08-13 19:35:02 +02:00
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 :
2022-08-13 20:05:29 +02:00
logging . debug ( " tts_allowed: %s " , self . tts_allowed )
2022-08-13 19:35:02 +02:00
self . sendmsg (
conf [ ' IRC_CHANNEL ' ] ,
" @ " + str ( user ) , conf [ ' MESSAGE ' ] [ ' WHITELISTONLY ' ]
)
return False
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 ,
" badges " : badges ,
" subscriber " : subscriber ,
" msgid " : msgid ,
" user " : user ,
" length " : msglen ,
" queuetime " : now ,
" timestamp " : timestamp
}
msg_queue_raw . append ( msg )
2022-08-10 20:58:51 +02:00
def get_response ( self ) :
2022-08-12 18:54:40 +02:00
""" Get and process response from IRC """
2022-08-10 20:58:51 +02:00
try :
resp = self . irc . recv ( 2048 ) . decode ( " UTF-8 " )
logging . debug ( ' resp: ' )
logging . debug ( resp )
except socket . timeout :
2022-08-13 19:35:02 +02:00
return
2022-08-12 18:54:40 +02:00
except Exception : # pylint: disable=broad-except
2022-08-12 10:06:42 +02:00
logging . exception ( ' An unknown error occured while getting a IRC response. ' )
2022-08-10 20:58:51 +02:00
sys . exit ( 255 )
if resp . find ( ' PING ' ) != - 1 :
2022-08-13 19:35:02 +02:00
self . resp_ping ( )
2022-08-10 20:58:51 +02:00
if resp . find ( ' CLEARMSG ' ) != - 1 :
2022-08-13 19:35:02 +02:00
self . resp_clearmsg ( resp )
2022-08-10 20:58:51 +02:00
2022-08-12 10:06:42 +02:00
if resp . find ( ' NOTICE ' ) != - 1 :
2022-08-13 19:35:02 +02:00
self . resp_notice ( resp )
2022-08-12 10:06:42 +02:00
2022-08-10 20:58:51 +02:00
if resp . find ( ' PRIVMSG ' ) != - 1 :
2022-08-13 19:35:02 +02:00
self . resp_privmsg ( resp )
2022-08-10 20:58:51 +02:00
2022-08-13 14:34:46 +02:00
class Commands ( ) :
""" Bot commands """
def __init__ ( self ) :
self . tts_denied = [ ]
self . tts_allowed = [ ]
self . quickvote_status = self . quickvote_status
self . votemsg = self . votemsg
self . poll = self . poll
self . pollcount = self . pollcount
2022-08-14 11:41:05 +02:00
def tts ( self , msg , tags ) :
2022-08-15 20:36:11 +02:00
""" !tts command
2022-08-14 11:41:05 +02:00
Check if message is valid and send it to queue
: param str msg : The IRC message triggering the command
: param dict tags : The message metadata
"""
user = tags [ ' user ' ]
if IRC . check_tts_disabled ( self , user ) :
logging . info ( ' TTS is disabled ' )
elif IRC . check_msg_too_long ( self , msg , user ) :
logging . info ( ' TTS message is too long ' )
elif IRC . check_user_denied ( self , user ) :
logging . info ( ' User is not allowed to use TTS ' )
elif IRC . check_subonly ( self , tags ) :
logging . info ( ' TTS is sub-only ' )
elif IRC . check_modonly ( self , tags ) :
logging . info ( ' TTS is mod-only ' )
elif IRC . check_whitelist ( self , user ) :
logging . info ( ' User is not on whitelist ' )
else :
logging . info ( ' Sending TTS message to raw_queue ' )
IRC . send_tts_msg ( self , msg , tags )
2022-08-13 14:34:46 +02:00
def quickvote ( self , msg ) :
""" !quickvote command
Starts or stops the ! quickvote function . On stop calculates the 5 most casted
votes and send them to chat . The highest vote is send to msg_queue .
: param str msg : The IRC message triggering the command
"""
if self . quickvote_status :
logging . debug ( ' Quickvote stopped ' )
if self . pollcount == 0 :
logging . info ( " Nobody voted " )
IRC . sendmsg ( self , conf [ ' IRC_CHANNEL ' ] , " @chat " , conf [ ' MESSAGE ' ] [ ' VOTEEND ' ] )
IRC . sendmsg ( self , 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_status = False
self . poll = { }
2022-08-13 19:35:02 +02:00
return
2022-08-13 14:34:46 +02:00
logging . info ( " Counting votes " )
count = 0
count = Counter ( self . poll . values ( ) ) . most_common ( 5 )
IRC . sendmsg ( self , conf [ ' IRC_CHANNEL ' ] , " @chat " , conf [ ' MESSAGE ' ] [ ' VOTEEND ' ] )
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 :
IRC . sendmsg (
self ,
conf [ ' IRC_CHANNEL ' ] , " * " ,
str ( key ) + " ( " + str ( value ) + " " + conf [ ' MESSAGE ' ] [ ' VOTES ' ] + " ) "
)
self . quickvote_status = False
self . poll = { }
self . pollcount = 0
return
logging . debug ( ' Quickvote started ' )
self . quickvote_status = True
self . votemsg = msg . split ( ' !quickvote ' , 1 ) [ 1 ] . strip ( )
if self . votemsg :
IRC . sendmsg ( self ,
conf [ ' IRC_CHANNEL ' ] , " @chat " ,
conf [ ' MESSAGE ' ] [ ' VOTESTART ' ] + " ( " + str ( self . votemsg ) + " ) "
)
else :
IRC . sendmsg ( self , conf [ ' IRC_CHANNEL ' ] , " @chat " , conf [ ' MESSAGE ' ] [ ' VOTESTART ' ] )
return
def random ( self , msg ) :
""" !random command
Read a random line from randomfile and put it into msg_queue
If no file is given in msg a standard file will be used
: param str msg : The IRC message triggering the command
: raise : FileNotFoundError if randomfile does not exists
: return : True if line was successfully read and added to msg_queue
: rtype : bool
"""
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
def ptts ( self , msg ) :
""" !ptts command
Add user to tts_allowed list and remove user from tts_denied list
: param str msg : The IRC message triggering the command
"""
user = msg . replace ( ' !ptts ' , ' ' ) . strip ( ) . lower ( )
if user . startswith ( ' @ ' ) :
logging . debug ( ' Removing " @ " from username ' )
user = user . replace ( ' @ ' , ' ' )
logging . info ( " Adding %s to whitelist " , user )
self . tts_allowed . append ( user )
if user in self . tts_denied :
logging . info ( " Removing %s from deny list " , user )
self . tts_denied . remove ( user )
return
def dtts ( self , msg ) :
""" !dtts command
Add user to tts_denied list and remove user from tts_allowed list
: param str msg : The IRC message triggering the command
"""
user = msg . replace ( ' !dtts ' , ' ' ) . strip ( ) . lower ( )
if user . startswith ( ' @ ' ) :
logging . debug ( ' Removing " @ " from username ' )
user = user . replace ( ' @ ' , ' ' )
if user not in self . tts_denied :
logging . info ( " Adding %s to deny list " , user )
self . tts_denied . append ( user )
if user in self . tts_allowed :
logging . info ( " Removing %s from allowed list " , user )
self . tts_allowed . remove ( user )
return
2022-08-10 20:58:51 +02:00
class HTTPserv ( BaseHTTPRequestHandler ) :
2022-08-12 18:54:40 +02:00
""" Simple HTTP Server """
def log_message ( self , format , * args ) : # pylint: disable=redefined-builtin
""" Suppress HTTP log messages """
2022-08-11 00:09:06 +02:00
return
2022-08-12 18:54:40 +02:00
def do_GET ( self ) : # pylint: disable=invalid-name
""" Process GET requests """
2022-08-10 20:58:51 +02:00
if self . path == ' / ' :
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/html ' )
self . end_headers ( )
2022-08-12 18:54:40 +02:00
with open ( " tts.html " , " rb " ) as file :
html = file . read ( )
2022-08-10 20:58:51 +02:00
self . wfile . write ( html )
2022-08-11 12:14:58 +02:00
elif self . path == ' /favicon.ico ' :
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' image/x-icon ' )
self . end_headers ( )
2022-08-12 18:54:40 +02:00
with open ( " favicon.ico " , " rb " ) as file :
icon = file . read ( )
2022-08-11 12:14:58 +02:00
self . wfile . write ( icon )
2022-08-11 10:42:56 +02:00
elif self . path == ' /tts.js ' :
2022-08-10 20:58:51 +02:00
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/javascript ' )
self . end_headers ( )
2022-08-12 18:54:40 +02:00
with open ( " tts.js " , " rb " ) as file :
html = file . read ( )
2022-08-10 20:58:51 +02:00
self . wfile . write ( html )
2022-08-11 10:42:56 +02:00
elif self . path == ' /jquery.js ' :
2022-08-10 20:58:51 +02:00
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/javascript ' )
self . end_headers ( )
2022-08-12 18:54:40 +02:00
with open ( " jquery.js " , " rb " ) as file :
html = file . read ( )
2022-08-10 20:58:51 +02:00
self . wfile . write ( html )
2022-08-11 10:42:56 +02:00
elif self . path == ' /bootstrap.min.css ' :
2022-08-10 20:58:51 +02:00
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/css ' )
self . end_headers ( )
2022-08-12 18:54:40 +02:00
with open ( " bootstrap.min.css " , " rb " ) as file :
html = file . read ( )
2022-08-10 20:58:51 +02:00
self . wfile . write ( html )
2022-08-11 10:42:56 +02:00
elif self . path . startswith ( ' /tts_queue ' ) :
2022-08-10 20:58:51 +02:00
tts_json = " "
tts = { }
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/json ' )
self . end_headers ( )
usermap = conf [ ' USERMAP ' ]
2022-08-13 23:08:29 +02:00
sorted_tts = { k : msg_queue [ k ] for k in sorted ( msg_queue , reverse = True ) }
2022-08-16 22:45:36 +02:00
logging . debug ( usermap )
2022-08-10 20:58:51 +02:00
for key in list ( sorted_tts . keys ( ) ) :
if key not in tts_done :
if msg_queue [ key ] [ 0 ] . lower ( ) in usermap :
2022-08-16 22:45:36 +02:00
logging . info ( ' Using usermap for user: %s ( %s ) ' , msg_queue [ key ] [ 0 ] , usermap [ msg_queue [ key ] [ 0 ] . lower ( ) ] )
2022-08-12 18:54:40 +02:00
tts = { str ( key ) : str ( usermap [ msg_queue [ key ] [ 0 ] . lower ( ) ] ) + " " + str ( conf [ ' MESSAGE ' ] [ ' SAYS ' ] ) + " : " + str ( msg_queue [ key ] [ 1 ] ) }
2022-08-10 20:58:51 +02:00
else :
2022-08-16 22:45:36 +02:00
logging . debug ( ' No usermap entry found for user: %s ' , msg_queue [ key ] [ 0 ] )
2022-08-12 18:54:40 +02:00
tts = { str ( key ) : str ( msg_queue [ key ] [ 0 ] ) + " " + str ( conf [ ' MESSAGE ' ] [ ' SAYS ' ] ) + " : " + str ( msg_queue [ key ] [ 1 ] ) }
2022-08-10 20:58:51 +02:00
tts_json = json . dumps ( tts )
self . wfile . write ( bytes ( str ( tts_json ) + " \n " , " utf-8 " ) )
2022-08-11 10:42:56 +02:00
elif self . path . startswith ( ' /tts_done ' ) :
2022-08-10 20:58:51 +02:00
get_params = parse_qs ( self . path )
if ' /tts_done?id ' in get_params :
logging . info ( " Removing message from queue " )
tts_done . append ( get_params [ ' /tts_done?id ' ] [ 0 ] )
self . send_response ( 200 )
self . send_header ( ' Content-type ' , ' text/plain ' )
self . end_headers ( )
self . wfile . write ( bytes ( " OK \n " , " utf-8 " ) )
else :
self . send_response ( 500 )
self . send_header ( ' Content-type ' , ' text/plain ' )
self . end_headers ( )
self . wfile . write ( bytes ( " Internal Server error \n " , " utf-8 " ) )
2022-08-14 11:41:05 +02:00
elif self . path . startswith ( ' /token ' ) :
2022-08-12 18:54:40 +02:00
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. ' )
2022-08-11 10:42:56 +02:00
else :
self . send_response ( 404 )
self . send_header ( ' Server ' , ' TTS ' )
self . send_header ( ' Content-type ' , ' text/plain ' )
self . end_headers ( )
self . wfile . write ( bytes ( " File not found. \n " , " utf-8 " ) )
2022-08-10 20:58:51 +02:00
return
def http_serve_forever ( httpd ) :
2022-08-12 18:54:40 +02:00
""" httpd loop """
httpd . serve_forever ( )
2022-08-10 20:58:51 +02:00
def load_config ( ) :
2022-08-12 18:54:40 +02:00
""" Loading config variables """
2022-08-10 20:58:51 +02:00
logging . info ( " Loading configfile " )
try :
2022-08-12 18:54:40 +02:00
with open ( " config.yml " , " r " , encoding = " UTF-8 " ) as ymlfile :
2022-08-10 20:58:51 +02:00
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. ' )
sys . exit ( 253 )
for section in cfg :
try :
2022-08-12 18:54:40 +02:00
logging . debug ( ' Fetching config: %s ' , section )
2022-08-13 09:19:26 +02:00
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 )
2022-08-12 18:54:40 +02:00
2022-08-13 09:19:26 +02:00
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 )
2022-08-13 20:43:00 +02:00
conf [ ' TTS_STARTENABLED ' ] = cfg . get ( ' bot ' , { } ) . get ( ' start_enabled ' , True )
2022-08-12 18:54:40 +02:00
2022-08-13 09:19:26 +02:00
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 " )
2022-08-10 20:58:51 +02:00
conf [ ' MESSAGE ' ] = { }
2022-08-13 09:19:26 +02:00
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 . 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 . get ( ' usermapping ' , [ ] )
2022-08-10 20:58:51 +02:00
if ' whitelist ' in cfg :
conf [ ' WHITELIST ' ] = True
conf [ ' WHITELIST_USER ' ] = cfg [ ' whitelist ' ]
else :
conf [ ' WHITELIST ' ] = False
2022-08-11 10:26:33 +02:00
except KeyError :
2022-08-10 20:58:51 +02:00
logging . exception ( ' Your config file is invalid, please check and try again. ' )
sys . exit ( 254 )
if conf [ ' WHITELIST ' ] :
logging . info ( ' Whitelist mode enabled ' )
2022-08-16 22:45:36 +02:00
logging . debug ( ' Whitelist: %s ' , conf [ ' WHITELIST_USER ' ] )
2022-08-10 20:58:51 +02:00
2022-08-11 10:26:33 +02:00
if not conf [ ' IRC_CHANNEL ' ] :
raise ValueError ( ' Please add your twitch channel to config.yml. ' )
if not conf [ ' IRC_USERNAME ' ] :
raise ValueError ( ' Please add the bots username to config.yml. ' )
if not conf [ ' IRC_OAUTH_TOKEN ' ] :
2022-08-12 18:54:40 +02:00
conf [ ' IRC_OAUTH_TOKEN ' ] = " Invalid "
return conf
2022-08-11 10:26:33 +02:00
if not conf [ ' IRC_OAUTH_TOKEN ' ] . startswith ( ' oauth: ' ) :
raise ValueError ( ' Your oauth-token is invalid, it has to start with: " oauth: " ' )
2022-08-10 20:58:51 +02:00
return conf
2022-08-13 19:35:02 +02:00
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 ' )
2022-08-14 11:41:05 +02:00
def get_url ( path = False ) :
""" Generate a valid URL from config values """
if conf [ ' HTTP_BIND ' ] == " 0.0.0.0 " :
url = " localhost "
else :
url = conf [ ' HTTP_BIND ' ]
2022-08-15 20:36:11 +02:00
2022-08-14 11:41:05 +02:00
url = " http:// " + str ( url ) + " : " + str ( conf [ ' HTTP_PORT ' ] ) + " / "
if path :
url = url + str ( path )
return url
2022-08-15 20:36:11 +02:00
def check_oauth_token ( ) :
2022-08-14 11:41:05 +02:00
""" Check for valid authentication via Twitch API """
2022-08-15 20:36:11 +02:00
global conf # pylint: disable=global-statement,invalid-name
2022-08-14 11:41:05 +02:00
logging . debug ( ' Checking OAuth Token ' )
2022-08-15 20:36:11 +02:00
2022-08-14 11:41:05 +02:00
try :
url = ' https://id.twitch.tv/oauth2/validate '
2022-08-15 20:36:11 +02:00
oauth = " OAuth " + str ( conf [ ' IRC_OAUTH_TOKEN ' ] . replace ( ' oauth: ' , ' ' ) )
2022-08-14 11:41:05 +02:00
request = urllib . request . Request ( url )
request . add_header ( ' Authorization ' , oauth )
urllib . request . urlopen ( request )
except HTTPError :
logging . fatal ( ' Twitch rejected your OAuth Token. Please check and generate a new one. ' )
logging . info ( ' Please open http:// %s : %s /token to generate your OAuth-Token. ' , conf [ ' HTTP_BIND ' ] , conf [ ' HTTP_PORT ' ] )
url = get_url ( " token " )
webbrowser . open_new_tab ( url )
logging . info ( ' Please complete the OAuth process and add the token into your " config.yml " within the next 5 minutes. ' )
time . sleep ( 300 )
conf = load_config ( )
check_oauth_token ( )
2022-08-15 20:36:11 +02:00
2022-08-16 22:45:36 +02:00
logging . info ( ' OAuth Token is valid ' )
2022-08-15 20:36:11 +02:00
return conf
2022-08-14 11:41:05 +02:00
2022-08-10 20:58:51 +02:00
def main ( ) :
2022-08-12 18:54:40 +02:00
""" Main loop """
2022-08-15 20:36:11 +02:00
global conf # pylint: disable=global-statement,invalid-name
2022-08-10 20:58:51 +02:00
conf = load_config ( )
2022-08-14 11:41:05 +02:00
2022-08-10 20:58:51 +02:00
lastreload = datetime . datetime . now ( )
logging . getLogger ( ) . setLevel ( conf [ ' LOG_LEVEL ' ] )
2022-08-11 10:26:33 +02:00
if conf [ ' LOG_LEVEL ' ] == ' DEBUG ' :
sys . tracebacklimit = 5
2022-08-12 18:54:40 +02:00
2022-08-10 20:58:51 +02:00
logging . info ( " Starting Webserver " )
httpd = socketserver . TCPServer ( ( conf [ ' HTTP_BIND ' ] , conf [ ' HTTP_PORT ' ] ) , HTTPserv )
httpd . allow_reuse_port = True
httpd . allow_reuse_address = True
http_thread = Thread ( target = http_serve_forever , daemon = True , args = ( httpd , ) )
http_thread . start ( )
2022-08-15 20:36:11 +02:00
check_oauth_token ( )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
logging . info ( " Starting IRC bot " )
irc = IRC ( )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
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 ' ] )
2022-08-12 18:54:40 +02:00
2022-08-16 22:45:36 +02:00
logging . info ( ' Connected and joined ' )
2022-08-14 11:41:05 +02:00
url = get_url ( )
logging . info ( " Please open your browser and visit: %s " , url )
2022-08-13 19:35:02 +02:00
webbrowser . open_new_tab ( url )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
while True :
if conf [ ' LOG_LEVEL ' ] == " DEBUG " :
time . sleep ( 1 )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
try :
irc . get_response ( )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
confreload = datetime . datetime . now ( )
if confreload - lastreload > datetime . timedelta ( seconds = 60 ) :
conf = load_config ( )
lastreload = datetime . datetime . now ( )
2022-08-12 18:54:40 +02:00
2022-08-13 19:35:02 +02:00
if irc . quickvote_status and irc . votemsg :
logging . info ( ' Quickvote is active ' )
irc . sendmsg ( conf [ ' IRC_CHANNEL ' ] , " @chat " , conf [ ' MESSAGE ' ] [ ' VOTESTART ' ] + " ( " + str ( irc . votemsg ) + " ) " )
2022-08-13 14:34:46 +02:00
2022-08-13 19:35:02 +02:00
if not irc . tts_status :
continue
2022-08-13 20:05:29 +02:00
logging . debug ( ' msg_queue_raw: %s ' , msg_queue_raw )
send_tts_queue ( )
2022-08-10 20:58:51 +02:00
2022-08-13 19:35:02 +02:00
except KeyboardInterrupt :
logging . info ( ' Exiting... ' )
sys . exit ( )
2022-08-10 20:58:51 +02:00
if __name__ == " __main__ " :
2022-08-15 20:36:11 +02:00
logging . basicConfig ( level = logging . DEBUG , format = ' %(asctime)s %(module)s %(threadName)s %(levelname)s : %(message)s ' )
sys . tracebacklimit = 3
2022-08-14 11:41:05 +02:00
conf = { }
tts_done = [ ]
msg_queue_raw = [ ]
msg_queue = { }
if sys . argv [ 1 : ] :
if sys . argv [ 1 ] == " --version " :
print ( ' Simple TTS Bot ' )
2022-08-16 22:45:36 +02:00
print ( ' Version 1.2.5 ' )
2022-08-14 11:41:05 +02:00
sys . exit ( 1 )
2022-08-10 20:58:51 +02:00
main ( )