Add TOML configuration #7

Merged
skeh merged 3 commits from feature/config into main 2021-04-10 04:50:16 +00:00
7 changed files with 111 additions and 54 deletions
Showing only changes of commit 44ba7d6273 - Show all commits

View file

@ -8,6 +8,7 @@ requests = "*"
pygame = "*" pygame = "*"
websocket-client = "*" websocket-client = "*"
miniirc = "*" miniirc = "*"
toml = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.9"

10
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "c6f4727f4fbf44c5155fd9276c2a575224bd4ce1914e7051b84c01dea7168336" "sha256": "78eab561bb582e47361961316e00224a4e606589ca122fc14f1d0c527c1c5dbe"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -110,6 +110,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0" "version": "==1.15.0"
}, },
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"index": "pypi",
"version": "==0.10.2"
},
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",

View file

@ -6,8 +6,6 @@ from .ChatProcess import ChatProcess
class CONTROL_MESSAGES(Enum): class CONTROL_MESSAGES(Enum):
CHANGE_DELAY = auto()
CHANGE_CHUNK = auto()
START_STOP = auto() START_STOP = auto()
@ -16,13 +14,14 @@ class STATES(Enum):
RUNNING = auto() RUNNING = auto()
class FakeChat(ChatProcess): class Process(ChatProcess):
CHAT_NAME = 'Fake Chat' CHAT_NAME = 'Fake Chat'
def __init__(self, *args): def __init__(self, *args, max_delay=10, max_messages_per_chunk=1, start_paused=True):
super().__init__(*args) super().__init__(*args)
self._max_messages_per_chunk = 1 self._max_delay = max_delay
self._max_delay = 10 self._max_messages_per_chunk = max_messages_per_chunk
self.state = STATES.PAUSED if start_paused else STATES.RUNNING
@property @property
def keybinds(self): def keybinds(self):
@ -35,15 +34,13 @@ class FakeChat(ChatProcess):
running = not self.state == STATES.RUNNING running = not self.state == STATES.RUNNING
text = 'Fake chat activated!' if running else 'Disabled fake chat' text = 'Fake chat activated!' if running else 'Disabled fake chat'
sys_msg = Message(text, FakeChat.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM) sys_msg = Message(text, Process.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM)
self._message_queue.put(sys_msg) self._message_queue.put(sys_msg)
return STATES.RUNNING if running else STATES.PAUSED return STATES.RUNNING if running else STATES.PAUSED
def loop(self, next_state): def loop(self, next_state):
if self.state is None: if self.state == STATES.PAUSED:
return STATES.PAUSED
elif self.state == STATES.PAUSED:
return None return None
while range(int(random.random() * (self._max_messages_per_chunk + 1))): while range(int(random.random() * (self._max_messages_per_chunk + 1))):

View file

@ -49,15 +49,17 @@ class NonBlockingWebsocket(Process):
return -1 return -1
class TwitchIRCProcess(ChatProcess): class Process(ChatProcess):
CHAT_NAME = 'Twitch IRC' CHAT_NAME = 'Twitch IRC'
WEBSOCKET_ADDRESS = 'wss://irc-ws.chat.twitch.tv:443' WEBSOCKET_ADDRESS = 'wss://irc-ws.chat.twitch.tv:443'
def __init__(self, username, token, *args): def __init__(self, *args, channel_name=None, oauth_key=None):
if channel_name is None or oauth_key is None:
raise ValueError('Twitch chat is missing config requirements')
super().__init__(*args) super().__init__(*args)
self._state_machine = self.bind_to_states(STATES) self._state_machine = self.bind_to_states(STATES)
self._username = username self._username = channel_name
self._token = token self._token = oauth_key
self.state = STATES.DISCONNECTED self.state = STATES.DISCONNECTED
@ -78,12 +80,12 @@ class TwitchIRCProcess(ChatProcess):
message = status_messages.get(new_state) message = status_messages.get(new_state)
if message is not None: if message is not None:
sys_msg = Message(message, TwitchIRCProcess.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM) sys_msg = Message(message, Process.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM)
self._message_queue.put(sys_msg) self._message_queue.put(sys_msg)
def on_disconnected(self, next_state): def on_disconnected(self, next_state):
self._ws = NonBlockingWebsocket() self._ws = NonBlockingWebsocket()
self._ws.connect(TwitchIRCProcess.WEBSOCKET_ADDRESS) self._ws.connect(Process.WEBSOCKET_ADDRESS)
self._ws.start() self._ws.start()
self._ws.send(f'PASS {self._token}') self._ws.send(f'PASS {self._token}')
self._ws.send(f'NICK {self._username}') self._ws.send(f'NICK {self._username}')
@ -103,7 +105,7 @@ class TwitchIRCProcess(ChatProcess):
for message in messages.splitlines(): for message in messages.splitlines():
cmd, hostmask, tags, args = ircv3_message_parser(message) cmd, hostmask, tags, args = ircv3_message_parser(message)
if cmd == 'PRIVMSG': if cmd == 'PRIVMSG':
normalized_message = TwitchIRCProcess.normalize_message(hostmask, tags, args) normalized_message = Process.normalize_message(hostmask, tags, args)
self._message_queue.put(normalized_message) self._message_queue.put(normalized_message)
elif cmd == 'PING': elif cmd == 'PING':
self._ws.send(f"PONG {' '.join(args)}") self._ws.send(f"PONG {' '.join(args)}")

View file

@ -1,4 +1,5 @@
import os import os
import json
import webbrowser import webbrowser
from enum import Enum, auto from enum import Enum, auto
@ -23,12 +24,20 @@ class CONTROL_MESSAGES(Enum):
RESTART = auto() RESTART = auto()
class YoutubeLiveProcess(ChatProcess): class Process(ChatProcess):
CHAT_NAME = 'YouTube Live' CHAT_NAME = 'YouTube Live'
GOOGLE_OAUTH_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' GOOGLE_OAUTH_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3' YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3'
def __init__(self, client_secrets, *args): def __init__(self, *args, client_secrets_path=None):
if client_secrets_path is None or not os.path.exists(client_secrets_path):
raise ValueError('Missing client secrets')
with open(client_secrets_path, 'r') as f:
client_secrets = json.load(f).get('installed')
if client_secrets is None:
raise ValueError('Malformed client secrets file - missing installed section')
super().__init__(*args) super().__init__(*args)
self._client_secrets = client_secrets self._client_secrets = client_secrets
self._state_machine = self.bind_to_states(STATES) self._state_machine = self.bind_to_states(STATES)
@ -71,7 +80,7 @@ class YoutubeLiveProcess(ChatProcess):
return next_state return next_state
def on_waiting_for_broadcast(self, next_state): def on_waiting_for_broadcast(self, next_state):
response = requests.get(f'{YoutubeLiveProcess.YOUTUBE_API_URL}/liveBroadcasts', response = requests.get(f'{Process.YOUTUBE_API_URL}/liveBroadcasts',
params={'part': 'snippet', 'broadcastStatus': 'active'}, params={'part': 'snippet', 'broadcastStatus': 'active'},
headers={'Authorization': f'Bearer {self._access_token}'}) headers={'Authorization': f'Bearer {self._access_token}'})
@ -88,7 +97,7 @@ class YoutubeLiveProcess(ChatProcess):
return 30 return 30
def on_polling(self, next_state): def on_polling(self, next_state):
response = requests.get(f'{YoutubeLiveProcess.YOUTUBE_API_URL}/liveChat/messages', response = requests.get(f'{Process.YOUTUBE_API_URL}/liveChat/messages',
params={'liveChatId': self._live_chat_id, params={'liveChatId': self._live_chat_id,
'part': 'snippet,authorDetails', 'part': 'snippet,authorDetails',
'hl': 'en_US', 'hl': 'en_US',
@ -104,7 +113,7 @@ class YoutubeLiveProcess(ChatProcess):
self._page_token = data['nextPageToken'] self._page_token = data['nextPageToken']
if len(data['items']): if len(data['items']):
for message in data['items']: for message in data['items']:
normalized = YoutubeLiveProcess.normalize_message(message) normalized = Process.normalize_message(message)
self._message_queue.put(normalized) self._message_queue.put(normalized)
sleep_milis = max(data['pollingIntervalMillis'], 5000) sleep_milis = max(data['pollingIntervalMillis'], 5000)
@ -123,7 +132,7 @@ class YoutubeLiveProcess(ChatProcess):
message = status_messages.get(new_state) message = status_messages.get(new_state)
if message is not None: if message is not None:
sys_msg = Message(message, YoutubeLiveProcess.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM) sys_msg = Message(message, Process.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM)
self._message_queue.put(sys_msg) self._message_queue.put(sys_msg)
def process_messages(self, message_type, args, next_state): def process_messages(self, message_type, args, next_state):
@ -133,10 +142,10 @@ class YoutubeLiveProcess(ChatProcess):
elif message_type == CONTROL_MESSAGES.RESTART: elif message_type == CONTROL_MESSAGES.RESTART:
if self.state == STATES.POLLING: if self.state == STATES.POLLING:
self._message_queue.put(Message('Restart requested, but service is in healthy state! Ignoring...', self._message_queue.put(Message('Restart requested, but service is in healthy state! Ignoring...',
YoutubeLiveProcess.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM)) Process.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM))
else: else:
self._message_queue.put(Message('Restarting service...', self._message_queue.put(Message('Restarting service...',
YoutubeLiveProcess.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM)) Process.CHAT_NAME, self.__class__.__name__, AUTHOR_TYPES.SYSTEM))
return None, None return None, None
@classmethod @classmethod
@ -167,7 +176,7 @@ class YoutubeLiveProcess(ChatProcess):
webbrowser.open(url) webbrowser.open(url)
def setup_oauth_consent(self, consent_code): def setup_oauth_consent(self, consent_code):
response = requests.post(YoutubeLiveProcess.GOOGLE_OAUTH_TOKEN_URL, data={ response = requests.post(Process.GOOGLE_OAUTH_TOKEN_URL, data={
'code': consent_code, 'code': consent_code,
'client_id': self._client_secrets['client_id'], 'client_id': self._client_secrets['client_id'],
'client_secret': self._client_secrets['client_secret'], 'client_secret': self._client_secrets['client_secret'],
@ -182,7 +191,7 @@ class YoutubeLiveProcess(ChatProcess):
self._refresh_token = auth['refresh_token'] self._refresh_token = auth['refresh_token']
def get_fresh_access_token(self): def get_fresh_access_token(self):
response = requests.post(YoutubeLiveProcess.GOOGLE_OAUTH_TOKEN_URL, data={ response = requests.post(Process.GOOGLE_OAUTH_TOKEN_URL, data={
'client_id': self._client_secrets['client_id'], 'client_id': self._client_secrets['client_id'],
'client_secret': self._client_secrets['client_secret'], 'client_secret': self._client_secrets['client_secret'],
'refresh_token': self._refresh_token, 'refresh_token': self._refresh_token,

26
config.toml Normal file
View file

@ -0,0 +1,26 @@
# How quickly to check for commands in the main event loop
# You shouldn't need to change this :)
event_poll_frequency = 0.1
[chat.FakeChat]
enabled = true
max_messages_per_chunk = 3
max_delay = 15
start_paused = false
[chat.YoutubeLive]
enabled = false
# # Where you put the yotube client secrets file
# # See https://seo-michael.co.uk/how-to-create-your-own-youtube-api-key-id-and-secret
# # TODO: The above link isnt specific for this task - maybe make your own and link back to repo docs?
# client_secrets_path = "googleapi.secret.json"
[chat.Twitch]
enabled = false
# # Your twitch username / name of your channel
# # REVIEW: Can these be different??
# channel_name = "cool_streamer"
# # The oauth key for your account, including the "oauth:" prefix
# # You can get this from https://twitchapps.com/tmi/
# oauth_key = "oauth:somethinglikethis"

66
main.py
View file

@ -1,50 +1,63 @@
import json
import os
import time import time
import importlib
import toml
from chats import YoutubeLive from chats import YoutubeLive
from chats.ChatProcess import LIFECYCLE_MESSAGES from chats.ChatProcess import LIFECYCLE_MESSAGES
from chats.FakeChat import FakeChat
from chats.Twitch import TwitchIRCProcess
from ui.App import App from ui.App import App
from plugins.PogCount import PogCounter
EVENT_POLL_FREQ = 0.1
GOOGLE_API_SECRETS_PATH = 'googleapi.secret.json'
TWITCH_SECRETS_PATH = 'twitch.secret.json'
if __name__ == '__main__': if __name__ == '__main__':
chat_processes = [FakeChat(EVENT_POLL_FREQ)] with open('config.toml', 'r') as f:
if os.path.exists(GOOGLE_API_SECRETS_PATH): config = toml.load(f)
with open(GOOGLE_API_SECRETS_PATH, 'r') as f:
client_secrets = json.load(f)['installed']
chat_processes.append(YoutubeLive.YoutubeLiveProcess(client_secrets, EVENT_POLL_FREQ))
else:
print('No client secrets - disabling youtube chat client')
if os.path.exists(TWITCH_SECRETS_PATH):
with open(TWITCH_SECRETS_PATH ,'r') as f:
secrets = json.load(f)
chat_processes.append(TwitchIRCProcess(secrets.get('username'), secrets.get('oauth'), EVENT_POLL_FREQ))
else:
print('No twitch config - disabling twitch chat client')
if len(chat_processes) == 1: EVENT_POLL_FREQUENCY = config['event_poll_frequency']
print('Hit "f" with the window focused to enable a testing client')
plugins = [PogCounter('../live-status.txt', prefix='Pog count: ')] # Load dynamic compoents
chat_processes = []
for chat_name, chat_config in config.get('chat', {}).items():
if not chat_config['enabled']:
continue
del chat_config['enabled']
chat_module = importlib.import_module(f'.{chat_name}', package='chats')
try:
chat_process = chat_module.Process(EVENT_POLL_FREQUENCY, **chat_config)
chat_processes.append(chat_process)
except Exception as e:
print(f'Failed to initalize {chat_name} - {e}')
if len(chat_processes) == 0:
print('No chats configured!')
plugins = []
for plugin_name, plugin_config in config.get('plugin', {}).items():
if not plugin_config['enabled']:
continue
del plugin_config['enabled']
plugin_module = importlib.import_module(f'.{plugin_name}', package='plugins')
try:
plugin = plugin_module.Plugin(**plugin_config)
plugins.append(plugin)
except Exception as e:
print(f'Failed to initalize {plugin_name} - {e}')
# Start the app and subprocesses
app = App(chat_processes, plugins) app = App(chat_processes, plugins)
for process in chat_processes: for process in chat_processes:
process.start() process.start()
app.start() app.start()
# Event loop
while True: while True:
if not app.is_alive(): if not app.is_alive():
break break
for process in chat_processes: for process in chat_processes:
# HACK: need to find a generic way to do this
if process.control_pipe.poll(): if process.control_pipe.poll():
chat_control_msg = process.control_pipe.recv() chat_control_msg = process.control_pipe.recv()
if chat_control_msg == YoutubeLive.CONTROL_MESSAGES.NEEDS_OAUTH_CONSENT_TOKEN: if chat_control_msg == YoutubeLive.CONTROL_MESSAGES.NEEDS_OAUTH_CONSENT_TOKEN:
@ -53,8 +66,9 @@ if __name__ == '__main__':
'type': YoutubeLive.CONTROL_MESSAGES.OAUTH_CONSENT_TOKEN, 'type': YoutubeLive.CONTROL_MESSAGES.OAUTH_CONSENT_TOKEN,
'token': code 'token': code
}) })
time.sleep(EVENT_POLL_FREQ) time.sleep(EVENT_POLL_FREQUENCY)
# Cleanup
for process in chat_processes: for process in chat_processes:
process.control_pipe.send({'type': LIFECYCLE_MESSAGES.SHUTDOWN}) process.control_pipe.send({'type': LIFECYCLE_MESSAGES.SHUTDOWN})
process.join(10) process.join(10)