iachat/app/services/whatsapp/wuzapi/media_handler.rb
Rodrigo Borba 5d3ce4e565 fix(whatsapp): corrige content-type audio/opus e extensão para OGG
- MediaHandler: adiciona sanitize_content_type que normaliza audio/opus → audio/ogg
- MediaHandler: detect_extension retorna .ogg (não .mp3) para áudios WhatsApp
- MediaHandler: final_filename força extensão .ogg em áudios que chegam com .mp3
- Attachment: normalize_opus_blob_content_type! agora verifica apenas content_type
  (remove checagem de extensão de filename que impedia normalização de blobs .mp3)
- Attachment: audio_metadata chama normalize_opus_blob_content_type! para corrigir
  blobs existentes na primeira vez que são acessados (lazy fix)

WhatsApp envia áudio como container OGG/Opus (bytes OggS = 4f 67 67 53),
mas declarava mimetype audio/opus. Browsers não conseguem reproduzir container
OGG via MIME audio/opus — precisam de audio/ogg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 18:40:06 -03:00

116 lines
3.4 KiB
Ruby

class Whatsapp::Wuzapi::MediaHandler
def initialize(inbox, parser)
@inbox = inbox
@parser = parser
end
def process(message, attachment_data)
file_io = download_or_decrypt_media(attachment_data, @parser.message_type)
return if file_io.blank?
message.attachments.new(
account_id: message.account_id,
file_type: file_content_type(@parser.message_type),
file: {
io: file_io,
filename: final_filename(attachment_data),
content_type: sanitize_content_type(attachment_data[:mimetype], @parser.message_type)
}
)
end
private
def download_or_decrypt_media(data, type)
url = data[:external_url]
decoded = download_via_wuzapi(url)
return StringIO.new(decoded) if decoded
encrypted_bytes = download_from_cdn(url)
return nil if encrypted_bytes.blank?
if data[:media_key].present?
decrypted = decrypt_media(encrypted_bytes, data[:media_key], type)
return decrypted if decrypted
end
StringIO.new(encrypted_bytes)
rescue StandardError => e
Rails.logger.error "WuzAPI: Media handling failed - #{e.message}"
nil
end
def download_via_wuzapi(url)
response = wuzapi_client.download_media(wuzapi_token, url)
return nil unless response.is_a?(Hash) && response['data'].present?
image_data = response['data'].sub(/^data:.*?;base64,/, '')
decoded = Base64.decode64(image_data)
decoded.bytesize > 1000 ? decoded : nil
rescue StandardError => e
Rails.logger.warn "WuzAPI: Endpoint download failed - #{e.message}"
nil
end
def download_from_cdn(url)
tempfile = Down.download(url, open_timeout: 10, read_timeout: 30, ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
tempfile.read.b
rescue StandardError => e
Rails.logger.error "WuzAPI: CDN download failed - #{e.message}"
nil
end
def decrypt_media(bytes, key, type)
Whatsapp::DecryptionService.new(key, file_content_type(type)).decrypt_bytes(bytes)
end
def final_filename(data)
name = data[:file_name] || "wuzapi_#{Time.now.to_i}"
ext = File.extname(name)
ext = detect_extension(data[:mimetype], @parser.message_type) if ext.blank?
# Normalise audio extension: WhatsApp always sends OGG/Opus, never MP3
ext = '.ogg' if @parser.message_type == :audio && ext == '.mp3'
"#{File.basename(name, '.*')}#{ext}"
end
def sanitize_content_type(mimetype, type)
# WhatsApp audio is OGG-wrapped Opus. audio/opus is raw Opus (wrong container header).
# Saving as audio/ogg ensures browsers can play via <audio> without issues.
return 'audio/ogg' if type == :audio && mimetype.to_s.include?('opus')
mimetype || 'application/octet-stream'
end
def detect_extension(mimetype, type)
return '.jpg' if type == :image || type == :sticker
return '.ogg' if type == :audio
return '.mp4' if type == :video
case mimetype
when 'image/png' then '.png'
when 'image/webp' then '.webp'
when 'image/gif' then '.gif'
when 'audio/ogg' then '.ogg'
when 'video/webm' then '.webm'
else '.bin'
end
end
def file_content_type(type)
case type
when :image, :sticker then :image
when :audio then :audio
when :video then :video
else :file
end
end
def wuzapi_client
@wuzapi_client ||= ::Wuzapi::Client.new(@inbox.channel.provider_config['wuzapi_base_url'])
end
def wuzapi_token
@inbox.channel.wuzapi_user_token
end
end