from dataclasses import dataclass from anydi import singleton from fansifter_common.auth.requests import AuthRequest from fansifter_common.auth.services import AuthService from fansifter_common.auth.types import Permission from dmp.adapters.db import DefaultDB, ReportingDB, transactional from dmp.adapters.features import Features from dmp.adapters.meta.client import FacebookClient from dmp.adapters.meta.exceptions import FacebookClientError from dmp.audiences.enums import AudienceExportJustification, AudienceSharePlatform from dmp.audiences.exceptions import ( AudienceShareActiveConnectionNotFoundError, EmptyAudienceError, ) from dmp.audiences.models import AudienceShare from dmp.audiences.repositories import AudienceShareRepository from dmp.audiences.services import AudienceService from dmp.core.types import PlainToken from dmp.meta.exceptions import ( InvalidMetaAdAccountId, MetaAdAccountNotFoundError, MetaAdAccountPermissionsError, MetaAdAccountTermsOfServiceError, ) from dmp.meta.models import MetaAudience, MetaUserConnection from dmp.meta.repositories import MetaAudienceRepository from dmp.meta.services import MetaAdAccountService, MetaUserConnectionService @dataclass class ShareMetaAudienceRequest(AuthRequest): audience_id: str ad_account_id: str justification: AudienceExportJustification reason_notes: str | None @singleton class ShareMetaAudienceHandler: permission = Permission("audience", "share_ad_target") ad_account_permission = Permission("ad_account", "view") audience_name_prefix = "[FS]" def __init__( self, db: DefaultDB, reporting_db: ReportingDB, facebook_client: FacebookClient, auth_service: AuthService, audience_service: AudienceService, audience_share_repository: AudienceShareRepository, meta_audience_repository: MetaAudienceRepository, ad_account_service: MetaAdAccountService, user_connection_service: MetaUserConnectionService, features: Features, ) -> None: self.db = db self.reporting_db = reporting_db self.facebook_client = facebook_client self.auth_service = auth_service self.audience_service = audience_service self.audience_share_repository = audience_share_repository self.meta_audience_repository = meta_audience_repository self.ad_account_service = ad_account_service self.user_connection_service = user_connection_service self.features = features @transactional( ignore=( MetaAdAccountPermissionsError, MetaAdAccountTermsOfServiceError, AudienceShareActiveConnectionNotFoundError, ), ) def handle(self, request: ShareMetaAudienceRequest) -> MetaAudience: audience = self.audience_service.get_audience(request.audience_id) self.auth_service.check_account_resource( request.identity_id, account=audience.account, permission=self.permission, resource_id=audience.id, ) latest_snapshot = audience.latest_snapshot custom_audience_name = f"{self.audience_name_prefix} {audience.name}" if latest_snapshot is None or not latest_snapshot.fan_count: raise EmptyAudienceError try: ad_account = self.ad_account_service.get_ad_account(request.ad_account_id) except MetaAdAccountNotFoundError as exc: raise InvalidMetaAdAccountId from exc if self.features.is_on_for_identity( "audience_allow_audience_sharing_to_all_ad_accounts", identity_id=request.identity_id, ): account_access = self.auth_service.authorize_for_permission( request.identity_id, permission=self.ad_account_permission ) user_connection = ( self.user_connection_service.get_active_audience_sharing_connection( audience_id=audience.id, ad_account_id=ad_account.id, identity_id=request.identity_id, vendor_ids=account_access.vendor_ids, subaccount_ids=account_access.subaccount_ids, ) ) else: user_connection = ( self.user_connection_service.get_active_audience_sharing_connection( audience_id=audience.id, ad_account_id=ad_account.id, identity_id=request.identity_id, ) ) if not user_connection: raise AudienceShareActiveConnectionNotFoundError user_access_token = self.user_connection_service.decrypt_token(user_connection) try: custom_audience = self.facebook_client.create_custom_audience( user_access_token, ad_account_external_id=ad_account.external_id, name=custom_audience_name, description="", ) except FacebookClientError as exc: if exc.is_terms_of_service_error: raise MetaAdAccountTermsOfServiceError( ad_account_external_id=ad_account.external_id ) from exc if exc.is_permissions_error: if user_connection.identity_id == request.identity_id: self._refresh_user_connection_ad_accounts( user_connection, user_access_token=user_access_token ) else: self._refresh_user_connection_ad_accounts( request.identity_id, user_access_token=None ) raise MetaAdAccountPermissionsError from exc raise exc share = AudienceShare( audience=audience, snapshot=latest_snapshot, platform=AudienceSharePlatform.META, justification=request.justification, reason_notes=request.reason_notes, created_by=request.identity_id, ) meta_audience = MetaAudience( name=custom_audience_name, external_id=custom_audience.id, ad_account=ad_account, share=share, ) self.audience_share_repository.add(share) self.meta_audience_repository.add(meta_audience) self.audience_service.export_audience_share_to_csv(share) return meta_audience def _refresh_user_connection_ad_accounts( self, user_connection_or_identity_id: MetaUserConnection | str, user_access_token: PlainToken | None, ) -> None: if isinstance(user_connection_or_identity_id, str): user_connection = self.user_connection_service.get_user_connection( user_connection_or_identity_id ) if user_connection is None: return else: user_connection = user_connection_or_identity_id if user_access_token is None: user_access_token = self.user_connection_service.decrypt_token( user_connection ) api_ad_accounts = self.facebook_client.get_user_ad_accounts( user_connection.user_id, user_access_token=user_access_token, ) ad_accounts = self.ad_account_service.get_ad_accounts_for_connection( user_connection, external_ad_accounts=api_ad_accounts, ) user_connection.ad_accounts = ad_accounts self.user_connection_service.save_user_connection(user_connection)