from dataclasses import dataclass from campaigns.auth.dtos import User from campaigns.connectors.db import Database, session_context from campaigns.connectors.facebook.arguments import CreateEngagementCustomAudienceArgs from campaigns.connectors.facebook.base import FacebookClient from campaigns.connectors.facebook.enums import ( AudienceBooleanLogicalOperator, AudienceEqualityOperator, AudienceEventSourceType, EngagementAudienceEventFilterField, ) from campaigns.connectors.facebook.models import ( EngagementAudienceEventFilter, EngagementAudienceEventSource, EngagementAudienceRule, EngagementAudienceRuleFilter, EngagementAudienceRuleSet, EngagementAudienceRuleSetRule, ) from campaigns.core.handler import Handler from campaigns.meta import models as db_models from campaigns.meta.constants import ENGAGEMENT_AUDIENCE_RETENTION_SECONDS from campaigns.meta.dtos import EngagementAudienceEvent, EngagementAudienceKey from campaigns.meta.enums import CampaignPlatform from campaigns.meta.exceptions import AudienceEventDoesNotExist from campaigns.meta.repositories import ( CampaignRepository, EngagementAudienceEventRepository, EngagementAudienceRepository, ) from campaigns.meta.services import CampaignService @dataclass(frozen=True) class CreateCampaignEngagementAudienceRequest: user: User campaign_id: str event_ids: list[str] @dataclass(frozen=True) class CreateCampaignEngagementAudienceResponse: id: str lower_bound: int upper_bound: int event_ids: list[str] @dataclass class CreateCampaignEngagementAudienceHandler( Handler[ CreateCampaignEngagementAudienceRequest, CreateCampaignEngagementAudienceResponse, ], ): db: Database campaign_repository: CampaignRepository campaign_service: CampaignService facebook_client: FacebookClient engagement_audience_event_repository: EngagementAudienceEventRepository engagement_audience_repository: EngagementAudienceRepository @session_context(in_transaction=True) async def handle( self, request: CreateCampaignEngagementAudienceRequest ) -> CreateCampaignEngagementAudienceResponse: campaign = await self.campaign_service.get_user_campaign( user=request.user, campaign_id=request.campaign_id ) audience_events = self.engagement_audience_event_repository.find_by_ids( ids=request.event_ids ) if not audience_events or len(audience_events) < len(request.event_ids): # Some or all ids provided by front end does not match with any ids saved raise AudienceEventDoesNotExist event_ids = [event.id for event in audience_events] facebook_filtered_rules = self._generate_rule_filters( rules=[ event for event in audience_events if event.platform == CampaignPlatform.FACEBOOK ] ) instagram_filtered_rules = self._generate_rule_filters( rules=[ event for event in audience_events if event.platform == CampaignPlatform.INSTAGRAM ] ) if not facebook_filtered_rules and not instagram_filtered_rules: raise AudienceEventDoesNotExist rules = [] if facebook_filtered_rules: filters = [ EngagementAudienceEventFilter( field=EngagementAudienceEventFilterField.EVENT, operator=AudienceEqualityOperator.EQUALS, value=rule.value, ) for rule in facebook_filtered_rules ] event_sources = [ EngagementAudienceEventSource( id=campaign.facebook_page.external_id, type=AudienceEventSourceType.PAGE, ) ] rules.append( EngagementAudienceRuleSetRule( event_sources=event_sources, retention_seconds=ENGAGEMENT_AUDIENCE_RETENTION_SECONDS, filter=EngagementAudienceRuleFilter( operator=AudienceBooleanLogicalOperator.OR, filters=filters, ), ) ) facebook_page_id = campaign.facebook_page.external_id else: facebook_page_id = None if instagram_filtered_rules and campaign.instagram_account_external_id: filters = [ EngagementAudienceEventFilter( field=EngagementAudienceEventFilterField.EVENT, operator=AudienceEqualityOperator.EQUALS, value=rule.value, ) for rule in instagram_filtered_rules ] event_sources = [ EngagementAudienceEventSource( id=campaign.instagram_account_external_id, type=AudienceEventSourceType.IG_BUSINESS, ) ] rules.append( EngagementAudienceRuleSetRule( event_sources=event_sources, retention_seconds=ENGAGEMENT_AUDIENCE_RETENTION_SECONDS, filter=EngagementAudienceRuleFilter( operator=AudienceBooleanLogicalOperator.OR, filters=filters, ), ) ) instagram_account_id = campaign.instagram_account_external_id else: instagram_account_id = None existing_engagement_audience = ( await self.engagement_audience_repository.get_by_audience_key( EngagementAudienceKey( facebook_page_external_id=facebook_page_id, instagram_account_external_id=instagram_account_id, event_ids=event_ids, ) ) ) if existing_engagement_audience: audience = await self.facebook_client.get_engagement_audience( audience_external_id=existing_engagement_audience.external_id, ) campaign.engagement_audience_id = existing_engagement_audience.id self.campaign_repository.add(campaign) return CreateCampaignEngagementAudienceResponse( id=existing_engagement_audience.id, upper_bound=audience.approximate_count_upper_bound, lower_bound=audience.approximate_count_lower_bound, event_ids=event_ids, ) rule = EngagementAudienceRule( inclusions=EngagementAudienceRuleSet( operator=AudienceBooleanLogicalOperator.OR, rules=rules, ) ) audience_name = self._generate_audience_name( facebook_page_id=facebook_page_id, instagram_account_id=instagram_account_id, event_ids=event_ids, ) audience = await self.facebook_client.create_engagement_audience( args=CreateEngagementCustomAudienceArgs( name=audience_name, rule=rule, ), ) engagement_audience = db_models.EngagementAudience( external_id=audience.id, event_ids=event_ids, facebook_page_external_id=facebook_page_id, instagram_account_external_id=instagram_account_id, ) self.engagement_audience_repository.add(engagement_audience) campaign.engagement_audience_id = engagement_audience.id self.campaign_repository.add(campaign) return CreateCampaignEngagementAudienceResponse( id=engagement_audience.id, upper_bound=audience.approximate_count_upper_bound, lower_bound=audience.approximate_count_lower_bound, event_ids=event_ids, ) @staticmethod def _generate_rule_filters( rules: list[EngagementAudienceEvent], ) -> list[EngagementAudienceEventFilter]: return [ EngagementAudienceEventFilter( field=EngagementAudienceEventFilterField.EVENT, operator=AudienceEqualityOperator.EQUALS, value=rule.value, ) for rule in rules ] @staticmethod def _generate_audience_name( facebook_page_id: str | None, instagram_account_id: str | None, event_ids: list[str], ) -> str: return ( f"fb: {facebook_page_id or 0} ig: {instagram_account_id or 0} " f"{'|'.join(sorted(event_ids))}" )