feat: Add backend APIs for Dyte integration (#6197)

- The backend changes required for Dyte Integration.
This commit is contained in:
Pranav Raj S
2023-01-09 12:37:18 +05:30
committed by GitHub
parent 50894fd591
commit cbfbe6dbad
12 changed files with 491 additions and 16 deletions
@@ -0,0 +1,48 @@
class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:create_a_meeting]
before_action :fetch_message, only: [:add_participant_to_meeting]
before_action :authorize_request
def create_a_meeting
render_response(dyte_processor_service.create_a_meeting(Current.user))
end
def add_participant_to_meeting
if @message.content_type != 'integrations'
return render json: {
error: I18n.t('errors.dyte.invalid_message_type')
}, status: :unprocessable_entity
end
render_response(
dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user)
)
end
private
def authorize_request
authorize @conversation.inbox, :show?
end
def render_response(response)
render json: response, status: response[:error].blank? ? :ok : :unprocessable_entity
end
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: Current.account, conversation: @conversation)
end
def permitted_params
params.permit(:conversation_id, :message_id)
end
def fetch_conversation
@conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id])
end
def fetch_message
@message = Current.account.messages.find(permitted_params[:message_id])
@conversation = @message.conversation
end
end
+2 -1
View File
@@ -57,7 +57,8 @@ class Message < ApplicationRecord
form: 6,
article: 7,
incoming_email: 8,
input_csat: 9
input_csat: 9,
integrations: 10
}
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
# [:submitted_email, :items, :submitted_values] : Used for bot message types
+28 -14
View File
@@ -60,20 +60,34 @@ dialogflow:
}
]
visible_properties: ['project_id']
fullcontact:
id: fullcontact
logo: fullcontact.png
i18n_key: fullcontact
action: /fullcontact
dyte:
id: dyte
logo: dyte.png
i18n_key: dyte
action: /dyte
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
settings_json_schema: {
"type": "object",
"properties": {
"api_key": { "type": "string" },
"organization_id": { "type": "string" },
},
"required": ["api_key", "organization_id"],
"additionalProperties": false,
}
settings_form_schema: [
{
'type': 'object',
'properties': { 'api_key': { 'type': 'string' } },
'required': ['api_key'],
'additionalProperties': false,
}
settings_form_schema:
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
visible_properties: ['api_key']
"label": "Organization ID",
"type": "text",
"name": "organization_id",
"validation": "required",
},
{
"label": "API Key",
"type": "text",
"name": "api_key",
"validation": "required",
},
]
visible_properties: ["organization_id"]
+7 -1
View File
@@ -51,13 +51,15 @@ en:
contacts:
import:
failed: File is blank
email:
email:
invalid: Invalid email
phone_number:
invalid: should be in e164 format
categories:
locale:
unique: should be unique in the category and portal
dyte:
invalid_message_type: "Invalid message type. Action not permitted"
inboxes:
imap:
socket_error: Please check the network connection, IMAP address and try again.
@@ -153,6 +155,10 @@ en:
online:
delete: "%{contact_name} is Online, please try again later"
integration_apps:
dyte:
name: "Dyte"
description: "Dyte is tool that helps you to add live audio & video to your application with just a few lines of code. This integration allows you to give an option to your agents to have a video or voice call with your customers from without leaving Chatwoot."
meeting_name: "%{agent_name} has started a meeting"
slack:
name: "Slack"
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
+6
View File
@@ -158,6 +158,12 @@ Rails.application.routes.draw do
resources :apps, only: [:index, :show]
resources :hooks, only: [:create, :update, :destroy]
resource :slack, only: [:create, :update, :destroy], controller: 'slack'
resource :dyte, controller: 'dyte', only: [] do
collection do
post :create_a_meeting
post :add_participant_to_meeting
end
end
end
resources :working_hours, only: [:update]
+58
View File
@@ -0,0 +1,58 @@
class Dyte
BASE_URL = 'https://api.cluster.dyte.in/v1'.freeze
API_KEY_HEADER = 'Authorization'.freeze
def initialize(organization_id, api_key)
@api_key = api_key
@organization_id = organization_id
raise ArgumentError, 'Missing Credentials' if @api_key.blank? || @organization_id.blank?
end
def create_a_meeting(title)
payload = {
'title': title,
'authorization': {
'waitingRoom': false,
'closed': false
},
'recordOnStart': false,
'liveStreamOnStart': false
}
path = "organizations/#{@organization_id}/meeting"
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 = {
'clientSpecificId': client_id.to_s,
'userDetails': {
'name': name,
'picture': avatar_url
}
}
path = "organizations/#{@organization_id}/meetings/#{meeting_id}/participant"
response = post(path, payload)
process_response(response)
end
private
def process_response(response)
return response.parsed_response['data'].with_indifferent_access if response.success?
{ error: response.parsed_response, error_code: response.code }
end
def post(path, payload)
HTTParty.post(
"#{BASE_URL}/#{path}", {
headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' },
body: payload.to_json
}
)
end
end
@@ -0,0 +1,55 @@
class Integrations::Dyte::ProcessorService
pattr_initialize [:account!, :conversation!]
def create_a_meeting(agent)
title = I18n.t('integration_apps.dyte.meeting_name', agent_name: agent.available_name)
response = dyte_client.create_a_meeting(title)
return response if response[:error].present?
meeting = response['meeting']
message = create_a_dyte_integration_message(meeting, title, agent)
message.push_event_data
end
def add_participant_to_meeting(meeting_id, user)
dyte_client.add_participant_to_meeting(meeting_id, user.id, user.name, avatar_url(user))
end
private
def create_a_dyte_integration_message(meeting, title, agent)
@conversation.messages.create!(
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :outgoing,
content_type: :integrations,
content: title,
content_attributes: {
type: 'dyte',
data: {
meeting_id: meeting['id'],
room_name: meeting['roomName']
}
},
sender: agent
}
)
end
def avatar_url(user)
return user.avatar_url if user.avatar_url.present?
"#{ENV.fetch('FRONTEND_URL', nil)}/integrations/slack/user.png"
end
def dyte_hook
@dyte_hook ||= account.hooks.find_by!(app_id: 'dyte')
end
def dyte_client
credentials = dyte_hook.settings
@dyte_client ||= Dyte.new(credentials['organization_id'], credentials['api_key'])
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,138 @@
require 'rails_helper'
RSpec.describe 'Dyte Integration API', type: :request do
let(:headers) { { 'Content-Type' => 'application/json' } }
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, status: :pending) }
let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
let(:integration_message) do
create(:message, content_type: 'integrations',
content_attributes: { type: 'dyte', data: { meeting_id: 'm_id' } },
conversation: conversation, account: account, inbox: conversation.inbox)
end
let(:agent) { create(:user, account: account, role: :agent) }
let(:unauthorized_agent) { create(:user, account: account, role: :agent) }
before do
create(:integrations_hook, :dyte, account: account)
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/create_a_meeting' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the agent does not have access to the inbox' do
it 'returns unauthorized' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: unauthorized_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent with inbox access and the Dyte API is a success' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(
status: 200,
body: { success: true, data: { meeting: { id: 'meeting_id', roomName: 'room_name' } } }.to_json,
headers: headers
)
end
it 'returns valid message payload' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = JSON.parse(response.body)
last_message = conversation.reload.messages.last
expect(conversation.display_id).to eq(response_body['conversation_id'])
expect(last_message.id).to eq(response_body['id'])
end
end
context 'when it is an agent with inbox access and the Dyte API is errored' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(
status: 422,
body: { success: false, data: { message: 'Title is required' } }.to_json,
headers: headers
)
end
it 'returns error payload' do
post create_a_meeting_api_v1_account_integrations_dyte_url(account),
params: { conversation_id: conversation.display_id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_body = JSON.parse(response.body)
expect(response_body['error']).to eq({ 'data' => { 'message' => 'Title is required' }, 'success' => false })
end
end
end
describe 'POST /api/v1/accounts/:account_id/integrations/dyte/add_participant_to_meeting' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the agent does not have access to the inbox' do
it 'returns unauthorized' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: message.id },
headers: unauthorized_agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an agent with inbox access and message_type is not integrations' do
it 'returns error' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: message.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when it is an agent with inbox access and message_type is integrations' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
.to_return(
status: 200,
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
headers: headers
)
end
it 'returns authResponse' do
post add_participant_to_meeting_api_v1_account_integrations_dyte_url(account),
params: { message_id: integration_message.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = JSON.parse(response.body)
expect(response_body['authResponse']).to eq(
{
'userAdded' => true, 'id' => 'random_uuid', 'auth_token' => 'json-web-token'
}
)
end
end
end
end
+5
View File
@@ -11,5 +11,10 @@ FactoryBot.define do
app_id { 'dialogflow' }
settings { { project_id: 'test', credentials: {} } }
end
trait :dyte do
app_id { 'dyte' }
settings { { api_key: 'api_key', organization_id: 'org_id' } }
end
end
end
+76
View File
@@ -0,0 +1,76 @@
require 'rails_helper'
describe Dyte do
let(:dyte_client) { described_class.new('org_id', 'api_key') }
let(:headers) { { 'Content-Type' => 'application/json' } }
it 'raises an exception if api_key or organization ID is absent' do
expect { described_class.new }.to raise_error(StandardError)
end
context 'when create_a_meeting is called' do
context 'when API response is success' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(
status: 200,
body: { success: true, data: { meeting: { id: 'meeting_id' } } }.to_json,
headers: headers
)
end
it 'returns api response' do
response = dyte_client.create_a_meeting('title_of_the_meeting')
expect(response).to eq({ 'meeting' => { 'id' => 'meeting_id' } })
end
end
context 'when API response is invalid' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(status: 422, body: { message: 'Title is required' }.to_json, headers: headers)
end
it 'returns error code with data' do
response = dyte_client.create_a_meeting('')
expect(response).to eq({ error: { 'message' => 'Title is required' }, error_code: 422 })
end
end
end
context 'when add_participant_to_meeting is called' do
context 'when API parameters are missing' do
it 'raises an exception' do
expect { dyte_client.add_participant_to_meeting }.to raise_error(StandardError)
end
end
context 'when API response is success' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
.to_return(
status: 200,
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
headers: headers
)
end
it 'returns api response' do
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
expect(response).to eq({ 'authResponse' => { 'userAdded' => true, 'id' => 'random_uuid', 'auth_token' => 'json-web-token' } })
end
end
context 'when API response is invalid' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
.to_return(status: 422, body: { message: 'Meeting ID is invalid' }.to_json, headers: headers)
end
it 'returns error code with data' do
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
expect(response).to eq({ error: { 'message' => 'Meeting ID is invalid' }, error_code: 422 })
end
end
end
end
@@ -0,0 +1,68 @@
require 'rails_helper'
describe Integrations::Dyte::ProcessorService do
let(:headers) { { 'Content-Type' => 'application/json' } }
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, status: :pending) }
let(:processor) { described_class.new(account: account, conversation: conversation) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:integrations_hook, :dyte, account: account)
end
describe '#create_a_meeting' do
context 'when the API response is success' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(
status: 200,
body: { success: true, data: { meeting: { id: 'meeting_id', roomName: 'room_name' } } }.to_json,
headers: headers
)
end
it 'creates an integration message in the conversation' do
response = processor.create_a_meeting(agent)
expect(response['content']).to eq("#{agent.available_name} has started a meeting")
expect(conversation.reload.messages.last.content_type).to eq('integrations')
end
end
context 'when the API response is errored' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meeting')
.to_return(
status: 422,
body: { success: false, data: { message: 'Title is required' } }.to_json,
headers: headers
)
end
it 'does not create an integration message in the conversation' do
response = processor.create_a_meeting(agent)
expect(response).to eq({ error: { 'data' => { 'message' => 'Title is required' }, 'success' => false }, error_code: 422 })
expect(conversation.reload.messages.count).to eq(0)
end
end
end
describe '#add_participant_to_meeting' do
context 'when the API response is success' do
before do
stub_request(:post, 'https://api.cluster.dyte.in/v1/organizations/org_id/meetings/m_id/participant')
.to_return(
status: 200,
body: { success: true, data: { authResponse: { userAdded: true, id: 'random_uuid', auth_token: 'json-web-token' } } }.to_json,
headers: headers
)
end
it 'return the authResponse' do
response = processor.add_participant_to_meeting('m_id', agent)
expect(response[:authResponse]).not_to be_nil
end
end
end
end