Dyte Integration V1

This commit is contained in:
Pranav Raj S 2022-12-17 20:54:14 -08:00
parent 38587b3aa1
commit 5fca522721
17 changed files with 368 additions and 87 deletions

View file

@ -0,0 +1,27 @@
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]
def create_a_meeting
dyte_processor_service.create_a_meeting(Current.user)
head :ok
end
def add_participant_to_meeting
dyte_processor_service.create_a_meeting(Current.user)
end
private
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: Current.account, conversation: @conversation)
end
def permitted_params
params.permit(:conversation_id, :meeting_id)
end
def fetch_conversation
@conversation = Current.account.conversations.find_by(display_id: permitted_params[:conversation_id])
end
end

View file

@ -0,0 +1,23 @@
/* global axios */
import ApiClient from '../ApiClient';
class DyteAPI extends ApiClient {
constructor() {
super('integrations/dyte', { accountScoped: true });
}
createAMeeting(conversationId) {
return axios.post(`${this.url}/create_a_meeting`, {
conversation_id: conversationId,
});
}
addParticipantToMeeting(messageId) {
return axios.post(this.baseUrl(), {
message_id: messageId,
});
}
}
export default new DyteAPI();

View file

@ -87,6 +87,7 @@
:title="'Whatsapp Templates'"
@click="$emit('selectWhatsappTemplate')"
/>
<video-call :conversation-id="conversationId" />
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
@ -124,13 +125,13 @@ import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages';
import VideoCall from './VideoCall.vue';
import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload },
components: { FileUpload, VideoCall },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
mode: {
@ -169,6 +170,10 @@ export default {
type: Boolean,
default: false,
},
conversationId: {
type: Number,
default: 0,
},
toggleEmojiPicker: {
type: Function,
default: () => {},

View file

@ -0,0 +1,57 @@
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="'Start a new video call with the customer'"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
:title="'Whatsapp Templates'"
@click="onClick"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import DyteAPI from '../../../api/integrations/dyte';
export default {
props: {
conversationId: {
type: Number,
default: 0,
},
},
data() {
return { isLoading: false };
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
isVideoIntegrationEnabled() {
return this.appIntegrations.find(integration => {
return integration.id === 'dyte' && !!integration.hooks.length;
});
},
},
mounted() {
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
},
methods: {
async onClick() {
this.isLoading = true;
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
// Ignore Error
} finally {
this.isLoading = false;
}
},
},
};
</script>

View file

@ -1,8 +1,5 @@
<template>
<li
v-if="hasAttachments || data.content || isEmailContentType"
:class="alignBubble"
>
<li v-if="shouldRenderMessage" :class="alignBubble">
<div :class="wrapClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head
@ -18,6 +15,7 @@
:readable-time="readableTime"
:display-quoted-button="displayQuotedButton"
/>
<span
v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent"
@ -183,6 +181,17 @@ export default {
};
},
computed: {
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isAnIntegrationMessage
);
},
isAnIntegrationMessage() {
return this.data.contentType === 'integrations';
},
emailMessageContent() {
const {
html_content: { full: fullHTMLContent } = {},

View file

@ -114,6 +114,7 @@
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:enable-multiple-file-upload="enableMultipleFileUpload"
:has-whatsapp-templates="hasWhatsappTemplates"
:conversation-id="conversationId"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
/>

View file

@ -0,0 +1,22 @@
<template>
<video-call
v-if="contentAttributes.type === 'dyte'"
:message-id="messageId"
/>
</template>
<script>
import VideoCall from './VideoCall.vue';
export default {
components: { VideoCall },
props: {
messageId: {
type: Number,
required: true,
},
contentAttributes: {
type: Object,
default: () => ({}),
},
},
};
</script>

View file

@ -0,0 +1,12 @@
<template>
<woot-button
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="save"
@click="onClickOpenAddFoldersModal"
>
Join the call
</woot-button>
</template>
<script></script>

View file

@ -16,14 +16,16 @@ const state = {
},
};
const isAValidAppIntegration = integration => {
return ['fullcontact', 'dialogflow', 'dyte'].includes(integration.id);
};
export const getters = {
getIntegrations($state) {
return $state.records.filter(
item => item.id !== 'fullcontact' && item.id !== 'dialogflow'
);
return $state.records.filter(item => !isAValidAppIntegration(item));
},
getAppIntegrations($state) {
return $state.records.filter(item => item.id === 'dialogflow');
return $state.records.filter(item => isAValidAppIntegration(item));
},
getIntegration: $state => integrationId => {
const [integration] = $state.records.filter(

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

View file

@ -30,50 +30,68 @@ dialogflow:
action: /dialogflow
hook_type: inbox
allow_multiple_hooks: true
settings_json_schema: {
"type": "object",
"properties": {
"project_id": { "type": "string" },
"credentials": { "type": "object" }
},
"required": ["project_id", "credentials"],
"additionalProperties": false
}
settings_form_schema: [
settings_json_schema:
{
"label": "Dialogflow Project ID",
"type": "text",
"name": "project_id",
"validation": "required",
"validationName": 'Project Id',
},
{
"label": "Dialogflow Project Key File",
"type": "textarea",
"name": "credentials",
"validation": "required|JSON",
"validationName": 'Credentials',
"validation-messages": {
"JSON": "Invalid JSON",
"required": "Credentials is required"
}
'type': 'object',
'properties':
{
'project_id': { 'type': 'string' },
'credentials': { 'type': 'object' },
},
'required': ['project_id', 'credentials'],
'additionalProperties': false,
}
]
settings_form_schema:
[
{
'label': 'Dialogflow Project ID',
'type': 'text',
'name': 'project_id',
'validation': 'required',
'validationName': 'Project Id',
},
{
'label': 'Dialogflow Project Key File',
'type': 'textarea',
'name': 'credentials',
'validation': 'required|JSON',
'validationName': 'Credentials',
'validation-messages':
{ 'JSON': 'Invalid JSON', 'required': 'Credentials is required' },
},
]
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:
{
'type': 'object',
'properties': { 'api_key': { 'type': 'string' } },
'required': ['api_key'],
'properties':
{
'api_key': { 'type': 'string' },
'organization_id': { 'type': 'string' },
},
'required': ['api_key', 'organization_id'],
'additionalProperties': false,
}
settings_form_schema:
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
visible_properties: ['api_key']
[
{
'label': 'API Key',
'type': 'text',
'name': 'api_key',
'validation': 'required',
},
{
'label': 'Organization ID',
'type': 'text',
'name': 'organization_id',
'validation': 'required',
},
]
visible_properties: ['organization_id']

View file

@ -30,7 +30,7 @@
# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"
hello: 'Hello world'
messages:
reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
reset_password_failure: Uh ho! We could not find any user with the specified email.
@ -43,7 +43,7 @@ en:
signup:
disposable_email: We do not allow disposable emails
invalid_email: You have entered an invalid email
email_already_exists: "You have already signed up for an account with %{email}"
email_already_exists: 'You have already signed up for an account with %{email}'
failed: Signup failed
data_import:
data_type:
@ -51,7 +51,7 @@ en:
contacts:
import:
failed: File is blank
email:
email:
invalid: Invalid email
phone_number:
invalid: should be in e164 format
@ -105,66 +105,69 @@ en:
notifications:
notification_title:
conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}"
conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you"
assigned_conversation_new_message: "[New message] - #%{display_id} %{content}"
conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}"
conversation_creation: '[New conversation] - #%{display_id} has been created in %{inbox_name}'
conversation_assignment: '[Assigned to you] - #%{display_id} has been assigned to you'
assigned_conversation_new_message: '[New message] - #%{display_id} %{content}'
conversation_mention: 'You have been mentioned in conversation [ID - %{display_id}] by %{name}'
conversations:
messages:
instagram_story_content: "%{story_sender} mentioned you in the story: "
instagram_story_content: '%{story_sender} mentioned you in the story: '
instagram_deleted_story_content: This story is no longer available.
deleted: This message was deleted
activity:
status:
resolved: "Conversation was marked resolved by %{user_name}"
contact_resolved: "Conversation was resolved by %{contact_name}"
open: "Conversation was reopened by %{user_name}"
pending: "Conversation was marked as pending by %{user_name}"
snoozed: "Conversation was snoozed by %{user_name}"
auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity"
resolved: 'Conversation was marked resolved by %{user_name}'
contact_resolved: 'Conversation was resolved by %{contact_name}'
open: 'Conversation was reopened by %{user_name}'
pending: 'Conversation was marked as pending by %{user_name}'
snoozed: 'Conversation was snoozed by %{user_name}'
auto_resolved: 'Conversation was marked resolved by system due to %{duration} days of inactivity'
assignee:
self_assigned: "%{user_name} self-assigned this conversation"
assigned: "Assigned to %{assignee_name} by %{user_name}"
removed: "Conversation unassigned by %{user_name}"
self_assigned: '%{user_name} self-assigned this conversation'
assigned: 'Assigned to %{assignee_name} by %{user_name}'
removed: 'Conversation unassigned by %{user_name}'
team:
assigned: "Assigned to %{team_name} by %{user_name}"
assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}"
removed: "Unassigned from %{team_name} by %{user_name}"
assigned: 'Assigned to %{team_name} by %{user_name}'
assigned_with_assignee: 'Assigned to %{assignee_name} via %{team_name} by %{user_name}'
removed: 'Unassigned from %{team_name} by %{user_name}'
labels:
added: "%{user_name} added %{labels}"
removed: "%{user_name} removed %{labels}"
muted: "%{user_name} has muted the conversation"
unmuted: "%{user_name} has unmuted the conversation"
added: '%{user_name} added %{labels}'
removed: '%{user_name} removed %{labels}'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
templates:
greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Give the team a way to reach you."
email_input_box_message_body: "Get notified by email"
csat_input_message_body: "Please rate the conversation"
greeting_message_body: '%{account_name} typically replies in a few hours.'
ways_to_reach_you_message_body: 'Give the team a way to reach you.'
email_input_box_message_body: 'Get notified by email'
csat_input_message_body: 'Please rate the conversation'
reply:
email:
header:
from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>'
reply_with_name: '%{assignee_name} from %{inbox_name} <reply+%{reply_email}>'
email_subject: "New messages on this conversation"
transcript_subject: "Conversation Transcript"
email_subject: 'New messages on this conversation'
transcript_subject: 'Conversation Transcript'
survey:
response: "Please rate this conversation, %{link}"
response: 'Please rate this conversation, %{link}'
contacts:
online:
delete: "%{contact_name} is Online, please try again later"
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.'
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."
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.'
webhooks:
name: "Webhooks"
name: 'Webhooks'
description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks."
dialogflow:
name: "Dialogflow"
description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent."
name: 'Dialogflow'
description: 'Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent.'
fullcontact:
name: "Fullcontact"
description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key."
name: 'Fullcontact'
description: 'FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key.'
public_portal:
search:
search_placeholder: Search for article by title or body...

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' do
collection do
post :create_a_meeting
post :add_participant_to_meeting
end
end
end
resources :working_hours, only: [:update]

View file

@ -38,7 +38,7 @@ class CsmlEngine
def process_response(response)
return response.parsed_response if response.success?
{ error: response.parsed_response, status: response.code }
{ error: response.parsed_response, status: response.code }.to_h
end
def post(path, payload)

56
lib/dyte.rb Normal file
View file

@ -0,0 +1,56 @@
class Dyte
BASE_URL = 'https://api.cluster.dyte.in/v1'.freeze
API_KEY_HEADER = 'Authorization'.freeze
def initialize(api_key, organization_id)
@api_key = api_key
@organization_id = organization_id
end
def create_a_meeting(title)
payload = {
'title': title,
'presetName': 'AV_Participant',
'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)
payload = {
'clientSpecificId': client_id,
'userDetails': {
'name': name,
'picture': avatar_url
},
'presetName': 'AV_Participant'
}
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 if response.success?
{ error: response.parsed_response, status: 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

View file

@ -0,0 +1,39 @@
class Integrations::Dyte::ProcessorService
pattr_initialize [:account!, :conversation!]
def create_a_meeting(agent)
response = dyte_client.create_a_meeting("Meeting with #{agent.available_name}")
return if response[:error].present?
meeting = response['data']['meeting']
@conversation.messages.create(
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :outgoing,
content_type: :integrations,
content_attributes: {
type: 'dyte',
data: { meeting_id: meeting['id'] }
},
sender: agent
}
)
end
def add_participant_to_meeting(meeting_id, user)
dyte_client.add_participant_to_meeting(meeting_id, user.id, user.name, user.avatar_url)
end
private
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['api_key'], credentials['organization_id'])
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB