128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
|
|
import hashlib
|
||
|
|
from urllib.parse import urlparse
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
|
||
|
|
class BlueBubblesAuthStrategy:
|
||
|
|
def apply(self, request_kwargs: dict, password: str) -> dict:
|
||
|
|
raise NotImplementedError
|
||
|
|
|
||
|
|
|
||
|
|
class QueryStringAuth(BlueBubblesAuthStrategy):
|
||
|
|
def apply(self, request_kwargs: dict, password: str) -> dict:
|
||
|
|
params = request_kwargs.get("params", {})
|
||
|
|
if isinstance(params, dict):
|
||
|
|
params["password"] = password
|
||
|
|
else:
|
||
|
|
params = {"password": password}
|
||
|
|
request_kwargs["params"] = params
|
||
|
|
return request_kwargs
|
||
|
|
|
||
|
|
|
||
|
|
class HeaderAuth(BlueBubblesAuthStrategy):
|
||
|
|
def __init__(self, header_name: str = "X-BB-Password"):
|
||
|
|
self.header_name = header_name
|
||
|
|
|
||
|
|
def apply(self, request_kwargs: dict, password: str) -> dict:
|
||
|
|
headers = request_kwargs.get("headers", {})
|
||
|
|
if isinstance(headers, dict):
|
||
|
|
headers[self.header_name] = password
|
||
|
|
else:
|
||
|
|
headers = {self.header_name: password}
|
||
|
|
request_kwargs["headers"] = headers
|
||
|
|
return request_kwargs
|
||
|
|
|
||
|
|
|
||
|
|
class BlueBubblesClient:
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
server_url: str,
|
||
|
|
password: str,
|
||
|
|
account_id: str = "default",
|
||
|
|
timeout_ms: int = 30000,
|
||
|
|
auth_strategy: BlueBubblesAuthStrategy | None = None,
|
||
|
|
allow_private_network: bool = False,
|
||
|
|
):
|
||
|
|
self.server_url = server_url.rstrip("/")
|
||
|
|
self.password = password
|
||
|
|
self.account_id = account_id
|
||
|
|
self.timeout = httpx.Timeout(timeout_ms / 1000.0)
|
||
|
|
self.auth_strategy = auth_strategy or QueryStringAuth()
|
||
|
|
self._trusted_hostname = self._resolve_hostname(server_url)
|
||
|
|
self._allow_private_network = allow_private_network
|
||
|
|
self._client: httpx.AsyncClient | None = None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _resolve_hostname(url: str) -> str | None:
|
||
|
|
try:
|
||
|
|
return urlparse(url).hostname
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _build_transport(self) -> httpx.AsyncHTTPTransport | None:
|
||
|
|
if self._allow_private_network:
|
||
|
|
return None
|
||
|
|
return None
|
||
|
|
|
||
|
|
async def _get_client(self) -> httpx.AsyncClient:
|
||
|
|
if self._client is None or self._client.is_closed:
|
||
|
|
self._client = httpx.AsyncClient(
|
||
|
|
base_url=self.server_url,
|
||
|
|
timeout=self.timeout,
|
||
|
|
transport=self._build_transport(),
|
||
|
|
)
|
||
|
|
return self._client
|
||
|
|
|
||
|
|
def _fingerprint(self) -> str:
|
||
|
|
raw = f"{self.account_id}:{self.password}:{type(self.auth_strategy).__name__}"
|
||
|
|
return hashlib.sha256(raw.encode()).hexdigest()[:8]
|
||
|
|
|
||
|
|
async def request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
||
|
|
kwargs = self.auth_strategy.apply(kwargs, self.password)
|
||
|
|
client = await self._get_client()
|
||
|
|
return await client.request(method, path, **kwargs)
|
||
|
|
|
||
|
|
async def get(self, path: str, **kwargs) -> httpx.Response:
|
||
|
|
return await self.request("GET", path, **kwargs)
|
||
|
|
|
||
|
|
async def post(self, path: str, **kwargs) -> httpx.Response:
|
||
|
|
return await self.request("POST", path, **kwargs)
|
||
|
|
|
||
|
|
async def put(self, path: str, **kwargs) -> httpx.Response:
|
||
|
|
return await self.request("PUT", path, **kwargs)
|
||
|
|
|
||
|
|
async def delete(self, path: str, **kwargs) -> httpx.Response:
|
||
|
|
return await self.request("DELETE", path, **kwargs)
|
||
|
|
|
||
|
|
async def ping(self) -> bool:
|
||
|
|
try:
|
||
|
|
resp = await self.get("/")
|
||
|
|
return resp.status_code < 500
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def close(self):
|
||
|
|
if self._client and not self._client.is_closed:
|
||
|
|
await self._client.aclose()
|
||
|
|
self._client = None
|
||
|
|
|
||
|
|
|
||
|
|
_client_cache: dict[str, BlueBubblesClient] = {}
|
||
|
|
|
||
|
|
|
||
|
|
def get_or_create_client(
|
||
|
|
server_url: str,
|
||
|
|
password: str,
|
||
|
|
account_id: str = "default",
|
||
|
|
**kwargs,
|
||
|
|
) -> BlueBubblesClient:
|
||
|
|
fingerprint = hashlib.sha256(f"{account_id}:{password}:{server_url}".encode()).hexdigest()[:12]
|
||
|
|
if fingerprint not in _client_cache:
|
||
|
|
_client_cache[fingerprint] = BlueBubblesClient(server_url, password, account_id, **kwargs)
|
||
|
|
return _client_cache[fingerprint]
|
||
|
|
|
||
|
|
|
||
|
|
def clear_client_cache():
|
||
|
|
_client_cache.clear()
|