chore: Provider API prototype (#3112)
Enabling Support for Whatsapp via 360Dialog as a prototype for the provider APIs. Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
40d0b2faf3
commit
bd7aeba484
31 changed files with 506 additions and 60 deletions
|
@ -101,7 +101,7 @@ FB_APP_SECRET=
|
||||||
FB_APP_ID=
|
FB_APP_ID=
|
||||||
|
|
||||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||||
IG_VERIFY_TOKEN
|
IG_VERIFY_TOKEN=
|
||||||
|
|
||||||
# Twitter
|
# Twitter
|
||||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||||
|
|
|
@ -96,6 +96,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
when 'telegram'
|
when 'telegram'
|
||||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
|
when 'whatsapp'
|
||||||
|
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
6
app/controllers/webhooks/whatsapp_controller.rb
Normal file
6
app/controllers/webhooks/whatsapp_controller.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class Webhooks::WhatsappController < ActionController::API
|
||||||
|
def process_payload
|
||||||
|
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="view-box fill-height">
|
<div class="view-box fill-height">
|
||||||
<div
|
<div
|
||||||
v-if="!currentChat.can_reply && !isATwilioWhatsappChannel"
|
v-if="!currentChat.can_reply && !isAWhatsappChannel"
|
||||||
class="banner messenger-policy--banner"
|
class="banner messenger-policy--banner"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!currentChat.can_reply && isATwilioWhatsappChannel"
|
v-if="!currentChat.can_reply && isAWhatsappChannel"
|
||||||
class="banner messenger-policy--banner"
|
class="banner messenger-policy--banner"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -156,7 +156,7 @@ export default {
|
||||||
return !!this.uiSettings.enter_to_send_enabled;
|
return !!this.uiSettings.enter_to_send_enabled;
|
||||||
},
|
},
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
if (this.currentChat.can_reply || this.isATwilioWhatsappChannel) {
|
if (this.currentChat.can_reply || this.isAWhatsappChannel) {
|
||||||
return this.isOnPrivateNote;
|
return this.isOnPrivateNote;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -203,7 +203,7 @@ export default {
|
||||||
if (this.isAFacebookInbox) {
|
if (this.isAFacebookInbox) {
|
||||||
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
||||||
}
|
}
|
||||||
if (this.isATwilioWhatsappChannel) {
|
if (this.isAWhatsappChannel) {
|
||||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||||
}
|
}
|
||||||
if (this.isATwilioSMSChannel) {
|
if (this.isATwilioSMSChannel) {
|
||||||
|
@ -278,7 +278,7 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReply || this.isATwilioWhatsappChannel) {
|
if (canReply || this.isAWhatsappChannel) {
|
||||||
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
||||||
} else {
|
} else {
|
||||||
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
|
@ -364,7 +364,7 @@ export default {
|
||||||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
||||||
const { can_reply: canReply } = this.currentChat;
|
const { can_reply: canReply } = this.currentChat;
|
||||||
|
|
||||||
if (canReply || this.isATwilioWhatsappChannel) this.replyType = mode;
|
if (canReply || this.isAWhatsappChannel) this.replyType = mode;
|
||||||
if (this.showRichContentEditor) {
|
if (this.showRichContentEditor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||||
? 'ion-social-whatsapp-outline'
|
? 'ion-social-whatsapp-outline'
|
||||||
: 'ion-android-textsms';
|
: 'ion-android-textsms';
|
||||||
|
|
||||||
|
case INBOX_TYPES.WHATSAPP:
|
||||||
|
return 'ion-social-whatsapp-outline';
|
||||||
|
|
||||||
case INBOX_TYPES.API:
|
case INBOX_TYPES.API:
|
||||||
return 'ion-cloud';
|
return 'ion-cloud';
|
||||||
|
|
||||||
|
|
|
@ -97,8 +97,8 @@
|
||||||
"SUBMIT_BUTTON": "Create inbox"
|
"SUBMIT_BUTTON": "Create inbox"
|
||||||
},
|
},
|
||||||
"TWILIO": {
|
"TWILIO": {
|
||||||
"TITLE": "Twilio SMS/Whatsapp Channel",
|
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or Whatsapp.",
|
"DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
|
||||||
"ACCOUNT_SID": {
|
"ACCOUNT_SID": {
|
||||||
"LABEL": "Account SID",
|
"LABEL": "Account SID",
|
||||||
"PLACEHOLDER": "Please enter your Twilio Account SID",
|
"PLACEHOLDER": "Please enter your Twilio Account SID",
|
||||||
|
@ -114,8 +114,8 @@
|
||||||
"ERROR": "This field is required"
|
"ERROR": "This field is required"
|
||||||
},
|
},
|
||||||
"CHANNEL_NAME": {
|
"CHANNEL_NAME": {
|
||||||
"LABEL": "Channel Name",
|
"LABEL": "Inbox Name",
|
||||||
"PLACEHOLDER": "Please enter a channel name",
|
"PLACEHOLDER": "Please enter a inbox name",
|
||||||
"ERROR": "This field is required"
|
"ERROR": "This field is required"
|
||||||
},
|
},
|
||||||
"PHONE_NUMBER": {
|
"PHONE_NUMBER": {
|
||||||
|
@ -136,9 +136,36 @@
|
||||||
"TITLE": "SMS Channel via Twilio",
|
"TITLE": "SMS Channel via Twilio",
|
||||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||||
},
|
},
|
||||||
"WHATSAPP": {
|
"WHATSAPP": {
|
||||||
"TITLE": "Whatsapp Channel via Twilio",
|
"TITLE": "WhatsApp Channel",
|
||||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
"DESC": "Start supporting your customers via WhatsApp.",
|
||||||
|
"PROVIDERS": {
|
||||||
|
"LABEL": "API Provider",
|
||||||
|
"TWILIO": "Twilio",
|
||||||
|
"360_DIALOG": "360Dialog"
|
||||||
|
},
|
||||||
|
"INBOX_NAME": {
|
||||||
|
"LABEL": "Inbox Name",
|
||||||
|
"PLACEHOLDER": "Please enter an inbox name",
|
||||||
|
"ERROR": "This field is required"
|
||||||
|
},
|
||||||
|
"PHONE_NUMBER": {
|
||||||
|
"LABEL": "Phone number",
|
||||||
|
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
|
||||||
|
"ERROR": "Please enter a valid value. Phone number should start with `+` sign."
|
||||||
|
},
|
||||||
|
"API_KEY": {
|
||||||
|
"LABEL": "API key",
|
||||||
|
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||||
|
"PLACEHOLDER": "API key",
|
||||||
|
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||||
|
"ERROR": "Please enter a valid value."
|
||||||
|
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"API_CHANNEL": {
|
"API_CHANNEL": {
|
||||||
"TITLE": "API Channel",
|
"TITLE": "API Channel",
|
||||||
|
@ -221,7 +248,7 @@
|
||||||
},
|
},
|
||||||
"AUTH": {
|
"AUTH": {
|
||||||
"TITLE": "Choose a channel",
|
"TITLE": "Choose a channel",
|
||||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||||
},
|
},
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"TITLE": "Agents",
|
"TITLE": "Agents",
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
{ key: 'website', name: 'Website' },
|
{ key: 'website', name: 'Website' },
|
||||||
{ key: 'facebook', name: 'Messenger' },
|
{ key: 'facebook', name: 'Messenger' },
|
||||||
{ key: 'twitter', name: 'Twitter' },
|
{ key: 'twitter', name: 'Twitter' },
|
||||||
{ key: 'whatsapp', name: 'WhatsApp via Twilio' },
|
{ key: 'whatsapp', name: 'WhatsApp' },
|
||||||
{ key: 'sms', name: 'SMS via Twilio' },
|
{ key: 'sms', name: 'SMS via Twilio' },
|
||||||
{ key: 'email', name: 'Email' },
|
{ key: 'email', name: 'Email' },
|
||||||
{
|
{
|
||||||
|
|
|
@ -45,6 +45,9 @@
|
||||||
<span v-if="item.channel_type === 'Channel::TwilioSms'">
|
<span v-if="item.channel_type === 'Channel::TwilioSms'">
|
||||||
Twilio SMS
|
Twilio SMS
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.channel_type === 'Channel::Whatsapp'">
|
||||||
|
Whatsapp
|
||||||
|
</span>
|
||||||
<span v-if="item.channel_type === 'Channel::Email'">
|
<span v-if="item.channel_type === 'Channel::Email'">
|
||||||
Email
|
Email
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -245,10 +245,7 @@
|
||||||
@click="updateInbox"
|
@click="updateInbox"
|
||||||
/>
|
/>
|
||||||
</settings-section>
|
</settings-section>
|
||||||
<facebook-reauthorize
|
<facebook-reauthorize v-if="isAFacebookInbox" :inbox-id="inbox.id" />
|
||||||
v-if="isAFacebookInbox && inbox.reauthorization_required"
|
|
||||||
:inbox-id="inbox.id"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- update agents in inbox -->
|
<!-- update agents in inbox -->
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<form class="row" @submit.prevent="createChannel()">
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.inboxName.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="inboxName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.PLACEHOLDER')"
|
||||||
|
@blur="$v.inboxName.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.inboxName.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.phoneNumber.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="phoneNumber"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.PLACEHOLDER')"
|
||||||
|
@blur="$v.phoneNumber.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.phoneNumber.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.apiKey.$error }">
|
||||||
|
<span>
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.LABEL') }}
|
||||||
|
<a
|
||||||
|
href="https://hub.360dialog.com/lp/whatsapp/L9dj7aPA"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
({{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.APPLY_FOR_ACCESS') }})
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model.trim="apiKey"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.PLACEHOLDER')"
|
||||||
|
@blur="$v.apiKey.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.apiKey.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-submit-button
|
||||||
|
:loading="uiFlags.isCreating"
|
||||||
|
:button-text="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
import router from '../../../../index';
|
||||||
|
|
||||||
|
const shouldStartWithPlusSign = (value = '') => value.startsWith('+');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [alertMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inboxName: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
apiKey: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'inboxes/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
inboxName: { required },
|
||||||
|
phoneNumber: { required, shouldStartWithPlusSign },
|
||||||
|
apiKey: { required },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async createChannel() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const whatsappChannel = await this.$store.dispatch(
|
||||||
|
'inboxes/createChannel',
|
||||||
|
{
|
||||||
|
name: this.inboxName,
|
||||||
|
channel: {
|
||||||
|
type: 'whatsapp',
|
||||||
|
phone_number: this.phoneNumber,
|
||||||
|
provider_config: {
|
||||||
|
api_key: this.apiKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.replace({
|
||||||
|
name: 'settings_inboxes_add_agents',
|
||||||
|
params: {
|
||||||
|
page: 'new',
|
||||||
|
inbox_id: whatsappChannel.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -206,7 +206,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scope: 'pages_manage_metadata,pages_messaging,instagram_basic,pages_show_list,instagram_manage_messages',
|
scope:
|
||||||
|
'pages_manage_metadata,pages_messaging,instagram_basic,pages_show_list,instagram_manage_messages',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -79,12 +79,12 @@ const shouldStartWithPlusSign = (value = '') => value.startsWith('+');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin],
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
accountSID: '',
|
accountSID: '',
|
||||||
|
|
|
@ -1,22 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wizard-body small-9 columns">
|
<div class="wizard-body small-9 columns">
|
||||||
<page-header
|
<page-header
|
||||||
:header-title="$t('INBOX_MGMT.ADD.WHATSAPP.TITLE')"
|
:header-title="$t('INBOX_MGMT.ADD.WHATSAPP.TITLE')"
|
||||||
:header-content="$t('INBOX_MGMT.ADD.WHATSAPP.DESC')"
|
:header-content="$t('INBOX_MGMT.ADD.WHATSAPP.DESC')"
|
||||||
/>
|
/>
|
||||||
<twilio type="whatsapp"></twilio>
|
<div class="medium-8 columns">
|
||||||
|
<label>
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.LABEL') }}
|
||||||
|
<select v-model="provider">
|
||||||
|
<option value="twilio">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO') }}
|
||||||
|
</option>
|
||||||
|
<option value="360dialog">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.360_DIALOG') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<twilio v-if="provider === 'twilio'" type="whatsapp"></twilio>
|
||||||
|
<three-sixty-dialog-whatsapp v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import PageHeader from '../../SettingsSubPageHeader';
|
import PageHeader from '../../SettingsSubPageHeader';
|
||||||
import Twilio from './Twilio';
|
import Twilio from './Twilio';
|
||||||
|
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Twilio,
|
Twilio,
|
||||||
|
ThreeSixtyDialogWhatsapp,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
provider: 'twilio',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -79,7 +79,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scope: 'pages_manage_metadata,pages_messaging',
|
scope:
|
||||||
|
'pages_manage_metadata,pages_messaging,instagram_basic,pages_show_list,instagram_manage_messages',
|
||||||
auth_type: 'reauthorize',
|
auth_type: 'reauthorize',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ export const INBOX_TYPES = {
|
||||||
FB: 'Channel::FacebookPage',
|
FB: 'Channel::FacebookPage',
|
||||||
TWITTER: 'Channel::TwitterProfile',
|
TWITTER: 'Channel::TwitterProfile',
|
||||||
TWILIO: 'Channel::TwilioSms',
|
TWILIO: 'Channel::TwilioSms',
|
||||||
|
WHATSAPP: 'Channel::Whatsapp',
|
||||||
API: 'Channel::Api',
|
API: 'Channel::Api',
|
||||||
EMAIL: 'Channel::Email',
|
EMAIL: 'Channel::Email',
|
||||||
TELEGRAM: 'Channel::Telegram',
|
TELEGRAM: 'Channel::Telegram',
|
||||||
|
@ -68,5 +69,11 @@ export default {
|
||||||
}
|
}
|
||||||
return this.channelType;
|
return this.channelType;
|
||||||
},
|
},
|
||||||
|
isAWhatsappChannel() {
|
||||||
|
return (
|
||||||
|
this.channelType === INBOX_TYPES.WHATSAPP ||
|
||||||
|
this.isATwilioWhatsappChannel
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,11 +8,7 @@ class SendReplyJob < ApplicationJob
|
||||||
|
|
||||||
case channel_name
|
case channel_name
|
||||||
when 'Channel::FacebookPage'
|
when 'Channel::FacebookPage'
|
||||||
if conversation.additional_attributes['type'] == 'instagram_direct_message'
|
send_on_facebook_page(message)
|
||||||
::Instagram::SendOnInstagramService.new(message: message).perform
|
|
||||||
else
|
|
||||||
::Facebook::SendOnFacebookService.new(message: message).perform
|
|
||||||
end
|
|
||||||
when 'Channel::TwitterProfile'
|
when 'Channel::TwitterProfile'
|
||||||
::Twitter::SendOnTwitterService.new(message: message).perform
|
::Twitter::SendOnTwitterService.new(message: message).perform
|
||||||
when 'Channel::TwilioSms'
|
when 'Channel::TwilioSms'
|
||||||
|
@ -21,6 +17,18 @@ class SendReplyJob < ApplicationJob
|
||||||
::Line::SendOnLineService.new(message: message).perform
|
::Line::SendOnLineService.new(message: message).perform
|
||||||
when 'Channel::Telegram'
|
when 'Channel::Telegram'
|
||||||
::Telegram::SendOnTelegramService.new(message: message).perform
|
::Telegram::SendOnTelegramService.new(message: message).perform
|
||||||
|
when 'Channel::Whatsapp'
|
||||||
|
::Whatsapp::SendOnWhatsappService.new(message: message).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_on_facebook_page(message)
|
||||||
|
if message.conversation.additional_attributes['type'] == 'instagram_direct_message'
|
||||||
|
::Instagram::SendOnInstagramService.new(message: message).perform
|
||||||
|
else
|
||||||
|
::Facebook::SendOnFacebookService.new(message: message).perform
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
13
app/jobs/webhooks/whatsapp_events_job.rb
Normal file
13
app/jobs/webhooks/whatsapp_events_job.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
return unless params[:phone_number]
|
||||||
|
|
||||||
|
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||||
|
return unless channel
|
||||||
|
|
||||||
|
# TODO: pass to appropriate provider service from here
|
||||||
|
Whatsapp::IncomingMessageService.new(inbox: channel.inbox, params: params['whatsapp'].with_indifferent_access).perform
|
||||||
|
end
|
||||||
|
end
|
|
@ -53,6 +53,7 @@ class Account < ApplicationRecord
|
||||||
has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api'
|
has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api'
|
||||||
has_many :line_channels, dependent: :destroy, class_name: '::Channel::Line'
|
has_many :line_channels, dependent: :destroy, class_name: '::Channel::Line'
|
||||||
has_many :telegram_channels, dependent: :destroy, class_name: '::Channel::Telegram'
|
has_many :telegram_channels, dependent: :destroy, class_name: '::Channel::Telegram'
|
||||||
|
has_many :whatsapp_channels, dependent: :destroy, class_name: '::Channel::Whatsapp'
|
||||||
has_many :canned_responses, dependent: :destroy
|
has_many :canned_responses, dependent: :destroy
|
||||||
has_many :webhooks, dependent: :destroy
|
has_many :webhooks, dependent: :destroy
|
||||||
has_many :labels, dependent: :destroy
|
has_many :labels, dependent: :destroy
|
||||||
|
|
67
app/models/channel/whatsapp.rb
Normal file
67
app/models/channel/whatsapp.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: channel_whatsapp
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# phone_number :string not null
|
||||||
|
# provider :string default("default")
|
||||||
|
# provider_config :jsonb
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_channel_whatsapp_on_phone_number (phone_number) UNIQUE
|
||||||
|
#
|
||||||
|
|
||||||
|
class Channel::Whatsapp < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
|
self.table_name = 'channel_whatsapp'
|
||||||
|
EDITABLE_ATTRS = [:phone_number, { provider_config: {} }].freeze
|
||||||
|
|
||||||
|
validates :phone_number, presence: true, uniqueness: true
|
||||||
|
before_save :validate_provider_config
|
||||||
|
|
||||||
|
def name
|
||||||
|
'Whatsapp'
|
||||||
|
end
|
||||||
|
|
||||||
|
# all this should happen in provider service . but hack mode on
|
||||||
|
def api_base_path
|
||||||
|
# provide the environment variable when testing against sandbox : 'https://waba-sandbox.360dialog.io/v1'
|
||||||
|
ENV.fetch('360DIALOG_BASE_URL', 'https://waba.360dialog.io/v1')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extract later into provider Service
|
||||||
|
def send_message(phone_number, message)
|
||||||
|
HTTParty.post(
|
||||||
|
"#{api_base_path}/messages",
|
||||||
|
headers: { 'D360-API-KEY': provider_config['api_key'], 'Content-Type': 'application/json' },
|
||||||
|
body: {
|
||||||
|
to: phone_number,
|
||||||
|
text: { body: message },
|
||||||
|
type: 'text'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_24_hour_messaging_window?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Extract later into provider Service
|
||||||
|
def validate_provider_config
|
||||||
|
response = HTTParty.post(
|
||||||
|
"#{api_base_path}/configs/webhook",
|
||||||
|
headers: { 'D360-API-KEY': provider_config['api_key'], 'Content-Type': 'application/json' },
|
||||||
|
body: {
|
||||||
|
url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}"
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
errors.add(:bot_token, 'error setting up the webook') unless response.success?
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,12 +9,18 @@ class Contacts::ContactableInboxesService
|
||||||
private
|
private
|
||||||
|
|
||||||
def get_contactable_inbox(inbox)
|
def get_contactable_inbox(inbox)
|
||||||
return twilio_contactable_inbox(inbox) if inbox.channel_type == 'Channel::TwilioSms'
|
case inbox.channel_type
|
||||||
return email_contactable_inbox(inbox) if inbox.channel_type == 'Channel::Email'
|
when 'Channel::TwilioSms'
|
||||||
return api_contactable_inbox(inbox) if inbox.channel_type == 'Channel::Api'
|
twilio_contactable_inbox(inbox)
|
||||||
return website_contactable_inbox(inbox) if inbox.channel_type == 'Channel::WebWidget'
|
when 'Channel::Whatsapp'
|
||||||
|
whatsapp_contactable_inbox(inbox)
|
||||||
nil
|
when 'Channel::Email'
|
||||||
|
email_contactable_inbox(inbox)
|
||||||
|
when 'Channel::Api'
|
||||||
|
api_contactable_inbox(inbox)
|
||||||
|
when 'Channel::WebWidget'
|
||||||
|
website_contactable_inbox(inbox)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def website_contactable_inbox(inbox)
|
def website_contactable_inbox(inbox)
|
||||||
|
@ -39,6 +45,13 @@ class Contacts::ContactableInboxesService
|
||||||
{ source_id: @contact.email, inbox: inbox }
|
{ source_id: @contact.email, inbox: inbox }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def whatsapp_contactable_inbox(inbox)
|
||||||
|
return unless @contact.phone_number
|
||||||
|
|
||||||
|
# Remove the plus since thats the format 360 dialog uses
|
||||||
|
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
|
||||||
|
end
|
||||||
|
|
||||||
def twilio_contactable_inbox(inbox)
|
def twilio_contactable_inbox(inbox)
|
||||||
return if @contact.phone_number.blank?
|
return if @contact.phone_number.blank?
|
||||||
|
|
||||||
|
|
61
app/services/whatsapp/incoming_message_service.rb
Normal file
61
app/services/whatsapp/incoming_message_service.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# Find the various telegram payload samples here: https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates
|
||||||
|
# https://core.telegram.org/bots/api#available-types
|
||||||
|
|
||||||
|
class Whatsapp::IncomingMessageService
|
||||||
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
set_contact
|
||||||
|
return unless @contact
|
||||||
|
|
||||||
|
set_conversation
|
||||||
|
|
||||||
|
return if params[:messages].blank?
|
||||||
|
|
||||||
|
@message = @conversation.messages.create(
|
||||||
|
content: params[:messages].first.dig(:text, :body),
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
inbox_id: @inbox.id,
|
||||||
|
message_type: :incoming,
|
||||||
|
sender: @contact,
|
||||||
|
source_id: params[:messages].first[:id].to_s
|
||||||
|
)
|
||||||
|
@message.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_contact
|
||||||
|
contact_params = params[:contacts]&.first
|
||||||
|
return if contact_params.blank?
|
||||||
|
|
||||||
|
contact_inbox = ::ContactBuilder.new(
|
||||||
|
source_id: contact_params[:wa_id],
|
||||||
|
inbox: inbox,
|
||||||
|
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{params[:messages].first[:from]}" }
|
||||||
|
).perform
|
||||||
|
|
||||||
|
@contact_inbox = contact_inbox
|
||||||
|
@contact = contact_inbox.contact
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
{
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
inbox_id: @inbox.id,
|
||||||
|
contact_id: @contact.id,
|
||||||
|
contact_inbox_id: @contact_inbox.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
@conversation = @contact_inbox.conversations.last
|
||||||
|
return if @conversation
|
||||||
|
|
||||||
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
end
|
11
app/services/whatsapp/send_on_whatsapp_service.rb
Normal file
11
app/services/whatsapp/send_on_whatsapp_service.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
||||||
|
private
|
||||||
|
|
||||||
|
def channel_class
|
||||||
|
Channel::Whatsapp
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_reply
|
||||||
|
channel.send_message(message.conversation.contact_inbox.source_id, message.content)
|
||||||
|
end
|
||||||
|
end
|
|
@ -251,6 +251,7 @@ Rails.application.routes.draw do
|
||||||
post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events'
|
post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events'
|
||||||
post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload'
|
post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload'
|
||||||
post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload'
|
post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload'
|
||||||
|
post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload'
|
||||||
get 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#verify'
|
get 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#verify'
|
||||||
post 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#events'
|
post 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#events'
|
||||||
|
|
||||||
|
|
11
db/migrate/20210916112533_add_whatsapp_channel.rb
Normal file
11
db/migrate/20210916112533_add_whatsapp_channel.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class AddWhatsappChannel < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :channel_whatsapp do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.string :phone_number, null: false, index: { unique: true }
|
||||||
|
t.string :provider, default: 'default'
|
||||||
|
t.jsonb :provider_config, default: {}
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
db/schema.rb
24
db/schema.rb
|
@ -245,26 +245,14 @@ ActiveRecord::Schema.define(version: 2021_09_22_082754) do
|
||||||
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
|
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "companies", force: :cascade do |t|
|
create_table "channel_whatsapp", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.integer "account_id", null: false
|
||||||
t.text "address"
|
t.string "phone_number", null: false
|
||||||
t.string "city", null: false
|
t.string "provider", default: "default"
|
||||||
t.string "state"
|
t.jsonb "provider_config", default: {}
|
||||||
t.string "country", null: false
|
|
||||||
t.integer "no_of_employees", null: false
|
|
||||||
t.string "industry_type"
|
|
||||||
t.bigint "annual_revenue"
|
|
||||||
t.text "website"
|
|
||||||
t.string "office_phone_number"
|
|
||||||
t.string "facebook"
|
|
||||||
t.string "twitter"
|
|
||||||
t.string "linkedin"
|
|
||||||
t.jsonb "additional_attributes"
|
|
||||||
t.bigint "contact_id"
|
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.index ["contact_id"], name: "index_companies_on_contact_id"
|
t.index ["phone_number"], name: "index_channel_whatsapp_on_phone_number", unique: true
|
||||||
t.index ["name"], name: "index_companies_on_name", unique: true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "contact_inboxes", force: :cascade do |t|
|
create_table "contact_inboxes", force: :cascade do |t|
|
||||||
|
|
12
spec/controllers/webhooks/whatsapp_controller_spec.rb
Normal file
12
spec/controllers/webhooks/whatsapp_controller_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Webhooks::WhatsappController', type: :request do
|
||||||
|
describe 'POST /webhooks/whatsapp/{:phone_number}' do
|
||||||
|
it 'call the whatsapp events job with the params' do
|
||||||
|
allow(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||||
|
expect(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||||
|
post '/webhooks/whatsapp/123221321', params: { content: 'hello' }
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
spec/factories/channel/channel_whatsapp.rb
Normal file
11
spec/factories/channel/channel_whatsapp.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :channel_whatsapp, class: 'Channel::Whatsapp' do
|
||||||
|
sequence(:phone_number) { |n| "+123456789#{n}1" }
|
||||||
|
account
|
||||||
|
provider_config { { 'api_key' => 'test_key' } }
|
||||||
|
|
||||||
|
after(:create) do |channel_whatsapp|
|
||||||
|
create(:inbox, channel: channel_whatsapp, account: channel_whatsapp.account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -64,5 +64,14 @@ RSpec.describe SendReplyJob, type: :job do
|
||||||
expect(process_service).to receive(:perform)
|
expect(process_service).to receive(:perform)
|
||||||
described_class.perform_now(message.id)
|
described_class.perform_now(message.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls ::Whatsapp:SendOnWhatsappService when its line message' do
|
||||||
|
whatsapp_channel = create(:channel_whatsapp)
|
||||||
|
message = create(:message, conversation: create(:conversation, inbox: whatsapp_channel.inbox))
|
||||||
|
allow(::Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message).and_return(process_service)
|
||||||
|
expect(::Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
21
spec/services/whatsapp/incoming_message_service_spec.rb
Normal file
21
spec/services/whatsapp/incoming_message_service_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::IncomingMessageService do
|
||||||
|
let!(:whatsapp_channel) { create(:channel_whatsapp) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when valid text message params' do
|
||||||
|
it 'creates appropriate conversations, message and contacts' do
|
||||||
|
params = {
|
||||||
|
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => '2423423243' }],
|
||||||
|
'messages' => [{ 'from' => '2423423243', 'id' => 'SDFADSf23sfasdafasdfa', 'text' => { 'body' => 'Test' },
|
||||||
|
'timestamp' => '1633034394', 'type' => 'text' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||||
|
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
|
||||||
|
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||||
|
expect(whatsapp_channel.inbox.messages.first.content).to eq('Test')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
spec/services/whatsapp/send_on_whatsapp_service_spec.rb
Normal file
23
spec/services/whatsapp/send_on_whatsapp_service_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::SendOnWhatsappService do
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when a valid message' do
|
||||||
|
it 'calls channel.send_message' do
|
||||||
|
whatsapp_request = double
|
||||||
|
whatsapp_channel = create(:channel_whatsapp)
|
||||||
|
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789')
|
||||||
|
conversation = create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox)
|
||||||
|
message = create(:message, message_type: :outgoing, content: 'test',
|
||||||
|
conversation: conversation)
|
||||||
|
allow(HTTParty).to receive(:post).and_return(whatsapp_request)
|
||||||
|
expect(HTTParty).to receive(:post).with(
|
||||||
|
'https://waba.360dialog.io/v1/messages',
|
||||||
|
headers: { 'D360-API-KEY': 'test_key', 'Content-Type': 'application/json' },
|
||||||
|
body: { to: '123456789', text: { body: 'test' }, type: 'text' }.to_json
|
||||||
|
)
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue