feat: migrate dyte integration to cloudflare realtimekit (#14752)

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>
This commit is contained in:
Muhsin Keloth
2026-06-24 11:04:08 +04:00
committed by GitHub
co-authored by Muhsin Sony Mathew
parent e8edc9ebf5
commit 74db16158d
20 changed files with 833 additions and 58 deletions
+46 -1
View File
@@ -30,6 +30,7 @@ class Integrations::Hook < ApplicationRecord
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
@@ -61,6 +62,10 @@ class Integrations::Hook < ApplicationRecord
app_id == 'openai'
end
def dyte?
app_id == 'dyte'
end
def notion?
app_id == 'notion'
end
@@ -96,6 +101,7 @@ class Integrations::Hook < ApplicationRecord
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
@@ -106,18 +112,57 @@ class Integrations::Hook < ApplicationRecord
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)
value&.dig('api_key') || value&.dig(:api_key)
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