feat: Line Channel (#2904)
- Ability to configure line bots as a channel in chatwoot - Receive a message sent to the line bot in chatwoot - Ability to reply to line users from chatwoot fixes: #2738
This commit is contained in:
parent
671c5c931f
commit
0a38632f14
37 changed files with 581 additions and 56 deletions
1
Gemfile
1
Gemfile
|
@ -78,6 +78,7 @@ gem 'wisper', '2.0.0'
|
||||||
##--- gems for channels ---##
|
##--- gems for channels ---##
|
||||||
# TODO: bump up gem to 2.0
|
# TODO: bump up gem to 2.0
|
||||||
gem 'facebook-messenger'
|
gem 'facebook-messenger'
|
||||||
|
gem 'line-bot-api'
|
||||||
gem 'twilio-ruby', '~> 5.32.0'
|
gem 'twilio-ruby', '~> 5.32.0'
|
||||||
# twitty will handle subscription of twitter account events
|
# twitty will handle subscription of twitter account events
|
||||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
|
|
|
@ -322,6 +322,7 @@ GEM
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
|
line-bot-api (1.21.0)
|
||||||
liquid (5.0.1)
|
liquid (5.0.1)
|
||||||
listen (3.6.0)
|
listen (3.6.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
|
@ -661,6 +662,7 @@ DEPENDENCIES
|
||||||
kaminari
|
kaminari
|
||||||
koala
|
koala
|
||||||
letter_opener
|
letter_opener
|
||||||
|
line-bot-api
|
||||||
liquid
|
liquid
|
||||||
listen
|
listen
|
||||||
maxminddb
|
maxminddb
|
||||||
|
|
|
@ -92,6 +92,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
when 'email'
|
when 'email'
|
||||||
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
|
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'
|
when 'telegram'
|
||||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
end
|
end
|
||||||
|
@ -122,6 +124,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
Channel::Email::EDITABLE_ATTRS
|
Channel::Email::EDITABLE_ATTRS
|
||||||
when 'Channel::Telegram'
|
when 'Channel::Telegram'
|
||||||
Channel::Telegram::EDITABLE_ATTRS
|
Channel::Telegram::EDITABLE_ATTRS
|
||||||
|
when 'Channel::Line'
|
||||||
|
Channel::Line::EDITABLE_ATTRS
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
6
app/controllers/webhooks/line_controller.rb
Normal file
6
app/controllers/webhooks/line_controller.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class Webhooks::LineController < ActionController::API
|
||||||
|
def process_payload
|
||||||
|
Webhooks::LineEventsJob.perform_later(params: params.to_unsafe_hash, signature: request.headers['x-line-signature'], post_body: request.raw_post)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
|
@ -76,7 +76,16 @@ export default {
|
||||||
if (key === 'email') {
|
if (key === 'email') {
|
||||||
return this.enabledFeatures.channel_email;
|
return this.enabledFeatures.channel_email;
|
||||||
}
|
}
|
||||||
return ['website', 'twilio', 'api', 'whatsapp', 'sms', 'telegram'].includes(key);
|
|
||||||
|
return [
|
||||||
|
'website',
|
||||||
|
'twilio',
|
||||||
|
'api',
|
||||||
|
'whatsapp',
|
||||||
|
'sms',
|
||||||
|
'telegram',
|
||||||
|
'line',
|
||||||
|
].includes(key);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -35,6 +35,13 @@
|
||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||||
/>
|
/>
|
||||||
|
<img
|
||||||
|
v-if="badge === 'Channel::Line'"
|
||||||
|
id="badge"
|
||||||
|
class="source-badge"
|
||||||
|
:style="badgeStyle"
|
||||||
|
src="~dashboard/assets/images/channels/line.png"
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'Channel::Telegram'"
|
v-if="badge === 'Channel::Telegram'"
|
||||||
id="badge"
|
id="badge"
|
||||||
|
|
|
@ -172,7 +172,32 @@
|
||||||
},
|
},
|
||||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||||
},
|
},
|
||||||
"TELEGRAM_CHANNEL": {
|
"LINE_CHANNEL": {
|
||||||
|
"TITLE": "LINE Channel",
|
||||||
|
"DESC": "Integrate with LINE channel and start supporting your customers.",
|
||||||
|
"CHANNEL_NAME": {
|
||||||
|
"LABEL": "Channel Name",
|
||||||
|
"PLACEHOLDER": "Please enter a channel name",
|
||||||
|
"ERROR": "This field is required"
|
||||||
|
},
|
||||||
|
"LINE_CHANNEL_ID": {
|
||||||
|
"LABEL": "LINE Channel ID",
|
||||||
|
"PLACEHOLDER": "LINE Channel ID"
|
||||||
|
},
|
||||||
|
"LINE_CHANNEL_SECRET": {
|
||||||
|
"LABEL": "LINE Channel Secret",
|
||||||
|
"PLACEHOLDER": "LINE Channel Secret"
|
||||||
|
},
|
||||||
|
"LINE_CHANNEL_TOKEN": {
|
||||||
|
"LABEL": "LINE Channel Token",
|
||||||
|
"PLACEHOLDER": "LINE Channel Token"
|
||||||
|
},
|
||||||
|
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||||
|
"API": {
|
||||||
|
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TELEGRAM_CHANNEL": {
|
||||||
"TITLE": "Telegram Channel",
|
"TITLE": "Telegram Channel",
|
||||||
"DESC": "Integrate with Telegram channel and start supporting your customers.",
|
"DESC": "Integrate with Telegram channel and start supporting your customers.",
|
||||||
"BOT_TOKEN": {
|
"BOT_TOKEN": {
|
||||||
|
|
|
@ -17,7 +17,15 @@
|
||||||
<woot-code
|
<woot-code
|
||||||
v-if="isATwilioInbox"
|
v-if="isATwilioInbox"
|
||||||
lang="html"
|
lang="html"
|
||||||
:script="twilioCallbackURL"
|
:script="currentInbox.webhook_url"
|
||||||
|
>
|
||||||
|
</woot-code>
|
||||||
|
</div>
|
||||||
|
<div class="medium-6 small-offset-3">
|
||||||
|
<woot-code
|
||||||
|
v-if="isALineInbox"
|
||||||
|
lang="html"
|
||||||
|
:script="currentInbox.webhook_url"
|
||||||
>
|
>
|
||||||
</woot-code>
|
</woot-code>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,6 +83,9 @@ export default {
|
||||||
isAEmailInbox() {
|
isAEmailInbox() {
|
||||||
return this.currentInbox.channel_type === 'Channel::Email';
|
return this.currentInbox.channel_type === 'Channel::Email';
|
||||||
},
|
},
|
||||||
|
isALineInbox() {
|
||||||
|
return this.currentInbox.channel_type === 'Channel::Line';
|
||||||
|
},
|
||||||
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(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Api from './channels/Api';
|
||||||
import Email from './channels/Email';
|
import Email from './channels/Email';
|
||||||
import Sms from './channels/Sms';
|
import Sms from './channels/Sms';
|
||||||
import Whatsapp from './channels/Whatsapp';
|
import Whatsapp from './channels/Whatsapp';
|
||||||
|
import Line from './channels/Line';
|
||||||
import Telegram from './channels/Telegram';
|
import Telegram from './channels/Telegram';
|
||||||
|
|
||||||
const channelViewList = {
|
const channelViewList = {
|
||||||
|
@ -15,6 +16,7 @@ const channelViewList = {
|
||||||
email: Email,
|
email: Email,
|
||||||
sms: Sms,
|
sms: Sms,
|
||||||
whatsapp: Whatsapp,
|
whatsapp: Whatsapp,
|
||||||
|
line: Line,
|
||||||
telegram: Telegram,
|
telegram: Telegram,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<div class="wizard-body small-9 columns">
|
||||||
|
<page-header
|
||||||
|
:header-title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.TITLE')"
|
||||||
|
:header-content="$t('INBOX_MGMT.ADD.LINE_CHANNEL.DESC')"
|
||||||
|
/>
|
||||||
|
<form class="row" @submit.prevent="createChannel()">
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.channelName.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.LINE_CHANNEL.CHANNEL_NAME.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="channelName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="
|
||||||
|
$t('INBOX_MGMT.ADD.LINE_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.channelName.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.channelName.$error" class="message">{{
|
||||||
|
$t('INBOX_MGMT.ADD.LINE_CHANNEL.CHANNEL_NAME.ERROR')
|
||||||
|
}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.lineChannelId.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_ID.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="lineChannelId"
|
||||||
|
type="text"
|
||||||
|
:placeholder="
|
||||||
|
$t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_ID.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.lineChannelId.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.lineChannelSecret.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_SECRET.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="lineChannelSecret"
|
||||||
|
type="text"
|
||||||
|
:placeholder="
|
||||||
|
$t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_SECRET.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.lineChannelSecret.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-8 columns">
|
||||||
|
<label :class="{ error: $v.lineChannelToken.$error }">
|
||||||
|
{{ $t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_TOKEN.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="lineChannelToken"
|
||||||
|
type="text"
|
||||||
|
:placeholder="
|
||||||
|
$t('INBOX_MGMT.ADD.LINE_CHANNEL.LINE_CHANNEL_TOKEN.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.lineChannelToken.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-submit-button
|
||||||
|
:loading="uiFlags.isCreating"
|
||||||
|
:button-text="$t('INBOX_MGMT.ADD.LINE_CHANNEL.SUBMIT_BUTTON')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
import router from '../../../../index';
|
||||||
|
import PageHeader from '../../SettingsSubPageHeader';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
channelName: '',
|
||||||
|
lineChannelId: '',
|
||||||
|
lineChannelSecret: '',
|
||||||
|
lineChannelToken: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'inboxes/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
channelName: { required },
|
||||||
|
lineChannelId: { required },
|
||||||
|
lineChannelSecret: { required },
|
||||||
|
lineChannelToken: { required },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async createChannel() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lineChannel = await this.$store.dispatch('inboxes/createChannel', {
|
||||||
|
name: this.channelName,
|
||||||
|
channel: {
|
||||||
|
type: 'line',
|
||||||
|
line_channel_id: this.lineChannelId,
|
||||||
|
line_channel_secret: this.lineChannelSecret,
|
||||||
|
line_channel_token: this.lineChannelToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
router.replace({
|
||||||
|
name: 'settings_inboxes_add_agents',
|
||||||
|
params: {
|
||||||
|
page: 'new',
|
||||||
|
inbox_id: lineChannel.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('INBOX_MGMT.ADD.LINE_CHANNEL.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -3,9 +3,6 @@ export default {
|
||||||
hostURL() {
|
hostURL() {
|
||||||
return window.chatwootConfig.hostURL;
|
return window.chatwootConfig.hostURL;
|
||||||
},
|
},
|
||||||
twilioCallbackURL() {
|
|
||||||
return `${this.hostURL}/twilio/callback`;
|
|
||||||
},
|
|
||||||
vapidPublicKey() {
|
vapidPublicKey() {
|
||||||
return window.chatwootConfig.vapidPublicKey;
|
return window.chatwootConfig.vapidPublicKey;
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,8 @@ class SendReplyJob < ApplicationJob
|
||||||
::Twitter::SendOnTwitterService.new(message: message).perform
|
::Twitter::SendOnTwitterService.new(message: message).perform
|
||||||
when 'Channel::TwilioSms'
|
when 'Channel::TwilioSms'
|
||||||
::Twilio::SendOnTwilioService.new(message: message).perform
|
::Twilio::SendOnTwilioService.new(message: message).perform
|
||||||
|
when 'Channel::Line'
|
||||||
|
::Line::SendOnLineService.new(message: message).perform
|
||||||
when 'Channel::Telegram'
|
when 'Channel::Telegram'
|
||||||
::Telegram::SendOnTelegramService.new(message: message).perform
|
::Telegram::SendOnTelegramService.new(message: message).perform
|
||||||
end
|
end
|
||||||
|
|
24
app/jobs/webhooks/line_events_job.rb
Normal file
24
app/jobs/webhooks/line_events_job.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
class Webhooks::LineEventsJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(params: {}, signature: '', post_body: '')
|
||||||
|
@params = params
|
||||||
|
return unless valid_event_payload?
|
||||||
|
return unless valid_post_body?(post_body, signature)
|
||||||
|
|
||||||
|
Line::IncomingMessageService.new(inbox: @channel.inbox, params: @params['line'].with_indifferent_access).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_event_payload?
|
||||||
|
@channel = Channel::Line.find_by(line_channel_id: @params[:line_channel_id]) if @params[:line_channel_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://developers.line.biz/en/reference/messaging-api/#signature-validation
|
||||||
|
# validate the line payload
|
||||||
|
def valid_post_body?(post_body, signature)
|
||||||
|
hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @channel.line_channel_secret, post_body)
|
||||||
|
Base64.strict_encode64(hash) == signature
|
||||||
|
end
|
||||||
|
end
|
|
@ -51,6 +51,7 @@ class Account < ApplicationRecord
|
||||||
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
||||||
has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email'
|
has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email'
|
||||||
has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api'
|
has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api'
|
||||||
|
has_many :line_channels, dependent: :destroy, class_name: '::Channel::Line'
|
||||||
has_many :telegram_channels, dependent: :destroy, class_name: '::Channel::Telegram'
|
has_many :telegram_channels, dependent: :destroy, class_name: '::Channel::Telegram'
|
||||||
has_many :canned_responses, dependent: :destroy
|
has_many :canned_responses, dependent: :destroy
|
||||||
has_many :webhooks, dependent: :destroy
|
has_many :webhooks, dependent: :destroy
|
||||||
|
|
|
@ -18,22 +18,15 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::Api < ApplicationRecord
|
class Channel::Api < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
self.table_name = 'channel_api'
|
self.table_name = 'channel_api'
|
||||||
EDITABLE_ATTRS = [:webhook_url].freeze
|
EDITABLE_ATTRS = [:webhook_url].freeze
|
||||||
|
|
||||||
validates :account_id, presence: true
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
has_secure_token :identifier
|
has_secure_token :identifier
|
||||||
has_secure_token :hmac_token
|
has_secure_token :hmac_token
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
|
|
||||||
def name
|
def name
|
||||||
'API'
|
'API'
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_24_hour_messaging_window?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,25 +16,20 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::Email < ApplicationRecord
|
class Channel::Email < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
self.table_name = 'channel_email'
|
self.table_name = 'channel_email'
|
||||||
EDITABLE_ATTRS = [:email].freeze
|
EDITABLE_ATTRS = [:email].freeze
|
||||||
|
|
||||||
validates :account_id, presence: true
|
|
||||||
belongs_to :account
|
|
||||||
validates :email, uniqueness: true
|
validates :email, uniqueness: true
|
||||||
validates :forward_to_email, uniqueness: true
|
validates :forward_to_email, uniqueness: true
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
before_validation :ensure_forward_to_email, on: :create
|
before_validation :ensure_forward_to_email, on: :create
|
||||||
|
|
||||||
def name
|
def name
|
||||||
'Email'
|
'Email'
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_24_hour_messaging_window?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_forward_to_email
|
def ensure_forward_to_email
|
||||||
|
|
|
@ -17,15 +17,12 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::FacebookPage < ApplicationRecord
|
class Channel::FacebookPage < ApplicationRecord
|
||||||
self.table_name = 'channel_facebook_pages'
|
include Channelable
|
||||||
|
|
||||||
include Reauthorizable
|
include Reauthorizable
|
||||||
|
|
||||||
validates :account_id, presence: true
|
self.table_name = 'channel_facebook_pages'
|
||||||
validates :page_id, uniqueness: { scope: :account_id }
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
validates :page_id, uniqueness: { scope: :account_id }
|
||||||
|
|
||||||
after_create_commit :subscribe
|
after_create_commit :subscribe
|
||||||
before_destroy :unsubscribe
|
before_destroy :unsubscribe
|
||||||
|
|
39
app/models/channel/line.rb
Normal file
39
app/models/channel/line.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: channel_line
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# line_channel_secret :string not null
|
||||||
|
# line_channel_token :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer not null
|
||||||
|
# line_channel_id :string not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_channel_line_on_line_channel_id (line_channel_id) UNIQUE
|
||||||
|
#
|
||||||
|
|
||||||
|
class Channel::Line < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
|
self.table_name = 'channel_line'
|
||||||
|
EDITABLE_ATTRS = [:line_channel_id, :line_channel_secret, :line_channel_token].freeze
|
||||||
|
|
||||||
|
validates :line_channel_id, uniqueness: true, presence: true
|
||||||
|
validates :line_channel_secret, presence: true
|
||||||
|
validates :line_channel_token, presence: true
|
||||||
|
|
||||||
|
def name
|
||||||
|
'LINE'
|
||||||
|
end
|
||||||
|
|
||||||
|
def client
|
||||||
|
@client ||= Line::Bot::Client.new do |config|
|
||||||
|
config.channel_id = line_channel_id
|
||||||
|
config.channel_secret = line_channel_secret
|
||||||
|
config.channel_token = line_channel_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,14 +15,12 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::Telegram < ApplicationRecord
|
class Channel::Telegram < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
self.table_name = 'channel_telegram'
|
self.table_name = 'channel_telegram'
|
||||||
EDITABLE_ATTRS = [:bot_token].freeze
|
EDITABLE_ATTRS = [:bot_token].freeze
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
before_validation :ensure_valid_bot_token, on: :create
|
before_validation :ensure_valid_bot_token, on: :create
|
||||||
validates :account_id, presence: true
|
|
||||||
validates :bot_token, presence: true, uniqueness: true
|
validates :bot_token, presence: true, uniqueness: true
|
||||||
before_save :setup_telegram_webhook
|
before_save :setup_telegram_webhook
|
||||||
|
|
||||||
|
@ -30,10 +28,6 @@ class Channel::Telegram < ApplicationRecord
|
||||||
'Telegram'
|
'Telegram'
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_24_hour_messaging_window?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def telegram_api_url
|
def telegram_api_url
|
||||||
"https://api.telegram.org/bot#{bot_token}"
|
"https://api.telegram.org/bot#{bot_token}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,19 +17,16 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::TwilioSms < ApplicationRecord
|
class Channel::TwilioSms < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
self.table_name = 'channel_twilio_sms'
|
self.table_name = 'channel_twilio_sms'
|
||||||
|
|
||||||
validates :account_id, presence: true
|
|
||||||
validates :account_sid, presence: true
|
validates :account_sid, presence: true
|
||||||
validates :auth_token, presence: true
|
validates :auth_token, presence: true
|
||||||
validates :phone_number, uniqueness: { scope: :account_id }, presence: true
|
validates :phone_number, uniqueness: { scope: :account_id }, presence: true
|
||||||
|
|
||||||
enum medium: { sms: 0, whatsapp: 1 }
|
enum medium: { sms: 0, whatsapp: 1 }
|
||||||
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
|
|
||||||
def name
|
def name
|
||||||
medium == 'sms' ? 'Twilio SMS' : 'Whatsapp'
|
medium == 'sms' ? 'Twilio SMS' : 'Whatsapp'
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::TwitterProfile < ApplicationRecord
|
class Channel::TwitterProfile < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
|
|
||||||
self.table_name = 'channel_twitter_profiles'
|
self.table_name = 'channel_twitter_profiles'
|
||||||
|
|
||||||
validates :account_id, presence: true
|
|
||||||
validates :profile_id, uniqueness: { scope: :account_id }
|
validates :profile_id, uniqueness: { scope: :account_id }
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
|
|
||||||
before_destroy :unsubscribe
|
before_destroy :unsubscribe
|
||||||
|
|
||||||
|
@ -30,10 +28,6 @@ class Channel::TwitterProfile < ApplicationRecord
|
||||||
'Twitter'
|
'Twitter'
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_24_hour_messaging_window?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_contact_inbox(profile_id, name, additional_attributes)
|
def create_contact_inbox(profile_id, name, additional_attributes)
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)
|
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)
|
||||||
|
|
|
@ -25,7 +25,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Channel::WebWidget < ApplicationRecord
|
class Channel::WebWidget < ApplicationRecord
|
||||||
|
include Channelable
|
||||||
include FlagShihTzu
|
include FlagShihTzu
|
||||||
|
|
||||||
self.table_name = 'channel_web_widgets'
|
self.table_name = 'channel_web_widgets'
|
||||||
EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled,
|
EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled,
|
||||||
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||||
|
@ -34,8 +36,6 @@ class Channel::WebWidget < ApplicationRecord
|
||||||
validates :website_url, presence: true
|
validates :website_url, presence: true
|
||||||
validates :widget_color, presence: true
|
validates :widget_color, presence: true
|
||||||
|
|
||||||
belongs_to :account
|
|
||||||
has_one :inbox, as: :channel, dependent: :destroy
|
|
||||||
has_secure_token :website_token
|
has_secure_token :website_token
|
||||||
has_secure_token :hmac_token
|
has_secure_token :hmac_token
|
||||||
|
|
||||||
|
@ -50,10 +50,6 @@ class Channel::WebWidget < ApplicationRecord
|
||||||
'Website'
|
'Website'
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_24_hour_messaging_window?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def web_widget_script
|
def web_widget_script
|
||||||
"
|
"
|
||||||
<script>
|
<script>
|
||||||
|
|
12
app/models/concerns/channelable.rb
Normal file
12
app/models/concerns/channelable.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module Channelable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
included do
|
||||||
|
validates :account_id, presence: true
|
||||||
|
belongs_to :account
|
||||||
|
has_one :inbox, as: :channel, dependent: :destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_24_hour_messaging_window?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
|
@ -93,6 +93,15 @@ class Inbox < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def webhook_url
|
||||||
|
case channel_type
|
||||||
|
when 'Channel::TwilioSMS'
|
||||||
|
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
||||||
|
when 'Channel::Line'
|
||||||
|
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def delete_round_robin_agents
|
def delete_round_robin_agents
|
||||||
|
|
65
app/services/line/incoming_message_service.rb
Normal file
65
app/services/line/incoming_message_service.rb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
class Line::IncomingMessageService
|
||||||
|
include ::FileTypeHelper
|
||||||
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
line_contact_info
|
||||||
|
set_contact
|
||||||
|
set_conversation
|
||||||
|
# TODO: iterate over the events and handle the attachments in future
|
||||||
|
# https://github.com/line/line-bot-sdk-ruby#synopsis
|
||||||
|
@message = @conversation.messages.create(
|
||||||
|
content: params[:events].first['message']['text'],
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
inbox_id: @inbox.id,
|
||||||
|
message_type: :incoming,
|
||||||
|
sender: @contact,
|
||||||
|
source_id: (params[:events].first['message']['id']).to_s
|
||||||
|
)
|
||||||
|
@message.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_contact_info
|
||||||
|
@line_contact_info ||= JSON.parse(inbox.channel.client.get_profile(params[:events].first['source']['userId']).body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_contact
|
||||||
|
contact_inbox = ::ContactBuilder.new(
|
||||||
|
source_id: line_contact_info['userId'],
|
||||||
|
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: line_contact_info['displayName'],
|
||||||
|
avatar_url: line_contact_info['pictureUrl']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
11
app/services/line/send_on_line_service.rb
Normal file
11
app/services/line/send_on_line_service.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class Line::SendOnLineService < Base::SendOnChannelService
|
||||||
|
private
|
||||||
|
|
||||||
|
def channel_class
|
||||||
|
Channel::Line
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_reply
|
||||||
|
channel.client.push_message(message.conversation.contact_inbox.source_id, [{ type: 'text', text: message.content }])
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,7 @@ json.out_of_office_message resource.out_of_office_message
|
||||||
json.csat_survey_enabled resource.csat_survey_enabled
|
json.csat_survey_enabled resource.csat_survey_enabled
|
||||||
json.working_hours resource.weekly_schedule
|
json.working_hours resource.weekly_schedule
|
||||||
json.timezone resource.timezone
|
json.timezone resource.timezone
|
||||||
|
json.webhook_url resource.webhook_url
|
||||||
json.avatar_url resource.try(:avatar_url)
|
json.avatar_url resource.try(:avatar_url)
|
||||||
json.page_id resource.channel.try(:page_id)
|
json.page_id resource.channel.try(:page_id)
|
||||||
json.widget_color resource.channel.try(:widget_color)
|
json.widget_color resource.channel.try(:widget_color)
|
||||||
|
|
|
@ -242,6 +242,7 @@ Rails.application.routes.draw do
|
||||||
mount Facebook::Messenger::Server, at: 'bot'
|
mount Facebook::Messenger::Server, at: 'bot'
|
||||||
get 'webhooks/twitter', to: 'api/v1/webhooks#twitter_crc'
|
get 'webhooks/twitter', to: 'api/v1/webhooks#twitter_crc'
|
||||||
post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events'
|
post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events'
|
||||||
|
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'
|
||||||
|
|
||||||
namespace :twitter do
|
namespace :twitter do
|
||||||
|
|
11
db/migrate/20210829124254_add_line_channel.rb
Normal file
11
db/migrate/20210829124254_add_line_channel.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class AddLineChannel < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :channel_line do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.string :line_channel_id, null: false, index: { unique: true }
|
||||||
|
t.string :line_channel_secret, null: false
|
||||||
|
t.string :line_channel_token, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
db/schema.rb
12
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_08_28_124043) do
|
ActiveRecord::Schema.define(version: 2021_08_29_124254) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -185,6 +185,16 @@ ActiveRecord::Schema.define(version: 2021_08_28_124043) do
|
||||||
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
|
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "channel_line", force: :cascade do |t|
|
||||||
|
t.integer "account_id", null: false
|
||||||
|
t.string "line_channel_id", null: false
|
||||||
|
t.string "line_channel_secret", null: false
|
||||||
|
t.string "line_channel_token", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", 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
|
||||||
|
|
|
@ -285,6 +285,28 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include('test@test.com')
|
expect(response.body).to include('test@test.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates an api inbox when administrator' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/inboxes",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: { name: 'API Inbox', channel: { type: 'api', webhook_url: 'http://test.com' } },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('API Inbox')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a line inbox when administrator' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/inboxes",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: { name: 'Line Inbox',
|
||||||
|
channel: { type: 'line', line_channel_id: SecureRandom.uuid, line_channel_secret: SecureRandom.uuid,
|
||||||
|
line_channel_token: SecureRandom.uuid } },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Line Inbox')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
12
spec/controllers/webhooks/line_controller_spec.rb
Normal file
12
spec/controllers/webhooks/line_controller_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Webhooks::LineController', type: :request do
|
||||||
|
describe 'POST /webhooks/line/{:line_channel_id}' do
|
||||||
|
it 'call the line events job with the params' do
|
||||||
|
allow(Webhooks::LineEventsJob).to receive(:perform_later)
|
||||||
|
expect(Webhooks::LineEventsJob).to receive(:perform_later)
|
||||||
|
post '/webhooks/line/line_channel_id', params: { content: 'hello' }
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
spec/factories/channel/channel_line.rb
Normal file
11
spec/factories/channel/channel_line.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :channel_line, class: 'Channel::Line' do
|
||||||
|
line_channel_id { SecureRandom.uuid }
|
||||||
|
line_channel_secret { SecureRandom.uuid }
|
||||||
|
line_channel_token { SecureRandom.uuid }
|
||||||
|
inbox
|
||||||
|
account
|
||||||
|
end
|
||||||
|
end
|
|
@ -55,5 +55,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 ::Line:SendOnLineService when its line message' do
|
||||||
|
line_channel = create(:channel_line)
|
||||||
|
message = create(:message, conversation: create(:conversation, inbox: line_channel.inbox))
|
||||||
|
allow(::Line::SendOnLineService).to receive(:new).with(message: message).and_return(process_service)
|
||||||
|
expect(::Line::SendOnLineService).to receive(:new).with(message: message)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Webhooks::LineEventsJob, type: :job do
|
||||||
|
subject(:job) { described_class.perform_later(params: params) }
|
||||||
|
|
||||||
|
let!(:line_channel) { create(:channel_line) }
|
||||||
|
let!(:params) { { line_channel_id: line_channel.line_channel_id, 'line' => { test: 'test' } } }
|
||||||
|
let(:post_body) { params.to_json }
|
||||||
|
let(:signature) { Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), line_channel.line_channel_secret, post_body)) }
|
||||||
|
|
||||||
|
it 'enqueues the job' do
|
||||||
|
expect { job }.to have_enqueued_job(described_class)
|
||||||
|
.with(params: params)
|
||||||
|
.on_queue('default')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invalid params' do
|
||||||
|
it 'returns nil when no line_channel_id' do
|
||||||
|
expect(described_class.perform_now(params: {})).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil when invalid bot_token' do
|
||||||
|
expect(described_class.perform_now(params: { 'line_channel_id' => 'invalid_id', 'line' => { test: 'test' } })).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when valid params' do
|
||||||
|
it 'calls Line::IncomingMessageService' do
|
||||||
|
process_service = double
|
||||||
|
allow(Line::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||||
|
allow(process_service).to receive(:perform)
|
||||||
|
expect(Line::IncomingMessageService).to receive(:new).with(inbox: line_channel.inbox,
|
||||||
|
params: params['line'].with_indifferent_access)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(params: params, post_body: post_body, signature: signature)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
59
spec/services/line/incoming_message_service_spec.rb
Normal file
59
spec/services/line/incoming_message_service_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Line::IncomingMessageService do
|
||||||
|
let!(:line_channel) { create(:channel_line) }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
'destination': '2342234234',
|
||||||
|
'events': [
|
||||||
|
{
|
||||||
|
'replyToken': '0f3779fba3b349968c5d07db31eab56f',
|
||||||
|
'type': 'message',
|
||||||
|
'mode': 'active',
|
||||||
|
'timestamp': 1_462_629_479_859,
|
||||||
|
'source': {
|
||||||
|
'type': 'user',
|
||||||
|
'userId': 'U4af4980629'
|
||||||
|
},
|
||||||
|
'message': {
|
||||||
|
'id': '325708',
|
||||||
|
'type': 'text',
|
||||||
|
'text': 'Hello, world'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'replyToken': '8cf9239d56244f4197887e939187e19e',
|
||||||
|
'type': 'follow',
|
||||||
|
'mode': 'active',
|
||||||
|
'timestamp': 1_462_629_479_859,
|
||||||
|
'source': {
|
||||||
|
'type': 'user',
|
||||||
|
'userId': 'U4af4980629'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when valid text message params' do
|
||||||
|
it 'creates appropriate conversations, message and contacts' do
|
||||||
|
line_bot = double
|
||||||
|
line_user_profile = double
|
||||||
|
allow(Line::Bot::Client).to receive(:new).and_return(line_bot)
|
||||||
|
allow(line_bot).to receive(:get_profile).and_return(line_user_profile)
|
||||||
|
allow(line_user_profile).to receive(:body).and_return(
|
||||||
|
{
|
||||||
|
'displayName': 'LINE Test',
|
||||||
|
'userId': 'U4af4980629',
|
||||||
|
'pictureUrl': 'https://test.com'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
described_class.new(inbox: line_channel.inbox, params: params).perform
|
||||||
|
expect(line_channel.inbox.conversations).not_to eq(0)
|
||||||
|
expect(Contact.all.first.name).to eq('LINE Test')
|
||||||
|
expect(line_channel.inbox.messages.first.content).to eq('Hello, world')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
spec/services/line/send_on_line_service_spec.rb
Normal file
18
spec/services/line/send_on_line_service_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Line::SendOnLineService do
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when a valid message' do
|
||||||
|
it 'calls @channel.client.push_message' do
|
||||||
|
line_client = double
|
||||||
|
line_channel = create(:channel_line)
|
||||||
|
message = create(:message, message_type: :outgoing, content: 'test',
|
||||||
|
conversation: create(:conversation, inbox: line_channel.inbox))
|
||||||
|
allow(line_client).to receive(:push_message)
|
||||||
|
allow(Line::Bot::Client).to receive(:new).and_return(line_client)
|
||||||
|
expect(line_client).to receive(:push_message)
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue