diff --git a/app/services/whatsapp/baileys_handlers/helpers.rb b/app/services/whatsapp/baileys_handlers/helpers.rb index 76db37756..6dd74f070 100644 --- a/app/services/whatsapp/baileys_handlers/helpers.rb +++ b/app/services/whatsapp/baileys_handlers/helpers.rb @@ -143,6 +143,23 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength (message_type == 'reaction' && message_content.blank?) end + def fetch_profile_picture_url(phone_number) + jid = "#{phone_number}@s.whatsapp.net" + response = inbox.channel.provider_service.get_profile_pic(jid) + response&.dig('data', 'profilePictureUrl') + rescue StandardError => e + Rails.logger.error "Failed to fetch profile picture for #{phone_number}: #{e.message}" + nil + end + + def try_update_contact_avatar + # TODO: Current logic will never update the contact avatar if their profile picture changes on WhatsApp. + return if @contact.avatar.attached? + + profile_pic_url = fetch_profile_picture_url(phone_number_from_jid) + ::Avatar::AvatarFromUrlJob.perform_later(@contact, profile_pic_url) if profile_pic_url + end + def message_under_process? key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id) Redis::Alfred.get(key) diff --git a/app/services/whatsapp/baileys_handlers/messages_upsert.rb b/app/services/whatsapp/baileys_handlers/messages_upsert.rb index d7742f874..ceef9e8f2 100644 --- a/app/services/whatsapp/baileys_handlers/messages_upsert.rb +++ b/app/services/whatsapp/baileys_handlers/messages_upsert.rb @@ -55,6 +55,7 @@ module Whatsapp::BaileysHandlers::MessagesUpsert @contact = contact_inbox.contact @contact.update!(name: push_name) if @contact.name == source_id + try_update_contact_avatar end def handle_create_message diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 0023680de..babd34b43 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -189,6 +189,19 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer true end + def get_profile_pic(jid) + response = HTTParty.get( + "#{provider_url}/connections/#{whatsapp_channel.phone_number}/profile-picture-url", + headers: api_headers, + query: { jid: jid }, + format: :json + ) + + return nil unless process_response(response) + + response.parsed_response + end + def on_whatsapp(phone_number) @phone_number = phone_number diff --git a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb index f24d5f384..addefff34 100644 --- a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb @@ -146,6 +146,9 @@ describe Whatsapp::IncomingMessageBaileysService do end context 'when processing messages.upsert event' do + # NOTE: We always try fetching profile picture for new contacts on messages.upsert, so we must stub this request. + before { stub_profile_pic_fetch } + let(:timestamp) { Time.current.to_i } let(:raw_message) do { @@ -176,6 +179,46 @@ describe Whatsapp::IncomingMessageBaileysService do expect(message.content_attributes[:external_created_at]).to eq(timestamp) end + context 'when updating contact avatar' do + it 'enqueues avatar job when profile picture is available' do + stub_profile_pic_fetch('https://example.com/avatar.jpg') + + described_class.new(inbox: inbox, params: params).perform + + conversation = inbox.conversations.last + contact = conversation.contact + + expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(contact, 'https://example.com/avatar.jpg') + end + + it 'does not enqueue avatar job when contact already has avatar attached' do + stub_profile_pic_fetch('https://example.com/avatar.jpg') + contact = create(:contact, account: inbox.account, name: 'John Doe', phone_number: '+5511912345678') + contact.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + + described_class.new(inbox: inbox, params: params).perform + + expect(Avatar::AvatarFromUrlJob).not_to have_been_enqueued + end + + it 'does not enqueue avatar job when profile picture is not available' do + described_class.new(inbox: inbox, params: params).perform + + expect(Avatar::AvatarFromUrlJob).not_to have_been_enqueued + end + + it 'logs error and does not enqueue avatar job when profile picture request fails' do + allow(Rails.logger).to receive(:error) + stub_request(:get, /profile-picture-url/) + .to_raise(StandardError.new('Profile picture request failed')) + + described_class.new(inbox: inbox, params: params).perform + + expect(Avatar::AvatarFromUrlJob).not_to have_been_enqueued + expect(Rails.logger).to have_received(:error).with('Failed to fetch profile picture for 5511912345678: Profile picture request failed') + end + end + context 'when message type is unsupported' do it 'creates message with is_unsupported' do raw_message[:message] = { unsupported: 'message' } @@ -829,4 +872,12 @@ describe Whatsapp::IncomingMessageBaileysService do .with('https://baileys.api/media/msg_123', headers: inbox.channel.api_headers) .and_return(StringIO.new('Media data')) end + + def stub_profile_pic_fetch(url = nil) + stub_request(:get, /profile-picture-url/) + .to_return( + status: 200, + body: { data: { profilePictureUrl: url } }.to_json + ) + end end diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index 69cd28074..9ff5dc193 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -850,6 +850,51 @@ describe Whatsapp::Providers::WhatsappBaileysService do end end + describe '#get_profile_pic' do + let(:test_jid) { '551187654321@s.whatsapp.net' } + + context 'when response is successful' do + it 'returns the profile picture URL data' do + stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/profile-picture-url") + .with( + headers: stub_headers(whatsapp_channel), + query: { jid: test_jid } + ) + .to_return( + status: 200, + body: { data: { profilePictureUrl: 'https://pps.whatsapp.net/v/t61.24694-24/avatar.jpg', jid: test_jid } }.to_json + ) + + result = service.get_profile_pic(test_jid) + + expect(result).to eq({ + 'data' => { + 'profilePictureUrl' => 'https://pps.whatsapp.net/v/t61.24694-24/avatar.jpg', + 'jid' => test_jid + } + }) + end + end + + context 'when response fails' do + it 'returns nil when profile picture not found (404)' do + stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/profile-picture-url") + .with( + headers: stub_headers(whatsapp_channel), + query: { jid: test_jid } + ) + .to_return( + status: 404, + body: { error: 'Profile picture not found' }.to_json + ) + + result = service.get_profile_pic(test_jid) + + expect(result).to be_nil + end + end + end + def stub_headers(channel) { 'Content-Type' => 'application/json',