twitchtts/tts.py

1633 lines
54 KiB
Python
Raw Permalink Normal View History

2022-08-10 20:58:51 +02:00
#!/usr/bin/python3
# -*- coding: utf-8 -*-
2022-08-27 22:07:35 +02:00
# pylint: disable=global-statement,broad-except,too-many-lines
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
2022-08-19 03:10:38 +02:00
import signal
2022-08-13 09:19:26 +02:00
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
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-19 03:10:38 +02:00
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
2022-08-12 18:54:40 +02:00
import yaml
2022-08-19 03:10:38 +02:00
import requests
2022-08-24 18:03:57 +02:00
import wikipedia
2022-08-10 20:58:51 +02:00
2022-08-18 16:45:11 +02:00
from fuzzywuzzy import process
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)
2022-08-28 11:43:12 +02:00
self.tts = {
"status": CONF['TTS_STARTENABLED'],
"whitelist": [],
"blacklist": []
}
self.quickvote = {
"status": False,
2022-08-28 12:33:46 +02:00
"message": False,
2022-08-28 11:43:12 +02:00
"count": 0,
"data": {}
}
self.pick = {
"status": False,
"number": 1,
"count": 0,
"pickme": []
}
2022-08-27 14:04:04 +02:00
if 'WHITELIST_USER' in CONF:
self.tts_allowed = CONF['WHITELIST_USER']
2022-08-10 20:58:51 +02:00
def connect(self, server, port, channel, botnick, botpass):
2022-08-27 15:19:44 +02:00
""" Connect to Twitch IRC servers
:param str server: Servername or IP
:param int port: Server Port
:param str channel: Channel to connect to
:param str botnick: Username
:param str botpass: OAuth Token
"""
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-28 11:43:12 +02:00
2022-08-10 20:58:51 +02:00
try:
self.irc.connect((server, port))
except ConnectionResetError:
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):
2022-08-27 15:19:44 +02:00
""" Send (a public) message to IRC channel
2022-08-12 18:54:40 +02:00
2022-08-27 15:19:44 +02:00
:param str channel: Channel to post msg to
:param str user: User to address message to
:param str msg: Message to send
2022-08-12 18:54:40 +02:00
"""
2022-08-10 20:58:51 +02:00
self.irc.send(bytes("PRIVMSG "+channel+" :"+user+" "+msg+"\r\n", "UTF-8"))
2022-08-28 12:45:30 +02:00
def __resp_ping(self):
2022-08-27 15:19:44 +02:00
""" Respond to PING
Respond to PING messages, to keep the connection alive.
"""
2022-08-13 19:35:02 +02:00
logging.debug('PING received')
self.irc.send(bytes('PONG :tmi.twitch.tv\r\n', "UTF-8"))
2022-08-28 12:45:30 +02:00
def __resp_notice(self, resp):
2022-08-27 15:19:44 +02:00
""" Respond to NOTICE
:param str resp: IRC Server message (in response to command)
"""
2022-08-13 19:35:02 +02:00
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)
2022-08-28 12:45:30 +02:00
def __resp_clearmsg(self, resp):
2022-08-27 15:44:57 +02:00
""" Respond to CLEARMSG
:param str resp: IRC Server message
"""
2022-08-13 19:35:02 +02:00
logging.info('CLEARMSG received')
msgid = False
2022-08-27 15:19:44 +02:00
global MSG_QUEUE_RAW
2022-08-13 19:35:02 +02:00
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)
2022-08-27 15:19:44 +02:00
for msg in list(MSG_QUEUE_RAW):
2022-08-13 19:35:02 +02:00
if msg['msgid'] == msgid:
logging.info('Suppressing message %s', msgid)
else:
filtered_msg_queue.append(msg)
2022-08-27 15:19:44 +02:00
MSG_QUEUE_RAW = filtered_msg_queue
2022-08-13 19:35:02 +02:00
2022-08-28 12:45:30 +02:00
def __resp_privmsg(self, resp):
2022-08-27 15:44:57 +02:00
""" Respond to PRIVMSG
:param str resp: IRC Server message
"""
2022-08-13 19:35:02 +02:00
logging.debug('PRIVMSG received')
2022-08-28 12:45:30 +02:00
tags = self.__get_tags(resp)
2022-10-20 02:42:51 +02:00
if not tags:
return False
2022-08-28 12:45:30 +02:00
message = self.__get_message(resp)
2022-08-13 19:35:02 +02:00
2022-08-28 12:45:30 +02:00
self.__priviledged_commands(message, tags)
self.__unpriviledged_commands(message, tags)
2022-08-19 04:38:19 +02:00
2022-08-28 12:45:30 +02:00
def __get_tags(self, resp):
2022-08-27 15:44:57 +02:00
""" Strip tags from response
:param str resp: IRC Server message
:return dict tags: Message metadata (tags)
"""
2022-08-13 19:35:02 +02:00
tags = resp.split(';')
2022-10-20 02:42:51 +02:00
2022-08-13 19:35:02 +02:00
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 = {}
2022-10-20 02:42:51 +02:00
try:
tags['badges'] = badges
tags['subscriber'] = subscriber
tags['msgid'] = msgid
tags['user'] = user
except: # pylint: disable=bare-except
logging.error('Received an invalid message')
return False
2022-08-13 19:35:02 +02:00
return tags
2022-08-28 12:45:30 +02:00
def __get_message(self, resp):
2022-08-27 15:44:57 +02:00
""" Transform IRC server message and determine length
:param str resp: IRC Server message
:return dict msg: Processed message
"""
2022-08-13 19:35:02 +02:00
msg = {}
msg['message'] = resp.rsplit('PRIVMSG #',1)[1].split(':',1)[1].replace('\r\n','')
msg['length'] = len(msg['message'])
return msg
2022-08-28 12:45:30 +02:00
def __unpriviledged_commands(self, message, tags):
2022-08-28 11:43:12 +02:00
""" Process unpriviledged commands
:param dict message: Message
:param dict tags: Message metadata (tags)
"""
msg = message['message']
user = tags['user']
if msg.startswith('#pickme') and self.pick['status'] is True:
logging.info('Pickme detected')
self.pick['count'] = self.pick['count'] + 1
self.pick['pickme'].append(user)
logging.debug("pickme %s added", user)
return
if msg.startswith('#') and self.quickvote['status'] is True:
logging.info('Quickvote: Cast detected')
2022-08-28 12:33:46 +02:00
self.quickvote['count'] += 1
self.quickvote['data'][user] = msg.lower()
logging.debug("quickvote: %s", self.quickvote)
2022-08-28 11:43:12 +02:00
return
if msg.startswith('!tts'):
logging.info('!tts command detected')
2022-08-28 12:45:30 +02:00
self.__ttscmd(message, tags)
2022-08-28 11:43:12 +02:00
return
if msg.startswith('!wiki') and CONF['FEATURE']['WIKI']:
logging.debug("!wiki command detected")
2022-08-28 12:45:30 +02:00
self.__wikicmd(tags, msg)
2022-08-28 11:43:12 +02:00
return
if CONF['FEATURE']['QUOTE']:
if msg.startswith('!addquote'):
logging.debug("!addquote command detected")
2022-08-28 12:45:30 +02:00
self.__addquotecmd(tags, msg)
2022-08-28 11:43:12 +02:00
return
if msg.startswith('!smartquote') or msg.startswith('!sq'):
logging.debug("!smartquote command detected")
2022-10-02 02:41:36 +02:00
self.__quotecmd(msg)
2022-08-28 11:43:12 +02:00
return
2022-08-28 12:45:30 +02:00
def __priviledged_commands(self, message, tags):
2022-08-27 15:44:57 +02:00
""" Process priviledged commands
:param dict message: Message
:param dict tags: Message metadata (tags)
"""
2022-08-13 19:35:02 +02:00
msg = message['message']
badges = tags['badges']
user = tags['user']
if 'broadcaster' in badges or 'moderator' in badges:
2022-08-27 14:04:04 +02:00
if msg.startswith('!ping') and CONF['FEATURE']['PING']:
2022-08-13 19:35:02 +02:00
logging.debug("Ping check received.")
2022-08-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
"Pong!"
)
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
elif msg.startswith('!version') and CONF['FEATURE']['VERSION']:
2022-08-24 18:03:57 +02:00
logging.debug("!version command detected")
2022-08-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
VERSION
)
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']
)
2022-08-24 20:35:26 +02:00
2022-08-27 14:04:04 +02:00
elif msg.startswith('!pick') and CONF['FEATURE']['PICK']:
2022-08-24 20:35:26 +02:00
logging.debug("!pick command detected")
2022-08-28 12:45:30 +02:00
self.__pickcmd(msg)
2022-08-24 18:03:57 +02:00
2022-08-27 14:04:04 +02:00
elif msg.startswith('!random') and CONF['FEATURE']['RANDOM']:
2022-08-13 19:35:02 +02:00
logging.info('!random command detected')
2022-08-28 12:45:30 +02:00
self.__randomcmd(msg)
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
elif msg.startswith('!quickvote') and CONF['FEATURE']['QUOTE']:
2022-08-13 19:35:02 +02:00
logging.info("!quickvote command detected")
2022-08-28 12:45:30 +02:00
self.__quickvotecmd(msg)
2022-08-13 19:35:02 +02:00
elif msg.startswith('!ptts'):
logging.debug("!ptts command detected")
2022-08-28 12:45:30 +02:00
self.__ptts(msg)
2022-08-13 19:35:02 +02:00
2022-08-27 11:30:39 +02:00
elif msg.startswith('!dtts'):
logging.debug("!dtts command detected")
2022-08-28 12:45:30 +02:00
self.__dtts(msg)
2022-08-27 11:30:39 +02:00
2022-08-23 14:13:58 +02:00
elif msg.startswith('!usermap'):
logging.info('!usermap command detected')
2022-08-28 12:45:30 +02:00
self.__usermap(msg)
2022-08-23 14:13:58 +02:00
elif msg.startswith('!delay'):
logging.info('!delay command detected')
2022-08-28 12:45:30 +02:00
self.__delay(msg)
2022-08-23 14:13:58 +02:00
2022-08-13 19:35:02 +02:00
def check_subonly(self, tags):
2022-08-27 15:44:57 +02:00
""" Check if subonly mode is enabled and sender is sub
:param dict tags: Message metadata (tags)
:return bool:
"""
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
if not CONF['IRC_SUBONLY']:
2022-08-13 20:43:00 +02:00
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-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
CONF['MESSAGE']['SUBONLY']
)
2022-08-13 19:35:02 +02:00
return True
def check_modonly(self, tags):
2022-08-27 15:44:57 +02:00
""" Check if modonly mode is enabled and sender is mod
:param dict tags: Message metadata (tags)
:return bool:
"""
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
if not CONF['IRC_MODONLY']:
2022-08-13 20:43:00 +02:00
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-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
CONF['MESSAGE']['MODONLY']
)
2022-08-13 19:35:02 +02:00
return True
2022-08-13 20:05:29 +02:00
def check_tts_disabled(self, user):
2022-08-27 15:44:57 +02:00
""" Check if TTS is disabled
:param str user: Username
:return bool:
"""
2022-08-28 11:43:12 +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):
2022-08-27 15:44:57 +02:00
""" Check if message is too long
:param dict message: Message
:param str user: Username
:return bool:
"""
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
if message['length'] > CONF['IRC_TTS_LEN']:
2022-08-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
CONF['MESSAGE']['TOO_LONG']
)
2022-08-13 19:35:02 +02:00
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):
2022-08-27 15:19:44 +02:00
""" Check if user is on denied list
Check if the given user is on the TTS blacklist
:param str user: Username
2022-08-27 15:44:57 +02:00
:return bool:
2022-08-27 15:19:44 +02:00
"""
2022-08-28 11:43:12 +02:00
if user in self.tts['denied']:
2022-08-13 19:35:02 +02:00
logging.info("%s is not allowed to use TTS", user)
2022-08-28 11:43:12 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
CONF['MESSAGE']['DENIED']
)
2022-08-13 19:35:02 +02:00
return True
logging.debug("%s is allowed to use TTS", user)
return False
def check_whitelist(self, user):
2022-08-27 15:19:44 +02:00
""" Check Whitelist
Checks if the given user is on the TTS whitelist
:param str user: Username
2022-08-27 15:44:57 +02:00
:return bool:
2022-08-27 15:19:44 +02:00
"""
2022-08-13 19:35:02 +02:00
2022-08-27 14:04:04 +02:00
if CONF['WHITELIST']:
2022-08-13 19:35:02 +02:00
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(
2022-08-27 14:04:04 +02:00
CONF['IRC_CHANNEL'],
2022-08-28 11:43:12 +02:00
"@"+str(user),
CONF['MESSAGE']['WHITELISTONLY']
2022-08-13 19:35:02 +02:00
)
return False
return True
return False
def send_tts_msg(self, message, tags):
2022-08-27 15:19:44 +02:00
""" Send message to TTS queue
:param dict tags: Message metadata (tags)
:param str message: IRC message
"""
2022-08-13 19:35:02 +02:00
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
}
2022-08-27 15:19:44 +02:00
MSG_QUEUE_RAW.append(msg)
2022-08-13 19:35:02 +02:00
2022-08-10 20:58:51 +02:00
def get_response(self):
2022-08-27 15:44:57 +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")
2022-08-27 15:44:57 +02:00
logging.debug('resp: %s', resp)
2022-08-10 20:58:51 +02:00
except socket.timeout:
2022-08-13 19:35:02 +02:00
return
2022-08-27 15:19:44 +02:00
except Exception:
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-28 12:45:30 +02:00
self.__resp_ping()
2022-08-10 20:58:51 +02:00
if resp.find('CLEARMSG') != -1:
2022-08-28 12:45:30 +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-28 12:45:30 +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-28 12:45:30 +02:00
self.__resp_privmsg(resp)
2022-08-10 20:58:51 +02:00
2022-08-28 12:45:30 +02:00
def __ttscmd(self, msg, tags):
2022-08-28 12:33:46 +02:00
""" !tts command
2022-08-28 12:33:46 +02:00
Check if message is valid and send it to queue
2022-08-28 12:33:46 +02:00
:param str msg: The IRC message triggering the command
:param dict tags: The message metadata
"""
2022-08-15 20:36:11 +02:00
2022-08-28 12:33:46 +02:00
user = tags['user']
2022-08-14 11:41:05 +02:00
2022-08-28 12:33:46 +02:00
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-28 12:45:30 +02:00
def __addquotecmd(self, tags, msg):
2022-08-28 12:33:46 +02:00
""" !addquote command
Adds a newline to quotes.txt
2022-08-14 11:41:05 +02:00
2022-08-28 12:33:46 +02:00
:param dict tags: Message metadata (tags)
:param str msg: IRC message triggering the command
"""
2022-08-14 11:41:05 +02:00
2022-08-28 12:33:46 +02:00
user = tags['user']
2022-08-19 03:10:38 +02:00
2022-08-28 12:33:46 +02:00
if 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')
else:
try:
with open("quotes.txt", "rb") as file:
nol = len(file.readlines())
file.close()
except FileNotFoundError:
logging.warning("quotes.txt does not exists, will create")
nol = 0
2022-08-27 15:19:44 +02:00
2022-08-28 12:33:46 +02:00
nol = nol + 1
quote = msg.replace("!addquote ", "").strip()
quote = quote.split(" ",1)
username = quote[0]
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
date = time.strftime("%d.%m.%Y")
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
try:
token = CONF['IRC_OAUTH_TOKEN'].replace('oauth:','')
login = CONF['IRC_CHANNEL'].replace('#','')
api_endpoint = f"https://api.twitch.tv/helix/users?login={login}"
headers = {
'Content-type': 'application/x-form-urlencoded',
'Authorization': 'Bearer '+token,
'Client-Id': 'ebo548vs6tq54c9zlrgin2yfzzlrrs'
2022-08-18 16:45:11 +02:00
}
2022-08-28 12:33:46 +02:00
req = requests.get(url=api_endpoint, headers=headers)
data = req.json()
user_id = data['data'][0]['id']
api_endpoint = f"https://api.twitch.tv/helix/channels?broadcaster_id={user_id}"
headers = {
'Content-type': 'application/x-form-urlencoded',
'Authorization': 'Bearer '+token,
'Client-Id': 'ebo548vs6tq54c9zlrgin2yfzzlrrs'
}
req = requests.get(url=api_endpoint, headers=headers)
data = req.json()
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
game = data['data'][0]['game_name']
quote = f"#{nol}: \"{quote[1]}\" -{username}/{game} ({date})\n"
except Exception:
logging.warning('Could not get metadata for quote')
quote = f"#{nol}: \"{quote[1]}\" -{username} ({date})\n"
2022-08-27 15:19:44 +02:00
2022-08-28 12:33:46 +02:00
logging.info('Adding quote %s', quote)
with open("quotes.txt", "ab") as file:
file.write(quote.encode('utf-8'))
2022-08-27 15:19:44 +02:00
2022-08-28 12:33:46 +02:00
message = f"{CONF['MESSAGE']['QUOTE_ADDED_PREFIX']} \
#{nol} {CONF['MESSAGE']['QUOTE_ADDED_SUFFIX']}"
raw_msg = {
"TTS": True,
"msg": message,
"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']]
2022-08-28 13:00:40 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
message
)
2022-08-24 18:03:57 +02:00
2022-08-28 12:45:30 +02:00
def __wikicmd(self, tags, msg):
2022-08-28 12:33:46 +02:00
""" !wiki command
2022-08-24 18:03:57 +02:00
2022-08-28 12:33:46 +02:00
Search for wikipedia articles and return the first 3 sentences
2022-08-24 18:03:57 +02:00
2022-08-28 12:33:46 +02:00
:param dict tags: Message metadata (tags)
:param str msg: IRC message triggering the command
"""
2022-08-24 18:03:57 +02:00
2022-08-28 12:33:46 +02:00
try:
user = tags['user']
wikipedia.set_lang(CONF['WIKI_LANG'])
msg = msg.replace('!wiki', '').strip()
wikiresult = wikipedia.summary(msg, sentences=3)
2022-08-18 16:45:11 +02:00
2022-08-28 13:00:40 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
wikiresult
)
self.sendmsg(
CONF['IRC_CHANNEL'],
"@"+str(user),
wikipedia.page(msg).url
)
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
message = wikiresult.replace('==', '')
raw_msg = {
"TTS": True,
"msg": message,
"badges": True,
"subscriber": True,
"msgid": True,
"user": 'wikipedia',
"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']]
2022-08-27 15:19:44 +02:00
2022-08-28 12:33:46 +02:00
except wikipedia.exceptions.DisambiguationError:
user = f"@{user}"
2022-08-28 13:00:40 +02:00
self.sendmsg(CONF['IRC_CHANNEL'], user, CONF['MESSAGE']['WIKI_TOO_MANY'])
2022-08-28 12:33:46 +02:00
except Exception:
user = f"@{user}"
2022-08-28 13:00:40 +02:00
self.sendmsg(CONF['IRC_CHANNEL'], user, CONF['MESSAGE']['WIKI_NO_RESULT'])
2022-08-18 16:45:11 +02:00
2022-10-02 02:41:36 +02:00
def __quotecmd(self, msg = False):
2022-08-28 12:33:46 +02:00
""" !smartquote command
2022-08-24 18:03:57 +02:00
2022-08-28 12:33:46 +02:00
Gets a line from quotes.txt. If a number if given as msg
it fetches the given line number. If a string is given
it fetches the best matching line. If nothing is given
it fetches a random line.
2022-08-19 03:10:38 +02:00
2022-08-28 12:33:46 +02:00
:param dict tags: Message metadata (tags)
:param str msg: IRC message triggering the command
"""
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
try:
query = msg.replace('!smartquote', '').strip()
query = msg.replace('!sq', '').strip()
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
if query.isdigit():
logging.info('Fetching quote #%s', query)
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
file = open("quotes.txt", "rb")
quotes = file.readlines()
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
for line in quotes:
if line.decode('utf-8').startswith("#"+str(query)+":"):
quote = line
break
file.close()
2022-08-19 03:10:38 +02:00
2022-08-28 12:33:46 +02:00
elif query != "":
logging.info('Fetching match for %s', query)
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
file = open("quotes.txt", "rb")
quotes = file.readlines()
matches = process.extract(query, quotes, limit=10)
quotes = []
2022-08-19 03:10:38 +02:00
2022-08-28 12:33:46 +02:00
for match, score in matches:
if score >= 60:
quotes.append(match)
logging.debug('Quotes: %s', quotes)
if len(quotes) >= 5:
quote = random.choice(quotes)
else:
quote = quotes[0]
2022-08-18 16:45:11 +02:00
else:
2022-08-28 12:33:46 +02:00
logging.info('Fetching random quote')
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
with open("quotes.txt", "rb") as file:
lines = file.read().splitlines()
quote = random.choice(lines)
except FileNotFoundError:
logging.error('"quotes.txt does not exists.')
except IndexError:
logging.error('Error fetching quote.')
if not 'quote' in vars():
logging.info('No quote found.')
quote = CONF['MESSAGE']['QUOTE_NOT_FOUND']
2022-08-28 13:00:40 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"",
quote
)
2022-08-28 12:33:46 +02:00
return False
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
if not isinstance(quote, str):
quote = quote.decode('utf-8')
2022-10-02 02:26:30 +02:00
logging.info('Sending quote to TTS')
logging.debug("Quote: %s", quote)
self.sendmsg(
CONF['IRC_CHANNEL'],
"",
quote
)
2022-08-28 12:33:46 +02:00
2022-10-02 02:26:30 +02:00
message = quote.rsplit('(', 1)[0]
raw_msg = {
"TTS": True,
"msg": message,
"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']]
2022-08-18 16:45:11 +02:00
2022-08-28 12:33:46 +02:00
return True
2022-08-23 14:13:58 +02:00
2022-08-28 12:45:30 +02:00
def __delay(self, msg):
2022-08-28 12:33:46 +02:00
""" !delay command
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
Adjust the delay setting in config.yml
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
:param str msg: The IRC message triggering the command
"""
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
try:
delay = msg.split(' ')[1]
except Exception:
delay = False
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
if delay:
with open('config.yml','r', encoding='utf-8') as yamlfile:
cur_yaml = yaml.safe_load(yamlfile)
cur_yaml['irc']['clearmsg_timeout'] = int(delay)
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
if cur_yaml:
with open('config.yml','w', encoding='utf-8') as yamlfile:
yaml.safe_dump(cur_yaml, yamlfile)
load_config()
2022-08-23 14:13:58 +02:00
2022-08-28 12:45:30 +02:00
def __usermap(self, msg):
2022-08-28 12:33:46 +02:00
""" !usermap command
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
Adds a new entry to usermapping in config.yml
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
:param str msg: The IRC message triggering the command
"""
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
try:
msg = msg.replace('!usermap ', '')
splitmsg = msg.split(" ")
username, *mappingname = splitmsg
mappingname = ' '.join(mappingname)
except Exception:
username = False
mappingname = False
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
if username and mappingname:
with open('config.yml','r', encoding='utf-8') as yamlfile:
cur_yaml = yaml.safe_load(yamlfile)
cur_yaml['usermapping'].update({username: mappingname})
2022-08-23 14:13:58 +02:00
2022-08-28 12:33:46 +02:00
if cur_yaml:
with open('config.yml','w', encoding='utf-8') as yamlfile:
yaml.safe_dump(cur_yaml, yamlfile)
load_config()
2022-08-27 15:44:57 +02:00
2022-08-28 12:45:30 +02:00
def __pickcmd(self, msg):
2022-08-28 12:33:46 +02:00
""" !pick command
2022-08-27 15:44:57 +02:00
2022-08-28 12:33:46 +02:00
Pick a number of users who typed #pickme in the chat
after the pick command was started.
2022-08-25 01:55:04 +02:00
2022-08-28 12:33:46 +02:00
:param str msg: Number of users to pick
"""
2022-08-25 01:55:04 +02:00
2022-08-28 12:33:46 +02:00
if self.pick['status']:
logging.info('Pick stopped')
logging.debug("Got %s participats, wanted %s",
self.pick['count'],
self.pick['number']
)
try:
if int(self.pick['count']) > int(self.pick['number']):
picks = random.sample(self.pick['pickme'], self.pick['number'])
logging.info('Got more than the requested number of participants')
2022-08-25 01:55:04 +02:00
else:
2022-08-28 12:33:46 +02:00
picks = self.pick['pickme']
logging.info('Got less than or exactly the \
requested number of participants')
2022-08-25 01:55:04 +02:00
2022-08-28 12:33:46 +02:00
converted_picks = [str(element) for element in picks]
joined_picks = " ".join(converted_picks)
except Exception:
logging.error("There was an error during picking.")
joined_picks = False
2022-08-25 01:55:04 +02:00
2022-08-28 12:33:46 +02:00
message = f"{CONF['MESSAGE']['PICKRESULT']} {joined_picks}"
if joined_picks:
raw_msg = {
"TTS": True,
"msg": message,
"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']]
2022-08-25 01:55:04 +02:00
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "",
CONF['MESSAGE']['PICKRESULT']
)
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "*",
joined_picks
)
else:
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "*",
CONF['MESSAGE']['PICKNONE']
)
2022-08-25 01:55:04 +02:00
2022-08-28 12:33:46 +02:00
self.pick['status'] = False
self.pick['pickme'] = []
self.pick['count'] = 0
2022-08-25 01:55:04 +02:00
return
2022-08-28 12:33:46 +02:00
logging.debug('Pick started')
self.pick['status'] = True
try:
msg = msg.split(' ')[1].strip()
self.pick['number'] = msg
except IndexError:
self.pick['number'] = self.pick['number']
2022-08-28 12:33:46 +02:00
logging.info("Will pick %s participants", self.pick['number'])
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "@chat",
CONF['MESSAGE']['PICKSTART']
)
return
2022-08-28 12:45:30 +02:00
def __quickvotecmd(self, msg):
2022-08-28 12:33:46 +02:00
""" !quickvote command
2022-08-28 12:33:46 +02:00
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.
2022-08-28 12:33:46 +02:00
:param str msg: The IRC message triggering the command
"""
2022-08-28 12:33:46 +02:00
if self.quickvote['status']:
logging.debug('Quickvote stopped')
2022-08-28 12:33:46 +02:00
if self.quickvote['count'] == 0:
logging.info("Nobody voted")
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'],
"@chat",
CONF['MESSAGE']['VOTEEND']
)
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'],
"*",
CONF['MESSAGE']['VOTENOBODY']
)
raw_msg = {
"TTS": True,
2022-08-28 12:33:46 +02:00
"msg": CONF['MESSAGE']['VOTENOBODY'],
"badges": True,
"subscriber": True,
"msgid": True,
2022-08-27 14:04:04 +02:00
"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']]
2022-08-28 12:33:46 +02:00
logging.info('The result is: %s', CONF['MESSAGE']['VOTENOBODY'])
logging.debug('Votemsg: %s', msg)
2022-08-28 12:33:46 +02:00
self.quickvote['status'] = False
self.quickvote['data'] = {}
return
2022-08-28 12:33:46 +02:00
logging.info("Counting votes")
count = 0
count = Counter(self.quickvote['data'].values()).most_common(5)
2022-08-28 13:00:40 +02:00
self.sendmsg(
CONF['IRC_CHANNEL'],
"@chat",
CONF['MESSAGE']['VOTEEND']
)
2022-08-27 15:44:57 +02:00
2022-08-28 12:33:46 +02:00
logging.debug(count)
result = str(count[0][0].replace('#',''))
message = f"{CONF['MESSAGE']['VOTERESULT']} {result}"
raw_msg = {
"TTS": True,
2022-08-28 12:33:46 +02:00
"msg": message ,
"badges": True,
"subscriber": True,
"msgid": True,
2022-08-27 14:04:04 +02:00
"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']]
2022-08-28 12:33:46 +02:00
logging.info('The result is: %s', CONF['MESSAGE']['VOTERESULT'] +" "+ str(count[0]))
logging.debug('Votemsg: %s', msg)
2022-08-28 12:33:46 +02:00
for key, value in count:
message = f"{key} ({value}) {CONF['MESSAGE']['VOTES']})"
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "*",
message
)
2022-08-28 12:33:46 +02:00
self.quickvote['status'] = False
self.quickvote['data'] = {}
self.quickvote['count'] = 0
return
2022-08-28 12:33:46 +02:00
logging.debug('Quickvote started')
self.quickvote['status'] = True
self.quickvote['message'] = msg.split('!quickvote', 1)[1].strip()
if self.quickvote['message']:
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'], "@chat",
CONF['MESSAGE']['VOTESTART'] + " (" + str(self.quickvote['message']) + ")"
)
else:
2022-08-28 13:00:40 +02:00
self.sendmsg(
2022-08-28 12:33:46 +02:00
CONF['IRC_CHANNEL'],
"@chat",
CONF['MESSAGE']['VOTESTART']
)
return
2022-08-28 12:45:30 +02:00
def __randomcmd(self, msg):
2022-08-28 12:33:46 +02:00
""" !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
:return bool:
"""
randomfile = msg.replace('!random', '').strip().lower()
if randomfile:
randomfile = f"random_{os.path.basename(randomfile)}.txt"
else:
randomfile = "random.txt"
2022-08-28 12:33:46 +02:00
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
2022-08-28 12:45:30 +02:00
def __ptts(self, msg):
2022-08-28 12:33:46 +02:00
""" !ptts command
2022-08-28 12:33:46 +02:00
Add user to tts_allowed list and remove user from tts_denied list
2022-08-28 12:33:46 +02:00
: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('@', '')
2022-08-28 12:33:46 +02:00
logging.info("Adding %s to whitelist", user)
self.tts['whitelist'].append(user)
2022-08-28 12:33:46 +02:00
if user in self.tts['blacklist']:
logging.info("Removing %s from deny list", user)
self.tts['blacklist'].remove(user)
2022-08-28 12:45:30 +02:00
def __dtts(self, msg):
2022-08-28 12:33:46 +02:00
""" !dtts command
Add user to tts_denied list and remove user from tts_allowed list
:param str msg: The IRC message triggering the command
"""
2022-08-28 12:33:46 +02:00
user = msg.replace('!dtts', '').strip().lower()
2022-08-28 12:33:46 +02:00
if user.startswith('@'):
logging.debug('Removing "@" from username')
user = user.replace('@', '')
2022-08-28 12:33:46 +02:00
if user not in self.tts['blacklist']:
logging.info("Adding %s to deny list", user)
self.tts['blacklist'].append(user)
2022-08-28 12:33:46 +02:00
if user in self.tts['whitelist']:
logging.info("Removing %s from allowed list", user)
self.tts['whitelist'].remove(user)
2022-08-19 03:10:38 +02:00
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
""" Threaded HTTP Server """
2022-08-10 20:58:51 +02:00
class HTTPserv(BaseHTTPRequestHandler):
2022-08-27 15:44:57 +02:00
""" Simple HTTP Server """
2022-08-12 18:54:40 +02:00
def log_message(self, format, *args): # pylint: disable=redefined-builtin
2022-08-27 15:44:57 +02:00
""" Suppress HTTP log messages """
return
2022-08-12 18:54:40 +02:00
def do_GET(self): # pylint: disable=invalid-name
2022-08-27 15:44:57 +02:00
""" 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)
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()
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()
2022-08-27 14:04:04 +02:00
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-27 22:07:35 +02:00
logging.debug('Using usermap for user: %s (%s)',
msg_queue[key][0],
usermap[msg_queue[key][0].lower()]
)
user = usermap[msg_queue[key][0].lower()]
tts = {str(key): f"{user} {CONF['MESSAGE']['SAYS']}: {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-27 22:07:35 +02:00
tts = {
str(key):
f"{msg_queue[key][0]} {CONF['MESSAGE']['SAYS']}: {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"
2022-08-27 14:04:04 +02:00
if CONF['HTTP_PORT'] == 80:
2022-08-12 18:54:40 +02:00
data['redirect_uri'] = "http://localhost/token"
2022-08-27 14:04:04 +02:00
elif CONF['HTTP_PORT'] == 8080:
2022-08-12 18:54:40 +02:00
data['redirect_uri'] = "http://localhost:8080/token"
2022-08-27 14:04:04 +02:00
elif CONF['HTTP_PORT'] == 3000:
2022-08-12 18:54:40 +02:00
data['redirect_uri'] = "http://localhost:3000/token"
else:
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
2022-08-27 22:07:35 +02:00
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")
)
2022-08-12 18:54:40 +02:00
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()
2022-08-27 22:07:35 +02:00
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")
)
2022-08-12 18:54:40 +02:00
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"))
2022-08-27 15:19:44 +02:00
except Exception:
2022-08-12 18:54:40 +02:00
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-27 15:44:57 +02:00
return True
2022-08-10 20:58:51 +02:00
def http_serve_forever(httpd):
2022-08-27 15:44:57 +02:00
""" httpd loop """
2022-08-12 18:54:40 +02:00
httpd.serve_forever()
2022-08-10 20:58:51 +02:00
2022-08-28 11:43:12 +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')
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']
url = f"http://{url}:{CONF['HTTP_PORT']}/"
if path:
url = f"{url}{path}"
return url
def check_oauth_token():
""" Check for valid authentication via Twitch API """
global CONF
logging.debug('Checking OAuth Token')
try:
url = 'https://id.twitch.tv/oauth2/validate'
oauth = CONF['IRC_OAUTH_TOKEN'].replace('oauth:','')
oauth = f"OAuth {oauth}"
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()
logging.info('OAuth Token is valid')
return CONF
2022-08-10 20:58:51 +02:00
def load_config():
2022-08-27 15:44:57 +02:00
""" Loading config variables """
2022-08-12 18:54:40 +02:00
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:
2022-08-27 22:07:35 +02:00
logging.fatal('Your config file is missing, please copy config-dist.yml \
to config.yml and review your settings.')
2022-08-10 20:58:51 +02:00
sys.exit(253)
for section in cfg:
try:
2022-08-12 18:54:40 +02:00
logging.debug('Fetching config: %s', section)
2022-08-27 22:07:35 +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
)
CONF['IRC_SUBONLY'] = cfg.get('bot', {}).get(
'subonly', False
)
CONF['IRC_MODONLY'] = cfg.get('bot', {}).get(
'modonly', False
)
CONF['IRC_TTS_LEN'] = cfg.get('bot', {}).get(
'message_length', 200
)
CONF['TTS_STARTENABLED'] = cfg.get('bot', {}).get(
'start_enabled', True
)
CONF['WIKI_LANG'] = cfg.get('bot', {}).get(
'language', 'en'
)
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-27 14:04:04 +02:00
CONF['MESSAGE'] = {}
2022-08-27 22:07:35 +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', "Votes"
)
CONF['MESSAGE']['PICKSTART'] = cfg.get('messages', {}).get(
'pickstart', "Pick started. Send #pickme to participate."
)
CONF['MESSAGE']['PICKRESULT'] = cfg.get('messages', {}).get(
'pickresult', "Pick ended. The results are:"
)
CONF['MESSAGE']['PICKNONE'] = cfg.get('messages', {}).get(
'picknone', "Pick ended. Nobody was picked."
)
CONF['MESSAGE']['QUOTE_NOT_FOUND'] = cfg.get('messages', {}).get(
'quotenotfound', "Sorry, no quote found."
)
CONF['MESSAGE']['QUOTE_ADDED_PREFIX'] = cfg.get('messages', {}).get(
'quoteaddedprefix', "Quote:"
)
CONF['MESSAGE']['QUOTE_ADDED_SUFFIX'] = cfg.get('messages', {}).get(
'quoteaddedsuffix', "added."
)
CONF['MESSAGE']['WIKI_TOO_MANY'] = cfg.get('messages', {}).get(
'wiki_too_many', "Sorry, there are too many possible results. \
Try a more narrow search."
)
CONF['MESSAGE']['WIKI_NO_RESULT'] = cfg.get('messages', {}).get(
'wiki_no_result', "Sorry, there was an error fetching the wikipedia answer."
)
2022-08-27 14:04:04 +02:00
CONF['FEATURE'] = {}
2022-08-28 11:43:12 +02:00
CONF['FEATURE']['WIKI'] = cfg.get('features', {}).get(
'wiki', True
)
CONF['FEATURE']['PICK'] = cfg.get('features', {}).get(
'pick', True
)
CONF['FEATURE']['VOTE'] = cfg.get('features', {}).get(
'vote', True
)
CONF['FEATURE']['QUOTE'] = cfg.get('features', {}).get(
'quote', True
)
CONF['FEATURE']['RANDOM'] = cfg.get('features', {}).get(
'random', True
)
CONF['FEATURE']['VERSION'] = cfg.get('features', {}).get(
'version', True
)
CONF['FEATURE']['PING'] = cfg.get('features', {}).get(
'ping', True
)
2022-08-27 14:04:04 +02:00
CONF['USERMAP'] = cfg.get('usermapping', [])
2022-08-10 20:58:51 +02:00
if 'whitelist' in cfg:
2022-08-27 14:04:04 +02:00
CONF['WHITELIST'] = True
CONF['WHITELIST_USER'] = cfg['whitelist']
2022-08-10 20:58:51 +02:00
else:
2022-08-27 14:04:04 +02:00
CONF['WHITELIST'] = False
2022-08-10 20:58:51 +02:00
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)
2022-08-27 14:04:04 +02:00
if CONF['WHITELIST']:
2022-08-10 20:58:51 +02:00
logging.info('Whitelist mode enabled')
2022-08-27 14:04:04 +02:00
logging.debug('Whitelist: %s', CONF['WHITELIST_USER'])
2022-08-10 20:58:51 +02:00
2022-08-27 14:04:04 +02:00
if not CONF['IRC_CHANNEL']:
2022-08-11 10:26:33 +02:00
raise ValueError('Please add your twitch channel to config.yml.')
2022-08-27 14:04:04 +02:00
if not CONF['IRC_USERNAME']:
2022-08-11 10:26:33 +02:00
raise ValueError('Please add the bots username to config.yml.')
2022-08-27 14:04:04 +02:00
if not CONF['IRC_OAUTH_TOKEN']:
CONF['IRC_OAUTH_TOKEN'] = "Invalid"
return CONF
if not CONF['IRC_OAUTH_TOKEN'].startswith('oauth:'):
2022-08-11 10:26:33 +02:00
raise ValueError('Your oauth-token is invalid, it has to start with: "oauth:"')
2022-08-27 14:04:04 +02:00
return CONF
2022-08-10 20:58:51 +02:00
def main():
2022-08-27 15:44:57 +02:00
""" Main loop """
2022-08-12 18:54:40 +02:00
2022-08-27 15:19:44 +02:00
global CONF
2022-08-27 14:04:04 +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()
2022-08-27 14:04:04 +02:00
logging.getLogger().setLevel(CONF['LOG_LEVEL'])
if CONF['LOG_LEVEL'] == 'DEBUG':
2022-08-11 10:26:33 +02:00
sys.tracebacklimit = 5
2022-08-12 18:54:40 +02:00
2022-08-10 20:58:51 +02:00
logging.info("Starting Webserver")
2022-08-28 11:43:12 +02:00
httpd = ThreadingSimpleServer(
(CONF['HTTP_BIND'],
CONF['HTTP_PORT']),
HTTPserv
)
2022-08-10 20:58:51 +02:00
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-27 22:07:35 +02:00
irc.connect(
CONF['IRC_SERVER'],
6667,
CONF['IRC_CHANNEL'],
CONF['IRC_USERNAME'],
CONF['IRC_OAUTH_TOKEN']
)
2022-08-28 11:43:12 +02:00
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:
2022-08-27 14:04:04 +02:00
if CONF['LOG_LEVEL'] == "DEBUG":
2022-08-13 19:35:02 +02:00
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):
2022-08-27 14:04:04 +02:00
CONF = load_config()
2022-08-13 19:35:02 +02:00
lastreload = datetime.datetime.now()
2022-08-12 18:54:40 +02:00
2022-08-28 11:43:12 +02:00
if irc.quickvote['status'] and irc.quickvote['message']:
2022-08-13 19:35:02 +02:00
logging.info('Quickvote is active')
2022-08-27 22:07:35 +02:00
irc.sendmsg(
CONF['IRC_CHANNEL'],
"@chat",
2022-08-28 11:43:12 +02:00
f"{CONF['MESSAGE']['VOTESTART']} ({irc.quickvote['message']})"
2022-08-27 22:07:35 +02:00
)
2022-08-28 11:43:12 +02:00
if not irc.tts['status']:
2022-08-13 19:35:02 +02:00
continue
2022-08-13 20:05:29 +02:00
2022-08-27 15:19:44 +02:00
logging.debug('MSG_QUEUE_RAW: %s', MSG_QUEUE_RAW)
2022-08-13 20:05:29 +02:00
send_tts_queue()
2022-08-10 20:58:51 +02:00
2022-08-13 19:35:02 +02:00
except KeyboardInterrupt:
2022-08-19 03:10:38 +02:00
httpd.shutdown()
2022-08-13 19:35:02 +02:00
logging.info('Exiting...')
2022-08-19 03:10:38 +02:00
os.kill(os.getpid(), signal.SIGTERM)
2022-08-10 20:58:51 +02:00
if __name__ == "__main__":
2022-08-27 22:07:35 +02:00
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(module)s %(threadName)s %(levelname)s: %(message)s'
)
2022-10-02 02:27:48 +02:00
2022-08-15 20:36:11 +02:00
sys.tracebacklimit = 3
2022-08-14 11:41:05 +02:00
2022-10-20 02:42:51 +02:00
VERSION = "1.7.3"
2022-08-27 14:04:04 +02:00
CONF = {}
2022-08-14 11:41:05 +02:00
tts_done = []
2022-08-27 15:19:44 +02:00
MSG_QUEUE_RAW = []
2022-08-14 11:41:05 +02:00
msg_queue = {}
if sys.argv[1:]:
if sys.argv[1] == "--version":
print('Simple TTS Bot')
2022-08-24 18:03:57 +02:00
print('Version %s', VERSION)
2022-08-14 11:41:05 +02:00
sys.exit(1)
2022-08-10 20:58:51 +02:00
main()