import logging import urllib.parse from dataclasses import dataclass from typing import Any import phonenumbers from anydi import singleton from fansifter_common.adapters.twilio.client import TwilioClient from fansifter_common.adapters.twilio.models import SendMessageRequest from fansifter_common.auth.requests import AuthRequest from fansifter_common.auth.services import AuthService from fansifter_common.auth.types import Permission from ows_text_campaigns.adapters.ows_url_shortener import OwsUrlShortenerClient from ows_text_campaigns.artist.exceptions import ( ArtistTwilioAccountNotConfiguredError, ) from ows_text_campaigns.artist.services import ArtistSettingsService from ows_text_campaigns.artist.types import PhoneNumberStr from ows_text_campaigns.campaigns.enums import MessageChannel from ows_text_campaigns.campaigns.exceptions import ( EmptyCampaignContentError, ) from ows_text_campaigns.campaigns.models import Campaign from ows_text_campaigns.campaigns.services import CampaignService from ows_text_campaigns.config import Settings from ows_text_campaigns.twilio.exceptions import TwilioAccountNotFoundError from ows_text_campaigns.twilio.services import TwilioAccountService logger = logging.getLogger(__name__) @dataclass(kw_only=True) class SendCampaignTestMessageRequest(AuthRequest): campaign_id: str phone_number: PhoneNumberStr @singleton class SendCampaignTestMessageHandler: permission = Permission("text_campaign", "view") def __init__( self, auth_service: AuthService, artist_settings_service: ArtistSettingsService, campaign_service: CampaignService, ows_url_shortener_client: OwsUrlShortenerClient, twilio_client: TwilioClient, twilio_account_service: TwilioAccountService, settings: Settings, ) -> None: self.auth_service = auth_service self.artist_settings_service = artist_settings_service self.campaign_service = campaign_service self.ows_url_shortener_client = ows_url_shortener_client self.twilio_client = twilio_client self.twilio_account_service = twilio_account_service self.settings = settings def handle(self, request: SendCampaignTestMessageRequest) -> None: campaign = self.campaign_service.get_campaign(request.campaign_id) # Check if user has permission to edit campaign self.auth_service.check_account_resource( request.identity_id, account=campaign.account, permission=self.permission, resource_id=campaign.id, ) if not campaign.get_content(): raise EmptyCampaignContentError # Get artist settings artist_settings = self.artist_settings_service.get_configured_settings( campaign.global_participant_id ) # Ensure that Twilio account is configured if ( not artist_settings.twilio_account_sid or not artist_settings.twilio_messaging_service_sid ): raise ArtistTwilioAccountNotConfiguredError try: api_key_id, api_secret = self.twilio_account_service.get_api_key( artist_settings.twilio_account_sid ) except TwilioAccountNotFoundError: raise ArtistTwilioAccountNotConfiguredError from None # Render message content message = self.campaign_service.render_message_for_channel( content=campaign.get_content(), channel=MessageChannel.SMS, shortened_urls=self._get_shortened_urls(campaign), ) # Determine the recipient phone number recipient = request.phone_number recipient_obj = phonenumbers.parse(recipient) recipient_country_code = phonenumbers.region_code_for_number(recipient_obj) # Determine the sender phone number based on the country code sender = artist_settings.get_send_phone_number(recipient_country_code) # Check if the country code is supported for MMS if recipient_country_code in self.settings.mms_send_supported_countries: # Get media URL for the campaign media_url = campaign.get_media_url() else: media_url = None # Set status callback url with query parameters query_string = urllib.parse.urlencode( sorted( [ ("campaign_id", campaign.id), ("country_code", recipient_country_code or "XX"), ("is_test", True), ] ) ) status_callback = f"{self.settings.twilio_status_callback_url}?{query_string}" # Send test message response = self.twilio_client.send_message( SendMessageRequest( to=recipient, from_=sender, body=message, media_url=media_url, messaging_service_sid=artist_settings.twilio_messaging_service_sid, account_sid=artist_settings.twilio_account_sid, status_callback=status_callback, ), auth=(api_key_id, api_secret), ) logger.info( "Test message sent successfully to %s from %s, SID: %s", recipient, sender, response.sid, extra={ "campaign_id": campaign.id, "recipient": recipient, "sender": sender, "sid": response.sid, "account_sid": artist_settings.twilio_account_sid, }, ) def _get_shortened_urls(self, campaign: Campaign) -> dict[str, Any]: """Prepare a dictionary of shortened URLs for all users.""" shortened_urls: dict[str, str] = {} for shortened_url in campaign.shortened_urls: if shortened_url.is_personalized: result = self.ows_url_shortener_client.shorten_url( url=shortened_url.url, domain=shortened_url.domain, path_prefix=shortened_url.path, additional_attributes={ "campaign_id": campaign.id, "shortened_url_id": shortened_url.id, "is_test": True, }, ) shortened_urls[shortened_url.id] = result.short_url else: shortened_urls[shortened_url.id] = shortened_url.preview_url return shortened_urls