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
|
# NOTE: for External services use this methods since redirect doesn't work effectively in a lot of cases
|
||||||
def download_url
|
def download_url
|
||||||
ActiveStorage::Current.url_options = Rails.application.routes.default_url_options if ActiveStorage::Current.url_options.blank?
|
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
|
end
|
||||||
|
|
||||||
def thumb_url
|
def thumb_url
|
||||||
@ -184,6 +187,16 @@ class Attachment < ApplicationRecord
|
|||||||
def media_file?(file_content_type)
|
def media_file?(file_content_type)
|
||||||
file_content_type.start_with?('image/', 'video/', 'audio/')
|
file_content_type.start_with?('image/', 'video/', 'audio/')
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
Attachment.include_mod_with('Concerns::Attachment')
|
Attachment.include_mod_with('Concerns::Attachment')
|
||||||
|
|||||||
@ -156,7 +156,6 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
|
|
||||||
def send_attachment_message(phone_number, message)
|
def send_attachment_message(phone_number, message)
|
||||||
attachment = message.attachments.first
|
attachment = message.attachments.first
|
||||||
normalize_opus_content_type(attachment)
|
|
||||||
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
||||||
type_content = { 'link' => attachment.download_url }
|
type_content = { 'link' => attachment.download_url }
|
||||||
type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
|
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'
|
type == 'audio' && attachment.meta&.dig('is_recorded_audio') && attachment.file.content_type == 'audio/ogg'
|
||||||
end
|
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)
|
def error_message(response)
|
||||||
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
|
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
|
||||||
response.parsed_response&.dig('error', 'message')
|
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')
|
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
|
expect(attachment.download_url).not_to be_nil
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe 'with_attached_file?' do
|
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)
|
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user