2022-06-05 12:40:55 +01:00
|
|
|
import base64
|
2022-07-25 23:51:30 +01:00
|
|
|
import hashlib
|
|
|
|
import json
|
2022-10-24 22:42:16 +01:00
|
|
|
import logging
|
2022-07-25 23:51:30 +01:00
|
|
|
from pathlib import Path
|
2022-06-05 12:40:55 +01:00
|
|
|
from random import randint
|
2023-03-09 01:34:44 +00:00
|
|
|
from typing import Optional
|
2022-06-05 12:40:55 +01:00
|
|
|
|
2022-07-25 23:51:30 +01:00
|
|
|
import websocket
|
2023-06-19 17:45:49 +01:00
|
|
|
from websocket import WebSocketTimeoutException
|
|
|
|
|
|
|
|
from .error import OBSSDKError, OBSSDKTimeoutError
|
2022-10-24 22:42:16 +01:00
|
|
|
|
2023-06-22 22:17:20 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
2022-07-25 23:51:30 +01:00
|
|
|
|
2022-07-26 21:46:59 +01:00
|
|
|
|
2023-06-22 22:17:20 +01:00
|
|
|
class ObsClient:
|
2022-07-25 23:51:30 +01:00
|
|
|
def __init__(self, **kwargs):
|
2023-06-22 22:17:20 +01:00
|
|
|
self.logger = logger.getChild(self.__class__.__name__)
|
2023-06-19 17:45:49 +01:00
|
|
|
defaultkwargs = {
|
|
|
|
"host": "localhost",
|
|
|
|
"port": 4455,
|
|
|
|
"password": "",
|
|
|
|
"subs": 0,
|
|
|
|
"timeout": None,
|
|
|
|
}
|
2022-11-17 11:30:39 +00:00
|
|
|
if not any(key in kwargs for key in ("host", "port", "password")):
|
2022-12-04 19:34:55 +00:00
|
|
|
kwargs |= self._conn_from_toml()
|
2022-07-25 23:51:30 +01:00
|
|
|
kwargs = defaultkwargs | kwargs
|
|
|
|
for attr, val in kwargs.items():
|
|
|
|
setattr(self, attr, val)
|
2022-11-17 11:30:39 +00:00
|
|
|
|
|
|
|
self.logger.info(
|
2023-06-19 17:45:49 +01:00
|
|
|
"Connecting with parameters: host='{host}' port={port} password='{password}' subs={subs} timeout={timeout}".format(
|
2022-11-17 11:30:39 +00:00
|
|
|
**self.__dict__
|
|
|
|
)
|
|
|
|
)
|
2022-07-25 23:51:30 +01:00
|
|
|
|
2023-06-19 17:45:49 +01:00
|
|
|
try:
|
|
|
|
self.ws = websocket.WebSocket()
|
|
|
|
self.ws.connect(f"ws://{self.host}:{self.port}", timeout=self.timeout)
|
|
|
|
self.server_hello = json.loads(self.ws.recv())
|
|
|
|
except ValueError as e:
|
|
|
|
self.logger.error(f"{type(e).__name__}: {e}")
|
|
|
|
raise
|
|
|
|
except (ConnectionRefusedError, WebSocketTimeoutException) as e:
|
|
|
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
|
|
|
raise
|
2022-06-05 12:40:55 +01:00
|
|
|
|
2022-11-17 11:30:39 +00:00
|
|
|
def _conn_from_toml(self) -> dict:
|
2022-12-04 19:34:55 +00:00
|
|
|
try:
|
|
|
|
import tomllib
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
import tomli as tomllib
|
2023-03-09 01:34:44 +00:00
|
|
|
|
|
|
|
def get_filepath() -> Optional[Path]:
|
|
|
|
"""
|
|
|
|
traverses a list of paths for a 'config.toml'
|
|
|
|
returns the first config file found or None.
|
|
|
|
"""
|
|
|
|
filepaths = [
|
|
|
|
Path.cwd() / "config.toml",
|
|
|
|
Path.home() / "config.toml",
|
|
|
|
Path.home() / ".config" / "obsws-python" / "config.toml",
|
|
|
|
]
|
|
|
|
for filepath in filepaths:
|
|
|
|
if filepath.exists():
|
|
|
|
return filepath
|
|
|
|
|
2022-11-17 11:30:39 +00:00
|
|
|
conn = {}
|
2023-03-09 01:34:44 +00:00
|
|
|
if filepath := get_filepath():
|
2022-11-17 11:30:39 +00:00
|
|
|
with open(filepath, "rb") as f:
|
2022-11-17 12:11:53 +00:00
|
|
|
conn = tomllib.load(f)
|
2023-03-09 01:34:44 +00:00
|
|
|
self.logger.info(f"loading config from {filepath}")
|
2022-11-17 11:30:39 +00:00
|
|
|
return conn["connection"] if "connection" in conn else conn
|
2022-07-25 23:51:30 +01:00
|
|
|
|
2022-06-05 12:40:55 +01:00
|
|
|
def authenticate(self):
|
2022-07-26 04:36:55 +01:00
|
|
|
payload = {
|
|
|
|
"op": 1,
|
|
|
|
"d": {
|
|
|
|
"rpcVersion": 1,
|
2022-07-27 19:39:33 +01:00
|
|
|
"eventSubscriptions": self.subs,
|
2022-07-26 04:36:55 +01:00
|
|
|
},
|
|
|
|
}
|
2022-07-25 23:51:30 +01:00
|
|
|
|
2022-10-24 22:42:16 +01:00
|
|
|
if "authentication" in self.server_hello["d"]:
|
2022-11-17 11:30:39 +00:00
|
|
|
if not self.password:
|
|
|
|
raise OBSSDKError("authentication enabled but no password provided")
|
2022-10-24 22:42:16 +01:00
|
|
|
secret = base64.b64encode(
|
|
|
|
hashlib.sha256(
|
|
|
|
(
|
|
|
|
self.password + self.server_hello["d"]["authentication"]["salt"]
|
|
|
|
).encode()
|
|
|
|
).digest()
|
|
|
|
)
|
|
|
|
|
|
|
|
auth = base64.b64encode(
|
|
|
|
hashlib.sha256(
|
|
|
|
(
|
|
|
|
secret.decode()
|
|
|
|
+ self.server_hello["d"]["authentication"]["challenge"]
|
|
|
|
).encode()
|
|
|
|
).digest()
|
|
|
|
).decode()
|
|
|
|
|
|
|
|
payload["d"]["authentication"] = auth
|
|
|
|
|
2022-06-05 12:40:55 +01:00
|
|
|
self.ws.send(json.dumps(payload))
|
2022-10-24 22:42:16 +01:00
|
|
|
try:
|
|
|
|
response = json.loads(self.ws.recv())
|
|
|
|
return response["op"] == 2
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
|
|
raise OBSSDKError("failed to identify client with the server")
|
2022-06-05 12:40:55 +01:00
|
|
|
|
|
|
|
def req(self, req_type, req_data=None):
|
2022-10-24 22:42:16 +01:00
|
|
|
payload = {
|
|
|
|
"op": 6,
|
2022-11-17 11:30:39 +00:00
|
|
|
"d": {"requestType": req_type, "requestId": randint(1, 1000)},
|
2022-10-24 22:42:16 +01:00
|
|
|
}
|
2022-07-25 23:51:30 +01:00
|
|
|
if req_data:
|
2022-10-24 22:42:16 +01:00
|
|
|
payload["d"]["requestData"] = req_data
|
2022-11-17 11:30:39 +00:00
|
|
|
self.logger.debug(f"Sending request {payload}")
|
2023-05-29 11:34:40 +01:00
|
|
|
try:
|
|
|
|
self.ws.send(json.dumps(payload))
|
2023-06-13 23:09:44 +01:00
|
|
|
response = json.loads(self.ws.recv())
|
2023-06-19 17:45:49 +01:00
|
|
|
except WebSocketTimeoutException as e:
|
|
|
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
|
|
|
raise OBSSDKTimeoutError("Timeout while trying to send the request") from e
|
2022-10-24 22:49:16 +01:00
|
|
|
self.logger.debug(f"Response received {response}")
|
2022-07-26 21:46:59 +01:00
|
|
|
return response["d"]
|