from __future__ import annotations import json import logging import urllib.parse import httpx from yuxi.channel.extensions.viber.config import ViberConfigAdapter from yuxi.channel.extensions.viber.errors import ViberErrorCode from yuxi.channel.extensions.viber.rate_limiter import ViberRateLimiter from yuxi.channel.extensions.viber.types import VIBER_API_BASE from yuxi.channel.extensions.viber.webhook import set_webhook_running logger = logging.getLogger(__name__) class ViberGatewayAdapter: def __init__(self): self._config = ViberConfigAdapter() self._running = False self._rate_limiter = ViberRateLimiter(max_requests=30, per_seconds=1.0) async def start(self, ctx) -> object: import asyncio account_id = getattr(ctx, "account_id", "default") config = getattr(ctx, "config", {}) or {} self._config.list_account_ids(config) account = await self._config.resolve_account(account_id) auth_token = account.get("auth_token", "") if not auth_token: logger.warning("Viber gateway start: auth_token not configured for account %s", account_id) self._running = True queue = getattr(ctx, "queue", asyncio.Queue()) return queue valid = await self._probe_bot(auth_token) if valid: logger.info("Viber bot connected for account %s", account_id) else: logger.warning("Viber bot probe failed for account %s", account_id) webhook_url = account.get("webhook_url", "") if webhook_url: ok = await self._set_webhook(auth_token, webhook_url) if ok: logger.info("Viber webhook set: %s", webhook_url) else: logger.error("Viber set_webhook failed for account %s", account_id) self._running = True set_webhook_running(True) queue = getattr(ctx, "queue", asyncio.Queue()) return queue async def stop(self, ctx) -> None: account_id = getattr(ctx, "account_id", "default") if self._running: account = await self._config.resolve_account(account_id) auth_token = account.get("auth_token", "") webhook_url = account.get("webhook_url", "") if auth_token and webhook_url: await self._remove_webhook(auth_token) self._running = False set_webhook_running(False) logger.info("Viber gateway stopped") async def probe_bot(self, account: dict) -> dict | None: auth_token = account.get("auth_token", "") if not auth_token: return None return await self._probe_bot_detail(auth_token) async def _probe_bot(self, auth_token: str) -> bool: try: result = await self._api_call(auth_token, "get_account_info", {}) return result.get("status") == 0 except Exception: logger.exception("Viber probe error") return False async def _probe_bot_detail(self, auth_token: str) -> dict | None: try: result = await self._api_call(auth_token, "get_account_info", {}) if result.get("status") == 0: return result return None except Exception: logger.exception("Viber probe detail error") return None async def _set_webhook(self, auth_token: str, webhook_url: str) -> bool: payload = { "url": webhook_url, "event_types": [ "delivered", "seen", "failed", "subscribed", "unsubscribed", "conversation_started", ], "send_name": True, "send_photo": True, } result = await self._api_call(auth_token, "set_webhook", payload) ok = result.get("status") == 0 if not ok: logger.error("Viber set_webhook failed: %s", result.get("status_message")) return ok async def _remove_webhook(self, auth_token: str) -> bool: payload = {"url": ""} result = await self._api_call(auth_token, "set_webhook", payload) return result.get("status") == 0 async def get_user_details(self, account: dict, user_id: str) -> dict | None: auth_token = account.get("auth_token", "") if not auth_token: return None try: result = await self._api_call(auth_token, "get_user_details", {"id": user_id}) if result.get("status") == 0: return result.get("result", result) self._log_error("get_user_details", result) return None except Exception: logger.exception("Viber get_user_details error") return None async def broadcast_message( self, account: dict, broadcast_list: list[str], message: dict, ) -> dict | None: auth_token = account.get("auth_token", "") if not auth_token: return None if len(broadcast_list) > 300: logger.warning("Viber broadcast_list exceeds 300 limit: %d", len(broadcast_list)) broadcast_list = broadcast_list[:300] try: results = [] for receiver_id in broadcast_list: msg_copy = json.loads(json.dumps(message)) msg_str = json.dumps(msg_copy) msg_str = msg_str.replace("replace_me_with_receiver_id", receiver_id) msg_str = msg_str.replace( "replace_me_with_url_encoded_receiver_id", urllib.parse.quote(receiver_id, safe=""), ) if "replace_me_with_user_name" in msg_str: user_name = await self._get_user_name(auth_token, receiver_id) msg_str = msg_str.replace("replace_me_with_user_name", user_name) msg_copy = json.loads(msg_str) msg_copy["receiver"] = receiver_id result = await self._api_call(auth_token, "send_message", msg_copy) results.append(result) failed = [] for i, result in enumerate(results): if result.get("status") != 0: failed.append({ "receiver": broadcast_list[i], "status": result.get("status"), "status_message": result.get("status_message", ""), }) if failed: logger.warning( "Viber broadcast partial failure: %d/%d failed", len(failed), len(broadcast_list), ) for f in failed: logger.warning( " - receiver=%s status=%s message=%s", f["receiver"], f["status"], f["status_message"], ) return { "status": 0, "broadcast_count": len(broadcast_list), "failed_count": len(failed), "failed_list": failed, "results": results, } except Exception: logger.exception("Viber broadcast_message error") return None async def _get_user_name(self, auth_token: str, user_id: str) -> str: try: result = await self._api_call(auth_token, "get_user_details", {"id": user_id}) if result.get("status") == 0: user_data = result.get("user", {}) name = user_data.get("name", "") if name: return name except Exception: logger.debug("Viber get_user_name failed for %s", user_id) return "User" async def get_online(self, account: dict, user_ids: list[str]) -> dict | None: auth_token = account.get("auth_token", "") if not auth_token: return None if len(user_ids) > 100: logger.warning("Viber get_online exceeds 100 user limit: %d", len(user_ids)) user_ids = user_ids[:100] try: result = await self._api_call(auth_token, "get_online", {"ids": user_ids}) if result.get("status") == 0: return result self._log_error("get_online", result) return None except Exception: logger.exception("Viber get_online error") return None async def _api_call(self, auth_token: str, endpoint: str, payload: dict) -> dict: await self._rate_limiter.acquire() headers = { "X-Viber-Auth-Token": auth_token, "Content-Type": "application/json", } async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client: resp = await client.post( f"{VIBER_API_BASE}/{endpoint}", json=payload, headers=headers, ) resp.raise_for_status() result = resp.json() self._log_error(endpoint, result) billing_status = result.get("billing_status") if billing_status is not None: logger.debug( "Viber billing: endpoint=%s status=%s", endpoint, billing_status, ) return result @staticmethod def _log_error(endpoint: str, result: dict) -> None: status = result.get("status") if status is not None and status != 0: status_msg = result.get("status_message", "") retryable = ViberErrorCode.is_retryable(status) auth_fail = ViberErrorCode.is_auth_error(status) extra = "" if retryable: extra = " [retryable]" if auth_fail: extra += " [auth_failure]" logger.warning( "Viber API %s failed: status=%s message=%s%s", endpoint, status, status_msg, extra, )