Dyte is sunsetting its existing infrastructure after the Cloudflare acquisition, so this migrates Chatwoot’s video call integration to Cloudflare RealtimeKit. The integration now uses Cloudflare Account ID, RealtimeKit App ID, and a Cloudflare API token with Realtime Admin permissions. Meeting creation and participant token generation now call Cloudflare’s RealtimeKit APIs, while the existing Chatwoot call experience remains unchanged for agents and customers. This also adds setup-time credential validation, so admins get clearer errors when the API token is invalid, the Cloudflare account or permissions are incorrect, or the RealtimeKit App ID does not belong to the selected account. Fixes https://linear.app/chatwoot/issue/PLA-176/migrate-dyte-integration-to-cloudflare-realtimekit **How to test** 1. Go to Settings → Integrations → Cloudflare RealtimeKit. 2. Add a Cloudflare Account ID, RealtimeKit App ID, and API token with Realtime Admin permissions. 3. Confirm the integration saves successfully with valid credentials. 4. Try invalid credentials and confirm the error identifies whether the token, account/permissions, or app ID is wrong. 5. Start a video call from a conversation and confirm the RealtimeKit meeting opens. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com>
181 lines
5.4 KiB
Ruby
181 lines
5.4 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: integrations_hooks
|
|
#
|
|
# id :bigint not null, primary key
|
|
# access_token :string
|
|
# hook_type :integer default("account")
|
|
# settings :jsonb
|
|
# status :integer default("enabled")
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer
|
|
# app_id :string
|
|
# inbox_id :integer
|
|
# reference_id :string
|
|
#
|
|
class Integrations::Hook < ApplicationRecord
|
|
include Reauthorizable
|
|
|
|
attr_readonly :app_id, :account_id, :inbox_id, :hook_type
|
|
before_validation :ensure_hook_type
|
|
after_create :trigger_setup_if_crm
|
|
|
|
# TODO: Remove guard once encryption keys become mandatory (target 3-4 releases out).
|
|
encrypts :access_token, deterministic: true if Chatwoot.encryption_configured?
|
|
|
|
validates :account_id, presence: true
|
|
validates :app_id, presence: true
|
|
validates :inbox_id, presence: true, if: -> { hook_type == 'inbox' }
|
|
validate :validate_settings_json_schema
|
|
validate :ensure_feature_enabled
|
|
validate :validate_openai_api_key, if: :validate_openai_api_key?
|
|
validate :validate_cloudflare_realtimekit_credentials, if: :validate_cloudflare_realtimekit_credentials?
|
|
validates :app_id, uniqueness: { scope: [:account_id], unless: -> { app.present? && app.params[:allow_multiple_hooks].present? } }
|
|
|
|
# TODO: This seems to be only used for slack at the moment
|
|
# We can add a validator when storing the integration settings and toggle this in future
|
|
enum status: { disabled: 0, enabled: 1 }
|
|
|
|
belongs_to :account
|
|
belongs_to :inbox, optional: true
|
|
has_secure_token :access_token
|
|
|
|
enum hook_type: { account: 0, inbox: 1 }
|
|
|
|
scope :account_hooks, -> { where(hook_type: 'account') }
|
|
scope :inbox_hooks, -> { where(hook_type: 'inbox') }
|
|
|
|
def app
|
|
@app ||= Integrations::App.find(id: app_id)
|
|
end
|
|
|
|
def slack?
|
|
app_id == 'slack'
|
|
end
|
|
|
|
def dialogflow?
|
|
app_id == 'dialogflow'
|
|
end
|
|
|
|
def openai?
|
|
app_id == 'openai'
|
|
end
|
|
|
|
def dyte?
|
|
app_id == 'dyte'
|
|
end
|
|
|
|
def notion?
|
|
app_id == 'notion'
|
|
end
|
|
|
|
def disable
|
|
update(status: 'disabled')
|
|
end
|
|
|
|
def process_event(_event)
|
|
# OpenAI integration migrated to Captain::EditorService
|
|
# Other integrations (slack, dialogflow, etc.) handled via HookJob
|
|
{ error: 'No processor found' }
|
|
end
|
|
|
|
def feature_allowed?
|
|
return true if app.blank?
|
|
|
|
flag = app.params[:feature_flag]
|
|
return true unless flag
|
|
|
|
account.feature_enabled?(flag)
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_feature_enabled
|
|
errors.add(:feature_flag, 'Feature not enabled') unless feature_allowed?
|
|
end
|
|
|
|
def ensure_hook_type
|
|
self.hook_type = app.params[:hook_type] if app.present?
|
|
end
|
|
|
|
def validate_settings_json_schema
|
|
return if app.blank? || app.params[:settings_json_schema].blank?
|
|
return if legacy_dyte_settings_unchanged?
|
|
|
|
errors.add(:settings, ': Invalid settings data') unless JSONSchemer.schema(app.params[:settings_json_schema]).valid?(settings)
|
|
end
|
|
|
|
# TODO: When adding credential validation for other integrations (dialogflow, dyte, etc.),
|
|
# extract this into an app-level config flag in apps.yml instead of hardcoding app_id checks.
|
|
def validate_openai_api_key?
|
|
openai? && enabled? && (new_record? || openai_api_key_changed? || will_save_change_to_status?)
|
|
end
|
|
|
|
def validate_cloudflare_realtimekit_credentials?
|
|
dyte? && enabled? && !legacy_dyte_settings_unchanged? &&
|
|
(new_record? || cloudflare_realtimekit_credentials_changed? || will_save_change_to_status?)
|
|
end
|
|
|
|
def openai_api_key_changed?
|
|
settings_api_key(settings) != settings_api_key(settings_in_database)
|
|
end
|
|
|
|
def cloudflare_realtimekit_credentials_changed?
|
|
settings_cloudflare_realtimekit_credentials(settings) != settings_cloudflare_realtimekit_credentials(settings_in_database)
|
|
end
|
|
|
|
def legacy_dyte_settings_unchanged?
|
|
dyte? && persisted? && !will_save_change_to_settings? && legacy_dyte_settings?(settings_in_database)
|
|
end
|
|
|
|
def legacy_dyte_settings?(value)
|
|
return false if value.blank?
|
|
|
|
%w[organization_id api_key].any? { |key| settings_value(value, key).present? } &&
|
|
%w[account_id app_id api_token].none? { |key| settings_value(value, key).present? }
|
|
end
|
|
|
|
def validate_openai_api_key
|
|
return if Integrations::Openai::KeyValidator.valid?(settings_api_key(settings))
|
|
|
|
errors.add(:base, I18n.t('errors.openai.invalid_api_key'))
|
|
end
|
|
|
|
def validate_cloudflare_realtimekit_credentials
|
|
result = Integrations::Cloudflare::RealtimeKitCredentialsValidator.validate(*settings_cloudflare_realtimekit_credentials(settings))
|
|
return if result.success?
|
|
|
|
errors.add(:base, I18n.t("errors.cloudflare.realtimekit.#{result.error}"))
|
|
end
|
|
|
|
def settings_api_key(value)
|
|
settings_value(value, 'api_key')
|
|
end
|
|
|
|
def settings_cloudflare_realtimekit_credentials(value)
|
|
[
|
|
settings_value(value, 'account_id'),
|
|
settings_value(value, 'app_id'),
|
|
settings_value(value, 'api_token')
|
|
]
|
|
end
|
|
|
|
def settings_value(value, key)
|
|
value&.dig(key) || value&.dig(key.to_sym)
|
|
end
|
|
|
|
def trigger_setup_if_crm
|
|
# we need setup services to create data prerequisite to functioning of the integration
|
|
# in case of Leadsquared, we need to create a custom activity type for capturing conversations and transcripts
|
|
# https://apidocs.leadsquared.com/create-new-activity-type-api/
|
|
return unless crm_integration?
|
|
|
|
::Crm::SetupJob.perform_later(id)
|
|
end
|
|
|
|
def crm_integration?
|
|
%w[leadsquared].include?(app_id)
|
|
end
|
|
end
|