diff --git a/Gemfile b/Gemfile
index 4a826347c..4225f2904 100644
--- a/Gemfile
+++ b/Gemfile
@@ -25,7 +25,7 @@ gem 'uglifier'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
-gem 'azure-storage', require: false
+gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'mini_magick'
@@ -62,9 +62,9 @@ gem 'chargebee'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'telegram-bot-ruby'
+gem 'twilio-ruby', '~> 5.32.0'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
-
# facebook client
gem 'koala'
# Random name generator
diff --git a/Gemfile.lock b/Gemfile.lock
index a3374ffea..0ad798ef4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -102,15 +102,13 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- azure-core (0.1.15)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- nokogiri (~> 1.6)
- azure-storage (0.15.0.preview)
- azure-core (~> 0.1)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- nokogiri (~> 1.6, >= 1.6.8)
+ azure-storage-blob (2.0.0)
+ azure-storage-common (~> 2.0)
+ nokogiri (~> 1.10.4)
+ azure-storage-common (2.0.1)
+ faraday (~> 1.0)
+ faraday_middleware (~> 1.0.0.rc1)
+ nokogiri (~> 1.10.4)
bcrypt (3.1.13)
bindex (0.8.1)
bootsnap (1.4.6)
@@ -172,10 +170,10 @@ GEM
railties (>= 4.2.0)
faker (2.11.0)
i18n (>= 1.6, < 2)
- faraday (0.17.3)
+ faraday (1.0.1)
multipart-post (>= 1.2, < 3)
- faraday_middleware (0.14.0)
- faraday (>= 0.7.4, < 1.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.1)
@@ -410,8 +408,8 @@ GEM
activerecord (>= 4)
activesupport (>= 4)
semantic_range (2.3.0)
- sentry-raven (2.13.0)
- faraday (>= 0.7.6, < 1.0)
+ sentry-raven (3.0.0)
+ faraday (>= 1.0)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.6)
@@ -449,6 +447,10 @@ GEM
time_diff (0.3.0)
activesupport
i18n
+ twilio-ruby (5.32.0)
+ faraday (~> 1.0.0)
+ jwt (>= 1.5, <= 2.5)
+ nokogiri (>= 1.6, < 2.0)
tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
@@ -496,7 +498,7 @@ DEPENDENCIES
annotate
attr_extras
aws-sdk-s3
- azure-storage
+ azure-storage-blob
bootsnap
brakeman
browser
@@ -553,6 +555,7 @@ DEPENDENCIES
spring-watcher-listen
telegram-bot-ruby
time_diff
+ twilio-ruby (~> 5.32.0)
twitty!
tzinfo-data
uglifier
diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb
new file mode 100644
index 000000000..70b994ac2
--- /dev/null
+++ b/app/builders/contact_builder.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb
new file mode 100644
index 000000000..c3d6554fd
--- /dev/null
+++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb
@@ -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
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
new file mode 100644
index 000000000..f6cb5356c
--- /dev/null
+++ b/app/controllers/twilio/callback_controller.rb
@@ -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
diff --git a/app/javascript/dashboard/api/channel/twilioChannel.js b/app/javascript/dashboard/api/channel/twilioChannel.js
new file mode 100644
index 000000000..a688a1f11
--- /dev/null
+++ b/app/javascript/dashboard/api/channel/twilioChannel.js
@@ -0,0 +1,9 @@
+import ApiClient from '../ApiClient';
+
+class TwilioChannel extends ApiClient {
+ constructor() {
+ super('channels/twilio_channel', { accountScoped: true });
+ }
+}
+
+export default new TwilioChannel();
diff --git a/app/javascript/dashboard/assets/images/channels/twilio.png b/app/javascript/dashboard/assets/images/channels/twilio.png
new file mode 100644
index 000000000..627a8e9d4
Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/twilio.png differ
diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue
index 66ce6b399..8a3932057 100644
--- a/app/javascript/dashboard/components/layout/SidebarItem.vue
+++ b/app/javascript/dashboard/components/layout/SidebarItem.vue
@@ -56,6 +56,7 @@ const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
+ TWILIO: 'Channel::TwilioSms',
};
const getInboxClassByType = type => {
switch (type) {
@@ -68,6 +69,9 @@ const getInboxClassByType = type => {
case INBOX_TYPES.TWITTER:
return 'ion-social-twitter';
+ case INBOX_TYPES.TWILIO:
+ return 'ion-android-textsms';
+
default:
return '';
}
diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue
index da22f71bd..948bff103 100644
--- a/app/javascript/dashboard/components/widgets/ChannelItem.vue
+++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue
@@ -24,6 +24,10 @@
v-if="channel === 'website'"
src="~dashboard/assets/images/channels/website.png"
/>
+