chore: Provider APIs for SMS Channel - Bandwidth (#3889)

fixes: #3888
This commit is contained in:
Sojan Jose
2022-02-03 15:22:13 -08:00
committed by GitHub
parent fba7f40bee
commit cf10f3d03b
40 changed files with 879 additions and 51 deletions

View File

@@ -4,7 +4,7 @@ class ContactInboxBuilder
def perform
@contact = Contact.find(contact_id)
@inbox = @contact.account.inboxes.find(inbox_id)
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
source_id = @source_id || generate_source_id
create_contact_inbox(source_id) if source_id.present?
@@ -13,12 +13,18 @@ class ContactInboxBuilder
private
def generate_source_id
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
return @contact.email if @inbox.channel_type == 'Channel::Email'
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
nil
case @inbox.channel_type
when 'Channel::TwilioSms'
twilio_source_id
when 'Channel::Whatsapp'
wa_source_id
when 'Channel::Email'
@contact.email
when 'Channel::Sms'
@contact.phone_number
when 'Channel::Api'
SecureRandom.uuid
end
end
def wa_source_id

View File

@@ -91,20 +91,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def create_channel
case permitted_params[:channel][:type]
when 'web_widget'
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
when 'api'
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
when 'email'
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
when 'line'
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
when 'telegram'
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
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
end
def update_channel_feature_flags
@@ -123,6 +112,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
)
end
def channel_type_from_params
{
'web_widget' => Channel::WebWidget,
'api' => Channel::Api,
'email' => Channel::Email,
'line' => Channel::Line,
'telegram' => Channel::Telegram,
'whatsapp' => Channel::Whatsapp,
'sms' => Channel::Sms
}[permitted_params[:channel][:type]]
end
def account_channels_method
{
'web_widget' => Current.account.web_widgets,
'api' => Current.account.api_channels,
'email' => Current.account.email_channels,
'line' => Current.account.line_channels,
'telegram' => Current.account.telegram_channels,
'whatsapp' => Current.account.whatsapp_channels,
'sms' => Current.account.sms_channels
}[permitted_params[:channel][:type]]
end
def get_channel_attributes(channel_type)
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
channel_type.constantize::EDITABLE_ATTRS.presence

View File

@@ -0,0 +1,6 @@
class Webhooks::SmsController < ActionController::API
def process_payload
Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash)
head :ok
end
end

View File

@@ -136,8 +136,56 @@
}
},
"SMS": {
"TITLE": "SMS Channel via Twilio",
"DESC": "Start supporting your customers via SMS with Twilio integration."
"TITLE": "SMS Channel",
"DESC": "Start supporting your customers via SMS.",
"PROVIDERS": {
"LABEL": "API Provider",
"TWILIO": "Twilio",
"BANDWIDTH": "Bandwidth"
},
"API": {
"ERROR_MESSAGE": "We were not able to save the SMS channel"
},
"BANDWIDTH": {
"ACCOUNT_ID": {
"LABEL": "Account ID",
"PLACEHOLDER": "Please enter your Bandwidth Account ID",
"ERROR": "This field is required"
},
"API_KEY": {
"LABEL": "API Key",
"PLACEHOLDER": "Please enter your Bandwith API Key",
"ERROR": "This field is required"
},
"API_SECRET": {
"LABEL": "API Secret",
"PLACEHOLDER": "Please enter your Bandwith API Secret",
"ERROR": "This field is required"
},
"APPLICATION_ID": {
"LABEL": "Application ID",
"PLACEHOLDER": "Please enter your Bandwidth Application ID",
"ERROR": "This field is required"
},
"INBOX_NAME": {
"LABEL": "Inbox Name",
"PLACEHOLDER": "Please enter a 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."
},
"SUBMIT_BUTTON": "Create Bandwidth Channel",
"API": {
"ERROR_MESSAGE": "We were not able to authenticate Bandwidth credentials, please try again"
},
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the message callback URL in Bandwidth with the URL mentioned here."
}
}
},
"WHATSAPP": {
"TITLE": "WhatsApp Channel",

View File

@@ -247,7 +247,7 @@ export default {
if (this.isOngoingType) {
return this.$store.getters['inboxes/getWebsiteInboxes'];
}
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
return this.$store.getters['inboxes/getSMSInboxes'];
},
sendersAndBotList() {
return [

View File

@@ -171,7 +171,7 @@ export default {
if (this.isOngoingType) {
return this.$store.getters['inboxes/getWebsiteInboxes'];
}
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
return this.$store.getters['inboxes/getSMSInboxes'];
},
pageTitle() {
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${

View File

@@ -50,7 +50,7 @@ export default {
{ key: 'facebook', name: 'Messenger' },
{ key: 'twitter', name: 'Twitter' },
{ key: 'whatsapp', name: 'WhatsApp' },
{ key: 'sms', name: 'SMS via Twilio' },
{ key: 'sms', name: 'SMS' },
{ key: 'email', name: 'Email' },
{
key: 'api',

View File

@@ -29,6 +29,14 @@
>
</woot-code>
</div>
<div class="medium-6 small-offset-3">
<woot-code
v-if="isASmsInbox"
lang="html"
:script="currentInbox.callback_webhook_url"
>
</woot-code>
</div>
<div class="medium-6 small-offset-3">
<woot-code
v-if="isAEmailInbox"
@@ -86,6 +94,9 @@ export default {
isALineInbox() {
return this.currentInbox.channel_type === 'Channel::Line';
},
isASmsInbox() {
return this.currentInbox.channel_type === 'Channel::Sms';
},
message() {
if (this.isATwilioInbox) {
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
@@ -93,6 +104,12 @@ export default {
)}`;
}
if (this.isASmsInbox) {
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
'INBOX_MGMT.ADD.SMS.BANDWIDTH.API_CALLBACK.SUBTITLE'
)}`;
}
if (this.isALineInbox) {
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE'
@@ -103,10 +120,11 @@ export default {
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
}
if (!this.currentInbox.web_widget_script) {
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
if (this.currentInbox.web_widget_script) {
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
}
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
},
},
};

View File

@@ -48,6 +48,9 @@
<span v-if="item.channel_type === 'Channel::Whatsapp'">
Whatsapp
</span>
<span v-if="item.channel_type === 'Channel::Sms'">
Sms
</span>
<span v-if="item.channel_type === 'Channel::Email'">
Email
</span>

View File

@@ -0,0 +1,181 @@
<template>
<form class="row" @submit.prevent="createChannel()">
<div class="medium-8 columns">
<label :class="{ error: $v.inboxName.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.LABEL') }}
<input
v-model.trim="inboxName"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.PLACEHOLDER')
"
@blur="$v.inboxName.$touch"
/>
<span v-if="$v.inboxName.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.INBOX_NAME.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.phoneNumber.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.LABEL') }}
<input
v-model.trim="phoneNumber"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.PLACEHOLDER')
"
@blur="$v.phoneNumber.$touch"
/>
<span v-if="$v.phoneNumber.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.PHONE_NUMBER.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.accountId.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.LABEL') }}
<input
v-model.trim="accountId"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.PLACEHOLDER')
"
@blur="$v.accountId.$touch"
/>
<span v-if="$v.accountId.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.ACCOUNT_ID.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.applicationId.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.LABEL') }}
<input
v-model.trim="applicationId"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.PLACEHOLDER')
"
@blur="$v.applicationId.$touch"
/>
<span v-if="$v.applicationId.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.APPLICATION_ID.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.apiKey.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.LABEL') }}
<input
v-model.trim="apiKey"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.PLACEHOLDER')"
@blur="$v.apiKey.$touch"
/>
<span v-if="$v.apiKey.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_KEY.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.apiSecret.$error }">
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.LABEL') }}
<input
v-model.trim="apiSecret"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.PLACEHOLDER')
"
@blur="$v.apiSecret.$touch"
/>
<span v-if="$v.apiSecret.$error" class="message">{{
$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_SECRET.ERROR')
}}</span>
</label>
</div>
<div class="medium-12 columns">
<woot-submit-button
:loading="uiFlags.isCreating"
:button-text="$t('INBOX_MGMT.ADD.SMS.BANDWIDTH.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 {
accountId: '',
apiKey: '',
apiSecret: '',
applicationId: '',
inboxName: '',
phoneNumber: '',
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
globalConfig: 'globalConfig/get',
}),
},
validations: {
inboxName: { required },
phoneNumber: { required, shouldStartWithPlusSign },
apiKey: { required },
apiSecret: { required },
applicationId: { required },
accountId: { required },
},
methods: {
async createChannel() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
const smsChannel = await this.$store.dispatch('inboxes/createChannel', {
name: this.inboxName,
channel: {
type: 'sms',
phone_number: this.phoneNumber,
provider_config: {
api_key: this.apiKey,
api_secret: this.apiSecret,
application_id: this.applicationId,
account_id: this.accountId,
},
},
});
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: smsChannel.id,
},
});
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.ADD.SMS.API.ERROR_MESSAGE'));
}
},
},
};
</script>

View File

@@ -4,18 +4,39 @@
:header-title="$t('INBOX_MGMT.ADD.SMS.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.SMS.DESC')"
/>
<twilio type="sms"></twilio>
<div class="medium-8 columns">
<label>
{{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.LABEL') }}
<select v-model="provider">
<option value="twilio">
{{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.TWILIO') }}
</option>
<option value="360dialog">
{{ $t('INBOX_MGMT.ADD.SMS.PROVIDERS.BANDWIDTH') }}
</option>
</select>
</label>
</div>
<twilio v-if="provider === 'twilio'" type="sms"></twilio>
<bandwidth-sms v-else />
</div>
</template>
<script>
import PageHeader from '../../SettingsSubPageHeader';
import BandwidthSms from './BandwidthSms.vue';
import Twilio from './Twilio';
export default {
components: {
PageHeader,
Twilio,
BandwidthSms,
},
data() {
return {
provider: 'twilio',
};
},
};
</script>

View File

@@ -78,9 +78,11 @@ export const getters = {
item => item.channel_type === INBOX_TYPES.TWILIO
);
},
getTwilioSMSInboxes($state) {
getSMSInboxes($state) {
return $state.records.filter(
item => item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms'
item =>
item.channel_type === INBOX_TYPES.SMS ||
(item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms')
);
},
dialogFlowEnabledInboxes($state) {

View File

@@ -55,4 +55,11 @@ export default [
website_token: 'randomid125',
enable_auto_assignment: true,
},
{
id: 6,
channel_id: 6,
name: 'Test Widget 6',
channel_type: 'Channel::Sms',
provider: 'default',
},
];

View File

@@ -19,14 +19,14 @@ describe('#getters', () => {
expect(getters.getTwilioInboxes(state).length).toEqual(1);
});
it('getTwilioSMSInboxes', () => {
it('getSMSInboxes', () => {
const state = { records: inboxList };
expect(getters.getTwilioSMSInboxes(state).length).toEqual(1);
expect(getters.getSMSInboxes(state).length).toEqual(2);
});
it('dialogFlowEnabledInboxes', () => {
const state = { records: inboxList };
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(5);
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6);
});
it('getInbox', () => {

View File

@@ -8,6 +8,7 @@ export const INBOX_TYPES = {
EMAIL: 'Channel::Email',
TELEGRAM: 'Channel::Telegram',
LINE: 'Channel::Line',
SMS: 'Channel::Sms',
};
export default {

View File

@@ -6,19 +6,20 @@ class SendReplyJob < ApplicationJob
conversation = message.conversation
channel_name = conversation.inbox.channel.class.to_s
services = {
'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService,
'Channel::TwilioSms' => ::Twilio::SendOnTwilioService,
'Channel::Line' => ::Line::SendOnLineService,
'Channel::Telegram' => ::Telegram::SendOnTelegramService,
'Channel::Whatsapp' => ::Whatsapp::SendOnWhatsappService,
'Channel::Sms' => ::Sms::SendOnSmsService
}
case channel_name
when 'Channel::FacebookPage'
send_on_facebook_page(message)
when 'Channel::TwitterProfile'
::Twitter::SendOnTwitterService.new(message: message).perform
when 'Channel::TwilioSms'
::Twilio::SendOnTwilioService.new(message: message).perform
when 'Channel::Line'
::Line::SendOnLineService.new(message: message).perform
when 'Channel::Telegram'
::Telegram::SendOnTelegramService.new(message: message).perform
when 'Channel::Whatsapp'
::Whatsapp::SendOnWhatsappService.new(message: message).perform
else
services[channel_name].new(message: message).perform if services[channel_name].present?
end
end

View File

@@ -0,0 +1,13 @@
class Webhooks::SmsEventsJob < ApplicationJob
queue_as :default
def perform(params = {})
return unless params[:type] == 'message-received'
channel = Channel::Sms.find_by(phone_number: params[:to])
return unless channel
# TODO: pass to appropriate provider service from here
Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform
end
end

View File

@@ -69,6 +69,7 @@ class Account < ApplicationRecord
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
has_many :webhooks, dependent: :destroy_async
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
has_many :working_hours, dependent: :destroy_async
has_many :automation_rules, dependent: :destroy

View File

@@ -58,6 +58,7 @@ class Campaign < ApplicationRecord
return if completed?
Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS'
Sms::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Sms'
end
private
@@ -69,14 +70,14 @@ class Campaign < ApplicationRecord
def validate_campaign_inbox
return unless inbox
errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS'].include? inbox.inbox_type
errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS', 'Sms'].include? inbox.inbox_type
end
# TO-DO we clean up with better validations when campaigns evolve into more inboxes
def ensure_correct_campaign_attributes
return if inbox.blank?
if inbox.inbox_type == 'Twilio SMS'
if ['Twilio SMS', 'Sms'].include?(inbox.inbox_type)
self.campaign_type = 'one_off'
self.scheduled_at ||= Time.now.utc
else

81
app/models/channel/sms.rb Normal file
View File

@@ -0,0 +1,81 @@
# == Schema Information
#
# Table name: channel_sms
#
# 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_sms_on_phone_number (phone_number) UNIQUE
#
class Channel::Sms < ApplicationRecord
include Channelable
self.table_name = 'channel_sms'
EDITABLE_ATTRS = [:phone_number, { provider_config: {} }].freeze
validates :phone_number, presence: true, uniqueness: true
# before_save :validate_provider_config
def name
'Sms'
end
# all this should happen in provider service . but hack mode on
def api_base_path
'https://messaging.bandwidth.com/api/v2'
end
# Extract later into provider Service
def send_message(phone_number, message)
if message.attachments.present?
send_attachment_message(phone_number, message)
else
send_text_message(phone_number, message.content)
end
end
def send_text_message(contact_number, message)
response = HTTParty.post(
"#{api_base_path}/users/#{provider_config['account_id']}/messages",
basic_auth: bandwidth_auth,
headers: { 'Content-Type' => 'application/json' },
body: {
'to' => contact_number,
'from' => phone_number,
'text' => message,
'applicationId' => provider_config['application_id']
}.to_json
)
response.success? ? response.parsed_response['id'] : nil
end
private
def send_attachment_message(phone_number, message)
# fix me
end
def bandwidth_auth
{ username: provider_config['api_key'], password: provider_config['api_secret'] }
end
# Extract later into provider Service
# let's revisit later
def validate_provider_config
response = HTTParty.post(
"#{api_base_path}/users/#{provider_config['account_id']}/messages",
basic_auth: bandwidth_auth,
headers: { 'Content-Type': 'application/json' }
)
errors.add(:provider_config, 'error setting up') unless response.success?
end
end

View File

@@ -149,6 +149,6 @@ class Channel::Whatsapp < ApplicationRecord
url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}"
}.to_json
)
errors.add(:bot_token, 'error setting up the webook') unless response.success?
errors.add(:provider_config, 'error setting up the webook') unless response.success?
end
end

View File

@@ -107,6 +107,8 @@ class Inbox < ApplicationRecord
case channel_type
when 'Channel::TwilioSms'
"#{ENV['FRONTEND_URL']}/twilio/callback"
when 'Channel::Sms'
"#{ENV['FRONTEND_URL']}/webhooks/sms/#{channel.phone_number.delete_prefix('+')}"
when 'Channel::Line'
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
end

View File

@@ -14,6 +14,8 @@ class Contacts::ContactableInboxesService
twilio_contactable_inbox(inbox)
when 'Channel::Whatsapp'
whatsapp_contactable_inbox(inbox)
when 'Channel::Sms'
sms_contactable_inbox(inbox)
when 'Channel::Email'
email_contactable_inbox(inbox)
when 'Channel::Api'
@@ -52,6 +54,12 @@ class Contacts::ContactableInboxesService
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
end
def sms_contactable_inbox(inbox)
return unless @contact.phone_number
{ source_id: @contact.phone_number, inbox: inbox }
end
def twilio_contactable_inbox(inbox)
return if @contact.phone_number.blank?

View File

@@ -0,0 +1,66 @@
class Sms::IncomingMessageService
include ::FileTypeHelper
pattr_initialize [:inbox!, :params!]
def perform
set_contact
set_conversation
@message = @conversation.messages.create(
content: params[:text],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: params[:id]
)
end
private
def account
@account ||= @inbox.account
end
def phone_number
params[:from]
end
def formatted_phone_number
TelephoneNumber.parse(phone_number).international_number
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
}
end
def set_conversation
@conversation = @contact_inbox.conversations.first
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
def contact_attributes
{
name: formatted_phone_number,
phone_number: phone_number
}
end
end

View File

@@ -0,0 +1,32 @@
class Sms::OneoffSmsCampaignService
pattr_initialize [:campaign!]
def perform
raise "Invalid campaign #{campaign.id}" if campaign.inbox.inbox_type != 'Sms' || !campaign.one_off?
raise 'Completed Campaign' if campaign.completed?
# marks campaign completed so that other jobs won't pick it up
campaign.completed!
audience_label_ids = campaign.audience.select { |audience| audience['type'] == 'Label' }.pluck('id')
audience_labels = campaign.account.labels.where(id: audience_label_ids).pluck(:title)
process_audience(audience_labels)
end
private
delegate :inbox, to: :campaign
delegate :channel, to: :inbox
def process_audience(audience_labels)
campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
next if contact.phone_number.blank?
send_message(to: contact.phone_number, content: campaign.message)
end
end
def send_message(to:, content:)
channel.send_text_message(to, content)
end
end

View File

@@ -0,0 +1,16 @@
class Sms::SendOnSmsService < Base::SendOnChannelService
private
def channel_class
Channel::Sms
end
def perform_reply
send_on_sms
end
def send_on_sms
message_id = channel.send_message(message.conversation.contact_inbox.source_id, message)
message.update!(source_id: message_id) if message_id.present?
end
end