Files
74db16158d 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>
2026-06-24 11:04:08 +04:00

96 lines
2.9 KiB
Ruby

class Dyte
BASE_URL = 'https://api.cloudflare.com/client/v4'.freeze
API_KEY_HEADER = 'Authorization'.freeze
PRESET_NAME = 'group-call-host'.freeze
LEGACY_PRESET_NAME = 'group_call_host'.freeze
def initialize(account_id = nil, app_id = nil, api_token = nil)
@account_id = account_id
@app_id = app_id
@api_token = api_token
raise ArgumentError, 'Missing Credentials' if @account_id.blank? || @app_id.blank? || @api_token.blank?
end
def create_a_meeting(title)
payload = {
'title': title
}
path = 'meetings'
response = post(path, payload)
process_response(response)
end
def add_participant_to_meeting(meeting_id, client_id, name, avatar_url)
raise ArgumentError, 'Missing information' if meeting_id.blank? || client_id.blank? || name.blank? || avatar_url.blank?
payload = {
'custom_participant_id': client_id.to_s,
'name': name,
'picture': avatar_url,
'preset_name': PRESET_NAME
}
path = "meetings/#{meeting_id}/participants"
response = process_response(post(path, payload))
return response unless preset_not_found?(response)
payload[:preset_name] = LEGACY_PRESET_NAME
process_response(post(path, payload))
end
def refresh_participant_token(meeting_id, participant_id)
raise ArgumentError, 'Missing information' if meeting_id.blank? || participant_id.blank?
path = "meetings/#{meeting_id}/participants/#{participant_id}/token"
response = post(path)
process_response(response)
end
def fetch_participants(meeting_id)
raise ArgumentError, 'Missing information' if meeting_id.blank?
response = get("meetings/#{meeting_id}/participants")
process_response(response)
end
private
def process_response(response)
return { error: response.parsed_response, error_code: response.code } unless response.success?
data = parsed_data(response)
return data.with_indifferent_access if data.is_a?(Hash)
return data.map(&:with_indifferent_access) if data.is_a?(Array)
{ error: :unexpected_response, error_code: response.code }
end
def parsed_data(response)
response.parsed_response['data']
end
def preset_not_found?(response)
error = response[:error]
message = error.dig('error', 'message') if error.is_a?(Hash) && error['error'].is_a?(Hash)
message ||= error['message'] if error.is_a?(Hash)
message ||= error.to_s
message.include?('No preset found')
end
def post(path, payload = nil)
HTTParty.post(
"#{BASE_URL}/accounts/#{@account_id}/realtime/kit/#{@app_id}/#{path}", {
headers: { API_KEY_HEADER => "Bearer #{@api_token}", 'Content-Type' => 'application/json' },
body: payload&.to_json
}.compact
)
end
def get(path)
HTTParty.get(
"#{BASE_URL}/accounts/#{@account_id}/realtime/kit/#{@app_id}/#{path}",
headers: { API_KEY_HEADER => "Bearer #{@api_token}", 'Content-Type' => 'application/json' }
)
end
end