mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Add Instagram Channel (#2955)
This commit is contained in:
		| @@ -100,6 +100,9 @@ FB_VERIFY_TOKEN= | |||||||
| FB_APP_SECRET= | FB_APP_SECRET= | ||||||
| FB_APP_ID= | FB_APP_ID= | ||||||
|  |  | ||||||
|  | # https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard | ||||||
|  | IG_VERIFY_TOKEN | ||||||
|  |  | ||||||
| # Twitter | # Twitter | ||||||
| # documentation: https://www.chatwoot.com/docs/twitter-app-setup | # documentation: https://www.chatwoot.com/docs/twitter-app-setup | ||||||
| TWITTER_APP_ID= | TWITTER_APP_ID= | ||||||
|   | |||||||
| @@ -4,10 +4,11 @@ | |||||||
| #    based on this we are showing "not sent from chatwoot" message in frontend | #    based on this we are showing "not sent from chatwoot" message in frontend | ||||||
| #    Hence there is no need to set user_id in message for outgoing echo messages. | #    Hence there is no need to set user_id in message for outgoing echo messages. | ||||||
|  |  | ||||||
| class Messages::Facebook::MessageBuilder | class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder | ||||||
|   attr_reader :response |   attr_reader :response | ||||||
|  |  | ||||||
|   def initialize(response, inbox, outgoing_echo: false) |   def initialize(response, inbox, outgoing_echo: false) | ||||||
|  |     super() | ||||||
|     @response = response |     @response = response | ||||||
|     @inbox = inbox |     @inbox = inbox | ||||||
|     @outgoing_echo = outgoing_echo |     @outgoing_echo = outgoing_echo | ||||||
| @@ -47,30 +48,12 @@ class Messages::Facebook::MessageBuilder | |||||||
|  |  | ||||||
|   def build_message |   def build_message | ||||||
|     @message = conversation.messages.create!(message_params) |     @message = conversation.messages.create!(message_params) | ||||||
|  |  | ||||||
|     @attachments.each do |attachment| |     @attachments.each do |attachment| | ||||||
|       process_attachment(attachment) |       process_attachment(attachment) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def process_attachment(attachment) |  | ||||||
|     return if attachment['type'].to_sym == :template |  | ||||||
|  |  | ||||||
|     attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) |  | ||||||
|     attachment_obj.save! |  | ||||||
|     attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def attach_file(attachment, file_url) |  | ||||||
|     attachment_file = Down.download( |  | ||||||
|       file_url |  | ||||||
|     ) |  | ||||||
|     attachment.file.attach( |  | ||||||
|       io: attachment_file, |  | ||||||
|       filename: attachment_file.original_filename, |  | ||||||
|       content_type: attachment_file.content_type |  | ||||||
|     ) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def ensure_contact_avatar |   def ensure_contact_avatar | ||||||
|     return if contact_params[:remote_avatar_url].blank? |     return if contact_params[:remote_avatar_url].blank? | ||||||
|     return if @contact.avatar.attached? |     return if @contact.avatar.attached? | ||||||
| @@ -89,28 +72,6 @@ class Messages::Facebook::MessageBuilder | |||||||
|                          )) |                          )) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def attachment_params(attachment) |  | ||||||
|     file_type = attachment['type'].to_sym |  | ||||||
|     params = { file_type: file_type, account_id: @message.account_id } |  | ||||||
|  |  | ||||||
|     if [:image, :file, :audio, :video].include? file_type |  | ||||||
|       params.merge!(file_type_params(attachment)) |  | ||||||
|     elsif file_type == :location |  | ||||||
|       params.merge!(location_params(attachment)) |  | ||||||
|     elsif file_type == :fallback |  | ||||||
|       params.merge!(fallback_params(attachment)) |  | ||||||
|     end |  | ||||||
|  |  | ||||||
|     params |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def file_type_params(attachment) |  | ||||||
|     { |  | ||||||
|       external_url: attachment['payload']['url'], |  | ||||||
|       remote_file_url: attachment['payload']['url'] |  | ||||||
|     } |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def location_params(attachment) |   def location_params(attachment) | ||||||
|     lat = attachment['payload']['coordinates']['lat'] |     lat = attachment['payload']['coordinates']['lat'] | ||||||
|     long = attachment['payload']['coordinates']['long'] |     long = attachment['payload']['coordinates']['long'] | ||||||
|   | |||||||
							
								
								
									
										150
									
								
								app/builders/messages/instagram/message_builder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								app/builders/messages/instagram/message_builder.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | # This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` | ||||||
|  | # Assumptions | ||||||
|  | # 1. Incase of an outgoing message which is echo, source_id will NOT be nil, | ||||||
|  | #    based on this we are showing "not sent from chatwoot" message in frontend | ||||||
|  | #    Hence there is no need to set user_id in message for outgoing echo messages. | ||||||
|  |  | ||||||
|  | class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder | ||||||
|  |   attr_reader :messaging | ||||||
|  |  | ||||||
|  |   def initialize(messaging, inbox, outgoing_echo: false) | ||||||
|  |     super() | ||||||
|  |     @messaging = messaging | ||||||
|  |     @inbox = inbox | ||||||
|  |     @outgoing_echo = outgoing_echo | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def perform | ||||||
|  |     return if @inbox.channel.reauthorization_required? | ||||||
|  |  | ||||||
|  |     ActiveRecord::Base.transaction do | ||||||
|  |       build_message | ||||||
|  |     end | ||||||
|  |   rescue Koala::Facebook::AuthenticationError | ||||||
|  |     @inbox.channel.authorization_error! | ||||||
|  |     raise | ||||||
|  |   rescue StandardError => e | ||||||
|  |     Sentry.capture_exception(e) | ||||||
|  |     true | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def attachments | ||||||
|  |     @messaging[:message][:attachments] || {} | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_type | ||||||
|  |     @outgoing_echo ? :outgoing : :incoming | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_source_id | ||||||
|  |     @outgoing_echo ? recipient_id : sender_id | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def sender_id | ||||||
|  |     @messaging[:sender][:id] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def recipient_id | ||||||
|  |     @messaging[:recipient][:id] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message | ||||||
|  |     @messaging[:message] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def contact | ||||||
|  |     @contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation | ||||||
|  |     @conversation ||= Conversation.find_by(conversation_params) || build_conversation | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_content | ||||||
|  |     @messaging[:message][:text] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def content_attributes | ||||||
|  |     { message_id: @messaging[:message][:mid] } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def build_message | ||||||
|  |     return if @outgoing_echo && already_sent_from_chatwoot? | ||||||
|  |  | ||||||
|  |     @message = conversation.messages.create!(message_params) | ||||||
|  |  | ||||||
|  |     attachments.each do |attachment| | ||||||
|  |       process_attachment(attachment) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def build_conversation | ||||||
|  |     @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id) | ||||||
|  |     Conversation.create!(conversation_params.merge( | ||||||
|  |                            contact_inbox_id: @contact_inbox.id | ||||||
|  |                          )) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation_params | ||||||
|  |     { | ||||||
|  |       account_id: @inbox.account_id, | ||||||
|  |       inbox_id: @inbox.id, | ||||||
|  |       contact_id: contact.id, | ||||||
|  |       additional_attributes: { | ||||||
|  |         type: 'instagram_direct_message' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_params | ||||||
|  |     { | ||||||
|  |       account_id: conversation.account_id, | ||||||
|  |       inbox_id: conversation.inbox_id, | ||||||
|  |       message_type: message_type, | ||||||
|  |       source_id: message_source_id, | ||||||
|  |       content: message_content, | ||||||
|  |       content_attributes: content_attributes, | ||||||
|  |       sender: @outgoing_echo ? nil : contact | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def already_sent_from_chatwoot? | ||||||
|  |     cw_message = conversation.messages.where( | ||||||
|  |       source_id: nil, | ||||||
|  |       message_type: 'outgoing', | ||||||
|  |       content: message_content, | ||||||
|  |       private: false, | ||||||
|  |       status: :sent | ||||||
|  |     ).first | ||||||
|  |     cw_message.update(content_attributes: content_attributes) if cw_message.present? | ||||||
|  |     cw_message.present? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   ### Sample response | ||||||
|  |   # { | ||||||
|  |   #   "object": "instagram", | ||||||
|  |   #   "entry": [ | ||||||
|  |   #     { | ||||||
|  |   #       "id": "<IGID>",// ig id of the business | ||||||
|  |   #       "time": 1569262486134, | ||||||
|  |   #       "messaging": [ | ||||||
|  |   #         { | ||||||
|  |   #           "sender": { | ||||||
|  |   #             "id": "<IGSID>" | ||||||
|  |   #           }, | ||||||
|  |   #           "recipient": { | ||||||
|  |   #             "id": "<IGID>" | ||||||
|  |   #           }, | ||||||
|  |   #           "timestamp": 1569262485349, | ||||||
|  |   #           "message": { | ||||||
|  |   #             "mid": "<MESSAGE_ID>", | ||||||
|  |   #             "text": "<MESSAGE_CONTENT>" | ||||||
|  |   #           } | ||||||
|  |   #         } | ||||||
|  |   #       ] | ||||||
|  |   #     } | ||||||
|  |   #   ], | ||||||
|  |   # } | ||||||
|  | end | ||||||
							
								
								
									
										42
									
								
								app/builders/messages/messenger/message_builder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/builders/messages/messenger/message_builder.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | class Messages::Messenger::MessageBuilder | ||||||
|  |   def process_attachment(attachment) | ||||||
|  |     return if attachment['type'].to_sym == :template | ||||||
|  |  | ||||||
|  |     attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) | ||||||
|  |     attachment_obj.save! | ||||||
|  |     attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def attach_file(attachment, file_url) | ||||||
|  |     attachment_file = Down.download( | ||||||
|  |       file_url | ||||||
|  |     ) | ||||||
|  |     attachment.file.attach( | ||||||
|  |       io: attachment_file, | ||||||
|  |       filename: attachment_file.original_filename, | ||||||
|  |       content_type: attachment_file.content_type | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def attachment_params(attachment) | ||||||
|  |     file_type = attachment['type'].to_sym | ||||||
|  |     params = { file_type: file_type, account_id: @message.account_id } | ||||||
|  |  | ||||||
|  |     if [:image, :file, :audio, :video].include? file_type | ||||||
|  |       params.merge!(file_type_params(attachment)) | ||||||
|  |     elsif file_type == :location | ||||||
|  |       params.merge!(location_params(attachment)) | ||||||
|  |     elsif file_type == :fallback | ||||||
|  |       params.merge!(fallback_params(attachment)) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     params | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def file_type_params(attachment) | ||||||
|  |     { | ||||||
|  |       external_url: attachment['payload']['url'], | ||||||
|  |       remote_file_url: attachment['payload']['url'] | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -12,6 +12,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController | |||||||
|         page_access_token: page_access_token |         page_access_token: page_access_token | ||||||
|       ) |       ) | ||||||
|       @facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel) |       @facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel) | ||||||
|  |       set_instagram_id(page_access_token, facebook_channel) | ||||||
|       set_avatar(@facebook_inbox, page_id) |       set_avatar(@facebook_inbox, page_id) | ||||||
|     rescue StandardError => e |     rescue StandardError => e | ||||||
|       Rails.logger.info e |       Rails.logger.info e | ||||||
| @@ -22,6 +23,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController | |||||||
|     @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts')) |     @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts')) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def set_instagram_id(page_access_token, facebook_channel) | ||||||
|  |     fb_object = Koala::Facebook::API.new(page_access_token) | ||||||
|  |     response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' }) | ||||||
|  |     return if response['instagram_business_account'].blank? | ||||||
|  |  | ||||||
|  |     instagram_id = response['instagram_business_account']['id'] | ||||||
|  |     facebook_channel.update(instagram_id: instagram_id) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   # get params[:inbox_id], current_account. params[:omniauth_token] |   # get params[:inbox_id], current_account. params[:omniauth_token] | ||||||
|   def reauthorize_page |   def reauthorize_page | ||||||
|     if @inbox&.facebook? |     if @inbox&.facebook? | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								app/controllers/api/v1/instagram_callbacks_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/controllers/api/v1/instagram_callbacks_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | class Api::V1::InstagramCallbacksController < ApplicationController | ||||||
|  |   skip_before_action :authenticate_user!, raise: false | ||||||
|  |   skip_before_action :set_current_user | ||||||
|  |  | ||||||
|  |   def verify | ||||||
|  |     if valid_instagram_token?(params['hub.verify_token']) | ||||||
|  |       Rails.logger.info('Instagram webhook verified') | ||||||
|  |       render json: params['hub.challenge'] | ||||||
|  |     else | ||||||
|  |       render json: { error: 'Error; wrong verify token', status: 403 } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def events | ||||||
|  |     Rails.logger.info('Instagram webhook received events') | ||||||
|  |     if params['object'].casecmp('instagram').zero? | ||||||
|  |       ::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry]) | ||||||
|  |       render json: :ok | ||||||
|  |     else | ||||||
|  |       Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}") | ||||||
|  |       head :unprocessable_entity | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def valid_instagram_token?(token) | ||||||
|  |     token == ENV['IG_VERIFY_TOKEN'] | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/channels/messenger.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/channels/messenger.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/instagram_direct.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/instagram_direct.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 78 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/messenger_direct.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/javascript/dashboard/assets/images/messenger_direct.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.4 KiB | 
| @@ -6,7 +6,7 @@ | |||||||
|   > |   > | ||||||
|     <img |     <img | ||||||
|       v-if="channel.key === 'facebook'" |       v-if="channel.key === 'facebook'" | ||||||
|       src="~dashboard/assets/images/channels/facebook.png" |       src="~dashboard/assets/images/channels/messenger.png" | ||||||
|     /> |     /> | ||||||
|     <img |     <img | ||||||
|       v-if="channel.key === 'twitter'" |       v-if="channel.key === 'twitter'" | ||||||
|   | |||||||
| @@ -14,12 +14,19 @@ | |||||||
|       color="white" |       color="white" | ||||||
|       :size="avatarSize" |       :size="avatarSize" | ||||||
|     /> |     /> | ||||||
|  |     <img | ||||||
|  |       v-if="badge === 'instagram_direct_message'" | ||||||
|  |       id="badge" | ||||||
|  |       class="source-badge" | ||||||
|  |       :style="badgeStyle" | ||||||
|  |       src="~dashboard/assets/images/instagram_direct.png" | ||||||
|  |     /> | ||||||
|     <img |     <img | ||||||
|       v-if="badge === 'Channel::FacebookPage'" |       v-if="badge === 'Channel::FacebookPage'" | ||||||
|       id="badge" |       id="badge" | ||||||
|       class="source-badge" |       class="source-badge" | ||||||
|       :style="badgeStyle" |       :style="badgeStyle" | ||||||
|       src="~dashboard/assets/images/fb-badge.png" |       src="~dashboard/assets/images/messenger_direct.png" | ||||||
|     /> |     /> | ||||||
|     <img |     <img | ||||||
|       v-if="badge === 'twitter-tweet'" |       v-if="badge === 'twitter-tweet'" | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|     <thumbnail |     <thumbnail | ||||||
|       v-if="!hideThumbnail" |       v-if="!hideThumbnail" | ||||||
|       :src="currentContact.thumbnail" |       :src="currentContact.thumbnail" | ||||||
|       :badge="inboxBadge" |       :badge="chatBadge" | ||||||
|       class="columns" |       class="columns" | ||||||
|       :username="currentContact.name" |       :username="currentContact.name" | ||||||
|       :status="currentContact.availability_status" |       :status="currentContact.availability_status" | ||||||
| @@ -119,6 +119,10 @@ export default { | |||||||
|       accountId: 'getCurrentAccountId', |       accountId: 'getCurrentAccountId', | ||||||
|     }), |     }), | ||||||
|  |  | ||||||
|  |     chatExtraAttributes() { | ||||||
|  |       return this.chat.additional_attributes; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     chatMetadata() { |     chatMetadata() { | ||||||
|       return this.chat.meta || {}; |       return this.chat.meta || {}; | ||||||
|     }, |     }, | ||||||
| @@ -127,6 +131,14 @@ export default { | |||||||
|       return this.chatMetadata.assignee || {}; |       return this.chatMetadata.assignee || {}; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     chatBadge() { | ||||||
|  |       if(this.chatExtraAttributes['type']){ | ||||||
|  |         return this.chatExtraAttributes['type'] | ||||||
|  |       } else { | ||||||
|  |         return this.chatMetadata.channel | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     currentContact() { |     currentContact() { | ||||||
|       return this.$store.getters['contacts/getContact']( |       return this.$store.getters['contacts/getContact']( | ||||||
|         this.chatMetadata.sender.id |         this.chatMetadata.sender.id | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|       <Thumbnail |       <Thumbnail | ||||||
|         :src="currentContact.thumbnail" |         :src="currentContact.thumbnail" | ||||||
|         size="40px" |         size="40px" | ||||||
|         :badge="inboxBadge" |         :badge="chatBadge" | ||||||
|         :username="currentContact.name" |         :username="currentContact.name" | ||||||
|         :status="currentContact.availability_status" |         :status="currentContact.availability_status" | ||||||
|       /> |       /> | ||||||
| @@ -73,9 +73,23 @@ export default { | |||||||
|       uiFlags: 'inboxAssignableAgents/getUIFlags', |       uiFlags: 'inboxAssignableAgents/getUIFlags', | ||||||
|       currentChat: 'getSelectedChat', |       currentChat: 'getSelectedChat', | ||||||
|     }), |     }), | ||||||
|  |  | ||||||
|  |     chatExtraAttributes() { | ||||||
|  |       return this.chat.additional_attributes; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     chatMetadata() { |     chatMetadata() { | ||||||
|       return this.chat.meta; |       return this.chat.meta; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     chatBadge() { | ||||||
|  |       if(this.chatExtraAttributes['type']){ | ||||||
|  |         return this.chatExtraAttributes['type'] | ||||||
|  |       } else { | ||||||
|  |         return this.chatMetadata.channel | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|     currentContact() { |     currentContact() { | ||||||
|       return this.$store.getters['contacts/getContact']( |       return this.$store.getters['contacts/getContact']( | ||||||
|         this.chat.meta.sender.id |         this.chat.meta.sender.id | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ export default { | |||||||
|       const { apiChannelName, apiChannelThumbnail } = this.globalConfig; |       const { apiChannelName, apiChannelThumbnail } = this.globalConfig; | ||||||
|       return [ |       return [ | ||||||
|         { key: 'website', name: 'Website' }, |         { key: 'website', name: 'Website' }, | ||||||
|         { key: 'facebook', name: 'Facebook' }, |         { key: 'facebook', name: 'Messenger' }, | ||||||
|         { key: 'twitter', name: 'Twitter' }, |         { key: 'twitter', name: 'Twitter' }, | ||||||
|         { key: 'whatsapp', name: 'WhatsApp via Twilio' }, |         { key: 'whatsapp', name: 'WhatsApp via Twilio' }, | ||||||
|         { key: 'sms', name: 'SMS via Twilio' }, |         { key: 'sms', name: 'SMS via Twilio' }, | ||||||
|   | |||||||
| @@ -206,7 +206,7 @@ export default { | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           scope: 'pages_manage_metadata,pages_messaging', |           scope: 'pages_manage_metadata,pages_messaging,instagram_basic,pages_show_list,instagram_manage_messages', | ||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -3,10 +3,16 @@ class SendReplyJob < ApplicationJob | |||||||
|  |  | ||||||
|   def perform(message_id) |   def perform(message_id) | ||||||
|     message = Message.find(message_id) |     message = Message.find(message_id) | ||||||
|     channel_name = message.conversation.inbox.channel.class.to_s |     conversation = message.conversation | ||||||
|  |     channel_name = conversation.inbox.channel.class.to_s | ||||||
|  |  | ||||||
|     case channel_name |     case channel_name | ||||||
|     when 'Channel::FacebookPage' |     when 'Channel::FacebookPage' | ||||||
|  |       if conversation.additional_attributes['type'] == 'instagram_direct_message' | ||||||
|  |         ::Instagram::SendOnInstagramService.new(message: message).perform | ||||||
|  |       else | ||||||
|         ::Facebook::SendOnFacebookService.new(message: message).perform |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|  |       end | ||||||
|     when 'Channel::TwitterProfile' |     when 'Channel::TwitterProfile' | ||||||
|       ::Twitter::SendOnTwitterService.new(message: message).perform |       ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|     when 'Channel::TwilioSms' |     when 'Channel::TwilioSms' | ||||||
|   | |||||||
							
								
								
									
										84
									
								
								app/jobs/webhooks/instagram_events_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/jobs/webhooks/instagram_events_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | class Webhooks::InstagramEventsJob < ApplicationJob | ||||||
|  |   queue_as :default | ||||||
|  |  | ||||||
|  |   include HTTParty | ||||||
|  |  | ||||||
|  |   base_uri 'https://graph.facebook.com/v11.0/me' | ||||||
|  |  | ||||||
|  |   # @return [Array] We will support further events like reaction or seen in future | ||||||
|  |   SUPPORTED_EVENTS = [:message].freeze | ||||||
|  |  | ||||||
|  |   # @see https://developers.facebook.com/docs/messenger-platform/instagram/features/webhook | ||||||
|  |   def perform(entries) | ||||||
|  |     @entries = entries | ||||||
|  |  | ||||||
|  |     if @entries[0].key?(:changes) | ||||||
|  |       Rails.logger.info('Probably Test data.') | ||||||
|  |       # grab the test entry for the review app | ||||||
|  |       create_test_text | ||||||
|  |       return | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     @entries.each do |entry| | ||||||
|  |       entry[:messaging].each do |messaging| | ||||||
|  |         send(@event_name, messaging) if event_name(messaging) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def event_name(messaging) | ||||||
|  |     @event_name ||= SUPPORTED_EVENTS.find { |key| messaging.key?(key) } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message(messaging) | ||||||
|  |     ::Instagram::MessageText.new(messaging).perform | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create_test_text | ||||||
|  |     messenger_channel = Channel::FacebookPage.last | ||||||
|  |     @inbox = ::Inbox.find_by!(channel: messenger_channel) | ||||||
|  |     @contact_inbox = @inbox.contact_inboxes.where(source_id: 'sender_username').first | ||||||
|  |     unless @contact_inbox | ||||||
|  |       @contact_inbox ||= @inbox.channel.create_contact_inbox( | ||||||
|  |         'sender_username', 'sender_username' | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  |     @contact = @contact_inbox.contact | ||||||
|  |  | ||||||
|  |     @conversation ||= Conversation.find_by(conversation_params) || build_conversation(conversation_params) | ||||||
|  |  | ||||||
|  |     @message = @conversation.messages.create!(message_params) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation_params | ||||||
|  |     { | ||||||
|  |       account_id: @inbox.account_id, | ||||||
|  |       inbox_id: @inbox.id, | ||||||
|  |       contact_id: @contact.id, | ||||||
|  |       additional_attributes: { | ||||||
|  |         type: 'instagram_direct_message' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_params | ||||||
|  |     { | ||||||
|  |       account_id: @conversation.account_id, | ||||||
|  |       inbox_id: @conversation.inbox_id, | ||||||
|  |       message_type: 'incoming', | ||||||
|  |       source_id: 'facebook_test_webhooks', | ||||||
|  |       content: 'This is a test message from facebook.', | ||||||
|  |       sender: @contact | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def build_conversation(conversation_params) | ||||||
|  |     Conversation.create!( | ||||||
|  |       conversation_params.merge( | ||||||
|  |         contact_inbox_id: @contact_inbox.id | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -8,6 +8,7 @@ | |||||||
| #  created_at        :datetime         not null | #  created_at        :datetime         not null | ||||||
| #  updated_at        :datetime         not null | #  updated_at        :datetime         not null | ||||||
| #  account_id        :integer          not null | #  account_id        :integer          not null | ||||||
|  | #  instagram_id      :string | ||||||
| #  page_id           :string           not null | #  page_id           :string           not null | ||||||
| # | # | ||||||
| # Indexes | # Indexes | ||||||
| @@ -35,6 +36,19 @@ class Channel::FacebookPage < ApplicationRecord | |||||||
|     true |     true | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def create_contact_inbox(instagram_id, name) | ||||||
|  |     ActiveRecord::Base.transaction do | ||||||
|  |       contact = inbox.account.contacts.create!(name: name) | ||||||
|  |       ::ContactInbox.create( | ||||||
|  |         contact_id: contact.id, | ||||||
|  |         inbox_id: inbox.id, | ||||||
|  |         source_id: instagram_id | ||||||
|  |       ) | ||||||
|  |     rescue StandardError => e | ||||||
|  |       Rails.logger.info e | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def subscribe |   def subscribe | ||||||
|     # ref https://developers.facebook.com/docs/messenger-platform/reference/webhook-events |     # ref https://developers.facebook.com/docs/messenger-platform/reference/webhook-events | ||||||
|     response = Facebook::Messenger::Subscriptions.subscribe( |     response = Facebook::Messenger::Subscriptions.subscribe( | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								app/services/instagram/message_text.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/services/instagram/message_text.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | class Instagram::MessageText < Instagram::WebhooksBaseService | ||||||
|  |   include HTTParty | ||||||
|  |  | ||||||
|  |   attr_reader :messaging | ||||||
|  |  | ||||||
|  |   base_uri 'https://graph.facebook.com/v11.0/' | ||||||
|  |  | ||||||
|  |   def initialize(messaging) | ||||||
|  |     super() | ||||||
|  |     @messaging = messaging | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def perform | ||||||
|  |     instagram_id, contact_id = if agent_message_via_echo? | ||||||
|  |                                  [@messaging[:sender][:id], @messaging[:recipient][:id]] | ||||||
|  |                                else | ||||||
|  |                                  [@messaging[:recipient][:id], @messaging[:sender][:id]] | ||||||
|  |                                end | ||||||
|  |     inbox_channel(instagram_id) | ||||||
|  |     ensure_contact(contact_id) | ||||||
|  |  | ||||||
|  |     create_message | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def ensure_contact(ig_scope_id) | ||||||
|  |     begin | ||||||
|  |       k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? | ||||||
|  |       result = k.get_object(ig_scope_id) || {} | ||||||
|  |     rescue Koala::Facebook::AuthenticationError | ||||||
|  |       @inbox.channel.authorization_error! | ||||||
|  |       raise | ||||||
|  |     rescue StandardError => e | ||||||
|  |       result = {} | ||||||
|  |       Sentry.capture_exception(e) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     find_or_create_contact(result) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def agent_message_via_echo? | ||||||
|  |     @messaging[:message][:is_echo].present? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create_message | ||||||
|  |     Messages::Instagram::MessageBuilder.new(@messaging, @inbox, outgoing_echo: agent_message_via_echo?).perform | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										99
									
								
								app/services/instagram/send_on_instagram_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/services/instagram/send_on_instagram_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | class Instagram::SendOnInstagramService < Base::SendOnChannelService | ||||||
|  |   include HTTParty | ||||||
|  |  | ||||||
|  |   pattr_initialize [:message!] | ||||||
|  |  | ||||||
|  |   base_uri 'https://graph.facebook.com/v11.0/me' | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   delegate :additional_attributes, to: :contact | ||||||
|  |  | ||||||
|  |   def channel_class | ||||||
|  |     Channel::FacebookPage | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def perform_reply | ||||||
|  |     send_to_facebook_page attachament_message_params if message.attachments.present? | ||||||
|  |     send_to_facebook_page message_params | ||||||
|  |   rescue StandardError => e | ||||||
|  |     Sentry.capture_exception(e) | ||||||
|  |     channel.authorization_error! | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_params | ||||||
|  |     { | ||||||
|  |       recipient: { id: contact.get_source_id(inbox.id) }, | ||||||
|  |       message: { | ||||||
|  |         text: message.content | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def attachament_message_params | ||||||
|  |     attachment = message.attachments.first | ||||||
|  |     { | ||||||
|  |       recipient: { id: contact.get_source_id(inbox.id) }, | ||||||
|  |       message: { | ||||||
|  |         attachment: { | ||||||
|  |           type: attachment_type(attachment), | ||||||
|  |           payload: { | ||||||
|  |             url: attachment.file_url | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   # Deliver a message with the given payload. | ||||||
|  |   # @see https://developers.facebook.com/docs/messenger-platform/instagram/features/send-message | ||||||
|  |   def send_to_facebook_page(message_content) | ||||||
|  |     access_token = channel.page_access_token | ||||||
|  |     app_secret_proof = calculate_app_secret_proof(ENV['FB_APP_SECRET'], access_token) | ||||||
|  |  | ||||||
|  |     query = { access_token: access_token } | ||||||
|  |     query[:appsecret_proof] = app_secret_proof if app_secret_proof | ||||||
|  |  | ||||||
|  |     # url = "https://graph.facebook.com/v11.0/me/messages?access_token=#{access_token}" | ||||||
|  |  | ||||||
|  |     response = HTTParty.post( | ||||||
|  |       'https://graph.facebook.com/v11.0/me/messages', | ||||||
|  |       body: message_content, | ||||||
|  |       query: query | ||||||
|  |     ) | ||||||
|  |     # response = HTTParty.post(url, options) | ||||||
|  |  | ||||||
|  |     Rails.logger.info("Instagram response: #{response} : #{message_content}") if response[:body][:error] | ||||||
|  |  | ||||||
|  |     response[:body] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def calculate_app_secret_proof(app_secret, access_token) | ||||||
|  |     Facebook::Messenger::Configuration::AppSecretProofCalculator.call( | ||||||
|  |       app_secret, access_token | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def attachment_type(attachment) | ||||||
|  |     return attachment.file_type if %w[image audio video file].include? attachment.file_type | ||||||
|  |  | ||||||
|  |     'file' | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation_type | ||||||
|  |     conversation.additional_attributes['type'] | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def sent_first_outgoing_message_after_24_hours? | ||||||
|  |     # we can send max 1 message after 24 hour window | ||||||
|  |     conversation.messages.outgoing.where('id > ?', last_incoming_message.id).count == 1 | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def last_incoming_message | ||||||
|  |     conversation.messages.incoming.last | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def config | ||||||
|  |     Facebook::Messenger.config | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										21
									
								
								app/services/instagram/webhooks_base_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/services/instagram/webhooks_base_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | class Instagram::WebhooksBaseService | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def inbox_channel(instagram_id) | ||||||
|  |     messenger_channel = Channel::FacebookPage.where(instagram_id: instagram_id) | ||||||
|  |     @inbox = ::Inbox.find_by!(channel: messenger_channel) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def find_or_create_contact(user) | ||||||
|  |     @contact_inbox = @inbox.contact_inboxes.where(source_id: user['id']).first | ||||||
|  |     @contact = @contact_inbox.contact if @contact_inbox | ||||||
|  |     return if @contact | ||||||
|  |  | ||||||
|  |     @contact_inbox = @inbox.channel.create_contact_inbox( | ||||||
|  |       user['id'], user['name'] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @contact = @contact_inbox.contact | ||||||
|  |     ContactAvatarJob.perform_later(@contact, user['profile_pic']) if user['profile_pic'] | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -251,6 +251,8 @@ Rails.application.routes.draw do | |||||||
|   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/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' | ||||||
|  |   get 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#verify' | ||||||
|  |   post 'instagram_callbacks/event', to: 'api/v1/instagram_callbacks#events' | ||||||
|  |  | ||||||
|   namespace :twitter do |   namespace :twitter do | ||||||
|     resource :callback, only: [:show] |     resource :callback, only: [:show] | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | class AddInstagramIdToFacebookPage < ActiveRecord::Migration[6.1] | ||||||
|  |   def up | ||||||
|  |     add_column :channel_facebook_pages, :instagram_id, :string | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def down | ||||||
|  |     remove_column :channel_facebook_pages, :instagram_id, :string | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										23
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -181,6 +181,7 @@ ActiveRecord::Schema.define(version: 2021_09_22_082754) do | |||||||
|     t.integer "account_id", null: false |     t.integer "account_id", null: false | ||||||
|     t.datetime "created_at", null: false |     t.datetime "created_at", null: false | ||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|  |     t.string "instagram_id" | ||||||
|     t.index ["page_id", "account_id"], name: "index_channel_facebook_pages_on_page_id_and_account_id", unique: true |     t.index ["page_id", "account_id"], name: "index_channel_facebook_pages_on_page_id_and_account_id", unique: true | ||||||
|     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 | ||||||
| @@ -244,6 +245,28 @@ ActiveRecord::Schema.define(version: 2021_09_22_082754) do | |||||||
|     t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true |     t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   create_table "companies", force: :cascade do |t| | ||||||
|  |     t.string "name", null: false | ||||||
|  |     t.text "address" | ||||||
|  |     t.string "city", null: false | ||||||
|  |     t.string "state" | ||||||
|  |     t.string "country", null: false | ||||||
|  |     t.integer "no_of_employees", null: false | ||||||
|  |     t.string "industry_type" | ||||||
|  |     t.bigint "annual_revenue" | ||||||
|  |     t.text "website" | ||||||
|  |     t.string "office_phone_number" | ||||||
|  |     t.string "facebook" | ||||||
|  |     t.string "twitter" | ||||||
|  |     t.string "linkedin" | ||||||
|  |     t.jsonb "additional_attributes" | ||||||
|  |     t.bigint "contact_id" | ||||||
|  |     t.datetime "created_at", precision: 6, null: false | ||||||
|  |     t.datetime "updated_at", precision: 6, null: false | ||||||
|  |     t.index ["contact_id"], name: "index_companies_on_contact_id" | ||||||
|  |     t.index ["name"], name: "index_companies_on_name", unique: true | ||||||
|  |   end | ||||||
|  |  | ||||||
|   create_table "contact_inboxes", force: :cascade do |t| |   create_table "contact_inboxes", force: :cascade do |t| | ||||||
|     t.bigint "contact_id" |     t.bigint "contact_id" | ||||||
|     t.bigint "inbox_id" |     t.bigint "inbox_id" | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								spec/builders/messages/instagram/message_builder_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								spec/builders/messages/instagram/message_builder_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | describe  ::Messages::Instagram::MessageBuilder do | ||||||
|  |   subject(:instagram_message_builder) { described_class } | ||||||
|  |  | ||||||
|  |   let!(:account) { create(:account) } | ||||||
|  |   let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } | ||||||
|  |   let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } | ||||||
|  |   let!(:dm_params) { build(:instagram_message_create_event).with_indifferent_access } | ||||||
|  |   let(:fb_object) { double } | ||||||
|  |   let(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') } | ||||||
|  |   let(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_inbox.id, source_id: 'Sender-id-1') } | ||||||
|  |  | ||||||
|  |   describe '#perform' do | ||||||
|  |     it 'creates contact and message for the facebook inbox' do | ||||||
|  |       allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) | ||||||
|  |       allow(fb_object).to receive(:get_object).and_return( | ||||||
|  |         { | ||||||
|  |           name: 'Jane', | ||||||
|  |           id: 'Sender-id-1', | ||||||
|  |           account_id: instagram_inbox.account_id, | ||||||
|  |           profile_pic: 'https://via.placeholder.com/250x250.png' | ||||||
|  |         }.with_indifferent_access | ||||||
|  |       ) | ||||||
|  |       messaging = dm_params[:entry][0]['messaging'][0] | ||||||
|  |       contact_inbox | ||||||
|  |       instagram_message_builder.new(messaging, instagram_inbox).perform | ||||||
|  |  | ||||||
|  |       instagram_inbox.reload | ||||||
|  |  | ||||||
|  |       expect(instagram_inbox.conversations.count).to be 1 | ||||||
|  |       expect(instagram_inbox.messages.count).to be 1 | ||||||
|  |  | ||||||
|  |       contact = instagram_channel.inbox.contacts.first | ||||||
|  |       message = instagram_channel.inbox.messages.first | ||||||
|  |  | ||||||
|  |       expect(contact.name).to eq('Jane Dae') | ||||||
|  |       expect(message.content).to eq('This is the first message from the customer') | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										10
									
								
								spec/factories/channel/insatgram_channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								spec/factories/channel/insatgram_channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | FactoryBot.define do | ||||||
|  |   factory :channel_instagram_fb_page, class: 'Channel::FacebookPage' do | ||||||
|  |     page_access_token { SecureRandom.uuid } | ||||||
|  |     user_access_token { SecureRandom.uuid } | ||||||
|  |     page_id { SecureRandom.uuid } | ||||||
|  |     account | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										58
									
								
								spec/factories/instagram/instagram_message_create_event.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								spec/factories/instagram/instagram_message_create_event.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | FactoryBot.define do | ||||||
|  |   factory :instagram_message_create_event, class: Hash do | ||||||
|  |     entry do | ||||||
|  |       [ | ||||||
|  |         { | ||||||
|  |           'id': 'instagram-message-id-123', | ||||||
|  |           'time': '2021-09-08T06:34:04+0000', | ||||||
|  |           'messaging': [ | ||||||
|  |             { | ||||||
|  |               'sender': { | ||||||
|  |                 'id': 'Sender-id-1' | ||||||
|  |               }, | ||||||
|  |               'recipient': { | ||||||
|  |                 'id': 'chatwoot-app-user-id-1' | ||||||
|  |               }, | ||||||
|  |               'timestamp': '2021-09-08T06:34:04+0000', | ||||||
|  |               'message': { | ||||||
|  |                 'mid': 'message-id-1', | ||||||
|  |                 'text': 'This is the first message from the customer' | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     end | ||||||
|  |     initialize_with { attributes } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   factory :instagram_test_text_event, class: Hash do | ||||||
|  |     entry do | ||||||
|  |       [ | ||||||
|  |         { | ||||||
|  |           'id': 'instagram-message-id-123', | ||||||
|  |           'time': '2021-09-08T06:34:04+0000', | ||||||
|  |           'changes': [ | ||||||
|  |             { | ||||||
|  |               'field': 'messages', | ||||||
|  |               'value': { | ||||||
|  |                 'event_type': 'TEXT', | ||||||
|  |                 'event_timestamp': '1527459824', | ||||||
|  |                 'event_data': { | ||||||
|  |                   'message_id': 'vcvacopiufqwehfawdnb', | ||||||
|  |                   'sender': { | ||||||
|  |                     'username': 'sender_username' | ||||||
|  |                   }, | ||||||
|  |                   'recipient': { | ||||||
|  |                     'thread_id': 'faeoqiehrkbfadsfawd' | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     end | ||||||
|  |     initialize_with { attributes } | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										31
									
								
								spec/factories/instagram_message/incoming_messages.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								spec/factories/instagram_message/incoming_messages.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | FactoryBot.define do | ||||||
|  |   factory :incoming_ig_text_message, class: Hash do | ||||||
|  |     messaging do | ||||||
|  |       [ | ||||||
|  |         { | ||||||
|  |           'id': 'instagram-message-id-123', | ||||||
|  |           'time': '2021-09-08T06:34:04+0000', | ||||||
|  |           'messaging': [ | ||||||
|  |             { | ||||||
|  |               'sender': { | ||||||
|  |                 'id': 'Sender-id-1' | ||||||
|  |               }, | ||||||
|  |               'recipient': { | ||||||
|  |                 'id': 'chatwoot-app-user-id-1' | ||||||
|  |               }, | ||||||
|  |               'timestamp': '2021-09-08T06:34:04+0000', | ||||||
|  |               'message': { | ||||||
|  |                 'mid': 'message-id-1', | ||||||
|  |                 'text': 'This is the first message from the customer' | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     initialize_with { attributes } | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										54
									
								
								spec/jobs/webhooks/instagram_events_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								spec/jobs/webhooks/instagram_events_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  | require 'webhooks/twitter' | ||||||
|  |  | ||||||
|  | describe Webhooks::InstagramEventsJob do | ||||||
|  |   subject(:instagram_webhook) { described_class } | ||||||
|  |  | ||||||
|  |   let!(:account) { create(:account) } | ||||||
|  |   let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } | ||||||
|  |   let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } | ||||||
|  |   let!(:dm_params) { build(:instagram_message_create_event).with_indifferent_access } | ||||||
|  |   let!(:test_params) { build(:instagram_test_text_event).with_indifferent_access } | ||||||
|  |   let(:fb_object) { double } | ||||||
|  |  | ||||||
|  |   describe '#perform' do | ||||||
|  |     context 'with direct_message params' do | ||||||
|  |       it 'creates incoming message in the instagram inbox' do | ||||||
|  |         allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) | ||||||
|  |         allow(fb_object).to receive(:get_object).and_return( | ||||||
|  |           { | ||||||
|  |             name: 'Jane', | ||||||
|  |             id: 'Sender-id-1', | ||||||
|  |             account_id: instagram_inbox.account_id, | ||||||
|  |             profile_pic: 'https://via.placeholder.com/250x250.png' | ||||||
|  |           }.with_indifferent_access | ||||||
|  |         ) | ||||||
|  |         instagram_webhook.perform_now(dm_params[:entry]) | ||||||
|  |  | ||||||
|  |         instagram_inbox.reload | ||||||
|  |  | ||||||
|  |         expect(instagram_inbox.contacts.count).to be 1 | ||||||
|  |         expect(instagram_inbox.conversations.count).to be 1 | ||||||
|  |         expect(instagram_inbox.messages.count).to be 1 | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'creates test text message in the instagram inbox' do | ||||||
|  |         allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) | ||||||
|  |         allow(fb_object).to receive(:get_object).and_return( | ||||||
|  |           { | ||||||
|  |             name: 'Jane', | ||||||
|  |             id: 'Sender-id-1', | ||||||
|  |             account_id: instagram_inbox.account_id, | ||||||
|  |             profile_pic: 'https://via.placeholder.com/250x250.png' | ||||||
|  |           }.with_indifferent_access | ||||||
|  |         ) | ||||||
|  |         instagram_webhook.perform_now(test_params[:entry]) | ||||||
|  |  | ||||||
|  |         instagram_inbox.reload | ||||||
|  |  | ||||||
|  |         expect(instagram_inbox.messages.count).to be 1 | ||||||
|  |         expect(instagram_inbox.messages.last.content).to eq('This is a test message from facebook.') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										45
									
								
								spec/services/instagram/send_on_instagram_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								spec/services/instagram/send_on_instagram_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | describe Instagram::SendOnInstagramService do | ||||||
|  |   subject(:send_reply_service) { described_class.new(message: message) } | ||||||
|  |  | ||||||
|  |   before do | ||||||
|  |     create(:message, message_type: :incoming, inbox: instagram_inbox, account: account, conversation: conversation) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   let!(:account) { create(:account) } | ||||||
|  |   let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } | ||||||
|  |   let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } | ||||||
|  |   let!(:contact) { create(:contact, account: account) } | ||||||
|  |   let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: instagram_inbox) } | ||||||
|  |   let(:conversation) { create(:conversation, contact: contact, inbox: instagram_inbox, contact_inbox: contact_inbox) } | ||||||
|  |   let(:response) { double } | ||||||
|  |  | ||||||
|  |   describe '#perform' do | ||||||
|  |     context 'with reply' do | ||||||
|  |       before do | ||||||
|  |         allow(Facebook::Messenger::Configuration::AppSecretProofCalculator).to receive(:call).and_return('app_secret_key', 'access_token') | ||||||
|  |         allow(HTTParty).to receive(:post).and_return( | ||||||
|  |           { | ||||||
|  |             body: { recipient: { id: contact_inbox.source_id } } | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'if message is sent from chatwoot and is outgoing' do | ||||||
|  |         message = create(:message, message_type: 'outgoing', inbox: instagram_inbox, account: account, conversation: conversation) | ||||||
|  |         response = ::Instagram::SendOnInstagramService.new(message: message).perform | ||||||
|  |         expect(response).to eq({ recipient: { id: contact_inbox.source_id } }) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'if message with attachment is sent from chatwoot and is outgoing' do | ||||||
|  |         message = build(:message, message_type: 'outgoing', inbox: instagram_inbox, account: account, conversation: conversation) | ||||||
|  |         attachment = message.attachments.new(account_id: message.account_id, file_type: :image) | ||||||
|  |         attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') | ||||||
|  |         message.save! | ||||||
|  |         response = ::Instagram::SendOnInstagramService.new(message: message).perform | ||||||
|  |         expect(response).to eq({ recipient: { id: contact_inbox.source_id } }) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Reference in New Issue
	
	Block a user
	 Tejaswini Chile
					Tejaswini Chile