import urllib.parse from collections.abc import Mapping from dataclasses import dataclass, field from typing import Annotated, Any, Protocol, Self from fansifter_common.auth.account import Account from pydantic import BaseModel, Field, model_validator from pydantic_core.core_schema import ValidationInfo from dmp.app_connections.enums import AppConnectionStatus from dmp.meta.enums import MetaUserConnectionStatus from dmp.meta.models import ( MetaUserConnectionAdAccount as MetaUserConnectionAdAccountModel, ) @dataclass(kw_only=True) class MetaUser: id: str name: str picture: str | None = None granted_scopes: list[str] def scopes_sufficient(self, scopes: list[str], /) -> bool: return set(self.granted_scopes).issuperset(scopes) @dataclass(kw_only=True) class MetaUserConnectionAdAccount: id: str external_id: str name: str business_account_name: str | None business_account_picture: str | None custom_audience_tos_accepted: bool custom_audience_tos_url: str = field(init=False) def __post_init__(self) -> None: self.custom_audience_tos_url = ( f"https://business.facebook.com/ads/manage/customaudiences/tos/" f"?act={self.external_id.removeprefix('act_')}" ) @classmethod def from_model( cls, obj: MetaUserConnectionAdAccountModel, ad_account_id: str ) -> Self: return cls( id=ad_account_id, external_id=obj.ad_account.external_id, name=obj.ad_account.name or obj.ad_account.external_id, business_account_name=obj.ad_account.business_account_name, business_account_picture=obj.ad_account.business_account_picture, custom_audience_tos_accepted=obj.custom_audience_tos_accepted, ) @dataclass(kw_only=True) class MetaUserConnectionResult: user: MetaUser | None status: MetaUserConnectionStatus required_scopes: list[str] ad_accounts: list[MetaUserConnectionAdAccount] = field(default_factory=list) ad_accounts_count: int = field(init=False) def __post_init__(self) -> None: self.ad_accounts_count = len(self.ad_accounts) if not self.all_custom_audience_tos_accepted: self.status = MetaUserConnectionStatus.TOS_NOT_ACCEPTED @property def is_connected(self) -> bool: return self.status == MetaUserConnectionStatus.CONNECTED @property def all_custom_audience_tos_accepted(self) -> bool: return all( ad_account.custom_audience_tos_accepted for ad_account in self.ad_accounts ) class BusinessAdAccount(Protocol): external_id: str name: str business_account_name: str | None campaigns_count: int class MetaUserAdAccountLabel(BaseModel): id: str external_id: str name: str business_account_name: str | None campaigns_count: int vendor_id: int | None subaccount_id: int | None business_account_picture: Annotated[str | None, Field(validate_default=True)] = None @model_validator(mode="before") def setup_fields( cls, values: Mapping[str, Any], info: ValidationInfo, ) -> dict[str, Any]: values = dict(values) if context := info.context: assets_cdn_domain = context.get("assets_cdn_domain") values["business_account_picture"] = cls.get_cdn_url_for_s3_path( assets_cdn_domain, values.get("business_account_picture_key") ) return values @classmethod def get_cdn_url_for_s3_path(cls, cdn_domain: str, path: str | None) -> str | None: if not path: return None return urllib.parse.urljoin( f"https://{cdn_domain}/", urllib.parse.quote_plus(path, safe="/"), ) class MetaAudienceAdAccount(BaseModel): id: str name: str external_id: str is_connected: bool is_self_connected: bool custom_audience_tos_accepted: bool custom_audience_tos_url: str | None = None def model_post_init(self, __context: Any) -> None: self.custom_audience_tos_url = ( f"https://business.facebook.com/ads/manage/customaudiences/tos/" f"?act={self.external_id.removeprefix('act_')}" ) @dataclass class AssignMetaUserAdAccountLabelData: ad_account_id: str vendor_id: int subaccount_id: int @property def account(self) -> Account: return Account(vendor_id=self.vendor_id, subaccount_id=self.subaccount_id) class MetaAdAccountOverview(BaseModel): id: str user_ad_account_id: str | None external_id: str name: str business_account_name: str | None vendor_id: int | None subaccount_id: int | None is_self_connected: bool user_connection_status: MetaUserConnectionStatus user_self_connected: bool ad_reporting_connection_status: AppConnectionStatus ad_reporting_self_connected: bool campaigns_count: int business_account_picture: Annotated[str | None, Field(validate_default=True)] = None @model_validator(mode="before") def setup_fields( cls, values: Mapping[str, Any], info: ValidationInfo, ) -> dict[str, Any]: values = dict(values) if context := info.context: assets_cdn_domain = context.get("assets_cdn_domain") values["business_account_picture"] = cls.get_cdn_url_for_s3_path( assets_cdn_domain, values.get("business_account_picture_key") ) return values @classmethod def get_cdn_url_for_s3_path(cls, cdn_domain: str, path: str | None) -> str | None: if not path: return None return urllib.parse.urljoin( f"https://{cdn_domain}/", urllib.parse.quote_plus(path, safe="/"), )