该提交新增了完整的BlueBubbles渠道插件,支持通过BlueBubbles Server集成iMessage功能,包含以下核心能力: 1. 支持私聊和群聊会话管理,自动区分会话类型 2. 完整的消息收发支持,包括文本、图片、语音、文件、视频消息 3. 支持消息反应、已读回执、消息编辑与撤回 4. 内置去重、防抖处理机制 5. 支持Webhook和WebSocket两种事件接收方式 6. 完善的权限与安全校验机制 7. 历史消息同步与抓包功能 8. TTS语音合成与发送支持 9. 群组管理能力,包括重命名、修改头像、增减成员等
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()
|