feat(channels): align whatsapp calling toggles
This commit is contained in:
@@ -48,11 +48,15 @@ func NewWhatsAppService(repository *Repository) *WhatsAppService {
|
||||
if apiVersion == "" {
|
||||
apiVersion = "v22.0"
|
||||
}
|
||||
cloudBase := os.Getenv("WHATSAPP_CLOUD_BASE_URL")
|
||||
if cloudBase == "" {
|
||||
cloudBase = "https://graph.facebook.com"
|
||||
}
|
||||
|
||||
return &WhatsAppService{
|
||||
client: client,
|
||||
repository: repository,
|
||||
graphAPIBase: "https://graph.facebook.com/" + apiVersion,
|
||||
graphAPIBase: fmt.Sprintf("%s/%s", cloudBase, apiVersion),
|
||||
dialogAPIBase: "https://waba.360dialog.io",
|
||||
}
|
||||
}
|
||||
@@ -400,6 +404,12 @@ func buildExpectedWhatsAppWebhookURL(phoneNumber string) string {
|
||||
// Cloud API: POST /v18.0/{business_account_id}/subscribed_apps
|
||||
// 360dialog: Webhook is configured via 360dialog Hub dashboard
|
||||
func (s *WhatsAppService) SetupWebhook(ctx context.Context, channel *channelmodel.ChannelWhatsApp, webhookURL string) error {
|
||||
return s.SetupWebhookFields(ctx, channel, webhookURL, nil)
|
||||
}
|
||||
|
||||
// SetupWebhookFields subscribes the app and overrides the callback URL/fields for a WABA.
|
||||
// Reference: Whatsapp::FacebookApiClient#subscribe_waba_webhook.
|
||||
func (s *WhatsAppService) SetupWebhookFields(ctx context.Context, channel *channelmodel.ChannelWhatsApp, webhookURL string, fields []string) error {
|
||||
if channel.IsCloudAPI() {
|
||||
url := fmt.Sprintf("%s/%s/subscribed_apps", s.graphAPIBase, channel.BusinessAccountID)
|
||||
resp, err := s.client.R().
|
||||
@@ -409,9 +419,29 @@ func (s *WhatsAppService) SetupWebhook(ctx context.Context, channel *channelmode
|
||||
if err != nil {
|
||||
return fmt.Errorf("WhatsApp Cloud API webhook subscription failed: %w", err)
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
if resp.StatusCode() < 200 || resp.StatusCode() >= 300 {
|
||||
return fmt.Errorf("WhatsApp Cloud API webhook subscription returned status %d", resp.StatusCode())
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
fields = []string{"messages", "smb_message_echoes", "calls"}
|
||||
}
|
||||
body := map[string]any{
|
||||
"override_callback_uri": webhookURL,
|
||||
"verify_token": channel.WebhookVerifyToken,
|
||||
"subscribed_fields": fields,
|
||||
}
|
||||
resp, err = s.client.R().
|
||||
SetContext(ctx).
|
||||
SetAuthToken(channel.AccessToken).
|
||||
SetBody(body).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WhatsApp Cloud API webhook callback override failed: %w", err)
|
||||
}
|
||||
if resp.StatusCode() < 200 || resp.StatusCode() >= 300 {
|
||||
return fmt.Errorf("WhatsApp Cloud API webhook callback override returned status %d", resp.StatusCode())
|
||||
}
|
||||
applogger.L().Info("WhatsApp Cloud API webhook subscription created",
|
||||
"business_account_id", channel.BusinessAccountID,
|
||||
)
|
||||
@@ -425,6 +455,45 @@ func (s *WhatsAppService) SetupWebhook(ctx context.Context, channel *channelmode
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCallingStatus enables or disables Meta's WhatsApp Calling setting for a phone number.
|
||||
// Reference: Enterprise::Whatsapp::Providers::WhatsappCloudService#update_calling_status.
|
||||
func (s *WhatsAppService) UpdateCallingStatus(ctx context.Context, channel *channelmodel.ChannelWhatsApp, status string) error {
|
||||
if !channel.IsCloudAPI() {
|
||||
return fmt.Errorf("WhatsApp calling requires a whatsapp_cloud inbox")
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/settings", s.graphAPIBase, channel.PhoneNumberID)
|
||||
resp, err := s.client.R().
|
||||
SetContext(ctx).
|
||||
SetAuthToken(channel.AccessToken).
|
||||
SetBody(map[string]any{"calling": map[string]any{"status": status}}).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WhatsApp calling status update failed: %w", err)
|
||||
}
|
||||
if resp.StatusCode() >= 200 && resp.StatusCode() < 300 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s", extractWhatsAppCallingError(resp.Body()))
|
||||
}
|
||||
|
||||
func extractWhatsAppCallingError(body []byte) string {
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return "Failed to update calling status"
|
||||
}
|
||||
errorValue, ok := parsed["error"].(map[string]any)
|
||||
if !ok {
|
||||
return "Failed to update calling status"
|
||||
}
|
||||
if value, ok := errorValue["error_user_msg"].(string); ok && value != "" {
|
||||
return value
|
||||
}
|
||||
if value, ok := errorValue["message"].(string); ok && value != "" {
|
||||
return value
|
||||
}
|
||||
return "Failed to update calling status"
|
||||
}
|
||||
|
||||
// === Internal Helper Methods ===
|
||||
|
||||
// validateAccessToken validates the WhatsApp access token by making a test API call.
|
||||
|
||||
@@ -45,6 +45,37 @@ func TestWhatsAppService_FetchHealthStatusFormatsChatwootPayload(t *testing.T) {
|
||||
assert.NotContains(t, payload, "healthy")
|
||||
}
|
||||
|
||||
func TestWhatsAppService_UpdateCallingStatusPostsSettings(t *testing.T) {
|
||||
svc := NewWhatsAppService(nil)
|
||||
svc.graphAPIBase = "https://graph.example.test/v22.0"
|
||||
svc.client.SetTransport(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
require.Equal(t, "/v22.0/phone-123/settings", r.URL.Path)
|
||||
assert.Equal(t, "Bearer token-abc", r.Header.Get("Authorization"))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"calling":{"status":"ENABLED"}}`, string(body))
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{"success":true}`))}, nil
|
||||
}))
|
||||
channel := &channelmodel.ChannelWhatsApp{Provider: "whatsapp_cloud", PhoneNumberID: "phone-123", AccessToken: "token-abc"}
|
||||
|
||||
err := svc.UpdateCallingStatus(context.Background(), channel, "ENABLED")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWhatsAppService_UpdateCallingStatusUsesMetaErrorMessage(t *testing.T) {
|
||||
svc := NewWhatsAppService(nil)
|
||||
svc.graphAPIBase = "https://graph.example.test/v22.0"
|
||||
svc.client.SetTransport(roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
body := `{"error":{"error_user_msg":"Calling is not available for this number","message":"fallback"}}`
|
||||
return &http.Response{StatusCode: http.StatusBadRequest, Body: io.NopCloser(bytes.NewBufferString(body))}, nil
|
||||
}))
|
||||
channel := &channelmodel.ChannelWhatsApp{Provider: "whatsapp_cloud", PhoneNumberID: "phone-123", AccessToken: "token-abc"}
|
||||
|
||||
err := svc.UpdateCallingStatus(context.Background(), channel, "ENABLED")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "Calling is not available for this number", err.Error())
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
Reference in New Issue
Block a user