wrap exceptions in _token_from_toml and raise SteamlabsSIOError errors

remove the assert from _token_from_toml

replace self.streamlabs,self.twitch and self.youtube with self.event_types
This commit is contained in:
Onyx and Iris 2025-01-20 13:32:11 +00:00
parent b655fd6360
commit 0b67bcd832

View File

@ -1,11 +1,11 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Union
import socketio import socketio
from observable import Observable from observable import Observable
from .error import SteamlabsSIOConnectionError from .error import SteamlabsSIOConnectionError, SteamlabsSIOError
from .models import as_dataclass from .models import as_dataclass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,93 +17,128 @@ class Client:
self.token = token or self._token_from_toml() self.token = token or self._token_from_toml()
self._raw = raw self._raw = raw
self.sio = socketio.Client() self.sio = socketio.Client()
self.sio.on("connect", self.connect_handler) self.sio.on('connect', self.connect_handler)
self.sio.on("event", self.event_handler) self.sio.on('event', self.event_handler)
self.sio.on("disconnect", self.disconnect_handler) self.sio.on('disconnect', self.disconnect_handler)
self.obs = Observable() self.obs = Observable()
self.streamlabs = ("donation",) self.event_types: set[str] = (
self.twitch = ("follow", "subscription", "host", "bits", "raid") {'donation'} # streamlabs
self.youtube = ("follow", "subscription", "superchat") | {'follow', 'subscription', 'host', 'bits', 'raid'} # twitch
| {'follow', 'subscription', 'superchat'} # youtube
)
def __enter__(self): def __enter__(self):
try: try:
self.sio.connect(f"https://sockets.streamlabs.com?token={self.token}") self.sio.connect(f'https://sockets.streamlabs.com?token={self.token}')
except socketio.exceptions.ConnectionError as e: except socketio.exceptions.ConnectionError as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f'{type(e).__name__}: {e}')
raise SteamlabsSIOConnectionError( raise SteamlabsSIOConnectionError(
"no connection could be established to the Streamlabs SIO server" 'no connection could be established to the Streamlabs SIO server'
) from e ) from e
self.log_mode() self.log_mode()
return self return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.sio.disconnect()
@property @property
def raw(self): def raw(self) -> bool:
return self._raw return self._raw
@raw.setter @raw.setter
def raw(self, val): def raw(self, val: bool) -> None:
self._raw = val self._raw = val
self.log_mode() self.log_mode()
def log_mode(self): def log_mode(self):
info = (f"Running client in {'raw' if self.raw else 'normal'} mode.",) info = (
if self.raw: 'Raw mode' if self.raw else 'Normal mode',
info += ("raw JSON messages will be passed to callbacks",) 'activated.',
else: 'JSON messages' if self.raw else 'Event objects',
info += ("event data objects will be passed to callbacks",) 'will be passed to callbacks.',
self.logger.info(" ".join(info)) )
self.logger.info(' '.join(info))
def _token_from_toml(self) -> str: def _token_from_toml(self) -> str:
"""
Retrieves the Streamlabs token from a TOML configuration file.
This method attempts to load the token from a 'config.toml' file located
either in the current working directory or in the user's home configuration
directory under '.config/streamlabsio/'.
Returns:
str: The Streamlabs token retrieved from the TOML configuration file.
Raises:
SteamlabsSIOError: If no configuration file is found, if the file cannot
be decoded, or if the required 'streamlabs' section or 'token' key is
missing from the configuration file.
"""
try: try:
import tomllib import tomllib
except ModuleNotFoundError: except ModuleNotFoundError:
import tomli as tomllib import tomli as tomllib
def get_filepath() -> Optional[Path]: def get_filepath() -> Union[Path, None]:
filepaths = ( filepaths = (
Path.cwd() / "config.toml", Path.cwd() / 'config.toml',
Path.home() / ".config" / "streamlabsio" / "config.toml", Path.home() / '.config' / 'streamlabsio' / 'config.toml',
) )
for filepath in filepaths: for filepath in filepaths:
if filepath.exists(): if filepath.exists():
return filepath return filepath
return None
try:
filepath = get_filepath() filepath = get_filepath()
if not filepath: if not filepath:
raise FileNotFoundError("config.toml was not found") raise SteamlabsSIOError('no token provided and no config.toml file found')
with open(filepath, "rb") as f:
try:
with open(filepath, 'rb') as f:
conn = tomllib.load(f) conn = tomllib.load(f)
assert ( except tomllib.TOMLDecodeError as e:
"streamlabs" in conn and "token" in conn["streamlabs"] ERR_MSG = f'Error decoding {filepath}: {e}'
), "expected [streamlabs][token] in config.toml" self.logger.exception(ERR_MSG)
return conn["streamlabs"]["token"] raise SteamlabsSIOError(ERR_MSG) from e
except (FileNotFoundError, tomllib.TOMLDecodeError) as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
def connect_handler(self): if 'streamlabs' not in conn or 'token' not in conn['streamlabs']:
self.logger.info("Connected to Streamlabs Socket API") ERR_MSG = (
'config.toml does not contain a "streamlabs" section '
'or the "streamlabs" section does not contain a "token" key'
)
self.logger.exception(ERR_MSG)
raise SteamlabsSIOError(ERR_MSG)
def event_handler(self, data): return conn['streamlabs']['token']
if "for" in data and data["type"] in set(
self.streamlabs + self.twitch + self.youtube def connect_handler(self) -> None:
): self.logger.info('Connected to Streamlabs Socket API')
message = data["message"][0]
def event_handler(self, data: Any) -> None:
"""
Handles incoming events and triggers corresponding OBS actions.
Args:
data (dict): The event data containing information about the event.
Expected keys:
- 'for': The target of the event.
- 'type': The type of the event.
- 'message': A list containing the event message.
Returns:
None
"""
if 'for' in data and data['type'] in self.event_types:
message = data['message'][0]
self.obs.trigger( self.obs.trigger(
data["for"], data['for'],
data["type"], data['type'],
message if self.raw else as_dataclass(data["type"], message), message if self.raw else as_dataclass(data['type'], message),
) )
self.logger.debug(data) self.logger.debug(data)
def disconnect_handler(self): def disconnect_handler(self) -> None:
self.logger.info("Disconnected from Streamlabs Socket API") self.logger.info('Disconnected from Streamlabs Socket API')
def __exit__(self, exc_type, exc_val, exc_tb):
self.sio.disconnect()
def connect(**kwargs): def connect(**kwargs) -> Client:
SIO_cls = Client SIO_cls = Client
return SIO_cls(**kwargs) return SIO_cls(**kwargs)