parent
8e59564793
commit
a1a81e3799
44 changed files with 918 additions and 33 deletions
4
Gemfile
4
Gemfile
|
@ -25,7 +25,7 @@ gem 'uglifier'
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'mini_magick'
|
gem 'mini_magick'
|
||||||
|
|
||||||
|
@ -62,9 +62,9 @@ gem 'chargebee'
|
||||||
##--- gems for channels ---##
|
##--- gems for channels ---##
|
||||||
gem 'facebook-messenger'
|
gem 'facebook-messenger'
|
||||||
gem 'telegram-bot-ruby'
|
gem 'telegram-bot-ruby'
|
||||||
|
gem 'twilio-ruby', '~> 5.32.0'
|
||||||
# twitty will handle subscription of twitter account events
|
# twitty will handle subscription of twitter account events
|
||||||
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
|
|
||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# Random name generator
|
# Random name generator
|
||||||
|
|
33
Gemfile.lock
33
Gemfile.lock
|
@ -102,15 +102,13 @@ GEM
|
||||||
descendants_tracker (~> 0.0.4)
|
descendants_tracker (~> 0.0.4)
|
||||||
ice_nine (~> 0.11.0)
|
ice_nine (~> 0.11.0)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
azure-core (0.1.15)
|
azure-storage-blob (2.0.0)
|
||||||
faraday (~> 0.9)
|
azure-storage-common (~> 2.0)
|
||||||
faraday_middleware (~> 0.10)
|
nokogiri (~> 1.10.4)
|
||||||
nokogiri (~> 1.6)
|
azure-storage-common (2.0.1)
|
||||||
azure-storage (0.15.0.preview)
|
faraday (~> 1.0)
|
||||||
azure-core (~> 0.1)
|
faraday_middleware (~> 1.0.0.rc1)
|
||||||
faraday (~> 0.9)
|
nokogiri (~> 1.10.4)
|
||||||
faraday_middleware (~> 0.10)
|
|
||||||
nokogiri (~> 1.6, >= 1.6.8)
|
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.4.6)
|
bootsnap (1.4.6)
|
||||||
|
@ -172,10 +170,10 @@ GEM
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
faker (2.11.0)
|
faker (2.11.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (0.17.3)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday_middleware (0.14.0)
|
faraday_middleware (1.0.0)
|
||||||
faraday (>= 0.7.4, < 1.0)
|
faraday (~> 1.0)
|
||||||
ffi (1.12.2)
|
ffi (1.12.2)
|
||||||
flag_shih_tzu (0.3.23)
|
flag_shih_tzu (0.3.23)
|
||||||
foreman (0.87.1)
|
foreman (0.87.1)
|
||||||
|
@ -410,8 +408,8 @@ GEM
|
||||||
activerecord (>= 4)
|
activerecord (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
sentry-raven (2.13.0)
|
sentry-raven (3.0.0)
|
||||||
faraday (>= 0.7.6, < 1.0)
|
faraday (>= 1.0)
|
||||||
shoulda-matchers (4.3.0)
|
shoulda-matchers (4.3.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
sidekiq (6.0.6)
|
sidekiq (6.0.6)
|
||||||
|
@ -449,6 +447,10 @@ GEM
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
i18n
|
i18n
|
||||||
|
twilio-ruby (5.32.0)
|
||||||
|
faraday (~> 1.0.0)
|
||||||
|
jwt (>= 1.5, <= 2.5)
|
||||||
|
nokogiri (>= 1.6, < 2.0)
|
||||||
tzinfo (1.2.7)
|
tzinfo (1.2.7)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2019.3)
|
tzinfo-data (1.2019.3)
|
||||||
|
@ -496,7 +498,7 @@ DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage
|
azure-storage-blob
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
browser
|
browser
|
||||||
|
@ -553,6 +555,7 @@ DEPENDENCIES
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
telegram-bot-ruby
|
telegram-bot-ruby
|
||||||
time_diff
|
time_diff
|
||||||
|
twilio-ruby (~> 5.32.0)
|
||||||
twitty!
|
twitty!
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier
|
uglifier
|
||||||
|
|
37
app/builders/contact_builder.rb
Normal file
37
app/builders/contact_builder.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
class ContactBuilder
|
||||||
|
pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
||||||
|
return contact_inbox if contact_inbox
|
||||||
|
|
||||||
|
build_contact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_contact
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
contact = account.contacts.create!(
|
||||||
|
name: contact_attributes[:name],
|
||||||
|
phone_number: contact_attributes[:phone_number],
|
||||||
|
email: contact_attributes[:email],
|
||||||
|
identifier: contact_attributes[:identifier],
|
||||||
|
additional_attributes: contact_attributes[:identifier]
|
||||||
|
)
|
||||||
|
contact_inbox = ::ContactInbox.create!(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: inbox.id,
|
||||||
|
source_id: source_id
|
||||||
|
)
|
||||||
|
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||||
|
contact_inbox
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
|
||||||
|
before_action :authorize_request
|
||||||
|
|
||||||
|
def create
|
||||||
|
authenticate_twilio
|
||||||
|
build_inbox
|
||||||
|
setup_webhooks
|
||||||
|
rescue Twilio::REST::TwilioError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
rescue StandardError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorize_request
|
||||||
|
authorize ::Inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_twilio
|
||||||
|
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||||
|
client.messages.list(limit: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_webhooks
|
||||||
|
::Twilio::WebhookSetupService.new(inbox: @inbox).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_inbox
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
twilio_sms = current_account.twilio_sms.create(
|
||||||
|
account_sid: permitted_params[:account_sid],
|
||||||
|
auth_token: permitted_params[:auth_token],
|
||||||
|
phone_number: permitted_params[:phone_number]
|
||||||
|
)
|
||||||
|
@inbox = current_account.inboxes.create(
|
||||||
|
name: permitted_params[:name],
|
||||||
|
channel: twilio_sms
|
||||||
|
)
|
||||||
|
rescue StandardError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.require(:twilio_channel).permit(
|
||||||
|
:account_id, :phone_number, :account_sid, :auth_token, :name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
29
app/controllers/twilio/callback_controller.rb
Normal file
29
app/controllers/twilio/callback_controller.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
class Twilio::CallbackController < ApplicationController
|
||||||
|
def create
|
||||||
|
::Twilio::IncomingMessageService.new(params: permitted_params).perform
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(
|
||||||
|
:ApiVersion,
|
||||||
|
:SmsSid,
|
||||||
|
:From,
|
||||||
|
:ToState,
|
||||||
|
:ToZip,
|
||||||
|
:AccountSid,
|
||||||
|
:MessageSid,
|
||||||
|
:FromCountry,
|
||||||
|
:ToCity,
|
||||||
|
:FromCity,
|
||||||
|
:To,
|
||||||
|
:FromZip,
|
||||||
|
:Body,
|
||||||
|
:ToCountry,
|
||||||
|
:FromState
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class TwilioChannel extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('channels/twilio_channel', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TwilioChannel();
|
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -56,6 +56,7 @@ const INBOX_TYPES = {
|
||||||
WEB: 'Channel::WebWidget',
|
WEB: 'Channel::WebWidget',
|
||||||
FB: 'Channel::FacebookPage',
|
FB: 'Channel::FacebookPage',
|
||||||
TWITTER: 'Channel::TwitterProfile',
|
TWITTER: 'Channel::TwitterProfile',
|
||||||
|
TWILIO: 'Channel::TwilioSms',
|
||||||
};
|
};
|
||||||
const getInboxClassByType = type => {
|
const getInboxClassByType = type => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -68,6 +69,9 @@ const getInboxClassByType = type => {
|
||||||
case INBOX_TYPES.TWITTER:
|
case INBOX_TYPES.TWITTER:
|
||||||
return 'ion-social-twitter';
|
return 'ion-social-twitter';
|
||||||
|
|
||||||
|
case INBOX_TYPES.TWILIO:
|
||||||
|
return 'ion-android-textsms';
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
v-if="channel === 'website'"
|
v-if="channel === 'website'"
|
||||||
src="~dashboard/assets/images/channels/website.png"
|
src="~dashboard/assets/images/channels/website.png"
|
||||||
/>
|
/>
|
||||||
|
<img
|
||||||
|
v-if="channel === 'twilio'"
|
||||||
|
src="~dashboard/assets/images/channels/twilio.png"
|
||||||
|
/>
|
||||||
<h3 class="channel__title">
|
<h3 class="channel__title">
|
||||||
{{ channel }}
|
{{ channel }}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -39,7 +43,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isActive(channel) {
|
isActive(channel) {
|
||||||
return ['facebook', 'website', 'twitter'].includes(channel);
|
return ['facebook', 'website', 'twitter', 'twilio'].includes(channel);
|
||||||
},
|
},
|
||||||
onItemClick() {
|
onItemClick() {
|
||||||
if (this.isActive(this.channel)) {
|
if (this.isActive(this.channel)) {
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"WEBSITE_CHANNEL": {
|
"WEBSITE_CHANNEL": {
|
||||||
"TITLE": "Website channel",
|
"TITLE": "Website channel",
|
||||||
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",
|
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",
|
||||||
|
"LOADING_MESSAGE": "Creating Website Support Channel",
|
||||||
"CHANNEL_NAME": {
|
"CHANNEL_NAME": {
|
||||||
"LABEL": "Website Name",
|
"LABEL": "Website Name",
|
||||||
"PLACEHOLDER": "Enter your website name (eg: Acme Inc)"
|
"PLACEHOLDER": "Enter your website name (eg: Acme Inc)"
|
||||||
|
@ -35,6 +36,34 @@
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON":"Create inbox"
|
"SUBMIT_BUTTON":"Create inbox"
|
||||||
},
|
},
|
||||||
|
"TWILIO": {
|
||||||
|
"TITLE": "Twilio SMS Channel",
|
||||||
|
"DESC": "Integrate Twilio and start supporting your customers via SMS.",
|
||||||
|
"ACCOUNT_SID": {
|
||||||
|
"LABEL": "Account SID",
|
||||||
|
"PLACEHOLDER": "Please enter your Twilio Account SID",
|
||||||
|
"ERROR": "This field is required"
|
||||||
|
},
|
||||||
|
"AUTH_TOKEN": {
|
||||||
|
"LABEL": "Auth Token",
|
||||||
|
"PLACEHOLDER": "Please enter your Twilio Auth Token",
|
||||||
|
"ERROR": "This field is required"
|
||||||
|
},
|
||||||
|
"CHANNEL_NAME": {
|
||||||
|
"LABEL": "Channel Name",
|
||||||
|
"PLACEHOLDER": "Please enter a channel 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."
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Create Twilio Channel",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
|
||||||
|
}
|
||||||
|
},
|
||||||
"AUTH": {
|
"AUTH": {
|
||||||
"TITLE": "Channels",
|
"TITLE": "Channels",
|
||||||
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
|
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
|
||||||
|
|
|
@ -128,11 +128,11 @@ export default {
|
||||||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||||
this.onClose();
|
this.onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response.status === 422) {
|
if (error.response.status === 422) {
|
||||||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE'));
|
this.showAlert(this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE'));
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE'));
|
this.showAlert(this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,7 +27,14 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
channelList: ['website', 'facebook', 'twitter', 'telegram', 'line'],
|
channelList: [
|
||||||
|
'website',
|
||||||
|
'facebook',
|
||||||
|
'twitter',
|
||||||
|
'twilio',
|
||||||
|
'telegram',
|
||||||
|
'line',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -28,12 +28,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import EmptyState from '../../../../components/widgets/EmptyState';
|
import EmptyState from '../../../../components/widgets/EmptyState';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
},
|
},
|
||||||
|
mixins: [configMixin],
|
||||||
computed: {
|
computed: {
|
||||||
currentInbox() {
|
currentInbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](
|
return this.$store.getters['inboxes/getInbox'](
|
||||||
|
|
|
@ -42,6 +42,9 @@
|
||||||
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
|
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
|
||||||
Twitter
|
Twitter
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.channel_type === 'Channel::TwilioSms'">
|
||||||
|
Twilio SMS
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="settings columns container">
|
<div class="settings columns container">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
:header-image="inbox.avatarUrl"
|
:header-image="inbox.avatarUrl"
|
||||||
:header-title="inbox.name"
|
:header-title="inboxName"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="inbox.channel_type === 'Channel::FacebookPage'"
|
v-if="inbox.channel_type === 'Channel::FacebookPage'"
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
||||||
import { Compact } from 'vue-color';
|
import { Compact } from 'vue-color';
|
||||||
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue';
|
import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -94,6 +95,7 @@ export default {
|
||||||
Compact,
|
Compact,
|
||||||
SettingsFormHeader,
|
SettingsFormHeader,
|
||||||
},
|
},
|
||||||
|
mixins: [configMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedAgents: [],
|
selectedAgents: [],
|
||||||
|
@ -113,6 +115,12 @@ export default {
|
||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
|
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
|
||||||
},
|
},
|
||||||
|
inboxName() {
|
||||||
|
if (this.inbox.channel_type === 'Channel::TwilioSms') {
|
||||||
|
return `${this.inbox.name} (${this.inbox.phone_number})`;
|
||||||
|
}
|
||||||
|
return this.inbox.name;
|
||||||
|
},
|
||||||
messengerScript() {
|
messengerScript() {
|
||||||
return createMessengerScript(this.inbox.page_id);
|
return createMessengerScript(this.inbox.page_id);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import Facebook from './channels/Facebook';
|
import Facebook from './channels/Facebook';
|
||||||
import Website from './channels/Website';
|
import Website from './channels/Website';
|
||||||
import Twitter from './channels/Twitter';
|
import Twitter from './channels/Twitter';
|
||||||
|
import Twilio from './channels/Twilio';
|
||||||
|
|
||||||
const channelViewList = {
|
const channelViewList = {
|
||||||
facebook: Facebook,
|
facebook: Facebook,
|
||||||
website: Website,
|
website: Website,
|
||||||
twitter: Twitter,
|
twitter: Twitter,
|
||||||
|
twilio: Twilio,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div class="wizard-body small-9 columns">
|
||||||
|
<page-header
|
||||||
|
:header-title="$t('INBOX_MGMT.ADD.TWILIO.TITLE')"
|
||||||
|
:header-content="$t('INBOX_MGMT.ADD.TWILIO.DESC')"
|
||||||
|
/>
|
||||||
|
<form class="row" @submit.prevent="createChannel()">
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.channelName.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.CHANNEL_NAME.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="channelName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.TWILIO.CHANNEL_NAME.PLACEHOLDER')"
|
||||||
|
@blur="$v.channelName.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.channelName.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.CHANNEL_NAME.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.phoneNumber.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.PHONE_NUMBER.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="phoneNumber"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.TWILIO.PHONE_NUMBER.PLACEHOLDER')"
|
||||||
|
@blur="$v.phoneNumber.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.phoneNumber.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.PHONE_NUMBER.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.accountSID.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.ACCOUNT_SID.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="accountSID"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.TWILIO.ACCOUNT_SID.PLACEHOLDER')"
|
||||||
|
@blur="$v.accountSID.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.accountSID.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.ACCOUNT_SID.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.authToken.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.AUTH_TOKEN.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="authToken"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('INBOX_MGMT.ADD.TWILIO.AUTH_TOKEN.PLACEHOLDER')"
|
||||||
|
@blur="$v.authToken.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.authToken.$error" class="message">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.TWILIO.AUTH_TOKEN.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-submit-button
|
||||||
|
:loading="uiFlags.isCreating"
|
||||||
|
:button-text="$t('INBOX_MGMT.ADD.TWILIO.SUBMIT_BUTTON')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
import router from '../../../../index';
|
||||||
|
import PageHeader from '../../SettingsSubPageHeader';
|
||||||
|
|
||||||
|
const shouldStartWithPlusSign = (value = '') => value.startsWith('+');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
accountSID: '',
|
||||||
|
authToken: '',
|
||||||
|
channelName: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'inboxes/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
channelName: { required },
|
||||||
|
phoneNumber: { required, shouldStartWithPlusSign },
|
||||||
|
authToken: { required },
|
||||||
|
accountSID: { required },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async createChannel() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const twilioChannel = await this.$store.dispatch(
|
||||||
|
'inboxes/createTwilioChannel',
|
||||||
|
{
|
||||||
|
twilio_channel: {
|
||||||
|
name: this.channelName,
|
||||||
|
account_sid: this.accountSID,
|
||||||
|
auth_token: this.authToken,
|
||||||
|
phone_number: this.phoneNumber,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.replace({
|
||||||
|
name: 'settings_inboxes_add_agents',
|
||||||
|
params: {
|
||||||
|
page: 'new',
|
||||||
|
inbox_id: twilioChannel.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('INBOX_MGMT.ADD.TWILIO.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -5,11 +5,15 @@
|
||||||
:header-content="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.DESC')"
|
:header-content="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.DESC')"
|
||||||
/>
|
/>
|
||||||
<woot-loading-state
|
<woot-loading-state
|
||||||
v-if="isCreating"
|
v-if="uiFlags.isCreating"
|
||||||
message="Creating Website Support Channel"
|
:message="$('INBOX_MGMT.ADD.WEBSITE_CHANNEL.LOADING_MESSAGE')"
|
||||||
>
|
>
|
||||||
</woot-loading-state>
|
</woot-loading-state>
|
||||||
<form v-if="!isCreating" class="row" @submit.prevent="createChannel()">
|
<form
|
||||||
|
v-if="!uiFlags.isCreating"
|
||||||
|
class="row"
|
||||||
|
@submit.prevent="createChannel()"
|
||||||
|
>
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<label>
|
<label>
|
||||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }}
|
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }}
|
||||||
|
@ -45,6 +49,7 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
|
:loading="uiFlags.isCreating"
|
||||||
:disabled="!websiteUrl || !websiteName"
|
:disabled="!websiteUrl || !websiteName"
|
||||||
:button-text="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.SUBMIT_BUTTON')"
|
:button-text="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.SUBMIT_BUTTON')"
|
||||||
/>
|
/>
|
||||||
|
@ -70,7 +75,6 @@ export default {
|
||||||
websiteName: '',
|
websiteName: '',
|
||||||
websiteUrl: '',
|
websiteUrl: '',
|
||||||
widgetColor: { hex: '#009CE0' },
|
widgetColor: { hex: '#009CE0' },
|
||||||
isCreating: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as types from '../mutation-types';
|
||||||
import InboxesAPI from '../../api/inboxes';
|
import InboxesAPI from '../../api/inboxes';
|
||||||
import WebChannel from '../../api/channel/webChannel';
|
import WebChannel from '../../api/channel/webChannel';
|
||||||
import FBChannel from '../../api/channel/fbChannel';
|
import FBChannel from '../../api/channel/fbChannel';
|
||||||
|
import TwilioChannel from '../../api/channel/twilioChannel';
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
records: [],
|
records: [],
|
||||||
|
@ -54,6 +55,18 @@ export const actions = {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
createTwilioChannel: async ({ commit }, params) => {
|
||||||
|
try {
|
||||||
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
|
||||||
|
const response = await TwilioChannel.create(params);
|
||||||
|
commit(types.default.ADD_INBOXES, response.data);
|
||||||
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
createFBChannel: async ({ commit }, params) => {
|
createFBChannel: async ({ commit }, params) => {
|
||||||
try {
|
try {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
|
||||||
|
|
8
app/javascript/shared/mixins/alertMixin.js
Normal file
8
app/javascript/shared/mixins/alertMixin.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* global bus */
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
showAlert(message) {
|
||||||
|
bus.$emit('newToastMessage', message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
7
app/javascript/shared/mixins/configMixin.js
Normal file
7
app/javascript/shared/mixins/configMixin.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
hostURL() {
|
||||||
|
return window.chatwootConfig.hostURL;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -23,9 +23,10 @@ class Account < ApplicationRecord
|
||||||
has_many :messages, dependent: :destroy
|
has_many :messages, dependent: :destroy
|
||||||
has_many :contacts, dependent: :destroy
|
has_many :contacts, dependent: :destroy
|
||||||
has_many :facebook_pages, dependent: :destroy, class_name: '::Channel::FacebookPage'
|
has_many :facebook_pages, dependent: :destroy, class_name: '::Channel::FacebookPage'
|
||||||
|
has_many :telegram_bots, dependent: :destroy
|
||||||
|
has_many :twilio_sms, dependent: :destroy, class_name: '::Channel::TwilioSms'
|
||||||
has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile'
|
has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile'
|
||||||
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
||||||
has_many :telegram_bots, dependent: :destroy
|
|
||||||
has_many :canned_responses, dependent: :destroy
|
has_many :canned_responses, dependent: :destroy
|
||||||
has_many :webhooks, dependent: :destroy
|
has_many :webhooks, dependent: :destroy
|
||||||
has_one :subscription, dependent: :destroy
|
has_one :subscription, dependent: :destroy
|
||||||
|
|
33
app/models/channel/twilio_sms.rb
Normal file
33
app/models/channel/twilio_sms.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: channel_twilio_sms
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# account_sid :string not null
|
||||||
|
# auth_token :string not null
|
||||||
|
# phone_number :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_channel_twilio_sms_on_account_id_and_phone_number (account_id,phone_number) UNIQUE
|
||||||
|
#
|
||||||
|
|
||||||
|
class Channel::TwilioSms < ApplicationRecord
|
||||||
|
self.table_name = 'channel_twilio_sms'
|
||||||
|
|
||||||
|
validates :account_id, presence: true
|
||||||
|
validates :account_sid, presence: true
|
||||||
|
validates :auth_token, presence: true
|
||||||
|
validates :phone_number, uniqueness: { scope: :account_id }, presence: true
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
has_one :inbox, as: :channel, dependent: :destroy
|
||||||
|
|
||||||
|
def name
|
||||||
|
'Twilio SMS'
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,10 @@
|
||||||
# account_id :integer not null
|
# account_id :integer not null
|
||||||
# profile_id :string not null
|
# profile_id :string not null
|
||||||
#
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_channel_twitter_profiles_on_account_id_and_profile_id (account_id,profile_id) UNIQUE
|
||||||
|
#
|
||||||
|
|
||||||
class Channel::TwitterProfile < ApplicationRecord
|
class Channel::TwitterProfile < ApplicationRecord
|
||||||
self.table_name = 'channel_twitter_profiles'
|
self.table_name = 'channel_twitter_profiles'
|
||||||
|
|
|
@ -113,6 +113,8 @@ class Message < ApplicationRecord
|
||||||
::Facebook::SendReplyService.new(message: self).perform
|
::Facebook::SendReplyService.new(message: self).perform
|
||||||
elsif channel_name == 'Channel::TwitterProfile'
|
elsif channel_name == 'Channel::TwitterProfile'
|
||||||
::Twitter::SendReplyService.new(message: self).perform
|
::Twitter::SendReplyService.new(message: self).perform
|
||||||
|
elsif channel_name == 'Channel::TwilioSms'
|
||||||
|
::Twilio::OutgoingMessageService.new(message: self).perform
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
# account_id :integer
|
# account_id :integer
|
||||||
# inbox_id :integer
|
# inbox_id :integer
|
||||||
#
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_webhooks_on_account_id_and_url (account_id,url) UNIQUE
|
||||||
|
#
|
||||||
|
|
||||||
class Webhook < ApplicationRecord
|
class Webhook < ApplicationRecord
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
77
app/services/twilio/incoming_message_service.rb
Normal file
77
app/services/twilio/incoming_message_service.rb
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
class Twilio::IncomingMessageService
|
||||||
|
pattr_initialize [:params!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
set_contact
|
||||||
|
set_conversation
|
||||||
|
@conversation.messages.create(
|
||||||
|
content: params[:Body],
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
inbox_id: @inbox.id,
|
||||||
|
message_type: :incoming,
|
||||||
|
contact_id: @contact.id,
|
||||||
|
source_id: params[:SmsSid]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def twilio_inbox
|
||||||
|
@twilio_inbox ||= ::Channel::TwilioSms.find_by!(
|
||||||
|
account_sid: params[:AccountSid],
|
||||||
|
phone_number: params[:To]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox ||= twilio_inbox.inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_contact
|
||||||
|
contact_inbox = ::ContactBuilder.new(
|
||||||
|
source_id: params[:From],
|
||||||
|
inbox: inbox,
|
||||||
|
contact_attributes: contact_attributes
|
||||||
|
).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,
|
||||||
|
additional_attributes: additional_attributes
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
@conversation = @contact_inbox.conversations.first
|
||||||
|
return if @conversation
|
||||||
|
|
||||||
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_attributes
|
||||||
|
{
|
||||||
|
name: params[:From],
|
||||||
|
phone_number: params[:From],
|
||||||
|
contact_attributes: additional_attributes
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def additional_attributes
|
||||||
|
{
|
||||||
|
from_zip_code: params[:FromZip],
|
||||||
|
from_country: params[:FromCountry],
|
||||||
|
from_state: params[:FromState]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
34
app/services/twilio/outgoing_message_service.rb
Normal file
34
app/services/twilio/outgoing_message_service.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
class Twilio::OutgoingMessageService
|
||||||
|
pattr_initialize [:message!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
return if message.private
|
||||||
|
return if message.source_id
|
||||||
|
return if inbox.channel.class.to_s != 'Channel::TwilioSms'
|
||||||
|
return unless message.outgoing?
|
||||||
|
|
||||||
|
twilio_message = client.messages.create(
|
||||||
|
body: message.content,
|
||||||
|
from: channel.phone_number,
|
||||||
|
to: contact.phone_number
|
||||||
|
)
|
||||||
|
message.update!(source_id: twilio_message.sid)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
delegate :conversation, to: :message
|
||||||
|
delegate :contact, to: :conversation
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox ||= message.inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel
|
||||||
|
@channel ||= inbox.channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def client
|
||||||
|
::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
|
||||||
|
end
|
||||||
|
end
|
35
app/services/twilio/webhook_setup_service.rb
Normal file
35
app/services/twilio/webhook_setup_service.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
class Twilio::WebhookSetupService
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
pattr_initialize [:inbox!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
if phone_numbers.empty?
|
||||||
|
Rails.logger.info "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
|
||||||
|
else
|
||||||
|
twilio_client
|
||||||
|
.incoming_phone_numbers(phonenumber_sid)
|
||||||
|
.update(sms_method: 'POST', sms_url: twilio_callback_index_url)
|
||||||
|
end
|
||||||
|
rescue Twilio::REST::TwilioError => e
|
||||||
|
Rails.logger.info "TWILIO_FAILURE: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def phonenumber_sid
|
||||||
|
phone_numbers.first.sid
|
||||||
|
end
|
||||||
|
|
||||||
|
def phone_numbers
|
||||||
|
@phone_numbers ||= twilio_client.incoming_phone_numbers.list(phone_number: channel.phone_number)
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel
|
||||||
|
@channel ||= inbox.channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def twilio_client
|
||||||
|
@twilio_client ||= ::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
json.id @inbox.id
|
||||||
|
json.channel_id @inbox.channel_id
|
||||||
|
json.name @inbox.name
|
||||||
|
json.channel_type @inbox.channel_type
|
||||||
|
json.enable_auto_assignment @inbox.enable_auto_assignment
|
||||||
|
json.phone_number @inbox.channel.phone_number
|
|
@ -10,5 +10,6 @@ json.payload do
|
||||||
json.website_token inbox.channel.try(:website_token)
|
json.website_token inbox.channel.try(:website_token)
|
||||||
json.enable_auto_assignment inbox.enable_auto_assignment
|
json.enable_auto_assignment inbox.enable_auto_assignment
|
||||||
json.web_widget_script inbox.channel.try(:web_widget_script)
|
json.web_widget_script inbox.channel.try(:web_widget_script)
|
||||||
|
json.phone_number inbox.channel.try(:phone_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<script>
|
<script>
|
||||||
window.chatwootConfig = {
|
window.chatwootConfig = {
|
||||||
|
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
|
||||||
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
||||||
billingEnabled: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('BILLING_ENABLED', false)) %>,
|
billingEnabled: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('BILLING_ENABLED', false)) %>,
|
||||||
signupEnabled: '<%= ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) %>'
|
signupEnabled: '<%= ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) %>'
|
||||||
|
|
|
@ -36,7 +36,9 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :canned_responses, except: [:show, :edit, :new]
|
resources :canned_responses, except: [:show, :edit, :new]
|
||||||
|
namespace :channels do
|
||||||
|
resource :twilio_channel, only: [:create]
|
||||||
|
end
|
||||||
resources :conversations, only: [:index, :show] do
|
resources :conversations, only: [:index, :show] do
|
||||||
scope module: :conversations do
|
scope module: :conversations do
|
||||||
resources :messages, only: [:index, :create]
|
resources :messages, only: [:index, :create]
|
||||||
|
@ -135,6 +137,10 @@ Rails.application.routes.draw do
|
||||||
resource :callback, only: [:show]
|
resource :callback, only: [:show]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :twilio do
|
||||||
|
resources :callback, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Used in mailer templates
|
# Used in mailer templates
|
||||||
resource :app, only: [:index] do
|
resource :app, only: [:index] do
|
||||||
|
|
11
db/migrate/20200330071706_create_channel_twilio_sms.rb
Normal file
11
db/migrate/20200330071706_create_channel_twilio_sms.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class CreateChannelTwilioSms < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :channel_twilio_sms do |t|
|
||||||
|
t.string :phone_number, null: false
|
||||||
|
t.string :auth_token, null: false
|
||||||
|
t.string :account_sid, null: false
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20200404135009_add_unique_validation_index.rb
Normal file
7
db/migrate/20200404135009_add_unique_validation_index.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class AddUniqueValidationIndex < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_index :channel_twitter_profiles, [:account_id, :profile_id], unique: true
|
||||||
|
add_index :channel_twilio_sms, [:account_id, :phone_number], unique: true
|
||||||
|
add_index :webhooks, [:account_id, :url], unique: true
|
||||||
|
end
|
||||||
|
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_03_31_095710) do
|
ActiveRecord::Schema.define(version: 2020_04_04_135009) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -115,6 +115,16 @@ ActiveRecord::Schema.define(version: 2020_03_31_095710) do
|
||||||
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
|
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "channel_twilio_sms", force: :cascade do |t|
|
||||||
|
t.string "phone_number", null: false
|
||||||
|
t.string "auth_token", null: false
|
||||||
|
t.string "account_sid", null: false
|
||||||
|
t.integer "account_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["account_id", "phone_number"], name: "index_channel_twilio_sms_on_account_id_and_phone_number", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "channel_twitter_profiles", force: :cascade do |t|
|
create_table "channel_twitter_profiles", force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "profile_id", null: false
|
t.string "profile_id", null: false
|
||||||
|
@ -123,6 +133,7 @@ ActiveRecord::Schema.define(version: 2020_03_31_095710) do
|
||||||
t.integer "account_id", null: false
|
t.integer "account_id", null: false
|
||||||
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 ["account_id", "profile_id"], name: "index_channel_twitter_profiles_on_account_id_and_profile_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
|
create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
|
||||||
|
@ -331,6 +342,7 @@ ActiveRecord::Schema.define(version: 2020_03_31_095710) do
|
||||||
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.integer "webhook_type", default: 0
|
t.integer "webhook_type", default: 0
|
||||||
|
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "account_users", "accounts"
|
add_foreign_key "account_users", "accounts"
|
||||||
|
|
40
spec/builders/contact_builder_spec.rb
Normal file
40
spec/builders/contact_builder_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ::ContactBuilder do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
|
let(:contact) { create(:contact, account: account) }
|
||||||
|
let(:existing_contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'doesnot create contact if it already exist' do
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
source_id: existing_contact_inbox.source_id,
|
||||||
|
inbox: inbox,
|
||||||
|
contact_attributes: {
|
||||||
|
name: 'Contact',
|
||||||
|
phone_number: '+1234567890',
|
||||||
|
email: 'testemail@example.com'
|
||||||
|
}
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.contact.id).to be(contact.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates contact if contact doesnot exist' do
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
source_id: '123456',
|
||||||
|
inbox: inbox,
|
||||||
|
contact_attributes: {
|
||||||
|
name: 'Contact',
|
||||||
|
phone_number: '+1234567890',
|
||||||
|
email: 'testemail@example.com'
|
||||||
|
}
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.contact.id).not_to eq(contact.id)
|
||||||
|
expect(contact_inbox.contact.name).to eq('Contact')
|
||||||
|
expect(contact_inbox.inbox_id).to eq(inbox.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,76 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
||||||
|
let(:message_double) { instance_double('message') }
|
||||||
|
let(:twilio_webhook_setup_service) { instance_double(::Twilio::WebhookSetupService) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||||
|
allow(::Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
|
||||||
|
allow(twilio_webhook_setup_service).to receive(:perform)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/accounts/{account.id}/channels/twilio_channel' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
twilio_channel: {
|
||||||
|
account_sid: 'sid',
|
||||||
|
auth_token: 'token',
|
||||||
|
phone_number: '+1234567890',
|
||||||
|
name: 'SMS Channel'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post api_v1_account_channels_twilio_channel_path(account), params: params
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is logged in' do
|
||||||
|
context 'with user as administrator' do
|
||||||
|
it 'creates inbox and returns inbox object' do
|
||||||
|
allow(twilio_client).to receive(:messages).and_return(message_double)
|
||||||
|
allow(message_double).to receive(:list).and_return([])
|
||||||
|
|
||||||
|
post api_v1_account_channels_twilio_channel_path(account),
|
||||||
|
params: params,
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['name']).to eq('SMS Channel')
|
||||||
|
expect(json_response['phone_number']).to eq('+1234567890')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return error if Twilio tokens are incorrect' do
|
||||||
|
allow(twilio_client).to receive(:messages).and_return(message_double)
|
||||||
|
allow(message_double).to receive(:list).and_raise(Twilio::REST::TwilioError)
|
||||||
|
|
||||||
|
post api_v1_account_channels_twilio_channel_path(account),
|
||||||
|
params: params,
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with user as agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post api_v1_account_channels_twilio_channel_path(account),
|
||||||
|
params: params,
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
spec/controllers/twilio/callbacks_controller_spec.rb
Normal file
18
spec/controllers/twilio/callbacks_controller_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Twilio::CallbacksController', type: :request do
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
let(:twilio_service) { instance_double(Twilio::IncomingMessageService) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Twilio::IncomingMessageService).to receive(:new).and_return(twilio_service)
|
||||||
|
allow(twilio_service).to receive(:perform)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /twilio/callback' do
|
||||||
|
it 'calls incoming message service' do
|
||||||
|
post twilio_callback_index_url, params: {}
|
||||||
|
expect(twilio_service).to have_received(:perform)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Twitter::CallbacksController', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /twitter/callback' do
|
describe 'GET /twitter/callback' do
|
||||||
it 'renders the page correctly when called with website_token' do
|
it 'returns correct response' do
|
||||||
get twitter_callback_url
|
get twitter_callback_url
|
||||||
account.reload
|
account.reload
|
||||||
expect(response).to redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
expect(response).to redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||||
|
|
9
spec/factories/channel/twilio_sms.rb
Normal file
9
spec/factories/channel/twilio_sms.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :channel_twilio_sms, class: 'Channel::TwilioSms' do
|
||||||
|
auth_token { SecureRandom.uuid }
|
||||||
|
account_sid { SecureRandom.uuid }
|
||||||
|
sequence(:phone_number) { |n| "+123456789#{n}1" }
|
||||||
|
inbox
|
||||||
|
account
|
||||||
|
end
|
||||||
|
end
|
39
spec/services/twilio/incoming_message_service_spec.rb
Normal file
39
spec/services/twilio/incoming_message_service_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Twilio::IncomingMessageService do
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let!(:twilio_sms) do
|
||||||
|
create(:channel_twilio_sms, account: account, phone_number: '+1234567890', account_sid: 'ACxxx')
|
||||||
|
end
|
||||||
|
let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: twilio_sms.inbox) }
|
||||||
|
let!(:conversation) { create(:conversation, contact: contact, inbox: twilio_sms.inbox, contact_inbox: contact_inbox) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'creates a new message in existing conversation' do
|
||||||
|
params = {
|
||||||
|
SmsSid: 'SMxx',
|
||||||
|
From: '+12345',
|
||||||
|
AccountSid: 'ACxxx',
|
||||||
|
To: '+1234567890',
|
||||||
|
Body: 'testing3'
|
||||||
|
}
|
||||||
|
|
||||||
|
described_class.new(params: params).perform
|
||||||
|
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new conversation' do
|
||||||
|
params = {
|
||||||
|
SmsSid: 'SMxx',
|
||||||
|
From: '+123456',
|
||||||
|
AccountSid: 'ACxxx',
|
||||||
|
To: '+1234567890',
|
||||||
|
Body: 'new conversation'
|
||||||
|
}
|
||||||
|
|
||||||
|
described_class.new(params: params).perform
|
||||||
|
expect(Conversation.count).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
59
spec/services/twilio/outgoing_message_service_spec.rb
Normal file
59
spec/services/twilio/outgoing_message_service_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Twilio::OutgoingMessageService do
|
||||||
|
subject(:outgoing_message_service) { described_class.new(message: message) }
|
||||||
|
|
||||||
|
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
||||||
|
let(:messages_double) { instance_double('messages') }
|
||||||
|
let(:message_record_double) { instance_double('message_record_double') }
|
||||||
|
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let!(:widget_inbox) { create(:inbox, account: account) }
|
||||||
|
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
||||||
|
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||||
|
let!(:contact) { create(:contact, account: account) }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox) }
|
||||||
|
let(:conversation) { create(:conversation, contact: contact, inbox: twilio_inbox, contact_inbox: contact_inbox) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||||
|
allow(twilio_client).to receive(:messages).and_return(messages_double)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'without reply' do
|
||||||
|
it 'if message is private' do
|
||||||
|
create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
|
||||||
|
expect(twilio_client).not_to have_received(:messages)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if inbox channel is not facebook page' do
|
||||||
|
create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
|
||||||
|
expect(twilio_client).not_to have_received(:messages)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if message is not outgoing' do
|
||||||
|
create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account)
|
||||||
|
expect(twilio_client).not_to have_received(:messages)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if message has an source id' do
|
||||||
|
create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid)
|
||||||
|
expect(twilio_client).not_to have_received(:messages)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with reply' do
|
||||||
|
it 'if message is sent from chatwoot and is outgoing' do
|
||||||
|
allow(messages_double).to receive(:create).and_return(message_record_double)
|
||||||
|
allow(message_record_double).to receive(:sid).and_return('1234')
|
||||||
|
|
||||||
|
outgoing_message = create(
|
||||||
|
:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(outgoing_message.reload.source_id).to eq('1234')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
spec/services/twilio/webhook_setup_service_spec.rb
Normal file
48
spec/services/twilio/webhook_setup_service_spec.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Twilio::WebhookSetupService do
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
let(:channel_twilio_sms) { create(:channel_twilio_sms) }
|
||||||
|
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
||||||
|
let(:phone_double) { instance_double('phone_double') }
|
||||||
|
let(:phone_record_double) { instance_double('phone_record_double') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
||||||
|
allow(phone_double).to receive(:update)
|
||||||
|
allow(phone_record_double).to receive(:sid).and_return('1234')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'logs error if phone_number is not found' do
|
||||||
|
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
|
||||||
|
allow(phone_double).to receive(:list).and_return([])
|
||||||
|
|
||||||
|
described_class.new(inbox: channel_twilio_sms.inbox).perform
|
||||||
|
|
||||||
|
expect(phone_double).not_to have_received(:update)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'update webhook_url if phone_number is found' do
|
||||||
|
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
|
||||||
|
allow(phone_double).to receive(:list).and_return([phone_record_double])
|
||||||
|
|
||||||
|
described_class.new(inbox: channel_twilio_sms.inbox).perform
|
||||||
|
|
||||||
|
expect(phone_double).to have_received(:update).with(
|
||||||
|
sms_method: 'POST',
|
||||||
|
sms_url: twilio_callback_index_url
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'doesnot call update if TwilioError is thrown' do
|
||||||
|
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
|
||||||
|
allow(phone_double).to receive(:list).and_raise(Twilio::REST::TwilioError)
|
||||||
|
|
||||||
|
described_class.new(inbox: channel_twilio_sms.inbox).perform
|
||||||
|
|
||||||
|
expect(phone_double).not_to have_received(:update)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue