fix: normalize audio/opus content type to audio/ogg for WhatsApp attachments (#223)
This commit is contained in:
parent
bce4e9b3a7
commit
72354a4459
@ -56,7 +56,10 @@ class Attachment < ApplicationRecord
|
||||
# NOTE: for External services use this methods since redirect doesn't work effectively in a lot of cases
|
||||
def download_url
|
||||
ActiveStorage::Current.url_options = Rails.application.routes.default_url_options if ActiveStorage::Current.url_options.blank?
|
||||
file.attached? ? file.blob.url : ''
|
||||
return '' unless file.attached?
|
||||
|
||||
normalize_opus_blob_content_type!
|
||||
file.blob.url
|
||||
end
|
||||
|
||||
def thumb_url
|
||||
@ -184,6 +187,16 @@ class Attachment < ApplicationRecord
|
||||
def media_file?(file_content_type)
|
||||
file_content_type.start_with?('image/', 'video/', 'audio/')
|
||||
end
|
||||
|
||||
# Marcel gem may detect OGG/Opus files as audio/opus instead of audio/ogg.
|
||||
# Lazily normalize existing blobs so presigned URLs serve the correct Content-Type.
|
||||
# Only applies to .ogg files — .opus files legitimately use audio/opus.
|
||||
def normalize_opus_blob_content_type!
|
||||
blob = file.blob
|
||||
return unless blob.content_type == 'audio/opus' && blob.filename.to_s.end_with?('.ogg')
|
||||
|
||||
blob.update_column(:content_type, 'audio/ogg') # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
Attachment.include_mod_with('Concerns::Attachment')
|
||||
|
||||
@ -156,7 +156,6 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
|
||||
def send_attachment_message(phone_number, message)
|
||||
attachment = message.attachments.first
|
||||
normalize_opus_content_type(attachment)
|
||||
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
||||
type_content = { 'link' => attachment.download_url }
|
||||
type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
|
||||
@ -181,21 +180,6 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
type == 'audio' && attachment.meta&.dig('is_recorded_audio') && attachment.file.content_type == 'audio/ogg'
|
||||
end
|
||||
|
||||
# Marcel gem may re-detect OGG/Opus files as audio/opus after ActiveStorage
|
||||
# blob attachment, but WhatsApp Cloud API requires audio/ogg content type
|
||||
# for voice messages. Normalize so the download URL serves the correct
|
||||
# Content-Type header. No-op when the frontend already uploads as audio/ogg.
|
||||
def normalize_opus_content_type(attachment)
|
||||
return unless attachment.file.attached?
|
||||
|
||||
blob = attachment.file.blob
|
||||
return unless blob.content_type == 'audio/opus'
|
||||
|
||||
return if blob.update(content_type: 'audio/ogg')
|
||||
|
||||
Rails.logger.error("Failed to normalize blob #{blob.id} content_type from audio/opus to audio/ogg")
|
||||
end
|
||||
|
||||
def error_message(response)
|
||||
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
|
||||
response.parsed_response&.dig('error', 'message')
|
||||
|
||||
18
config/initializers/active_storage_opus_fix.rb
Normal file
18
config/initializers/active_storage_opus_fix.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# Marcel gem may detect OGG Opus files as audio/opus instead of audio/ogg.
|
||||
# This is problematic because WhatsApp Cloud API (and other services)
|
||||
# expect audio/ogg for OGG container files with the Opus codec.
|
||||
#
|
||||
# This initializer patches ActiveStorage::Blob to normalize audio/opus → audio/ogg
|
||||
# at identification time for .ogg files, preventing the wrong content_type from
|
||||
# being persisted. Files with .opus extension are left as audio/opus since they
|
||||
# are genuinely Opus-only files.
|
||||
ActiveSupport.on_load(:active_storage_blob) do
|
||||
prepend(Module.new do
|
||||
private
|
||||
|
||||
def identify_content_type(io = nil)
|
||||
detected = super
|
||||
detected == 'audio/opus' && filename.to_s.end_with?('.ogg') ? 'audio/ogg' : detected
|
||||
end
|
||||
end)
|
||||
end
|
||||
@ -30,6 +30,20 @@ RSpec.describe Attachment do
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
expect(attachment.download_url).not_to be_nil
|
||||
end
|
||||
|
||||
it 'normalizes audio/opus to audio/ogg in blob content_type' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :audio)
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'sample.ogg', content_type: 'audio/ogg')
|
||||
attachment.save!
|
||||
# Simulate Marcel detecting audio/opus
|
||||
attachment.file.blob.update_column(:content_type, 'audio/opus') # rubocop:disable Rails/SkipsModelValidations
|
||||
attachment.file.blob.reload
|
||||
|
||||
expect(attachment.file.blob.content_type).to eq('audio/opus')
|
||||
attachment.download_url
|
||||
expect(attachment.file.blob.content_type).to eq('audio/ogg')
|
||||
expect(attachment.file.blob.reload.content_type).to eq('audio/ogg')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with_attached_file?' do
|
||||
|
||||
@ -146,6 +146,28 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
end
|
||||
|
||||
it 'normalizes audio/opus to audio/ogg and sends voice flag for recorded audio' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :audio, meta: { 'is_recorded_audio' => true })
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'sample.ogg', content_type: 'audio/ogg')
|
||||
attachment.save!
|
||||
# Simulate Marcel detecting audio/opus (as happens with OGG Opus files in Marcel 1.1.0)
|
||||
attachment.file.blob.update_column(:content_type, 'audio/opus') # rubocop:disable Rails/SkipsModelValidations
|
||||
attachment.file.blob.reload
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
type: 'audio',
|
||||
audio: WebMock::API.hash_including({ link: anything, voice: true })
|
||||
})
|
||||
)
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
expect(attachment.file.blob.reload.content_type).to eq('audio/ogg')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user