parent
fba7f40bee
commit
cf10f3d03b
40 changed files with 879 additions and 51 deletions
|
@ -4,7 +4,7 @@ class ContactInboxBuilder
|
||||||
def perform
|
def perform
|
||||||
@contact = Contact.find(contact_id)
|
@contact = Contact.find(contact_id)
|
||||||
@inbox = @contact.account.inboxes.find(inbox_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
|
source_id = @source_id || generate_source_id
|
||||||
create_contact_inbox(source_id) if source_id.present?
|
create_contact_inbox(source_id) if source_id.present?
|
||||||
|
@ -13,12 +13,18 @@ class ContactInboxBuilder
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_source_id
|
def generate_source_id
|
||||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
case @inbox.channel_type
|
||||||
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
|
when 'Channel::TwilioSms'
|
||||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
twilio_source_id
|
||||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
when 'Channel::Whatsapp'
|
||||||
|
wa_source_id
|
||||||
nil
|
when 'Channel::Email'
|
||||||
|
@contact.email
|
||||||
|
when 'Channel::Sms'
|
||||||
|
@contact.phone_number
|
||||||
|
when 'Channel::Api'
|
||||||
|
SecureRandom.uuid
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wa_source_id
|
def wa_source_id
|
||||||
|
|
|
@ -91,20 +91,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_channel
|
def create_channel
|
||||||
case permitted_params[:channel][:type]
|
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||||
when 'web_widget'
|
|
||||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
account_channels_method.create!(permitted_params(channel_type_from_params::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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_channel_feature_flags
|
def update_channel_feature_flags
|
||||||
|
@ -123,6 +112,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
)
|
)
|
||||||
end
|
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)
|
def get_channel_attributes(channel_type)
|
||||||
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
|
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
|
||||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||||
|
|
6
app/controllers/webhooks/sms_controller.rb
Normal file
6
app/controllers/webhooks/sms_controller.rb
Normal 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
|
|
@ -136,8 +136,56 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SMS": {
|
"SMS": {
|
||||||
"TITLE": "SMS Channel via Twilio",
|
"TITLE": "SMS Channel",
|
||||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
"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": {
|
"WHATSAPP": {
|
||||||
"TITLE": "WhatsApp Channel",
|
"TITLE": "WhatsApp Channel",
|
||||||
|
|
|
@ -247,7 +247,7 @@ export default {
|
||||||
if (this.isOngoingType) {
|
if (this.isOngoingType) {
|
||||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
||||||
}
|
}
|
||||||
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
|
return this.$store.getters['inboxes/getSMSInboxes'];
|
||||||
},
|
},
|
||||||
sendersAndBotList() {
|
sendersAndBotList() {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -171,7 +171,7 @@ export default {
|
||||||
if (this.isOngoingType) {
|
if (this.isOngoingType) {
|
||||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
||||||
}
|
}
|
||||||
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
|
return this.$store.getters['inboxes/getSMSInboxes'];
|
||||||
},
|
},
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${
|
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default {
|
||||||
{ key: 'facebook', name: 'Messenger' },
|
{ key: 'facebook', name: 'Messenger' },
|
||||||
{ key: 'twitter', name: 'Twitter' },
|
{ key: 'twitter', name: 'Twitter' },
|
||||||
{ key: 'whatsapp', name: 'WhatsApp' },
|
{ key: 'whatsapp', name: 'WhatsApp' },
|
||||||
{ key: 'sms', name: 'SMS via Twilio' },
|
{ key: 'sms', name: 'SMS' },
|
||||||
{ key: 'email', name: 'Email' },
|
{ key: 'email', name: 'Email' },
|
||||||
{
|
{
|
||||||
key: 'api',
|
key: 'api',
|
||||||
|
|
|
@ -29,6 +29,14 @@
|
||||||
>
|
>
|
||||||
</woot-code>
|
</woot-code>
|
||||||
</div>
|
</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">
|
<div class="medium-6 small-offset-3">
|
||||||
<woot-code
|
<woot-code
|
||||||
v-if="isAEmailInbox"
|
v-if="isAEmailInbox"
|
||||||
|
@ -86,6 +94,9 @@ export default {
|
||||||
isALineInbox() {
|
isALineInbox() {
|
||||||
return this.currentInbox.channel_type === 'Channel::Line';
|
return this.currentInbox.channel_type === 'Channel::Line';
|
||||||
},
|
},
|
||||||
|
isASmsInbox() {
|
||||||
|
return this.currentInbox.channel_type === 'Channel::Sms';
|
||||||
|
},
|
||||||
message() {
|
message() {
|
||||||
if (this.isATwilioInbox) {
|
if (this.isATwilioInbox) {
|
||||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
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) {
|
if (this.isALineInbox) {
|
||||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||||
'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE'
|
'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE'
|
||||||
|
@ -103,10 +120,11 @@ export default {
|
||||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.currentInbox.web_widget_script) {
|
if (this.currentInbox.web_widget_script) {
|
||||||
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
|
||||||
}
|
}
|
||||||
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
|
|
||||||
|
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
<span v-if="item.channel_type === 'Channel::Whatsapp'">
|
<span v-if="item.channel_type === 'Channel::Whatsapp'">
|
||||||
Whatsapp
|
Whatsapp
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.channel_type === 'Channel::Sms'">
|
||||||
|
Sms
|
||||||
|
</span>
|
||||||
<span v-if="item.channel_type === 'Channel::Email'">
|
<span v-if="item.channel_type === 'Channel::Email'">
|
||||||
Email
|
Email
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -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>
|
|
@ -4,18 +4,39 @@
|
||||||
:header-title="$t('INBOX_MGMT.ADD.SMS.TITLE')"
|
:header-title="$t('INBOX_MGMT.ADD.SMS.TITLE')"
|
||||||
:header-content="$t('INBOX_MGMT.ADD.SMS.DESC')"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PageHeader from '../../SettingsSubPageHeader';
|
import PageHeader from '../../SettingsSubPageHeader';
|
||||||
|
import BandwidthSms from './BandwidthSms.vue';
|
||||||
import Twilio from './Twilio';
|
import Twilio from './Twilio';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Twilio,
|
Twilio,
|
||||||
|
BandwidthSms,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
provider: 'twilio',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -78,9 +78,11 @@ export const getters = {
|
||||||
item => item.channel_type === INBOX_TYPES.TWILIO
|
item => item.channel_type === INBOX_TYPES.TWILIO
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getTwilioSMSInboxes($state) {
|
getSMSInboxes($state) {
|
||||||
return $state.records.filter(
|
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) {
|
dialogFlowEnabledInboxes($state) {
|
||||||
|
|
|
@ -55,4 +55,11 @@ export default [
|
||||||
website_token: 'randomid125',
|
website_token: 'randomid125',
|
||||||
enable_auto_assignment: true,
|
enable_auto_assignment: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
channel_id: 6,
|
||||||
|
name: 'Test Widget 6',
|
||||||
|
channel_type: 'Channel::Sms',
|
||||||
|
provider: 'default',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -19,14 +19,14 @@ describe('#getters', () => {
|
||||||
expect(getters.getTwilioInboxes(state).length).toEqual(1);
|
expect(getters.getTwilioInboxes(state).length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getTwilioSMSInboxes', () => {
|
it('getSMSInboxes', () => {
|
||||||
const state = { records: inboxList };
|
const state = { records: inboxList };
|
||||||
expect(getters.getTwilioSMSInboxes(state).length).toEqual(1);
|
expect(getters.getSMSInboxes(state).length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dialogFlowEnabledInboxes', () => {
|
it('dialogFlowEnabledInboxes', () => {
|
||||||
const state = { records: inboxList };
|
const state = { records: inboxList };
|
||||||
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(5);
|
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getInbox', () => {
|
it('getInbox', () => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const INBOX_TYPES = {
|
||||||
EMAIL: 'Channel::Email',
|
EMAIL: 'Channel::Email',
|
||||||
TELEGRAM: 'Channel::Telegram',
|
TELEGRAM: 'Channel::Telegram',
|
||||||
LINE: 'Channel::Line',
|
LINE: 'Channel::Line',
|
||||||
|
SMS: 'Channel::Sms',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -6,19 +6,20 @@ class SendReplyJob < ApplicationJob
|
||||||
conversation = message.conversation
|
conversation = message.conversation
|
||||||
channel_name = conversation.inbox.channel.class.to_s
|
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
|
case channel_name
|
||||||
when 'Channel::FacebookPage'
|
when 'Channel::FacebookPage'
|
||||||
send_on_facebook_page(message)
|
send_on_facebook_page(message)
|
||||||
when 'Channel::TwitterProfile'
|
else
|
||||||
::Twitter::SendOnTwitterService.new(message: message).perform
|
services[channel_name].new(message: message).perform if services[channel_name].present?
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
13
app/jobs/webhooks/sms_events_job.rb
Normal file
13
app/jobs/webhooks/sms_events_job.rb
Normal 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
|
|
@ -69,6 +69,7 @@ class Account < ApplicationRecord
|
||||||
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
|
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
|
||||||
has_many :webhooks, dependent: :destroy_async
|
has_many :webhooks, dependent: :destroy_async
|
||||||
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
|
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 :working_hours, dependent: :destroy_async
|
||||||
has_many :automation_rules, dependent: :destroy
|
has_many :automation_rules, dependent: :destroy
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ class Campaign < ApplicationRecord
|
||||||
return if completed?
|
return if completed?
|
||||||
|
|
||||||
Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS'
|
Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS'
|
||||||
|
Sms::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Sms'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -69,14 +70,14 @@ class Campaign < ApplicationRecord
|
||||||
def validate_campaign_inbox
|
def validate_campaign_inbox
|
||||||
return unless 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
|
end
|
||||||
|
|
||||||
# TO-DO we clean up with better validations when campaigns evolve into more inboxes
|
# TO-DO we clean up with better validations when campaigns evolve into more inboxes
|
||||||
def ensure_correct_campaign_attributes
|
def ensure_correct_campaign_attributes
|
||||||
return if inbox.blank?
|
return if inbox.blank?
|
||||||
|
|
||||||
if inbox.inbox_type == 'Twilio SMS'
|
if ['Twilio SMS', 'Sms'].include?(inbox.inbox_type)
|
||||||
self.campaign_type = 'one_off'
|
self.campaign_type = 'one_off'
|
||||||
self.scheduled_at ||= Time.now.utc
|
self.scheduled_at ||= Time.now.utc
|
||||||
else
|
else
|
||||||
|
|
81
app/models/channel/sms.rb
Normal file
81
app/models/channel/sms.rb
Normal 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
|
|
@ -149,6 +149,6 @@ class Channel::Whatsapp < ApplicationRecord
|
||||||
url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}"
|
url: "#{ENV['FRONTEND_URL']}/webhooks/whatsapp/#{phone_number}"
|
||||||
}.to_json
|
}.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
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,6 +107,8 @@ class Inbox < ApplicationRecord
|
||||||
case channel_type
|
case channel_type
|
||||||
when 'Channel::TwilioSms'
|
when 'Channel::TwilioSms'
|
||||||
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
||||||
|
when 'Channel::Sms'
|
||||||
|
"#{ENV['FRONTEND_URL']}/webhooks/sms/#{channel.phone_number.delete_prefix('+')}"
|
||||||
when 'Channel::Line'
|
when 'Channel::Line'
|
||||||
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,8 @@ class Contacts::ContactableInboxesService
|
||||||
twilio_contactable_inbox(inbox)
|
twilio_contactable_inbox(inbox)
|
||||||
when 'Channel::Whatsapp'
|
when 'Channel::Whatsapp'
|
||||||
whatsapp_contactable_inbox(inbox)
|
whatsapp_contactable_inbox(inbox)
|
||||||
|
when 'Channel::Sms'
|
||||||
|
sms_contactable_inbox(inbox)
|
||||||
when 'Channel::Email'
|
when 'Channel::Email'
|
||||||
email_contactable_inbox(inbox)
|
email_contactable_inbox(inbox)
|
||||||
when 'Channel::Api'
|
when 'Channel::Api'
|
||||||
|
@ -52,6 +54,12 @@ class Contacts::ContactableInboxesService
|
||||||
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
|
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sms_contactable_inbox(inbox)
|
||||||
|
return unless @contact.phone_number
|
||||||
|
|
||||||
|
{ source_id: @contact.phone_number, inbox: inbox }
|
||||||
|
end
|
||||||
|
|
||||||
def twilio_contactable_inbox(inbox)
|
def twilio_contactable_inbox(inbox)
|
||||||
return if @contact.phone_number.blank?
|
return if @contact.phone_number.blank?
|
||||||
|
|
||||||
|
|
66
app/services/sms/incoming_message_service.rb
Normal file
66
app/services/sms/incoming_message_service.rb
Normal 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
|
32
app/services/sms/oneoff_sms_campaign_service.rb
Normal file
32
app/services/sms/oneoff_sms_campaign_service.rb
Normal 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
|
16
app/services/sms/send_on_sms_service.rb
Normal file
16
app/services/sms/send_on_sms_service.rb
Normal 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
|
|
@ -271,6 +271,7 @@ Rails.application.routes.draw do
|
||||||
post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload'
|
post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload'
|
||||||
post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload'
|
post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload'
|
||||||
post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload'
|
post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload'
|
||||||
|
post 'webhooks/sms/:phone_number', to: 'webhooks/sms#process_payload'
|
||||||
get 'webhooks/instagram', to: 'webhooks/instagram#verify'
|
get 'webhooks/instagram', to: 'webhooks/instagram#verify'
|
||||||
post 'webhooks/instagram', to: 'webhooks/instagram#events'
|
post 'webhooks/instagram', to: 'webhooks/instagram#events'
|
||||||
|
|
||||||
|
|
11
db/migrate/20220129024443_add_sms_channel.rb
Normal file
11
db/migrate/20220129024443_add_sms_channel.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class AddSmsChannel < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :channel_sms do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.string :phone_number, null: false, index: { unique: true }
|
||||||
|
t.string :provider, default: 'default'
|
||||||
|
t.jsonb :provider_config, default: {}
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
db/schema.rb
10
db/schema.rb
|
@ -228,6 +228,16 @@ ActiveRecord::Schema.define(version: 2022_01_31_081750) do
|
||||||
t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true
|
t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "channel_sms", force: :cascade do |t|
|
||||||
|
t.integer "account_id", null: false
|
||||||
|
t.string "phone_number", null: false
|
||||||
|
t.string "provider", default: "default"
|
||||||
|
t.jsonb "provider_config", default: {}
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["phone_number"], name: "index_channel_sms_on_phone_number", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "channel_telegram", force: :cascade do |t|
|
create_table "channel_telegram", force: :cascade do |t|
|
||||||
t.string "bot_name"
|
t.string "bot_name"
|
||||||
t.integer "account_id", null: false
|
t.integer "account_id", null: false
|
||||||
|
|
|
@ -99,6 +99,53 @@ describe ::ContactInboxBuilder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'sms inbox' do
|
||||||
|
let!(:sms_channel) { create(:channel_sms, account: account) }
|
||||||
|
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
|
||||||
|
|
||||||
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: sms_inbox.id,
|
||||||
|
source_id: contact.phone_number
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: sms_inbox.id
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: sms_inbox.id,
|
||||||
|
source_id: '+224213223422'
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
||||||
|
expect(contact_inbox.source_id).not_to be('+224213223422')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: sms_inbox.id
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.source_id).not_to be(contact.phone_number)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'email inbox' do
|
describe 'email inbox' do
|
||||||
let!(:email_channel) { create(:channel_email, account: account) }
|
let!(:email_channel) { create(:channel_email, account: account) }
|
||||||
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
|
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
|
||||||
|
|
|
@ -309,6 +309,18 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||||
expect(response.body).to include('callback_webhook_url')
|
expect(response.body).to include('callback_webhook_url')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a sms inbox when administrator' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/inboxes",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: { name: 'Sms Inbox',
|
||||||
|
channel: { type: 'sms', phone_number: '+123456789', provider_config: { test: 'test' } } },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Sms Inbox')
|
||||||
|
expect(response.body).to include('+123456789')
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates the webwidget inbox that allow messages after conversation is resolved' do
|
it 'creates the webwidget inbox that allow messages after conversation is resolved' do
|
||||||
post "/api/v1/accounts/#{account.id}/inboxes",
|
post "/api/v1/accounts/#{account.id}/inboxes",
|
||||||
headers: admin.create_new_auth_token,
|
headers: admin.create_new_auth_token,
|
||||||
|
|
12
spec/controllers/webhooks/sms_controller_spec.rb
Normal file
12
spec/controllers/webhooks/sms_controller_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Webhooks::SmsController', type: :request do
|
||||||
|
describe 'POST /webhooks/sms/{:phone_number}' do
|
||||||
|
it 'call the sms events job with the params' do
|
||||||
|
allow(Webhooks::SmsEventsJob).to receive(:perform_later)
|
||||||
|
expect(Webhooks::SmsEventsJob).to receive(:perform_later)
|
||||||
|
post '/webhooks/sms/123221321', params: { content: 'hello' }
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
16
spec/factories/channel/channel_sms.rb
Normal file
16
spec/factories/channel/channel_sms.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :channel_sms, class: 'Channel::Sms' do
|
||||||
|
sequence(:phone_number) { |n| "+123456789#{n}1" }
|
||||||
|
account
|
||||||
|
provider_config do
|
||||||
|
{ 'account_id' => '1',
|
||||||
|
'application_id' => '1',
|
||||||
|
'api_key' => '1',
|
||||||
|
'api_secret' => '1' }
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:create) do |channel_sms|
|
||||||
|
create(:inbox, channel: channel_sms, account: channel_sms.account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -75,5 +75,14 @@ RSpec.describe SendReplyJob, type: :job do
|
||||||
expect(process_service).to receive(:perform)
|
expect(process_service).to receive(:perform)
|
||||||
described_class.perform_now(message.id)
|
described_class.perform_now(message.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls ::Sms::SendOnSmsService when its sms message' do
|
||||||
|
sms_channel = create(:channel_sms)
|
||||||
|
message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox))
|
||||||
|
allow(::Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service)
|
||||||
|
expect(::Sms::SendOnSmsService).to receive(:new).with(message: message)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
56
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
56
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Webhooks::SmsEventsJob, type: :job do
|
||||||
|
subject(:job) { described_class.perform_later(params) }
|
||||||
|
|
||||||
|
let!(:sms_channel) { create(:channel_sms) }
|
||||||
|
let!(:params) do
|
||||||
|
{
|
||||||
|
time: '2022-02-02T23:14:05.309Z',
|
||||||
|
type: 'message-received',
|
||||||
|
to: sms_channel.phone_number,
|
||||||
|
description: 'Incoming message received',
|
||||||
|
message: {
|
||||||
|
'id': '3232420-2323-234324',
|
||||||
|
'owner': sms_channel.phone_number,
|
||||||
|
'applicationId': '2342349-324234d-32432432',
|
||||||
|
'time': '2022-02-02T23:14:05.262Z',
|
||||||
|
'segmentCount': 1,
|
||||||
|
'direction': 'in',
|
||||||
|
'to': [
|
||||||
|
sms_channel.phone_number
|
||||||
|
],
|
||||||
|
'from': '+14234234234',
|
||||||
|
'text': 'test message'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'enqueues the job' do
|
||||||
|
expect { job }.to have_enqueued_job(described_class)
|
||||||
|
.with(params)
|
||||||
|
.on_queue('default')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invalid params' do
|
||||||
|
it 'returns nil when no bot_token' do
|
||||||
|
expect(described_class.perform_now({})).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when invalid type' do
|
||||||
|
expect(described_class.perform_now({ type: 'invalid' })).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when valid params' do
|
||||||
|
it 'calls Sms::IncomingMessageService' do
|
||||||
|
process_service = double
|
||||||
|
allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||||
|
allow(process_service).to receive(:perform)
|
||||||
|
expect(Sms::IncomingMessageService).to receive(:new).with(inbox: sms_channel.inbox,
|
||||||
|
params: params[:message].with_indifferent_access)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -78,6 +78,27 @@ RSpec.describe Campaign, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when SMS campaign' do
|
||||||
|
let!(:sms_channel) { create(:channel_sms) }
|
||||||
|
let!(:sms_inbox) { create(:inbox, channel: sms_channel) }
|
||||||
|
let(:campaign) { build(:campaign, inbox: sms_inbox) }
|
||||||
|
|
||||||
|
it 'only saves campaign type as oneoff and wont leave scheduled_at empty' do
|
||||||
|
campaign.campaign_type = 'ongoing'
|
||||||
|
campaign.save!
|
||||||
|
expect(campaign.reload.campaign_type).to eq 'one_off'
|
||||||
|
expect(campaign.scheduled_at.present?).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls sms service on trigger!' do
|
||||||
|
sms_service = double
|
||||||
|
expect(Sms::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
|
||||||
|
expect(sms_service).to receive(:perform)
|
||||||
|
campaign.save!
|
||||||
|
campaign.trigger!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when Website campaign' do
|
context 'when Website campaign' do
|
||||||
let(:campaign) { build(:campaign) }
|
let(:campaign) { build(:campaign) }
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ describe Contacts::ContactableInboxesService do
|
||||||
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
|
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
|
||||||
let!(:api_channel) { create(:channel_api, account: account) }
|
let!(:api_channel) { create(:channel_api, account: account) }
|
||||||
let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
|
let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
|
||||||
let!(:website_channel) { create(:channel_widget, account: account) }
|
let!(:website_inbox) { create(:inbox, channel: create(:channel_widget, account: account), account: account) }
|
||||||
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
|
let!(:sms_inbox) { create(:inbox, channel: create(:channel_sms, account: account), account: account) }
|
||||||
|
|
||||||
describe '#get' do
|
describe '#get' do
|
||||||
it 'returns the contactable inboxes for the contact' do
|
it 'returns the contactable inboxes for the contact' do
|
||||||
|
@ -25,7 +25,7 @@ describe Contacts::ContactableInboxesService do
|
||||||
expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: twilio_sms_inbox })
|
expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: twilio_sms_inbox })
|
||||||
expect(contactable_inboxes).to include({ source_id: "whatsapp:#{contact.phone_number}", inbox: twilio_whatsapp_inbox })
|
expect(contactable_inboxes).to include({ source_id: "whatsapp:#{contact.phone_number}", inbox: twilio_whatsapp_inbox })
|
||||||
expect(contactable_inboxes).to include({ source_id: contact.email, inbox: email_inbox })
|
expect(contactable_inboxes).to include({ source_id: contact.email, inbox: email_inbox })
|
||||||
expect(contactable_inboxes.pluck(:inbox)).to include(api_inbox)
|
expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: sms_inbox })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'doest not return the non contactable inboxes for the contact' do
|
it 'doest not return the non contactable inboxes for the contact' do
|
||||||
|
|
31
spec/services/sms/incoming_message_service_spec.rb
Normal file
31
spec/services/sms/incoming_message_service_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Sms::IncomingMessageService do
|
||||||
|
describe '#perform' do
|
||||||
|
let!(:sms_channel) { create(:channel_sms) }
|
||||||
|
|
||||||
|
context 'when valid text message params' do
|
||||||
|
it 'creates appropriate conversations, message and contacts' do
|
||||||
|
params = {
|
||||||
|
|
||||||
|
'id': '3232420-2323-234324',
|
||||||
|
'owner': sms_channel.phone_number,
|
||||||
|
'applicationId': '2342349-324234d-32432432',
|
||||||
|
'time': '2022-02-02T23:14:05.262Z',
|
||||||
|
'segmentCount': 1,
|
||||||
|
'direction': 'in',
|
||||||
|
'to': [
|
||||||
|
sms_channel.phone_number
|
||||||
|
],
|
||||||
|
'from': '+14234234234',
|
||||||
|
'text': 'test message'
|
||||||
|
|
||||||
|
}.with_indifferent_access
|
||||||
|
described_class.new(inbox: sms_channel.inbox, params: params).perform
|
||||||
|
expect(sms_channel.inbox.conversations.count).not_to eq(0)
|
||||||
|
expect(Contact.all.first.name).to eq('+1 423-423-4234')
|
||||||
|
expect(sms_channel.inbox.messages.first.content).to eq('test message')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
47
spec/services/sms/oneoff_sms_campaign_service_spec.rb
Normal file
47
spec/services/sms/oneoff_sms_campaign_service_spec.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Sms::OneoffSmsCampaignService do
|
||||||
|
subject(:sms_campaign_service) { described_class.new(campaign: campaign) }
|
||||||
|
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let!(:sms_channel) { create(:channel_sms) }
|
||||||
|
let!(:sms_inbox) { create(:inbox, channel: sms_channel) }
|
||||||
|
let(:label1) { create(:label, account: account) }
|
||||||
|
let(:label2) { create(:label, account: account) }
|
||||||
|
let!(:campaign) do
|
||||||
|
create(:campaign, inbox: sms_inbox, account: account,
|
||||||
|
audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages').to_return(
|
||||||
|
status: 200,
|
||||||
|
body: { 'id' => '1' }.to_json,
|
||||||
|
headers: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if the campaign is completed' do
|
||||||
|
campaign.completed!
|
||||||
|
|
||||||
|
expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error invalid campaign when its not a oneoff sms campaign' do
|
||||||
|
campaign = create(:campaign)
|
||||||
|
|
||||||
|
expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'send messages to contacts in the audience and marks the campaign completed' do
|
||||||
|
contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account)
|
||||||
|
contact_with_label1.update_labels([label1.title])
|
||||||
|
contact_with_label2.update_labels([label2.title])
|
||||||
|
contact_with_both_labels.update_labels([label1.title, label2.title])
|
||||||
|
sms_campaign_service.perform
|
||||||
|
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
|
||||||
|
expect(campaign.reload.completed?).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
spec/services/sms/send_on_sms_service_spec.rb
Normal file
28
spec/services/sms/send_on_sms_service_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Sms::SendOnSmsService do
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when a valid message' do
|
||||||
|
let(:sms_request) { double }
|
||||||
|
let!(:sms_channel) { create(:channel_sms) }
|
||||||
|
let!(:contact_inbox) { create(:contact_inbox, inbox: sms_channel.inbox, source_id: '+123456789') }
|
||||||
|
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: sms_channel.inbox) }
|
||||||
|
|
||||||
|
it 'calls channel.send_message' do
|
||||||
|
message = create(:message, message_type: :outgoing, content: 'test',
|
||||||
|
conversation: conversation)
|
||||||
|
allow(HTTParty).to receive(:post).and_return(sms_request)
|
||||||
|
allow(sms_request).to receive(:success?).and_return(true)
|
||||||
|
allow(sms_request).to receive(:parsed_response).and_return({ 'id' => '123456789' })
|
||||||
|
expect(HTTParty).to receive(:post).with(
|
||||||
|
'https://messaging.bandwidth.com/api/v2/users/1/messages',
|
||||||
|
basic_auth: { username: '1', password: '1' },
|
||||||
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
|
body: { 'to' => '+123456789', 'from' => sms_channel.phone_number, 'text' => 'test', 'applicationId' => '1' }.to_json
|
||||||
|
)
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue