feat: Add Instagram Channel (#2955)
This commit is contained in:
parent
30244f79a6
commit
40d0b2faf3
30 changed files with 825 additions and 50 deletions
|
@ -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'
|
||||||
::Facebook::SendOnFacebookService.new(message: message).perform
|
if conversation.additional_attributes['type'] == 'instagram_direct_message'
|
||||||
|
::Instagram::SendOnInstagramService.new(message: message).perform
|
||||||
|
else
|
||||||
|
::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
|
Loading…
Add table
Add a link
Reference in a new issue