ForcePilot/backend/package/yuxi/channel/extensions/zalouser/sidecar_client.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。

Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools

Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading

Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
2026-05-21 12:06:26 +08:00

798 lines
31 KiB
Python

from __future__ import annotations
import asyncio
import logging
from typing import Any
import httpx
from yuxi.channel.extensions.zalouser.errors import (
ZaloUserAuthError,
ZaloUserConnectionError,
ZaloUserQRDeclinedError,
ZaloUserQRExpiredError,
ZaloUserSendError,
classify_http_error,
)
from yuxi.channel.extensions.zalouser.types import (
ZaloFriend,
ZaloGroup,
ZaloGroupMember,
ZaloUserInfo,
)
logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 30.0
QR_POLL_INTERVAL = 0.4
class ZcaSidecarClient:
def __init__(self, sidecar_url: str, profile: str = "default"):
self._sidecar_url = sidecar_url.rstrip("/")
self._profile = profile
self._http: httpx.AsyncClient | None = None
async def _get_http(self) -> httpx.AsyncClient:
if self._http is None:
self._http = httpx.AsyncClient(timeout=httpx.Timeout(DEFAULT_TIMEOUT))
return self._http
@property
def sidecar_url(self) -> str:
return self._sidecar_url
async def close(self) -> None:
if self._http:
await self._http.aclose()
self._http = None
async def login_qr_start(self, force: bool = False, timeout_ms: int = 30_000) -> dict:
http = await self._get_http()
payload = {"profile": self._profile, "force": force, "timeout_ms": timeout_ms}
try:
resp = await http.post(
f"{self._sidecar_url}/api/qr/start",
json=payload,
timeout=30.0,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserConnectionError(f"Failed to start QR login: {e}") from e
async def login_qr_wait(self, timeout_ms: int = 120_000) -> dict:
http = await self._get_http()
deadline = asyncio.get_event_loop().time() + timeout_ms / 1000.0
while asyncio.get_event_loop().time() < deadline:
try:
resp = await http.get(f"{self._sidecar_url}/api/qr/status", timeout=10.0)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
status = resp.json()
state = status.get("state", "")
if state == "confirmed":
return {
"connected": True,
"user_id": status.get("userId", ""),
"display_name": status.get("displayName", ""),
}
if state == "declined":
raise ZaloUserQRDeclinedError("QR login was declined")
if state == "expired":
raise ZaloUserQRExpiredError("QR code expired")
except (httpx.RequestError, ZaloUserQRDeclinedError, ZaloUserQRExpiredError):
raise
except Exception:
pass
await asyncio.sleep(QR_POLL_INTERVAL)
raise ZaloUserQRExpiredError("QR login timed out")
async def login_qr_cancel(self) -> dict:
http = await self._get_http()
try:
resp = await http.post(
f"{self._sidecar_url}/api/qr/cancel",
json={"profile": self._profile},
timeout=10.0,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserConnectionError(f"Failed to cancel QR login: {e}") from e
async def ensure_session(self, timeout_ms: int = 20_000) -> dict:
http = await self._get_http()
payload = {"profile": self._profile, "timeout_ms": timeout_ms}
try:
resp = await http.post(
f"{self._sidecar_url}/api/ensure-session",
json=payload,
timeout=25.0,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserConnectionError(f"Failed to ensure session: {e}") from e
async def logout(self) -> dict:
http = await self._get_http()
try:
resp = await http.post(
f"{self._sidecar_url}/api/logout",
json={"profile": self._profile},
timeout=10.0,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserConnectionError(f"Failed to logout: {e}") from e
async def send_message(
self,
thread_id: str,
message: str,
*,
is_group: bool = False,
styles: list[dict] | None = None,
media_url: str | None = None,
quote: dict | None = None,
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"message": message,
}
if styles:
payload["styles"] = styles
if media_url:
payload["mediaUrl"] = media_url
if quote:
payload["quote"] = quote
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/send",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send message: {e}") from e
async def send_image(
self,
thread_id: str,
media_url: str,
*,
caption: str = "",
is_group: bool = False,
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"mediaUrl": media_url,
"mediaType": "image",
}
if caption:
payload["message"] = caption
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/image",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send image: {e}") from e
async def send_voice(self, thread_id: str, media_url: str, is_group: bool = False) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"mediaUrl": media_url,
"mediaType": "voice",
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/voice",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send voice: {e}") from e
async def send_attachment(
self, thread_id: str, media_url: str, media_type: str, is_group: bool = False
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"mediaUrl": media_url,
"mediaType": media_type,
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/attachment",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send attachment: {e}") from e
async def send_link(
self, thread_id: str, url: str, caption: str = "", is_group: bool = False
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"link": url,
"msg": caption,
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/link",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send link: {e}") from e
async def send_reaction(
self, thread_id: str, msg_id: str, cli_msg_id: str, icon: str, is_group: bool = False
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"msgId": msg_id,
"cliMsgId": cli_msg_id,
"icon": icon,
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/reaction",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send reaction: {e}") from e
async def remove_reaction(
self, thread_id: str, msg_id: str, cli_msg_id: str, is_group: bool = False
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"msgId": msg_id,
"cliMsgId": cli_msg_id,
"remove": True,
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/reaction",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to remove reaction: {e}") from e
async def send_typing(self, thread_id: str, is_group: bool = False) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/typing",
json=payload,
)
return resp.json() if resp.text else {}
except httpx.RequestError:
return {}
async def send_delivered(self, thread_id: str, msg_id: str) -> dict:
http = await self._get_http()
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/delivered",
json={"threadId": thread_id, "msgId": msg_id},
)
return resp.json() if resp.text else {}
except httpx.RequestError:
return {}
async def send_seen(self, thread_id: str, msg_id: str) -> dict:
http = await self._get_http()
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/seen",
json={"threadId": thread_id, "msgId": msg_id},
)
return resp.json() if resp.text else {}
except httpx.RequestError:
return {}
async def get_me(self) -> ZaloUserInfo | None:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/me")
if resp.status_code >= 400:
logger.warning("get_me returned %d", resp.status_code)
return None
data = resp.json()
return ZaloUserInfo(
user_id=data.get("userId", ""),
display_name=data.get("displayName", ""),
avatar_url=data.get("avatar"),
)
except Exception as e:
logger.warning("get_me failed: %s", e)
return None
async def list_friends(self, query: str | None = None) -> list[ZaloFriend]:
http = await self._get_http()
params = {}
if query:
params["query"] = query
try:
resp = await http.get(f"{self._sidecar_url}/api/friends", params=params)
if resp.status_code >= 400:
logger.warning("list_friends returned %d", resp.status_code)
return []
data = resp.json()
friends = data.get("friends", []) or data.get("data", [])
return [
ZaloFriend(
user_id=f.get("userId", ""),
display_name=f.get("displayName", ""),
avatar_url=f.get("avatar"),
)
for f in friends
]
except Exception as e:
logger.warning("list_friends failed: %s", e)
return []
async def list_groups(self, query: str | None = None) -> list[ZaloGroup]:
http = await self._get_http()
params = {}
if query:
params["query"] = query
try:
resp = await http.get(f"{self._sidecar_url}/api/groups", params=params)
if resp.status_code >= 400:
logger.warning("list_groups returned %d", resp.status_code)
return []
data = resp.json()
groups = data.get("groups", []) or data.get("data", [])
return [
ZaloGroup(
group_id=g.get("groupId", ""),
name=g.get("name", ""),
avatar_url=g.get("avatar"),
member_count=g.get("memberCount", 0),
)
for g in groups
]
except Exception as e:
logger.warning("list_groups failed: %s", e)
return []
async def list_group_members(self, group_id: str) -> list[ZaloGroupMember]:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/groups/{group_id}/members")
if resp.status_code >= 400:
logger.warning("list_group_members returned %d", resp.status_code)
return []
data = resp.json()
members = data.get("members", []) or data.get("data", [])
return [
ZaloGroupMember(
user_id=m.get("userId", ""),
display_name=m.get("displayName", ""),
avatar_url=m.get("avatar"),
is_admin=m.get("isAdmin", False),
)
for m in members
]
except Exception as e:
logger.warning("list_group_members failed: %s", e)
return []
async def get_status(self) -> dict:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/status", timeout=10.0)
if resp.status_code >= 400:
return {"authenticated": False, "connected": False}
data = resp.json() if resp.text else {}
return data
except Exception:
return {"authenticated": False, "connected": False}
async def check_auth(self) -> bool:
status = await self.get_status()
return status.get("authenticated", False) or status.get("connected", False)
async def delete_message(
self,
thread_id: str,
msg_id: str,
cli_msg_id: str = "",
is_group: bool = False,
is_owner: bool = True,
) -> dict:
http = await self._get_http()
payload: dict[str, Any] = {
"threadId": thread_id,
"type": "Group" if is_group else "User",
"msgId": msg_id,
"cliMsgId": cli_msg_id,
"isOwner": is_owner,
}
try:
resp = await http.post(
f"{self._sidecar_url}/api/message/delete",
json=payload,
)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to delete message: {e}") from e
async def poll_events(self, timeout: float = 30.0) -> list[dict]:
http = await self._get_http()
try:
resp = await http.get(
f"{self._sidecar_url}/api/events/poll",
timeout=timeout,
)
if resp.status_code == 200:
data = resp.json() if resp.text else {}
if isinstance(data, list):
return data
if isinstance(data, dict):
return [data]
elif resp.status_code >= 400:
logger.warning("poll_events returned %d", resp.status_code)
return []
except httpx.TimeoutException:
return []
except httpx.ConnectError:
raise
except Exception as e:
logger.warning("poll_events error: %s", e)
return []
async def create_group(self, name: str, member_ids: list[str]) -> dict:
http = await self._get_http()
payload = {"name": name, "memberIds": member_ids}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/create", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to create group: {e}") from e
async def disband_group(self, group_id: str) -> dict:
http = await self._get_http()
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/disband", json={})
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to disband group: {e}") from e
async def add_user_to_group(self, user_id: str, group_id: str) -> dict:
http = await self._get_http()
payload = {"userId": user_id}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/members/add", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to add user to group: {e}") from e
async def remove_user_from_group(self, user_id: str, group_id: str) -> dict:
http = await self._get_http()
payload = {"userId": user_id}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/members/remove", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to remove user from group: {e}") from e
async def change_group_name(self, group_id: str, name: str) -> dict:
http = await self._get_http()
payload = {"name": name}
try:
resp = await http.put(f"{self._sidecar_url}/api/groups/{group_id}/name", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to change group name: {e}") from e
async def change_group_avatar(self, group_id: str, avatar_url: str) -> dict:
http = await self._get_http()
payload = {"avatarUrl": avatar_url}
try:
resp = await http.put(f"{self._sidecar_url}/api/groups/{group_id}/avatar", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to change group avatar: {e}") from e
async def change_group_owner(self, group_id: str, member_id: str) -> dict:
http = await self._get_http()
payload = {"memberId": member_id}
try:
resp = await http.put(f"{self._sidecar_url}/api/groups/{group_id}/owner", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to change group owner: {e}") from e
async def add_group_deputies(self, group_id: str, member_ids: list[str]) -> dict:
http = await self._get_http()
payload = {"memberIds": member_ids}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/deputies/add", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to add deputy: {e}") from e
async def remove_group_deputies(self, group_id: str, member_ids: list[str]) -> dict:
http = await self._get_http()
payload = {"memberIds": member_ids}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/deputies/remove", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to remove deputy: {e}") from e
async def create_poll(self, group_id: str, question: str, options: list[str]) -> dict:
http = await self._get_http()
payload = {"groupId": group_id, "question": question, "options": options}
try:
resp = await http.post(f"{self._sidecar_url}/api/message/poll/create", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to create poll: {e}") from e
async def lock_poll(self, poll_id: str, group_id: str) -> dict:
http = await self._get_http()
payload = {"pollId": poll_id, "groupId": group_id}
try:
resp = await http.post(f"{self._sidecar_url}/api/message/poll/lock", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to lock poll: {e}") from e
async def search_stickers(self, keyword: str) -> list[dict]:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/stickers/search", params={"keyword": keyword})
if resp.status_code >= 400:
return []
return resp.json().get("stickers", [])
except Exception:
return []
async def get_sticker_detail(self, sticker_id: str) -> dict | None:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/stickers/{sticker_id}")
if resp.status_code >= 400:
return None
return resp.json()
except Exception:
return None
async def send_sticker(self, thread_id: str, sticker_obj: dict, is_group: bool = False) -> dict:
http = await self._get_http()
payload = {"threadId": thread_id, "type": "Group" if is_group else "User", "sticker": sticker_obj}
try:
resp = await http.post(f"{self._sidecar_url}/api/message/sticker", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send sticker: {e}") from e
async def find_user(self, phone_number: str) -> dict | None:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/users/find", params={"phoneNumber": phone_number})
if resp.status_code >= 400:
return None
return resp.json()
except Exception as e:
logger.warning("find_user failed: %s", e)
return None
async def get_user_info(self, user_id: str) -> ZaloUserInfo | None:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/users/{user_id}")
if resp.status_code >= 400:
return None
data = resp.json()
return ZaloUserInfo(
user_id=data.get("userId", ""),
display_name=data.get("displayName", ""),
avatar_url=data.get("avatar"),
)
except Exception as e:
logger.warning("get_user_info failed: %s", e)
return None
async def get_group_info(self, group_id: str) -> ZaloGroup | None:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/groups/{group_id}/info")
if resp.status_code >= 400:
return None
data = resp.json()
return ZaloGroup(
group_id=data.get("groupId", ""),
name=data.get("name", ""),
avatar_url=data.get("avatar"),
member_count=data.get("memberCount", 0),
)
except Exception as e:
logger.warning("get_group_info failed: %s", e)
return None
async def block_user(self, user_id: str) -> dict:
http = await self._get_http()
try:
resp = await http.post(f"{self._sidecar_url}/api/users/{user_id}/block", json={})
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to block user: {e}") from e
async def unblock_user(self, user_id: str) -> dict:
http = await self._get_http()
try:
resp = await http.post(f"{self._sidecar_url}/api/users/{user_id}/unblock", json={})
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to unblock user: {e}") from e
async def change_friend_alias(self, user_id: str, alias: str) -> dict:
http = await self._get_http()
payload = {"alias": alias}
try:
resp = await http.put(f"{self._sidecar_url}/api/users/{user_id}/alias", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to change alias: {e}") from e
async def send_friend_request(self, user_id: str, msg: str = "") -> dict:
http = await self._get_http()
payload = {"userId": user_id, "msg": msg}
try:
resp = await http.post(f"{self._sidecar_url}/api/friends/request", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send friend request: {e}") from e
async def accept_friend_request(self, user_id: str) -> dict:
http = await self._get_http()
payload = {"userId": user_id}
try:
resp = await http.post(f"{self._sidecar_url}/api/friends/request/accept", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to accept friend request: {e}") from e
async def send_card(self, thread_id: str, user_id: str, phone: str, name: str, is_group: bool = False) -> dict:
http = await self._get_http()
payload = {"threadId": thread_id, "type": "Group" if is_group else "User", "userId": user_id, "phone": phone, "name": name}
try:
resp = await http.post(f"{self._sidecar_url}/api/message/card", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to send card: {e}") from e
async def create_note(self, group_id: str, title: str, content: str, color: int = 0) -> dict:
http = await self._get_http()
payload = {"title": title, "content": content, "color": color}
try:
resp = await http.post(f"{self._sidecar_url}/api/groups/{group_id}/notes", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to create note: {e}") from e
async def edit_note(self, group_id: str, note_id: str, title: str, content: str, color: int = 0) -> dict:
http = await self._get_http()
payload = {"title": title, "content": content, "color": color}
try:
resp = await http.put(f"{self._sidecar_url}/api/groups/{group_id}/notes/{note_id}", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to edit note: {e}") from e
async def pin_conversations(self, thread_ids: list[str]) -> dict:
http = await self._get_http()
payload = {"threadIds": thread_ids}
try:
resp = await http.post(f"{self._sidecar_url}/api/conversations/pin", json=payload)
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to pin conversations: {e}") from e
async def get_cookie(self) -> dict:
http = await self._get_http()
try:
resp = await http.get(f"{self._sidecar_url}/api/session/cookie")
if resp.status_code >= 400:
raise classify_http_error(resp.status_code, resp.text)
return resp.json()
except httpx.RequestError as e:
raise ZaloUserSendError(f"Failed to get cookie: {e}") from e