package twilio // TwilioService provides high-level operations for the Twilio SMS channel. // Reference: Twilio SMS API — https://www.twilio.com/docs/sms // // This service coordinates: // - Sending SMS/MMS messages via Twilio REST API // - Fetching phone number information // - Validating account credentials // - Processing delivery status callbacks // - Managing channel configuration // // Design: Follows the same service pattern as LineService and WhatsAppService. import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/go-resty/resty/v2" applogger "github.com/gochat/gochat/pkg/logger" ) // TwilioService provides Twilio SMS API operations. type TwilioService struct { repo *Repository client *resty.Client } // NewTwilioService creates a Twilio SMS service instance. func NewTwilioService(repo *Repository) *TwilioService { client := resty.New() client.SetTimeout(30 * time.Second) return &TwilioService{ repo: repo, client: client, } } // === Message Sending === // SendMessage sends an SMS or MMS message via Twilio REST API. // Reference: https://www.twilio.com/docs/sms/api/message-resource func (s *TwilioService) SendMessage(ctx context.Context, accountSID, authToken, from, to, body string, mediaURLs []string) (*OutboundSMSResponse, error) { apiURL := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", accountSID) formData := map[string]string{ "From": from, "To": to, "Body": body, } // Add media URLs for MMS for i, mediaURL := range mediaURLs { formData[fmt.Sprintf("MediaUrl%d", i)] = mediaURL } var result OutboundSMSResponse resp, err := s.client.R(). SetContext(ctx). SetBasicAuth(accountSID, authToken). SetFormData(formData). SetResult(&result). Post(apiURL) if err != nil { return nil, fmt.Errorf("twilio SendMessage: API call failed: %w", err) } if resp.StatusCode() != 201 { // Parse Twilio error response var errResp map[string]interface{} if json.Unmarshal(resp.Body(), &errResp) == nil { msg, _ := errResp["message"].(string) code, _ := errResp["code"].(float64) return nil, fmt.Errorf("twilio SendMessage: API error %d: %s", int(code), msg) } return nil, fmt.Errorf("twilio SendMessage: API returned status %d", resp.StatusCode()) } applogger.L().Debugf("Twilio SendMessage: SID=%s to=%s", result.SID, to) return &result, nil } // === Phone Number Info === // GetPhoneNumberInfo fetches details about a Twilio phone number. func (s *TwilioService) GetPhoneNumberInfo(ctx context.Context, accountSID, authToken, phoneSID string) (*PhoneNumberInfo, error) { apiURL := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/IncomingPhoneNumbers/%s.json", accountSID, phoneSID) var result PhoneNumberInfo resp, err := s.client.R(). SetContext(ctx). SetBasicAuth(accountSID, authToken). SetResult(&result). Get(apiURL) if err != nil { return nil, fmt.Errorf("twilio GetPhoneNumberInfo: API call failed: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("twilio GetPhoneNumberInfo: API returned status %d", resp.StatusCode()) } return &result, nil } // === Credential Validation === // ValidateCredentials verifies Twilio account SID and auth token. func (s *TwilioService) ValidateCredentials(ctx context.Context, accountSID, authToken string) error { apiURL := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s.json", accountSID) resp, err := s.client.R(). SetContext(ctx). SetBasicAuth(accountSID, authToken). Get(apiURL) if err != nil { return fmt.Errorf("twilio ValidateCredentials: API call failed: %w", err) } if resp.StatusCode() != 200 { return fmt.Errorf("twilio ValidateCredentials: invalid credentials (status %d)", resp.StatusCode()) } return nil } // === Configuration === // UpdateChannel updates Twilio SMS channel configuration fields. func (s *TwilioService) UpdateChannel(ctx context.Context, channelID uint, updates map[string]interface{}) error { return s.repo.UpdateFields(ctx, channelID, updates) } // MarkReauthorizationRequired flags a channel needing credential reconfiguration. func (s *TwilioService) MarkReauthorizationRequired(ctx context.Context, channelID uint) error { return s.UpdateChannel(ctx, channelID, map[string]interface{}{ "reauthorization_required": true, }) } // ParsePhoneNumber normalizes a phone number to E.164 format. func (s *TwilioService) ParsePhoneNumber(phone string) string { // Strip all non-digit characters phone = strings.ReplaceAll(phone, "-", "") phone = strings.ReplaceAll(phone, " ", "") phone = strings.ReplaceAll(phone, "+", "") phone = strings.ReplaceAll(phone, ".", "") phone = strings.ReplaceAll(phone, "(", "") phone = strings.ReplaceAll(phone, ")", "") // Add + prefix for E.164 if !strings.HasPrefix(phone, "+") { phone = "+" + phone } return phone }