feat(whatsapp): get contact profile picture when using Baileys service (#98)

* feat(whatsapp): add profile picture fetching for Baileys provider

- Add `get_profile_pic` method to `WhatsappBaileysService` for fetching contact avatars
- Include `avatar_url` in contact attributes during contact creation
- Update `MessagesUpsert` handler to fetch and set profile pictures
- Add proper error handling and graceful degradation when avatar fetching fails

* test(whatsapp): add test coverage for Baileys profile picture fetching

- Add test suite for `get_profile_pic` method
- Include test scenarios for successful responses, missing avatars, and error handling
- Add profile picture HTTP stubbing and avatar job expectations to message processing tests

* feat(whatsapp): optimize Baileys avatar fetching and update avatar for existing inboxes

Previously, the profile picture was fetched on every incoming message and avatar updates were only attempted for new inboxes.

This change optimizes the process by:
- Only fetching the WhatsApp profile picture if the contact does not already have an avatar.
- Attempting to update the avatar for both new and existing inboxes.

This logic is based on Telegram's version (app/services/telegram/incoming_message_service.rb).

* refactor(whatsapp): remove unnecessary safe navigation when accessing contact

This reverts an unnecessary change introduced in b918a92.

* refactor(whatsapp): manually rollback change introduced in b918a92

* refactor(whatsapp): rename endpoint to profile-picture-url

* refactor(whatsapp): simplify profile picture response handling

Replaced manual JSON parsing with HTTParty's parsed_response and removed redundant JSON parsing tests.

* test(whatsapp): separate avatar processing test from message creation test

* test(whatsapp): update profile picture test to match actual API behavior

Change test to expect a 404 response when no profile picture exists, instead of a 200 response with partial data.

* refactor: move new functions to helper module

* test: more specs

---------

Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
Gabriel Coelho 2025-08-27 13:02:04 -03:00 committed by GitHub
parent 29a2474d05
commit d2a0d53fab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',