import logging import httpx logger = logging.getLogger(__name__) MENU_MAX_TOP_LEVEL = 3 MENU_MAX_SUB_LEVEL = 5 MENU_TITLE_MAX_CHARS = 30 MENU_PAYLOAD_MAX_CHARS = 1000 def _build_menu_api_url(api_version: str = "v24.0") -> str: return f"https://graph.facebook.com/{api_version}/me/messenger_profile" def _compute_appsecret_proof(access_token: str, app_secret: str) -> str: import hashlib import hmac return hmac.new( app_secret.encode("utf-8"), access_token.encode("utf-8"), hashlib.sha256, ).hexdigest() async def set_persistent_menu( access_token: str, menu_items: list[dict], *, app_secret: str = "", api_version: str = "v24.0", composer_input_disabled: bool = False, ) -> dict: url = _build_menu_api_url(api_version) params: dict = {"access_token": access_token} if app_secret: params["appsecret_proof"] = _compute_appsecret_proof(access_token, app_secret) payload = { "persistent_menu": [ { "locale": "default", "composer_input_disabled": composer_input_disabled, "call_to_actions": menu_items, } ] } async with httpx.AsyncClient(timeout=15.0) as client: try: resp = await client.post(url, json=payload, params=params) if resp.status_code == 200: logger.info("Persistent menu set successfully") return {"success": True, "data": resp.json()} logger.error("Failed to set persistent menu: %d %s", resp.status_code, resp.text[:200]) return {"success": False, "error": resp.text[:200]} except httpx.RequestError as exc: logger.error("Persistent menu network error: %s", exc) return {"success": False, "error": str(exc)} async def delete_persistent_menu( access_token: str, *, app_secret: str = "", api_version: str = "v24.0", ) -> dict: url = _build_menu_api_url(api_version) params: dict = {"access_token": access_token} if app_secret: params["appsecret_proof"] = _compute_appsecret_proof(access_token, app_secret) payload = {"fields": ["persistent_menu"]} async with httpx.AsyncClient(timeout=15.0) as client: try: resp = await client.delete(url, json=payload, params=params) if resp.status_code == 200: logger.info("Persistent menu deleted successfully") return {"success": True, "data": resp.json()} logger.error("Failed to delete persistent menu: %d %s", resp.status_code, resp.text[:200]) return {"success": False, "error": resp.text[:200]} except httpx.RequestError as exc: logger.error("Persistent menu delete network error: %s", exc) return {"success": False, "error": str(exc)} async def get_persistent_menu( access_token: str, *, app_secret: str = "", api_version: str = "v24.0", ) -> dict: url = _build_menu_api_url(api_version) params: dict = { "access_token": access_token, "fields": "persistent_menu", } if app_secret: params["appsecret_proof"] = _compute_appsecret_proof(access_token, app_secret) async with httpx.AsyncClient(timeout=15.0) as client: try: resp = await client.get(url, params=params) if resp.status_code == 200: data = resp.json() return {"success": True, "data": data.get("data", [])} logger.error("Failed to get persistent menu: %d %s", resp.status_code, resp.text[:200]) return {"success": False, "error": resp.text[:200]} except httpx.RequestError as exc: logger.error("Persistent menu get network error: %s", exc) return {"success": False, "error": str(exc)}