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