mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										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 | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose