from __future__ import annotations import base64 import hashlib import hmac import logging from requests_oauthlib import OAuth1Session logger = logging.getLogger(__name__) X_API_V1_BASE = "https://api.x.com" def _build_oauth(account: dict) -> OAuth1Session: return OAuth1Session( account["api_key"], client_secret=account["api_secret"], resource_owner_key=account["access_token"], resource_owner_secret=account["access_secret"], ) def _resolve_webhook_env(account: dict) -> str: return account.get("webhook_env", "dev") def verify_crc_token(crc_token: str, consumer_secret: str) -> str: digest = hmac.new( consumer_secret.encode("utf-8"), crc_token.encode("utf-8"), hashlib.sha256, ).digest() return "sha256=" + base64.b64encode(digest).decode() def verify_webhook_signature(signature: str, body: bytes, consumer_secret: str) -> bool: expected = hmac.new( consumer_secret.encode("utf-8"), body, hashlib.sha256, ).digest() expected_b64 = base64.b64encode(expected).decode() return hmac.compare_digest(f"sha256={expected_b64}", signature) async def register_webhook(account: dict, env_name: str | None = None) -> dict | None: oauth = _build_oauth(account) webhook_url = account.get("webhook_url", "") if not webhook_url: logger.error("Twitter webhook: no webhook_url configured") return None env_name = env_name or _resolve_webhook_env(account) url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/webhooks.json" try: resp = oauth.post(url, data={"url": webhook_url}, timeout=15) if resp.status_code in (200, 201): return resp.json() logger.error( "Twitter webhook register failed: HTTP %d %s", resp.status_code, resp.text[:200], ) except Exception as e: logger.error("Twitter webhook register error: %s", e) return None async def subscribe_account_activity( account: dict, env_name: str | None = None ) -> bool: oauth = _build_oauth(account) env_name = env_name or _resolve_webhook_env(account) url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/subscriptions.json" try: resp = oauth.post(url, timeout=10) if resp.status_code in (200, 204): logger.info("Twitter account activity subscribed for env %s", env_name) return True logger.error( "Twitter subscription failed: HTTP %d %s", resp.status_code, resp.text[:200] ) except Exception as e: logger.error("Twitter subscription error: %s", e) return False async def unsubscribe_account_activity( account: dict, env_name: str | None = None ) -> bool: oauth = _build_oauth(account) user_id = account.get("user_id", "") if not user_id: return False env_name = env_name or _resolve_webhook_env(account) url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/subscriptions/{user_id}.json" try: resp = oauth.delete(url, timeout=10) if resp.status_code in (200, 204): logger.info("Twitter account activity unsubscribed for user %s", user_id) return True except Exception as e: logger.error("Twitter unsubscribe error: %s", e) return False