from __future__ import annotations import logging from collections.abc import Iterator from typing import Any from urllib.parse import urljoin from fansifter_common.httpclient.base import HTTPClient from httpx._types import QueryParamTypes # noqa from dmp.core.types import PlainToken from .exceptions import FacebookClientError from .models import ( AccessToken, CustomAudience, DebugToken, DisconnectUserResponse, Response, User, UserAdAccount, ) logger = logging.getLogger(__name__) class FacebookClient(HTTPClient): base_url = "https://graph.facebook.com" graph_api_version = "v25.0" sensitive_params = { "client_secret", "fb_exchange_token", "input_token", "access_token", } DEFAULT_REQUEST_TIMEOUT = 10 exception_class = FacebookClientError def __init__( self, client_id: str, client_secret: str, default_timeout: int | None = None, ) -> None: self.client_id = client_id self.client_secret = client_secret super().__init__( client_options={"timeout": default_timeout or self.DEFAULT_REQUEST_TIMEOUT} ) def build_url(self, path: str) -> str: if not path.startswith("/"): path = f"/{path}" return urljoin(self.base_url, f"{self.graph_api_version}{path}") def get_user(self, user_id: str, user_access_token: PlainToken) -> User: return self.request( "GET", self.build_url(f"/{user_id}"), type=User, params={ "access_token": user_access_token, "fields": ",".join(["id", "name", "picture"]), }, ) def disconnect_user(self, user_id: str, user_access_token: PlainToken) -> bool: response = self.request( "DELETE", self.build_url(f"/{user_id}/permissions"), type=DisconnectUserResponse, params={ "access_token": user_access_token, }, timeout=20, ) return response.success def get_oauth_access_token(self, exchange_token: PlainToken) -> AccessToken: return self.request( "POST", self.build_url("/oauth/access_token"), type=AccessToken, json={ "grant_type": "fb_exchange_token", "client_id": self.client_id, "client_secret": self.client_secret, "fb_exchange_token": exchange_token, }, ) def debug_token(self, access_token: PlainToken) -> DebugToken: response_obj = self.request( "GET", self.build_url("/debug_token"), type=Response[DebugToken], params={ "input_token": access_token, "access_token": f"{self.client_id}|{self.client_secret}", }, ) return response_obj.data def get_user_ad_accounts( self, user_id: str, user_access_token: PlainToken ) -> list[UserAdAccount]: return list(self.iter_user_ad_accounts(user_id, user_access_token)) def iter_user_ad_accounts( self, user_id: str, user_access_token: PlainToken, after: str | None = None ) -> Iterator[UserAdAccount]: params: dict[str, Any] = { "access_token": user_access_token, "fields": ",".join( [ "id", "name", "user_tasks", "business{id,name,picture{url}}", "campaigns.summary(total_count)", "tos_accepted", ] ), } if after: params["after"] = after response_obj = self.request( "GET", self.build_url(f"/{user_id}/adaccounts"), type=Response[list[UserAdAccount]], params=params, ) yield from response_obj.data if response_obj.paging and response_obj.paging.next: yield from self.iter_user_ad_accounts( user_id=user_id, user_access_token=user_access_token, after=response_obj.paging.cursors.after, ) def create_custom_audience( self, user_access_token: str, ad_account_external_id: str, name: str, description: str | None, ) -> CustomAudience: return self.request( "POST", self.build_url(f"/{ad_account_external_id}/customaudiences"), type=CustomAudience, json={ "name": name, "subtype": "CUSTOM", "description": description or "", "customer_file_source": "USER_PROVIDED_ONLY", "access_token": user_access_token, }, )