feat: Facebook delivery reports (#8136)

This commit is contained in:
Muhsin Keloth
2023-11-20 12:22:45 +05:30
committed by GitHub
parent feead30b0b
commit 6c8dacfa0d
7 changed files with 195 additions and 20 deletions

View File

@@ -0,0 +1,8 @@
class Webhooks::FacebookDeliveryJob < ApplicationJob
queue_as :low
def perform(message)
response = ::Integrations::Facebook::MessageParser.new(message)
Integrations::Facebook::DeliveryStatus.new(params: response).perform
end
end

View File

@@ -29,14 +29,13 @@ Rails.application.reloader.to_prepare do
end
Facebook::Messenger::Bot.on :delivery do |delivery|
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
# delivery.sender # => { 'id' => '1008372609250235' }
# delivery.recipient # => { 'id' => '2015573629214912' }
# delivery.at # => 2016-04-22 21:30:36 +0200
# delivery.seq # => 37
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
updater.perform
Rails.logger.info "Human was online at #{delivery.at}"
Rails.logger.info "Recieved delivery status #{delivery.to_json}"
Webhooks::FacebookDeliveryJob.perform_later(delivery.to_json)
end
Facebook::Messenger::Bot.on :read do |read|
Rails.logger.info "Recieved read status #{read.to_json}"
Webhooks::FacebookDeliveryJob.perform_later(read.to_json)
end
Facebook::Messenger::Bot.on :message_echo do |message|

View File

@@ -1,32 +1,37 @@
# frozen_string_literal: true
class Integrations::Facebook::DeliveryStatus
def initialize(params)
@params = params
end
pattr_initialize [:params!]
def perform
update_message_status
return if facebook_channel.blank?
return unless conversation
process_delivery_status if params.delivery_watermark
process_read_status if params.read_watermark
end
private
def sender_id
@params.sender['id']
def process_delivery_status
timestamp = Time.zone.at(params.delivery_watermark.to_i).to_datetime.utc
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :delivered)
end
def process_read_status
timestamp = Time.zone.at(params.read_watermark.to_i).to_datetime.utc
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :read)
end
def contact
::ContactInbox.find_by(source_id: sender_id)&.contact
::ContactInbox.find_by(source_id: params.sender_id)&.contact
end
def conversation
@conversation ||= ::Conversation.find_by(contact_id: contact.id) if contact.present?
end
def update_message_status
return unless conversation
conversation.contact_last_seen_at = @params.at
conversation.save!
def facebook_channel
@facebook_channel ||= Channel::FacebookPage.find_by(page_id: params.recipient_id)
end
end

View File

@@ -34,6 +34,22 @@ class Integrations::Facebook::MessageParser
@messaging.dig('message', 'mid')
end
def delivery
@messaging['delivery']
end
def read
@messaging['read']
end
def read_watermark
read&.dig('watermark')
end
def delivery_watermark
delivery&.dig('watermark')
end
def echo?
@messaging.dig('message', 'is_echo')
end

View File

@@ -10,4 +10,24 @@ FactoryBot.define do
initialize_with { attributes }
end
factory :message_deliveries, class: Hash do
messaging do
{ sender: { id: '3383290475046708' },
recipient: { id: '117172741761305' },
delivery: { watermark: '1648581633369' } }
end
initialize_with { attributes }
end
factory :message_reads, class: Hash do
messaging do
{ sender: { id: '3383290475046708' },
recipient: { id: '117172741761305' },
read: { watermark: '1648581633369' } }
end
initialize_with { attributes }
end
end

View File

@@ -0,0 +1,44 @@
require 'rails_helper'
RSpec.describe Webhooks::FacebookDeliveryJob do
include ActiveJob::TestHelper
let(:message) { 'test_message' }
let(:parsed_message) { instance_double(Integrations::Facebook::MessageParser) }
let(:delivery_status) { instance_double(Integrations::Facebook::DeliveryStatus) }
before do
allow(Integrations::Facebook::MessageParser).to receive(:new).with(message).and_return(parsed_message)
allow(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message).and_return(delivery_status)
allow(delivery_status).to receive(:perform)
end
after do
clear_enqueued_jobs
end
describe '#perform_later' do
it 'enqueues the job' do
expect do
described_class.perform_later(message)
end.to have_enqueued_job(described_class).with(message).on_queue('low')
end
end
describe '#perform' do
it 'calls the MessageParser with the correct argument' do
expect(Integrations::Facebook::MessageParser).to receive(:new).with(message)
described_class.perform_now(message)
end
it 'calls the DeliveryStatus with the correct argument' do
expect(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message)
described_class.perform_now(message)
end
it 'executes perform on the DeliveryStatus instance' do
expect(delivery_status).to receive(:perform)
described_class.perform_now(message)
end
end
end

View File

@@ -0,0 +1,83 @@
require 'rails_helper'
describe Integrations::Facebook::DeliveryStatus do
subject(:message_builder) { described_class.new(message_deliveries, facebook_channel.inbox).perform }
before do
stub_request(:post, /graph\.facebook\.com/)
end
let!(:account) { create(:account) }
let!(:facebook_channel) { create(:channel_facebook_page, page_id: '117172741761305') }
let!(:message_delivery_object) { build(:message_deliveries).to_json }
let!(:message_deliveries) { Integrations::Facebook::MessageParser.new(message_delivery_object) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_channel.inbox, source_id: '3383290475046708') }
let!(:conversation) { create(:conversation, inbox: facebook_channel.inbox, contact: contact, contact_inbox: contact_inbox) }
let!(:message_read_object) { build(:message_reads).to_json }
let!(:message_reads) { Integrations::Facebook::MessageParser.new(message_read_object) }
let!(:message1) do
create(:message, content: 'facebook message', message_type: 'outgoing', inbox: facebook_channel.inbox, conversation: conversation)
end
let!(:message2) do
create(:message, content: 'facebook message', message_type: 'incoming', inbox: facebook_channel.inbox, conversation: conversation)
end
describe '#perform' do
context 'when message_deliveries callback fires' do
before do
allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
end
it 'updates all messages if the status is delivered' do
described_class.new(params: message_deliveries).perform
expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
message1.conversation.id,
Time.zone.at(message_deliveries.delivery['watermark'].to_i).to_datetime,
:delivered
)
end
it 'does not update the message status if the message is incoming' do
described_class.new(params: message_deliveries).perform
expect(message2.reload.status).to eq('sent')
end
it 'does not update the message status if the message was created after the watermark' do
message1.update(created_at: 1.day.from_now)
message_deliveries.delivery['watermark'] = 1.day.ago.to_i
described_class.new(params: message_deliveries).perform
expect(message1.reload.status).to eq('sent')
end
end
context 'when message_reads callback fires' do
before do
allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
end
it 'updates all messages if the status is read' do
described_class.new(params: message_reads).perform
expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
message1.conversation.id,
Time.zone.at(message_reads.read['watermark'].to_i).to_datetime,
:read
)
end
it 'does not update the message status if the message is incoming' do
described_class.new(params: message_reads).perform
expect(message2.reload.status).to eq('sent')
end
it 'does not update the message status if the message was created after the watermark' do
message1.update(created_at: 1.day.from_now)
message_reads.read['watermark'] = 1.day.ago.to_i
described_class.new(params: message_reads).perform
expect(message1.reload.status).to eq('sent')
end
end
end
end