## Description Solves issue https://github.com/chatwoot/chatwoot/issues/14690 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How has this been tested? - UI flows ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
150 lines
5.5 KiB
Ruby
150 lines
5.5 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: channel_whatsapp
|
|
#
|
|
# id :bigint not null, primary key
|
|
# message_templates :jsonb
|
|
# message_templates_last_updated :datetime
|
|
# phone_number :string not null
|
|
# provider :string default("default")
|
|
# provider_config :jsonb
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_channel_whatsapp_on_phone_number (phone_number) UNIQUE
|
|
#
|
|
|
|
class Channel::Whatsapp < ApplicationRecord
|
|
include Channelable
|
|
include Reauthorizable
|
|
|
|
self.table_name = 'channel_whatsapp'
|
|
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
|
|
|
# default at the moment is 360dialog lets change later.
|
|
PROVIDERS = %w[default whatsapp_cloud].freeze
|
|
before_validation :ensure_webhook_verify_token
|
|
|
|
validates :provider, inclusion: { in: PROVIDERS }
|
|
validates :phone_number, presence: true, uniqueness: true
|
|
validate :validate_provider_config
|
|
|
|
after_create :sync_templates
|
|
before_destroy :teardown_webhooks
|
|
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
|
|
|
def name
|
|
'Whatsapp'
|
|
end
|
|
|
|
# Mirrors Channel::TwilioSms#voice_enabled? so the call subsystem can duck-type across providers.
|
|
# Meta's Calling API is available to any whatsapp_cloud inbox (embedded-signup or manual keys);
|
|
# only 360dialog (default provider) can't reach the call APIs.
|
|
def voice_enabled?
|
|
voice_calling_supported? &&
|
|
provider_config['calling_enabled'].present? &&
|
|
account.feature_enabled?('channel_voice')
|
|
end
|
|
|
|
# Mutes only the incoming side of calling; default on, so only an explicit false disables inbound.
|
|
def inbound_calls_enabled?
|
|
provider_config['inbound_calls_enabled'] != false
|
|
end
|
|
|
|
# Whether this inbox can do WhatsApp calling at all. Meta's Calling API is
|
|
# reachable by any whatsapp_cloud inbox, so 360dialog inboxes can't be toggled
|
|
# on even though calling_enabled would persist.
|
|
def voice_calling_supported?
|
|
provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def provider_service
|
|
if provider == 'whatsapp_cloud'
|
|
Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self)
|
|
else
|
|
Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self)
|
|
end
|
|
end
|
|
|
|
# Enables voice: turns calling on at Meta (idempotent), then re-registers webhooks
|
|
# with the in-memory calling_enabled flag so the `calls` field is subscribed. The
|
|
# flag is persisted only after registration succeeds, so a webhook failure can't
|
|
# leave the inbox reporting voice_enabled? while the WABA isn't subscribed to calls.
|
|
# Saved with validate: false to skip validate_provider_config's remote credential
|
|
# re-check, which could spuriously fail and desync the flag from Meta.
|
|
def enable_voice_calling!
|
|
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
|
|
raise 'WhatsApp calling requires the channel_voice feature' unless account.feature_enabled?('channel_voice')
|
|
|
|
provider_service.update_calling_status('ENABLED')
|
|
self.provider_config = provider_config.merge('calling_enabled' => true)
|
|
webhook_setup_service.register_callback
|
|
save!(validate: false)
|
|
end
|
|
|
|
# Disables voice: unsets calling_enabled (gates the call subsystem) and re-registers
|
|
# webhooks, which drops `calls` from the subscription (best-effort, so a Meta outage
|
|
# can't trap admins). Leaves Meta's WABA calling.status untouched.
|
|
def disable_voice_calling!
|
|
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
|
|
|
|
self.provider_config = provider_config.merge('calling_enabled' => false)
|
|
save!(validate: false)
|
|
begin
|
|
webhook_setup_service.register_callback
|
|
rescue StandardError => e
|
|
Rails.logger.warn "[WHATSAPP CALL] disable webhook re-subscribe failed: #{e.message}"
|
|
end
|
|
end
|
|
|
|
def mark_message_templates_updated
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
update_column(:message_templates_last_updated, Time.zone.now)
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
end
|
|
|
|
delegate :send_message, to: :provider_service
|
|
delegate :send_template, to: :provider_service
|
|
delegate :sync_templates, to: :provider_service
|
|
delegate :media_url, to: :provider_service
|
|
delegate :api_headers, to: :provider_service
|
|
|
|
def setup_webhooks
|
|
perform_webhook_setup
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WHATSAPP] Webhook setup failed: #{e.message}"
|
|
prompt_reauthorization!
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_webhook_verify_token
|
|
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def validate_provider_config
|
|
errors.add(:provider_config, 'Invalid Credentials') unless provider_service.validate_provider_config?
|
|
end
|
|
|
|
def perform_webhook_setup
|
|
webhook_setup_service.perform
|
|
end
|
|
|
|
def webhook_setup_service
|
|
Whatsapp::WebhookSetupService.new(self, provider_config['business_account_id'], provider_config['api_key'])
|
|
end
|
|
|
|
def teardown_webhooks
|
|
Whatsapp::WebhookTeardownService.new(self).perform
|
|
end
|
|
|
|
def should_auto_setup_webhooks?
|
|
# Only auto-setup webhooks for whatsapp_cloud provider with manual setup
|
|
# Embedded signup calls setup_webhooks explicitly in EmbeddedSignupService
|
|
provider == 'whatsapp_cloud' && provider_config['source'] != 'embedded_signup'
|
|
end
|
|
end
|