From 598ece9a2d5be7ddbd9cb336feb480d43da0b309 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 6 Mar 2026 08:49:41 +0400 Subject: [PATCH] fix: Handle Facebook reel attachment type (#13691) Fixes https://linear.app/chatwoot/issue/PLA-96/argumenterror-reel-is-not-a-valid-file-type-argumenterror Fixes a crash in `when a user shares a Facebook reel via Messenger. Facebook sends these attachments with `type: "reel"`, which isn't a valid `file_type` enum value on the Attachment model (only `ig_reel` exists, matching Instagram's format). --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sojan Jose --- .../messages/messenger/message_builder.rb | 35 +++++++++++-- app/models/attachment.rb | 2 +- .../messages/facebook/message_builder_spec.rb | 51 +++++++++++++++++++ spec/models/attachment_spec.rb | 24 +++++++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 8821da9d5..be1f98b43 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -2,12 +2,17 @@ class Messages::Messenger::MessageBuilder include ::FileTypeHelper def process_attachment(attachment) - # This check handles very rare case if there are multiple files to attach with only one usupported file + # This check handles very rare case if there are multiple files to attach with only one unsupported file return if unsupported_file_type?(attachment['type']) - attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) + params = attachment_params(attachment) + attachment_obj = @message.attachments.new(params.except(:remote_file_url)) attachment_obj.save! - attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + if facebook_reel?(attachment) + update_facebook_reel_content(attachment) + elsif params[:remote_file_url] + attach_file(attachment_obj, params[:remote_file_url]) + end 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' @@ -26,7 +31,7 @@ class Messages::Messenger::MessageBuilder end def attachment_params(attachment) - file_type = attachment['type'].to_sym + file_type = normalize_file_type(attachment['type']) params = { file_type: file_type, account_id: @message.account_id } if [:image, :file, :audio, :video, :share, :story_mention, :ig_reel, :ig_post, :ig_story].include? file_type @@ -100,6 +105,28 @@ class Messages::Messenger::MessageBuilder private + # 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 + + def normalize_file_type(type) + sym = type.to_sym + FACEBOOK_FILE_TYPE_MAP.fetch(sym, sym) + 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) + attachment['type'].to_sym == :reel + end + + def update_facebook_reel_content(attachment) + url = attachment.dig('payload', 'url') + return if url.blank? + + @message.update!(content: url) if @message.content.blank? + end + def unsupported_file_type?(attachment_type) [:template, :unsupported_type, :ephemeral].include? attachment_type.to_sym end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 5ee784243..a1cc062a0 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -89,7 +89,7 @@ class Attachment < ApplicationRecord when :embed embed_data else - file_metadata + file.attached? ? file_metadata : { data_url: external_url, thumb_url: '' } end end diff --git a/spec/builders/messages/facebook/message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb index 525ad7736..f3244bc21 100644 --- a/spec/builders/messages/facebook/message_builder_spec.rb +++ b/spec/builders/messages/facebook/message_builder_spec.rb @@ -89,6 +89,57 @@ describe Messages::Facebook::MessageBuilder do expect(message.content_attributes['external_echo']).to be true end + context 'when message contains a reel attachment' do + let(:reel_message_object) do + { + messaging: { + sender: { id: '3383290475046708' }, + recipient: { id: facebook_channel.page_id }, + timestamp: 1_772_452_164_516, + message: { + mid: 'm_reel_test', + attachments: [ + { + type: 'reel', + payload: { + url: 'https://www.facebook.com/reel/123456', + title: 'Test Reel Title', + reel_video_id: 123_456 + } + } + ] + } + } + }.to_json + end + let(:reel_message) { Integrations::Facebook::MessageParser.new(reel_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 + ) + end + + it 'creates an ig_reel attachment without downloading the file' do + expect(Down).not_to receive(:download) + described_class.new(reel_message, facebook_channel.inbox).perform + + message = facebook_channel.inbox.messages.find_by(source_id: 'm_reel_test') + expect(message).to be_present + expect(message.attachments.first.file_type).to eq('ig_reel') + expect(message.attachments.first.external_url).to eq('https://www.facebook.com/reel/123456') + expect(message.attachments.first.file.attached?).to be false + end + + it 'sets the reel URL as message content' do + described_class.new(reel_message, facebook_channel.inbox).perform + + message = facebook_channel.inbox.messages.find_by(source_id: 'm_reel_test') + expect(message.content).to eq('https://www.facebook.com/reel/123456') + end + end + context 'when lock to single conversation' do subject(:mocked_message_builder) do described_class.new(mocked_incoming_fb_text_message, facebook_channel.inbox).perform diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index f668576e3..a17839174 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -155,6 +155,30 @@ RSpec.describe Attachment do end end + describe 'push_event_data for ig_reel attachments' do + it 'returns external_url as data_url when no file is attached' do + attachment = message.attachments.create!( + account_id: message.account_id, + file_type: :ig_reel, + external_url: 'https://www.facebook.com/reel/123456' + ) + + event_data = attachment.push_event_data + expect(event_data[:data_url]).to eq('https://www.facebook.com/reel/123456') + expect(event_data[:thumb_url]).to eq('') + end + + it 'returns file_url as data_url when file is attached' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :ig_reel, + external_url: 'https://www.instagram.com/reel/123') + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + attachment.save! + + event_data = attachment.push_event_data + expect(event_data[:data_url]).to be_present + end + end + describe 'push_event_data for embed attachments' do it 'returns external url as data_url' do attachment = message.attachments.create!(account_id: message.account_id, file_type: :embed, external_url: 'https://example.com/embed')