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" /> +

{{ channel }}

@@ -39,7 +43,7 @@ export default { }, methods: { isActive(channel) { - return ['facebook', 'website', 'twitter'].includes(channel); + return ['facebook', 'website', 'twitter', 'twilio'].includes(channel); }, onItemClick() { if (this.isActive(this.channel)) { diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index d84be8f1e..0dcdd60d5 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -21,6 +21,7 @@ "WEBSITE_CHANNEL": { "TITLE": "Website channel", "DESC": "Create a channel for your website and start supporting your customers via our website widget.", + "LOADING_MESSAGE": "Creating Website Support Channel", "CHANNEL_NAME": { "LABEL": "Website Name", "PLACEHOLDER": "Enter your website name (eg: Acme Inc)" @@ -35,6 +36,34 @@ }, "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": { "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." diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue index 728d753d9..0fa469d9b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue @@ -128,11 +128,11 @@ export default { this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE')); this.onClose(); } catch (error) { - if (error.response.status === 422) { - this.showAlert(this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE')); - } else { - this.showAlert(this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE')); - } + if (error.response.status === 422) { + this.showAlert(this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE')); + } else { + this.showAlert(this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE')); + } } }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue index 3b3ba9dc3..12bba91d3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue @@ -27,7 +27,14 @@ export default { }, data() { return { - channelList: ['website', 'facebook', 'twitter', 'telegram', 'line'], + channelList: [ + 'website', + 'facebook', + 'twitter', + 'twilio', + 'telegram', + 'line', + ], }; }, methods: { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 9da9ccfab..accba4cfb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -28,12 +28,14 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue index 8641210db..83bf17866 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Website.vue @@ -5,11 +5,15 @@ :header-content="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.DESC')" /> -
+