fix(facebook): handle messenger sticker attachment type (#14793)
Facebook Messenger sticker messages no longer break message ingestion. Meta recently changed its webhook payloads so a sticker now arrives as a new `sticker` attachment type (alongside the existing `image` attachment during the transition period). Chatwoot didn't recognise `sticker` as a valid attachment file type, so the webhook job crashed and the message — and any others in the same batch — failed to sync. Stickers now appear in the conversation as a single image, just like before. Fixes https://linear.app/chatwoot/issue/PLA-177 ## How to reproduce 1. Connect a Facebook Page inbox. 2. Send a sticker from Messenger to that page. 3. Before this change: the `Webhooks::FacebookEventsJob` raises `ArgumentError: 'sticker' is not a valid file_type` and the message is dropped. 4. After this change: the sticker shows up in the conversation as a single image attachment. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Muhsin
Claude Opus 4.8
parent
6fcc888aee
commit
a4be38f8f3
@@ -6,6 +6,11 @@ class Messages::Messenger::MessageBuilder
|
||||
return if unsupported_file_type?(attachment['type'])
|
||||
|
||||
params = attachment_params(attachment)
|
||||
# During Meta's sticker webhook transition, a sticker message carries both an `image`
|
||||
# and a `sticker` attachment pointing to the same URL. Skip the redundant sticker so it
|
||||
# isn't attached twice, while still storing legitimate duplicate attachments of other types.
|
||||
return if duplicate_sticker?(attachment, params[:external_url])
|
||||
|
||||
attachment_obj = @message.attachments.new(params.except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
if facebook_reel?(attachment)
|
||||
@@ -13,10 +18,14 @@ class Messages::Messenger::MessageBuilder
|
||||
elsif params[:remote_file_url]
|
||||
attach_file(attachment_obj, params[:remote_file_url])
|
||||
end
|
||||
fetch_attachment_links(attachment_obj)
|
||||
update_attachment_file_type(attachment_obj)
|
||||
end
|
||||
|
||||
def fetch_attachment_links(attachment_obj)
|
||||
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
|
||||
fetch_ig_story_link(attachment_obj) if attachment_obj.file_type == 'ig_story'
|
||||
fetch_ig_post_link(attachment_obj) if attachment_obj.file_type == 'ig_post'
|
||||
update_attachment_file_type(attachment_obj)
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
@@ -111,13 +120,20 @@ class Messages::Messenger::MessageBuilder
|
||||
|
||||
# Facebook may send attachment types that don't directly match our file_type enum.
|
||||
# Map known aliases to their canonical enum values.
|
||||
FACEBOOK_FILE_TYPE_MAP = { reel: :ig_reel }.freeze
|
||||
FACEBOOK_FILE_TYPE_MAP = { reel: :ig_reel, sticker: :image }.freeze
|
||||
|
||||
def normalize_file_type(type)
|
||||
sym = type.to_sym
|
||||
FACEBOOK_FILE_TYPE_MAP.fetch(sym, sym)
|
||||
end
|
||||
|
||||
def duplicate_sticker?(attachment, url)
|
||||
return false unless attachment['type'].to_sym == :sticker
|
||||
return false if url.blank?
|
||||
|
||||
@message.attachments.any? { |existing| existing.external_url == url }
|
||||
end
|
||||
|
||||
# Facebook sends reel URLs as webpage links (facebook.com/reel/...) rather than
|
||||
# direct video URLs. Downloading these yields HTML, not video content.
|
||||
def facebook_reel?(attachment)
|
||||
|
||||
@@ -140,6 +140,66 @@ describe Messages::Facebook::MessageBuilder do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message contains a sticker attachment' do
|
||||
let(:sticker_url) { 'https://scontent.xx.fbcdn.net/sticker.png' }
|
||||
let(:sticker_message_object) do
|
||||
{
|
||||
messaging: {
|
||||
sender: { id: '3383290475046708' },
|
||||
recipient: { id: facebook_channel.page_id },
|
||||
timestamp: 1_772_452_164_516,
|
||||
message: {
|
||||
mid: 'm_sticker_test',
|
||||
attachments: [
|
||||
{ type: 'image', payload: { url: sticker_url } },
|
||||
{ type: 'sticker', payload: { url: sticker_url } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
let(:sticker_message) { Integrations::Facebook::MessageParser.new(sticker_message_object) }
|
||||
|
||||
before do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{ first_name: 'Jane', last_name: 'Dae', profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access
|
||||
)
|
||||
stub_request(:get, sticker_url).to_return(status: 200, body: 'sticker_data', headers: { 'Content-Type' => 'image/png' })
|
||||
end
|
||||
|
||||
it 'stores the sticker as a single image attachment' do
|
||||
described_class.new(sticker_message, facebook_channel.inbox).perform
|
||||
|
||||
message = facebook_channel.inbox.messages.find_by(source_id: 'm_sticker_test')
|
||||
expect(message.attachments.count).to eq(1)
|
||||
expect(message.attachments.first.file_type).to eq('image')
|
||||
expect(message.attachments.first.external_url).to eq(sticker_url)
|
||||
end
|
||||
|
||||
it 'keeps duplicate non-sticker attachments that share a URL' do
|
||||
duplicate_image_object = {
|
||||
messaging: {
|
||||
sender: { id: '3383290475046708' },
|
||||
recipient: { id: facebook_channel.page_id },
|
||||
message: {
|
||||
mid: 'm_duplicate_image_test',
|
||||
attachments: [
|
||||
{ type: 'image', payload: { url: sticker_url } },
|
||||
{ type: 'image', payload: { url: sticker_url } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}.to_json
|
||||
duplicate_image_message = Integrations::Facebook::MessageParser.new(duplicate_image_object)
|
||||
|
||||
described_class.new(duplicate_image_message, facebook_channel.inbox).perform
|
||||
|
||||
message = facebook_channel.inbox.messages.find_by(source_id: 'm_duplicate_image_test')
|
||||
expect(message.attachments.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
{
|
||||
source_id: 'm_fallback_test',
|
||||
|
||||
Reference in New Issue
Block a user