160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
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
|
|
}
|