557 lines
19 KiB
Go
557 lines
19 KiB
Go
package whatsapp
|
|
|
|
// WhatsAppService provides high-level operations for the WhatsApp channel.
|
|
// Reference: Chatwoot app/services/whatsapp_service.rb — wraps channel model methods
|
|
//
|
|
// This service coordinates between:
|
|
// - ChannelWhatsApp model (CRUD)
|
|
// - WhatsAppProvider (Cloud API / 360dialog API calls)
|
|
// - IncomingMessageProcessor (pipeline integration)
|
|
// - OutgoingMessageProcessor (pipeline integration)
|
|
//
|
|
// Design: Service pattern follows Chatwoot's service layer where each channel
|
|
// has a service module that wraps channel-specific business logic.
|
|
//
|
|
// WhatsApp Cloud API: https://developers.facebook.com/docs/whatsapp/cloud-api
|
|
// 360dialog API: https://docs.360dialog.com/docs/whatsapp-api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// WhatsAppService handles WhatsApp channel business logic.
|
|
type WhatsAppService struct {
|
|
client *resty.Client
|
|
repository *Repository
|
|
graphAPIBase string // Cloud API base URL (e.g., "https://graph.facebook.com/v18.0")
|
|
dialogAPIBase string // 360dialog API base URL (e.g., "https://waba.360dialog.io")
|
|
}
|
|
|
|
// NewWhatsAppService creates a new WhatsApp service.
|
|
func NewWhatsAppService(repository *Repository) *WhatsAppService {
|
|
client := resty.New()
|
|
client.SetTimeout(30 * time.Second)
|
|
client.SetRetryCount(3)
|
|
client.SetRetryWaitTime(1 * time.Second)
|
|
client.SetRetryMaxWaitTime(5 * time.Second)
|
|
|
|
apiVersion := os.Getenv("WHATSAPP_API_VERSION")
|
|
if apiVersion == "" {
|
|
apiVersion = "v22.0"
|
|
}
|
|
|
|
return &WhatsAppService{
|
|
client: client,
|
|
repository: repository,
|
|
graphAPIBase: "https://graph.facebook.com/" + apiVersion,
|
|
dialogAPIBase: "https://waba.360dialog.io",
|
|
}
|
|
}
|
|
|
|
// === CRUD Operations ===
|
|
|
|
// CreateChannel creates a new WhatsApp channel.
|
|
// Reference: Chatwoot WhatsApp channel creation flow:
|
|
//
|
|
// validate phone_number_id → create ChannelWhatsApp → setup webhook → create Inbox
|
|
//
|
|
// Flow:
|
|
// 1. Validate provider (whatsapp_cloud or 360dialog)
|
|
// 2. Validate access token via provider API
|
|
// 3. Create ChannelWhatsApp record in DB
|
|
// 4. Setup webhook subscription (provider-specific)
|
|
func (s *WhatsAppService) CreateChannel(ctx context.Context, accountID uint, params map[string]interface{}) (*channelmodel.ChannelWhatsApp, error) {
|
|
phoneNumber, _ := params["phone_number"].(string)
|
|
phoneNumberID, _ := params["phone_number_id"].(string)
|
|
businessAccountID, _ := params["business_account_id"].(string)
|
|
accessToken, _ := params["access_token"].(string)
|
|
provider, _ := params["provider"].(string)
|
|
webhookVerifyToken, _ := params["webhook_verify_token"].(string)
|
|
|
|
// Validate required fields
|
|
if phoneNumber == "" {
|
|
return nil, fmt.Errorf("phone_number is required")
|
|
}
|
|
if phoneNumberID == "" {
|
|
return nil, fmt.Errorf("phone_number_id is required")
|
|
}
|
|
if accessToken == "" {
|
|
return nil, fmt.Errorf("access_token is required")
|
|
}
|
|
|
|
// Validate provider choice
|
|
if provider == "" {
|
|
provider = "whatsapp_cloud" // default to Cloud API
|
|
}
|
|
if provider != "whatsapp_cloud" && provider != "360dialog" {
|
|
return nil, fmt.Errorf("provider must be 'whatsapp_cloud' or '360dialog'")
|
|
}
|
|
|
|
// Validate access token via provider API
|
|
if err := s.validateAccessToken(ctx, provider, accessToken, phoneNumberID); err != nil {
|
|
return nil, fmt.Errorf("access token validation failed: %w", err)
|
|
}
|
|
|
|
// Get account name from provider
|
|
accountName := s.fetchAccountName(ctx, provider, accessToken, phoneNumberID)
|
|
|
|
channel := &channelmodel.ChannelWhatsApp{
|
|
AccountID: accountID,
|
|
PhoneNumber: phoneNumber,
|
|
PhoneNumberID: phoneNumberID,
|
|
BusinessAccountID: businessAccountID,
|
|
WhatsAppAccountName: accountName,
|
|
AccessToken: accessToken,
|
|
Provider: provider,
|
|
WebhookVerifyToken: webhookVerifyToken,
|
|
AutoCreateContact: true,
|
|
}
|
|
|
|
if err := s.repository.Create(ctx, channel); err != nil {
|
|
return nil, fmt.Errorf("failed to create WhatsApp channel: %w", err)
|
|
}
|
|
|
|
applogger.L().Info("WhatsApp channel created",
|
|
"account_id", accountID,
|
|
"phone_number", phoneNumber,
|
|
"provider", provider,
|
|
)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// UpdateChannel updates an existing WhatsApp channel configuration.
|
|
// Reference: Chatwoot WhatsApp channel update (facebook_pages_controller#update analog)
|
|
func (s *WhatsAppService) UpdateChannel(ctx context.Context, channelID uint, params map[string]interface{}) (*channelmodel.ChannelWhatsApp, error) {
|
|
channel, err := s.repository.GetByID(ctx, channelID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find WhatsApp channel: %w", err)
|
|
}
|
|
|
|
// Update fields from params
|
|
if phoneNumber, ok := params["phone_number"].(string); ok {
|
|
channel.PhoneNumber = phoneNumber
|
|
}
|
|
if phoneNumberID, ok := params["phone_number_id"].(string); ok {
|
|
channel.PhoneNumberID = phoneNumberID
|
|
}
|
|
if businessAccountID, ok := params["business_account_id"].(string); ok {
|
|
channel.BusinessAccountID = businessAccountID
|
|
}
|
|
if accessToken, ok := params["access_token"].(string); ok && accessToken != "" {
|
|
channel.AccessToken = accessToken
|
|
}
|
|
if accountName, ok := params["whatsapp_account_name"].(string); ok {
|
|
channel.WhatsAppAccountName = accountName
|
|
}
|
|
if provider, ok := params["provider"].(string); ok {
|
|
if provider != "whatsapp_cloud" && provider != "360dialog" {
|
|
return nil, fmt.Errorf("provider must be 'whatsapp_cloud' or '360dialog'")
|
|
}
|
|
channel.Provider = provider
|
|
}
|
|
if webhookVerifyToken, ok := params["webhook_verify_token"].(string); ok {
|
|
channel.WebhookVerifyToken = webhookVerifyToken
|
|
}
|
|
if autoCreate, ok := params["auto_create_contact"].(bool); ok {
|
|
channel.AutoCreateContact = autoCreate
|
|
}
|
|
|
|
if err := s.repository.Update(ctx, channel); err != nil {
|
|
return nil, fmt.Errorf("failed to update WhatsApp channel: %w", err)
|
|
}
|
|
|
|
applogger.L().Info("WhatsApp channel updated",
|
|
"channel_id", channelID,
|
|
)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// DeleteChannel removes a WhatsApp channel.
|
|
// Reference: Chatwoot WhatsApp channel destroy (after_destroy :delete_webhook)
|
|
func (s *WhatsAppService) DeleteChannel(ctx context.Context, channelID uint) error {
|
|
channel, err := s.repository.GetByID(ctx, channelID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find WhatsApp channel: %w", err)
|
|
}
|
|
|
|
// Cleanup webhook subscription before deleting
|
|
if err := s.deleteWebhook(ctx, channel); err != nil {
|
|
applogger.L().Warn("Failed to delete WhatsApp webhook during channel destroy",
|
|
"error", err,
|
|
"channel_id", channelID,
|
|
)
|
|
// Don't fail deletion if webhook cleanup fails
|
|
}
|
|
|
|
if err := s.repository.Delete(ctx, channelID); err != nil {
|
|
return fmt.Errorf("failed to delete WhatsApp channel: %w", err)
|
|
}
|
|
|
|
applogger.L().Info("WhatsApp channel deleted",
|
|
"channel_id", channelID,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// === WhatsApp Business API Operations ===
|
|
|
|
// SendMessage sends a text message via WhatsApp Business API.
|
|
// Reference: Chatwoot SendOnWhatsappService
|
|
//
|
|
// Cloud API: POST /v18.0/{phone_number_id}/messages
|
|
// 360dialog: POST /v1/messages
|
|
func (s *WhatsAppService) SendMessage(ctx context.Context, channel *channelmodel.ChannelWhatsApp, recipientPhone string, content string) (*WASendMessageResponse, error) {
|
|
outbound := &WASendMessageRequest{
|
|
MessagingProduct: "whatsapp",
|
|
RecipientType: "individual",
|
|
To: recipientPhone,
|
|
Type: "text",
|
|
Text: &WASendText{
|
|
Body: content,
|
|
},
|
|
}
|
|
|
|
return s.sendOutboundMessage(ctx, channel, outbound)
|
|
}
|
|
|
|
// SendTemplateMessage sends a template message via WhatsApp Business API.
|
|
// Reference: Chatwoot template message support
|
|
func (s *WhatsAppService) SendTemplateMessage(ctx context.Context, channel *channelmodel.ChannelWhatsApp, recipientPhone string, templateName string, languageCode string, components []WATemplateComponent) (*WASendMessageResponse, error) {
|
|
outbound := &WASendMessageRequest{
|
|
MessagingProduct: "whatsapp",
|
|
RecipientType: "individual",
|
|
To: recipientPhone,
|
|
Type: "template",
|
|
Template: &WASendTemplate{
|
|
Name: templateName,
|
|
Language: &WATemplateLanguage{Code: languageCode},
|
|
Components: components,
|
|
},
|
|
}
|
|
|
|
return s.sendOutboundMessage(ctx, channel, outbound)
|
|
}
|
|
|
|
// SendMediaMessage sends a media message (image, document, audio, video, sticker) via WhatsApp Business API.
|
|
func (s *WhatsAppService) SendMediaMessage(ctx context.Context, channel *channelmodel.ChannelWhatsApp, recipientPhone string, mediaType string, mediaURL string, caption string) (*WASendMessageResponse, error) {
|
|
outbound := &WASendMessageRequest{
|
|
MessagingProduct: "whatsapp",
|
|
RecipientType: "individual",
|
|
To: recipientPhone,
|
|
Type: mediaType,
|
|
}
|
|
|
|
// Set media content based on type
|
|
switch mediaType {
|
|
case "image":
|
|
outbound.Image = &WASendMedia{Link: mediaURL, Caption: caption}
|
|
case "document":
|
|
outbound.Document = &WASendDocument{Link: mediaURL, Caption: caption}
|
|
case "audio":
|
|
outbound.Audio = &WASendMedia{Link: mediaURL}
|
|
case "video":
|
|
outbound.Video = &WASendMedia{Link: mediaURL, Caption: caption}
|
|
case "sticker":
|
|
outbound.Sticker = &WASendMedia{Link: mediaURL}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported media type: %s", mediaType)
|
|
}
|
|
|
|
return s.sendOutboundMessage(ctx, channel, outbound)
|
|
}
|
|
|
|
// GetPhoneNumberDetails fetches phone number quality rating and verification status.
|
|
// Cloud API: GET /v18.0/{phone_number_id}
|
|
func (s *WhatsAppService) GetPhoneNumberDetails(ctx context.Context, channel *channelmodel.ChannelWhatsApp) (*WAPhoneNumber, error) {
|
|
if !channel.IsCloudAPI() {
|
|
// 360dialog doesn't have equivalent API
|
|
return nil, fmt.Errorf("phone number details API only available for Cloud API")
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", s.graphAPIBase, channel.PhoneNumberID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(channel.AccessToken).
|
|
SetResult(&WAPhoneNumber{}).
|
|
Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("WhatsApp phone number API call failed: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("WhatsApp phone number API returned status %d", resp.StatusCode())
|
|
}
|
|
|
|
return resp.Result().(*WAPhoneNumber), nil
|
|
}
|
|
|
|
// FetchMessageTemplates retrieves available message templates for the WhatsApp Business Account.
|
|
// Cloud API: GET /v18.0/{business_account_id}/message_templates
|
|
func (s *WhatsAppService) FetchMessageTemplates(ctx context.Context, channel *channelmodel.ChannelWhatsApp) ([]interface{}, error) {
|
|
if !channel.IsCloudAPI() {
|
|
return nil, fmt.Errorf("message templates API only available for Cloud API")
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s/message_templates", s.graphAPIBase, channel.BusinessAccountID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(channel.AccessToken).
|
|
Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("WhatsApp templates API call failed: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("WhatsApp templates API returned status %d", resp.StatusCode())
|
|
}
|
|
|
|
var result struct {
|
|
Data []interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse templates response: %w", err)
|
|
}
|
|
|
|
// Cache templates in channel model
|
|
templateJSON, _ := json.Marshal(result.Data)
|
|
channel.MessageTemplates = string(templateJSON)
|
|
s.repository.Update(ctx, channel)
|
|
|
|
return result.Data, nil
|
|
}
|
|
|
|
// FetchHealthStatus retrieves WhatsApp Cloud phone-number health data and formats
|
|
// it to the payload consumed by Chatwoot's inbox settings screen.
|
|
// Reference: reference/chatwoot/app/services/whatsapp/health_service.rb
|
|
func (s *WhatsAppService) FetchHealthStatus(ctx context.Context, channel *channelmodel.ChannelWhatsApp) (map[string]interface{}, error) {
|
|
if channel == nil {
|
|
return nil, fmt.Errorf("Channel is required")
|
|
}
|
|
if channel.AccessToken == "" {
|
|
return nil, fmt.Errorf("API key is missing")
|
|
}
|
|
if channel.PhoneNumberID == "" {
|
|
return nil, fmt.Errorf("Phone number ID is missing")
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
url := fmt.Sprintf("%s/%s", s.graphAPIBase, channel.PhoneNumberID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetQueryParams(map[string]string{
|
|
"fields": whatsappHealthFields(),
|
|
"access_token": channel.AccessToken,
|
|
}).
|
|
SetResult(&result).
|
|
Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch WhatsApp health status: %w", err)
|
|
}
|
|
if resp.IsError() {
|
|
return nil, fmt.Errorf("WhatsApp API request failed: %d - %s", resp.StatusCode(), resp.String())
|
|
}
|
|
|
|
return formatWhatsAppHealthResponse(result, channel), nil
|
|
}
|
|
|
|
func whatsappHealthFields() string {
|
|
return "id,quality_rating,messaging_limit_tier,code_verification_status,account_mode,display_phone_number,name_status,verified_name,webhook_configuration,throughput,last_onboarded_time,platform_type,certificate"
|
|
}
|
|
|
|
func formatWhatsAppHealthResponse(response map[string]interface{}, channel *channelmodel.ChannelWhatsApp) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"id": response["id"],
|
|
"display_phone_number": response["display_phone_number"],
|
|
"verified_name": response["verified_name"],
|
|
"name_status": response["name_status"],
|
|
"quality_rating": response["quality_rating"],
|
|
"messaging_limit_tier": response["messaging_limit_tier"],
|
|
"account_mode": response["account_mode"],
|
|
"code_verification_status": response["code_verification_status"],
|
|
"webhook_configuration": response["webhook_configuration"],
|
|
"expected_webhook_url": buildExpectedWhatsAppWebhookURL(channel.PhoneNumber),
|
|
"throughput": response["throughput"],
|
|
"last_onboarded_time": response["last_onboarded_time"],
|
|
"platform_type": response["platform_type"],
|
|
"certificate": response["certificate"],
|
|
"business_id": channel.BusinessAccountID,
|
|
}
|
|
}
|
|
|
|
func buildExpectedWhatsAppWebhookURL(phoneNumber string) string {
|
|
return fmt.Sprintf("%s/webhooks/whatsapp/%s", os.Getenv("FRONTEND_URL"), phoneNumber)
|
|
}
|
|
|
|
// === Webhook Management ===
|
|
|
|
// SetupWebhook registers webhook subscription with WhatsApp provider.
|
|
// 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 {
|
|
if channel.IsCloudAPI() {
|
|
url := fmt.Sprintf("%s/%s/subscribed_apps", s.graphAPIBase, channel.BusinessAccountID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(channel.AccessToken).
|
|
Post(url)
|
|
if err != nil {
|
|
return fmt.Errorf("WhatsApp Cloud API webhook subscription failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return fmt.Errorf("WhatsApp Cloud API webhook subscription returned status %d", resp.StatusCode())
|
|
}
|
|
applogger.L().Info("WhatsApp Cloud API webhook subscription created",
|
|
"business_account_id", channel.BusinessAccountID,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// 360dialog: webhook is configured via their dashboard, no API call needed
|
|
applogger.L().Info("WhatsApp 360dialog webhook configured via dashboard",
|
|
"phone_number", channel.PhoneNumber,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// === Internal Helper Methods ===
|
|
|
|
// validateAccessToken validates the WhatsApp access token by making a test API call.
|
|
func (s *WhatsAppService) validateAccessToken(ctx context.Context, provider string, accessToken string, phoneNumberID string) error {
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
if provider == "whatsapp_cloud" {
|
|
// Cloud API: GET /v18.0/{phone_number_id} to validate token
|
|
url := fmt.Sprintf("%s/%s", s.graphAPIBase, phoneNumberID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(accessToken).
|
|
Get(url)
|
|
if err != nil {
|
|
return fmt.Errorf("WhatsApp Cloud API token validation failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return fmt.Errorf("WhatsApp Cloud API returned status %d — token may be invalid", resp.StatusCode())
|
|
}
|
|
} else {
|
|
// 360dialog: GET /v1/settings/application to validate API key
|
|
url := fmt.Sprintf("%s/v1/settings/application", s.dialogAPIBase)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("D360-API-KEY", accessToken).
|
|
Get(url)
|
|
if err != nil {
|
|
return fmt.Errorf("WhatsApp 360dialog API key validation failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return fmt.Errorf("WhatsApp 360dialog returned status %d — API key may be invalid", resp.StatusCode())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchAccountName retrieves the WhatsApp business account display name.
|
|
func (s *WhatsAppService) fetchAccountName(ctx context.Context, provider string, accessToken string, phoneNumberID string) string {
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
if provider == "whatsapp_cloud" {
|
|
url := fmt.Sprintf("%s/%s", s.graphAPIBase, phoneNumberID)
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(accessToken).
|
|
SetResult(&WAPhoneNumber{}).
|
|
Get(url)
|
|
if err == nil && resp.StatusCode() == 200 {
|
|
info := resp.Result().(*WAPhoneNumber)
|
|
if info.VerifiedName != nil && info.VerifiedName.Name != "" {
|
|
return info.VerifiedName.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use phone number as display name
|
|
return ""
|
|
}
|
|
|
|
// sendOutboundMessage sends a message via the appropriate WhatsApp provider API.
|
|
func (s *WhatsAppService) sendOutboundMessage(ctx context.Context, channel *channelmodel.ChannelWhatsApp, outbound *WASendMessageRequest) (*WASendMessageResponse, error) {
|
|
var url string
|
|
var req *resty.Request
|
|
|
|
if channel.IsCloudAPI() {
|
|
// Cloud API: POST /v18.0/{phone_number_id}/messages
|
|
url = fmt.Sprintf("%s/%s/messages", s.graphAPIBase, channel.PhoneNumberID)
|
|
req = s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(channel.AccessToken).
|
|
SetBody(outbound).
|
|
SetResult(&WASendMessageResponse{})
|
|
} else {
|
|
// 360dialog: POST /v1/messages
|
|
url = fmt.Sprintf("%s/v1/messages", s.dialogAPIBase)
|
|
req = s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("D360-API-KEY", channel.AccessToken).
|
|
SetBody(outbound).
|
|
SetResult(&WASendMessageResponse{})
|
|
}
|
|
|
|
resp, err := req.Post(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("WhatsApp outbound message API call failed: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode() != 200 && resp.StatusCode() != 201 {
|
|
// Parse error response
|
|
var apiError WACloudAPIError
|
|
if jsonErr := json.Unmarshal(resp.Body(), &apiError); jsonErr == nil {
|
|
return nil, fmt.Errorf("WhatsApp API error: %s (code %d)", apiError.Message, apiError.Code)
|
|
}
|
|
return nil, fmt.Errorf("WhatsApp outbound API returned status %d", resp.StatusCode())
|
|
}
|
|
|
|
result := resp.Result().(*WASendMessageResponse)
|
|
applogger.L().Info("WhatsApp message sent",
|
|
"phone_number_id", channel.PhoneNumberID,
|
|
"recipient", outbound.To,
|
|
"message_type", outbound.Type,
|
|
)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// deleteWebhook removes webhook subscription from WhatsApp provider.
|
|
func (s *WhatsAppService) deleteWebhook(ctx context.Context, channel *channelmodel.ChannelWhatsApp) error {
|
|
if channel.IsCloudAPI() {
|
|
url := fmt.Sprintf("%s/%s/subscribed_apps", s.graphAPIBase, channel.BusinessAccountID)
|
|
_, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetAuthToken(channel.AccessToken).
|
|
Delete(url)
|
|
if err != nil {
|
|
return fmt.Errorf("WhatsApp Cloud API webhook deletion failed: %w", err)
|
|
}
|
|
}
|
|
// 360dialog: webhook managed via dashboard, no API deletion needed
|
|
return nil
|
|
}
|
|
|
|
// GetChannelByInboxID retrieves the WhatsApp channel associated with an inbox.
|
|
func (s *WhatsAppService) GetChannelByInboxID(ctx context.Context, inboxID uint) (*channelmodel.ChannelWhatsApp, error) {
|
|
return s.repository.GetByInboxID(ctx, inboxID)
|
|
}
|