diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index ecd6f06ea..712a24608 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -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) diff --git a/spec/builders/messages/facebook/message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb index afa9d5f34..7718d23eb 100644 --- a/spec/builders/messages/facebook/message_builder_spec.rb +++ b/spec/builders/messages/facebook/message_builder_spec.rb @@ -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',