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 ---##
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
|
|
|
@ -322,6 +322,7 @@ GEM
|
|||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
line-bot-api (1.21.0)
|
||||
liquid (5.0.1)
|
||||
listen (3.6.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
|
@ -661,6 +662,7 @@ DEPENDENCIES
|
|||
kaminari
|
||||
koala
|
||||
letter_opener
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
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))
|
||||
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))
|
||||
end
|
||||
|
@ -122,6 +124,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
Channel::Email::EDITABLE_ATTRS
|
||||
when 'Channel::Telegram'
|
||||
Channel::Telegram::EDITABLE_ATTRS
|
||||
when 'Channel::Line'
|
||||
Channel::Line::EDITABLE_ATTRS
|
||||
else
|
||||
[]
|
||||
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') {
|
||||
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: {
|
||||
|
|
|
@ -35,6 +35,13 @@
|
|||
:style="badgeStyle"
|
||||
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
|
||||
v-if="badge === 'Channel::Telegram'"
|
||||
id="badge"
|
||||
|
|
|
@ -172,6 +172,31 @@
|
|||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"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",
|
||||
"DESC": "Integrate with Telegram channel and start supporting your customers.",
|
||||
|
|
|
@ -17,7 +17,15 @@
|
|||
<woot-code
|
||||
v-if="isATwilioInbox"
|
||||
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>
|
||||
</div>
|
||||
|
@ -75,6 +83,9 @@ export default {
|
|||
isAEmailInbox() {
|
||||
return this.currentInbox.channel_type === 'Channel::Email';
|
||||
},
|
||||
isALineInbox() {
|
||||
return this.currentInbox.channel_type === 'Channel::Line';
|
||||
},
|
||||
message() {
|
||||
if (this.isATwilioInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
|
|
|
@ -5,6 +5,7 @@ import Api from './channels/Api';
|
|||
import Email from './channels/Email';
|
||||
import Sms from './channels/Sms';
|
||||
import Whatsapp from './channels/Whatsapp';
|
||||
import Line from './channels/Line';
|
||||
import Telegram from './channels/Telegram';
|
||||
|
||||
const channelViewList = {
|
||||
|
@ -15,6 +16,7 @@ const channelViewList = {
|
|||
email: Email,
|
||||
sms: Sms,
|
||||
whatsapp: Whatsapp,
|
||||
line: Line,
|
||||
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() {
|
||||
return window.chatwootConfig.hostURL;
|
||||
},
|
||||
twilioCallbackURL() {
|
||||
return `${this.hostURL}/twilio/callback`;
|
||||
},
|
||||
vapidPublicKey() {
|
||||
return window.chatwootConfig.vapidPublicKey;
|
||||
},
|
||||
|
|
|
@ -11,6 +11,8 @@ class SendReplyJob < ApplicationJob
|
|||
::Twitter::SendOnTwitterService.new(message: message).perform
|
||||
when 'Channel::TwilioSms'
|
||||
::Twilio::SendOnTwilioService.new(message: message).perform
|
||||
when 'Channel::Line'
|
||||
::Line::SendOnLineService.new(message: message).perform
|
||||
when 'Channel::Telegram'
|
||||
::Telegram::SendOnTelegramService.new(message: message).perform
|
||||
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 :email_channels, dependent: :destroy, class_name: '::Channel::Email'
|
||||
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 :canned_responses, dependent: :destroy
|
||||
has_many :webhooks, dependent: :destroy
|
||||
|
|
|
@ -18,22 +18,15 @@
|
|||
#
|
||||
|
||||
class Channel::Api < ApplicationRecord
|
||||
include Channelable
|
||||
|
||||
self.table_name = 'channel_api'
|
||||
EDITABLE_ATTRS = [:webhook_url].freeze
|
||||
|
||||
validates :account_id, presence: true
|
||||
belongs_to :account
|
||||
|
||||
has_secure_token :identifier
|
||||
has_secure_token :hmac_token
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
|
||||
def name
|
||||
'API'
|
||||
end
|
||||
|
||||
def has_24_hour_messaging_window?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,25 +16,20 @@
|
|||
#
|
||||
|
||||
class Channel::Email < ApplicationRecord
|
||||
include Channelable
|
||||
|
||||
self.table_name = 'channel_email'
|
||||
EDITABLE_ATTRS = [:email].freeze
|
||||
|
||||
validates :account_id, presence: true
|
||||
belongs_to :account
|
||||
validates :email, uniqueness: true
|
||||
validates :forward_to_email, uniqueness: true
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
before_validation :ensure_forward_to_email, on: :create
|
||||
|
||||
def name
|
||||
'Email'
|
||||
end
|
||||
|
||||
def has_24_hour_messaging_window?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_forward_to_email
|
||||
|
|
|
@ -17,15 +17,12 @@
|
|||
#
|
||||
|
||||
class Channel::FacebookPage < ApplicationRecord
|
||||
self.table_name = 'channel_facebook_pages'
|
||||
|
||||
include Channelable
|
||||
include Reauthorizable
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :page_id, uniqueness: { scope: :account_id }
|
||||
belongs_to :account
|
||||
self.table_name = 'channel_facebook_pages'
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
validates :page_id, uniqueness: { scope: :account_id }
|
||||
|
||||
after_create_commit :subscribe
|
||||
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
|
||||
include Channelable
|
||||
|
||||
self.table_name = 'channel_telegram'
|
||||
EDITABLE_ATTRS = [:bot_token].freeze
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
belongs_to :account
|
||||
|
||||
before_validation :ensure_valid_bot_token, on: :create
|
||||
validates :account_id, presence: true
|
||||
validates :bot_token, presence: true, uniqueness: true
|
||||
before_save :setup_telegram_webhook
|
||||
|
||||
|
@ -30,10 +28,6 @@ class Channel::Telegram < ApplicationRecord
|
|||
'Telegram'
|
||||
end
|
||||
|
||||
def has_24_hour_messaging_window?
|
||||
false
|
||||
end
|
||||
|
||||
def telegram_api_url
|
||||
"https://api.telegram.org/bot#{bot_token}"
|
||||
end
|
||||
|
|
|
@ -17,19 +17,16 @@
|
|||
#
|
||||
|
||||
class Channel::TwilioSms < ApplicationRecord
|
||||
include Channelable
|
||||
|
||||
self.table_name = 'channel_twilio_sms'
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :account_sid, presence: true
|
||||
validates :auth_token, presence: true
|
||||
validates :phone_number, uniqueness: { scope: :account_id }, presence: true
|
||||
|
||||
enum medium: { sms: 0, whatsapp: 1 }
|
||||
|
||||
belongs_to :account
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
|
||||
def name
|
||||
medium == 'sms' ? 'Twilio SMS' : 'Whatsapp'
|
||||
end
|
||||
|
|
|
@ -16,13 +16,11 @@
|
|||
#
|
||||
|
||||
class Channel::TwitterProfile < ApplicationRecord
|
||||
include Channelable
|
||||
|
||||
self.table_name = 'channel_twitter_profiles'
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :profile_id, uniqueness: { scope: :account_id }
|
||||
belongs_to :account
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
|
||||
before_destroy :unsubscribe
|
||||
|
||||
|
@ -30,10 +28,6 @@ class Channel::TwitterProfile < ApplicationRecord
|
|||
'Twitter'
|
||||
end
|
||||
|
||||
def has_24_hour_messaging_window?
|
||||
false
|
||||
end
|
||||
|
||||
def create_contact_inbox(profile_id, name, additional_attributes)
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)
|
||||
|
|
|
@ -25,7 +25,9 @@
|
|||
#
|
||||
|
||||
class Channel::WebWidget < ApplicationRecord
|
||||
include Channelable
|
||||
include FlagShihTzu
|
||||
|
||||
self.table_name = 'channel_web_widgets'
|
||||
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] },
|
||||
|
@ -34,8 +36,6 @@ class Channel::WebWidget < ApplicationRecord
|
|||
validates :website_url, 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 :hmac_token
|
||||
|
||||
|
@ -50,10 +50,6 @@ class Channel::WebWidget < ApplicationRecord
|
|||
'Website'
|
||||
end
|
||||
|
||||
def has_24_hour_messaging_window?
|
||||
false
|
||||
end
|
||||
|
||||
def web_widget_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
|
||||
|
||||
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
|
||||
|
||||
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.working_hours resource.weekly_schedule
|
||||
json.timezone resource.timezone
|
||||
json.webhook_url resource.webhook_url
|
||||
json.avatar_url resource.try(:avatar_url)
|
||||
json.page_id resource.channel.try(:page_id)
|
||||
json.widget_color resource.channel.try(:widget_color)
|
||||
|
|
|
@ -242,6 +242,7 @@ Rails.application.routes.draw do
|
|||
mount Facebook::Messenger::Server, at: 'bot'
|
||||
get 'webhooks/twitter', to: 'api/v1/webhooks#twitter_crc'
|
||||
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'
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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"
|
||||
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|
|
||||
t.string "bot_name"
|
||||
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.body).to include('test@test.com')
|
||||
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
|
||||
|
||||
|
|
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)
|
||||
described_class.perform_now(message.id)
|
||||
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
|
||||
|
|
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