from collections.abc import Iterable from typing import Annotated, Any from anydi.ext.fastapi import Inject from fansifter_common.api.schemas import response_errors from fastapi import APIRouter, Path, Query from starlette import status from starlette.responses import Response from dmp.api import auth from dmp.api.schemas import ( AdReportingConnectionCredentials, LimitOffsetPage, SyncFivetranTablesStateInput, ) from dmp.api.tiktok import schemas from dmp.api.tiktok.schemas import GetTikTokAdAccountsInput from dmp.app_connections.exceptions import AppConnectionNotFoundError from dmp.audiences.exceptions import ( AudienceShareActiveConnectionNotFoundError, AudienceShareError, AudienceTooSmallError, EmptyAudienceError, ) from dmp.tiktok.dtos import AssignTikTokUserAdAccountLabelData from dmp.tiktok.exceptions import ( InvalidTikTokAdAccountId, TikTokAdAccountNotFoundError, TikTokAdReportingConnectionNotFoundError, TikTokAudienceNotFoundError, TikTokAudienceUpdateError, TikTokConnectionNotFoundError, TikTokUserAdAccountNotFoundError, TikTokUserConnectionNotFoundError, TikTokUserConnectionScopesError, ) from dmp.tiktok.handlers import ( AssignLabelsToTikTokUserAdAccountsHandler, AssignLabelsToTikTokUserAdAccountsRequest, ConnectTikTokAdReportingHandler, ConnectTikTokAdReportingRequest, ConnectTikTokUserHandler, ConnectTikTokUserRequest, DeleteTikTokAdAccountsHandler, DeleteTikTokAdAccountsRequest, DeleteTikTokUserAdAccountHandler, DeleteTikTokUserAdAccountRequest, GetTikTokAdAccountsHandler, GetTikTokAdAccountsRequest, GetTikTokAdAccountsV2Handler, GetTikTokAdAccountsV2Request, GetTikTokAdReportingConnectionHandler, GetTikTokAdReportingConnectionRequest, GetTikTokAudienceShareHandler, GetTikTokAudienceShareRequest, GetTikTokAudienceShareStatusHandler, GetTikTokAudienceShareStatusRequest, GetTikTokFivetranConnectionsHandler, GetTikTokUserAdAccountsHandler, GetTikTokUserAdAccountsRequest, GetTikTokUserConnectionHandler, GetTikTokUserConnectionRequest, NotifyTikTokAdReportingSyncCompletedHandler, RefreshTikTokAdReportingConnectionHandler, RefreshTikTokAdReportingConnectionRequest, ShareTikTokAudienceHandler, ShareTikTokAudienceRequest, SyncTikTokFivetranTablesStateHandler, SyncTikTokFivetranTablesStateRequest, UpdateTikTokAudienceHandler, UpdateTikTokAudienceRequest, ) router = APIRouter(prefix="/tiktok", tags=["TikTok"]) @router.post( "/user/connection", operation_id="connectTikTokUser", response_model=schemas.TikTokUserConnectionResponse, responses=response_errors( TikTokUserConnectionScopesError, ), ) def connect_tiktok_user( identity_id: auth.IdentityId, data: schemas.ConnectTikTokUserInput, handler: Annotated[ConnectTikTokUserHandler, Inject()], ) -> Any: return handler.handle( ConnectTikTokUserRequest( identity_id=identity_id, access_token=data.access_token, ) ) @router.get( "/user/connection", operation_id="getTikTokUserConnection", response_model=schemas.TikTokUserConnectionResponse, ) def get_tiktok_user_connection( identity_id: auth.IdentityId, data: Annotated[schemas.TikTokUserInput, Query()], handler: Annotated[GetTikTokUserConnectionHandler, Inject()], ) -> Any: return handler.handle( GetTikTokUserConnectionRequest(identity_id=identity_id, user_id=data.user_id) ) @router.get( "/user/ad-accounts", operation_id="getTikTokUserAdAccounts", response_model=list[schemas.TikTokUserAdAccount], responses=response_errors( TikTokConnectionNotFoundError, ), ) def get_user_ad_accounts( identity_id: auth.IdentityId, data: Annotated[schemas.TikTokUserInput, Query()], handler: Annotated[GetTikTokUserAdAccountsHandler, Inject()], ) -> Any: return handler.handle( GetTikTokUserAdAccountsRequest( identity_id=identity_id, user_id=data.user_id, ) ) @router.post( "/user/ad-accounts", operation_id="assignLabelsToTikTokUserAdAccounts", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def assign_labels_to_user_ad_accounts( identity_id: auth.IdentityId, data: schemas.AssignLabelsToTikTokUserAdAccountsInput, handler: Annotated[AssignLabelsToTikTokUserAdAccountsHandler, Inject()], ) -> None: handler.handle( AssignLabelsToTikTokUserAdAccountsRequest( identity_id=identity_id, assignments=[ AssignTikTokUserAdAccountLabelData( ad_account_id=assignment.ad_account_id, vendor_id=assignment.vendor_id, subaccount_id=assignment.subaccount_id, ) for assignment in data ], ) ) return None @router.post( "/ad-reporting/connection", operation_id="connectTikTokAdReporting", response_model=AdReportingConnectionCredentials, response_model_by_alias=True, ) def connect_tiktok_ad_reporting( identity_id: auth.IdentityId, data: schemas.ConnectTikTokAdReportingInput, handler: Annotated[ConnectTikTokAdReportingHandler, Inject()], ) -> dict[str, Any]: (connection, connect_card) = handler.handle( ConnectTikTokAdReportingRequest( identity_id=identity_id, redirect_uri=str(data.redirect_uri), user_id=str(data.user_id), ) ) return { "connectionId": connection.id, "connectCardUri": connect_card.uri, } @router.get( "/ad-reporting/connection", operation_id="GetTikTokAdReportingConnection", response_model=schemas.TikTokAdReportingConnection, response_model_by_alias=True, ) def get_tiktok_ad_reporting_connection( identity_id: auth.IdentityId, data: Annotated[schemas.TikTokUserInput, Query()], handler: Annotated[GetTikTokAdReportingConnectionHandler, Inject()], ) -> Any: return handler.handle( GetTikTokAdReportingConnectionRequest( identity_id=identity_id, user_id=data.user_id, ) ) @router.put( "/ad-reporting/connection/refresh", operation_id="refreshTikTokAdReportingConnection", tags=["internal"], summary="Refresh TikTok Ad Reporting connection. Internal use only.", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( TikTokAdReportingConnectionNotFoundError, ), ) def refresh_tiktok_ad_reporting_connection( data: schemas.RefreshTikTokAdReportingConnectionInput, handler: Annotated[RefreshTikTokAdReportingConnectionHandler, Inject()], ) -> None: handler.handle( RefreshTikTokAdReportingConnectionRequest( fivetran_connection_id=data.fivetran_connection_id ) ) @router.get( "/ad-accounts", operation_id="getTikTokAdAccounts", response_model=list[schemas.TikTokAdAccount], ) def get_tiktok_ad_accounts( identity_id: auth.IdentityId, handler: Annotated[GetTikTokAdAccountsHandler, Inject()], ) -> Any: return handler.handle(GetTikTokAdAccountsRequest(identity_id=identity_id)) @router.get( "/ad-accounts-v2", operation_id="getTikTokAdAccountsV2", response_model=LimitOffsetPage[schemas.TikTokAdAccount], response_model_by_alias=True, ) def get_tiktok_ad_accounts_v2( identity_id: auth.IdentityId, handler: Annotated[GetTikTokAdAccountsV2Handler, Inject()], data: Annotated[GetTikTokAdAccountsInput, Query()], ) -> Any: limit = data.limit or GetTikTokAdAccountsV2Request.DEFAULT_LIMIT offset = data.offset or GetTikTokAdAccountsV2Request.DEFAULT_OFFSET order_by = data.order_by or GetTikTokAdAccountsV2Request.DEFAULT_ORDER_BY response = handler.handle( GetTikTokAdAccountsV2Request( identity_id=identity_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, limit=limit, offset=offset, order_by=order_by, ), ) return LimitOffsetPage( items=response.items, total=response.total, limit=limit, offset=offset, ) @router.delete( "/ad-accounts/{adAccountId}", operation_id="deleteTikTokAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( TikTokAdAccountNotFoundError, TikTokUserAdAccountNotFoundError, ), ) def delete_tiktok_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], data: Annotated[schemas.DeleteTikTokAdAccountsInput, Query()], handler: Annotated[DeleteTikTokAdAccountsHandler, Inject()], ) -> None: handler.handle( DeleteTikTokAdAccountsRequest( identity_id=identity_id, ad_account_id=ad_account_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, ) ) @router.delete( "/user/ad-accounts/{adAccountId}", operation_id="deleteTikTokUserAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( TikTokAdAccountNotFoundError, ), ) def delete_tiktok_user_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], handler: Annotated[DeleteTikTokUserAdAccountHandler, Inject()], ) -> None: handler.handle( DeleteTikTokUserAdAccountRequest( identity_id=identity_id, ad_account_id=ad_account_id ) ) @router.post( "/ad-reporting/handle-dbt-sync", operation_id="handleTikTokAdReportingDbtSync", tags=["internal"], response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def handle_ad_reporting_dbt_sync( handler: Annotated[NotifyTikTokAdReportingSyncCompletedHandler, Inject()], ) -> None: handler.handle() return None @router.post( "/audiences", operation_id="shareTikTokAudience", response_model=schemas.TikTokAudience, responses=response_errors( EmptyAudienceError, InvalidTikTokAdAccountId, AudienceShareError, AudienceTooSmallError, AudienceShareActiveConnectionNotFoundError, ), status_code=status.HTTP_201_CREATED, ) def share_tiktok_audience( identity_id: auth.IdentityId, data: schemas.ShareTikTokAudienceInput, handler: Annotated[ShareTikTokAudienceHandler, Inject()], ) -> Any: return handler.handle( ShareTikTokAudienceRequest( identity_id=identity_id, audience_id=data.audience_id, ad_account_id=data.ad_account_id, justification=data.justification, reason_notes=data.reason_notes, ) ) @router.get( "/audiences/{tiktokAudienceId}/share-status", operation_id="getTikTokAudienceShareStatus", response_model=schemas.TikTokAudienceShareStatus, responses=response_errors( TikTokAudienceNotFoundError, ), ) def get_tiktok_audience_share_status( identity_id: auth.IdentityId, tiktok_audience_id: Annotated[str, Path(alias="tiktokAudienceId")], handler: Annotated[GetTikTokAudienceShareStatusHandler, Inject()], ) -> Any: return handler.handle( GetTikTokAudienceShareStatusRequest( identity_id=identity_id, tiktok_audience_id=tiktok_audience_id ) ) @router.get( "/audience-shares/{shareId}", tags=["internal"], operation_id="getTikTokAudienceShare", response_model=schemas.TikTokAudienceShare, responses=response_errors( TikTokAudienceNotFoundError, TikTokUserConnectionNotFoundError, ), ) def get_tiktok_audience_share( share_id: Annotated[str, Path(alias="shareId")], handler: Annotated[GetTikTokAudienceShareHandler, Inject()], ) -> Any: return handler.handle(GetTikTokAudienceShareRequest(share_id=share_id)) @router.patch( "/audiences/{audienceId}", tags=["internal"], operation_id="updateTikTokAudience", summary="Update external_id for TikTokAudience. Internal use only.", response_model=schemas.TikTokAudience, responses=response_errors( TikTokAudienceNotFoundError, TikTokAudienceUpdateError, ), ) def update_tiktok_audience_share( audience_id: Annotated[str, Path(alias="audienceId")], data: schemas.UpdateTikTokAudienceInput, handler: Annotated[UpdateTikTokAudienceHandler, Inject()], ) -> Any: return handler.handle( UpdateTikTokAudienceRequest( audience_id=audience_id, external_id=data.external_id, ) ) @router.post( "/sync-fivetran-tables-state", operation_id="syncTikTokFivetranTablesState", summary="Sync TikTok Fivetran tables state for particular connection or all at once. Internal use only.", tags=["internal"], response_model=Iterable[str], responses=response_errors( AppConnectionNotFoundError, ), ) def handle_sync_tiktok_fivetran_tables_state( data: SyncFivetranTablesStateInput, handler: Annotated[SyncTikTokFivetranTablesStateHandler, Inject()], ) -> Any: return handler.handle( SyncTikTokFivetranTablesStateRequest( fivetran_connection_id=data.fivetran_connection_id if data else None, ), ) @router.get( "/fivetran-connectors", operation_id="getTikTokFivetranConnectorsIds", summary="Get all Fivetran connectors ids for TikTok connections. Internal use only.", tags=["internal"], response_model=Iterable[str], ) def handle_get_meta_fivetran_connectors_ids( handler: Annotated[GetTikTokFivetranConnectionsHandler, Inject()], ) -> Any: return handler.handle()