8000 feat: mark message as read by CayoPOliveira · Pull Request #43 · fazer-ai/chatwoot · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: mark message as read #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 8, 2025
4 changes: 4 additions & 0 deletions app/controllers/api/v1/accounts/conversations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ def toggle_typing_status
end

def update_last_seen
# NOTE: Use old `agent_last_seen_at`, so we reference messages received after that
Rails.configuration.dispatcher.dispatch(Events::Types::MESSAGES_READ, Time.zone.now, conversation: @conversation,
last_seen_at: @conversation.agent_last_seen_at)

update_last_seen_on_conversation(DateTime.now.utc, assignee?)
end

Expand Down
13 changes: 13 additions & 0 deletions app/listeners/channel_listener.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ def account_presence_updated(event)
end
end

def messages_read(event)
conversation, last_seen_at = event.data.values_at(:conversation, :last_seen_at)

channel = conversation.inbox.channel
return unless channel.respond_to?(:send_read_messages)

messages = conversation.messages.where(message_type: :incoming)
.where('updated_at > ?', last_seen_at)
.where.not(status: :read)

channel.send_read_messages(messages, conversation: conversation) if messages.any?
end

private

def handle_typing_event(event)
Expand Down
6 changes: 6 additions & 0 deletions app/models/channel/whatsapp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ def update_presence(status)
provider_service.update_presence(status)
end

def send_read_messages(messages, conversation:)
return unless provider_service.respond_to?(:send_read_messages)

provider_service.send_read_messages(conversation.contact.phone_number, messages)
end

delegate :setup_channel_provider, to: :provider_service
delegate :disconnect_channel_provider, to: :provider_service
delegate :send_message, to: :provider_service
Expand Down
30 changes: 28 additions & 2 deletions app/services/whatsapp/providers/whatsapp_baileys_service.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseService
class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseService # rubocop:disable Metrics/ClassLength
class MessageContentTypeNotSupported < StandardError; end
class MessageNotSentError < StandardError; end

Expand Down Expand Up @@ -104,6 +104,27 @@ def update_presence(status)
process_response(response)
end

def send_read_messages(phone_number, messages)
@phone_number = phone_number

response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/read-messages",
headers: api_headers,
body: {
keys: messages.map do |message|
{
id: message.source_id,
remoteJid: remote_jid,
# NOTE: It only makes sense to mark received messages as read
fromMe: false
}
end
}.to_json
)

process_response(response)
end

private

def provider_url
Expand Down Expand Up @@ -190,5 +211,10 @@ def handle_channel_error
whatsapp_channel.update_provider_connection!(connection: 'close')
end

with_error_handling :setup_channel_provider, :disconnect_channel_provider, :send_message
with_error_handling :setup_channel_provider,
:disconnect_channel_provider,
:send_message,
:toggle_typing_status,
:update_presence,
:send_read_messages
end
1 change: 1 addition & 0 deletions lib/events/types.rb
Original file line number Diff line number Diff line change
EDBE Expand Up @@ -37,6 +37,7 @@ module Events::Types
FIRST_REPLY_CREATED = 'first.reply.created'
REPLY_CREATED = 'reply.created'
MESSAGE_UPDATED = 'message.updated'
MESSAGES_READ = 'messages.read'

# contact events
CONTACT_CREATED = 'contact.created'
Expand Down
14 changes: 14 additions & 0 deletions spec/controllers/api/v1/accounts/conversations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,20 @@
expect(response).to have_http_status(:success)
expect(conversation.reload.assignee_last_seen_at).not_to be_nil
end

it 'dispatches messages.read event' do
freeze_time
conversation.update!(agent_last_seen_at: 1.hour.ago)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
.with(Events::Types::MESSAGES_READ, Time.zone.now, conversation: conversation, last_seen_at: conversation.agent_last_seen_at)

post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json

expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
end
end
end

Expand Down
25 changes: 25 additions & 0 deletions spec/listeners/channel_listener_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@
end
end

describe '#messages_read' do
let(:channel) { create(:channel_whatsapp, sync_templates: false, validate_provider_config: false) }
let(:conversation) { create(:conversation, inbox: create(:inbox, channel: channel)) }
let(:last_seen_at) { 1.day.ago }

it 'sends read messages to the channel' do
sent_message = create(:message, conversation: conversation, message_type: :incoming, status: :sent)
create(:message, conversation: conversation, message_type: :incoming, status: :read)
allow(channel).to receive(:send_read_messages).with([sent_message], conversation: conversation)
event = Events::Base.new(Events::Types::MESSAGES_READ, Time.zone.now, conversation: conversation, last_seen_at: last_seen_at)

listener.messages_read(event)

expect(channel).to have_received(:send_read_messages)
end

it 'skips the event if the channel does not respond to send_read_messages' do
create(:channel_api, inbox: conversation.inbox)

expect do
listener.messages_read(Events::Base.new(Events::Types::MESSAGES_READ, Time.zone.now, conversation: conversation, last_seen_at: last_seen_at))
end.not_to raise_error
end
end

def build_typing_event(event_name, conversation:, is_private: false)< F438 /td>
Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: create(:user), is_private: is_private)
end
Expand Down
36 changes: 34 additions & 2 deletions spec/models/channel/whatsapp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@

it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
allow(provider_double).to receive(:toggle_typing_status)
.with(conversation.contact.phone_number, Events::Types::CONVERSATION_TYPING_ON)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)

channel.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation)

expect(provider_double).to have_received(:toggle_typing_status)
.with(conversation.contact.phone_number, Events::Types::CONVERSATION_TYPING_ON)
end

it 'does not call method if provider service does not implement it' do
Expand All @@ -96,13 +97,14 @@

it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, update_presence: nil)
allow(provider_double).to receive(:update_presence).with('online')
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)

channel.update_presence('online')

expect(provider_double).to have_received(:update_presence).with('online')
expect(provider_double).to have_received(:update_presence)
end

it 'does not call method if provider service does not implement it' do
Expand All @@ -118,6 +120,36 @@
end
end

describe '#send_read_messages' do
let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) }
let(:conversation) { create(:conversation) }
let(:message) { create(:message) }

it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, send_read_messages: nil)
allow(provider_double).to receive(:send_read_messages).with([message], conversation.contact.phone_number)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix parameter order in test stub.

The parameter order in the test stub doesn't match the actual implementation in the Channel::Whatsapp model.

According to the provided code snippet from app/models/channel/whatsapp.rb, the method calls the provider with:

provider_service.send_read_messages(conversation.contact.phone_number, messages)

But the test is setting up the stub with parameters in reverse order:

-allow(provider_double).to receive(:send_read_messages).with([message], conversation.contact.phone_number)
+allow(provider_double).to receive(:send_read_messages).with(conversation.contact.phone_number, [message])

10000
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)

channel.send_read_messages([message], conversation: conversation)

expect(provider_double).to have_received(:send_read_messages)
end

it 'does not call method if provider service does not implement it' do
channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false)
provider_double = instance_double(Whatsapp::Providers::WhatsappCloudService)
allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)

expect do
channel.send_read_messages([message], conversation: conversation)
end.not_to raise_error
end
end

describe 'callbacks' do
describe '#disconnect_channel_provider' do
context 'when provider is baileys' do
Expand Down
30 changes: 21 additions & 9 deletions spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }

let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false) }
let(:message) { create(:message) }
let(:message) { create(:message, source_id: 'msg_123') }

let(:test_send_phone_number) { '551187654321' }
let(:test_send_jid) { '551187654321@s.whatsapp.net' }
Expand All @@ -29,7 +29,7 @@

response = service.setup_channel_provider

expect(response).to be true
expect(response).to be(true)
end
end

Expand Down Expand Up @@ -68,7 +68,7 @@

response = service.disconnect_channel_provider

expect(response).to be true
expect(response).to be(true)
end
end

Expand Down Expand Up @@ -312,10 +312,8 @@
end

describe '#api_headers' do
context 'when called' do
it 'returns the headers' do
expect(service.api_headers).to eq('x-api-key' => 'test_key', 'Content-Type' => 'application/json')
end
it 'returns the headers' do
expect(service.api_headers).to eq('x-api-key' => 'test_key', 'Content-Type' => 'application/json')
end
end

Expand All @@ -326,7 +324,7 @@
.with(headers: stub_headers(whatsapp_channel))
.to_return(status: 200, body: '', headers: {})

expect(service.validate_provider_config?).to be true
expect(service.validate_provider_config?).to be(true)
end
end

Expand All @@ -337,7 +335,7 @@
.to_return(status: 400, body: 'error message', headers: {})
allow(Rails.logger).to receive(:error).with('error message')

expect(service.validate_provider_config?).to be false
expect(service.validate_provider_config?).to be(false)
expect(Rails.logger).to have_received(:error)
end

Expand All @@ -363,6 +361,20 @@
end
end

describe '#send_read_messages' do
it 'send read messages request' do
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/read-messages")
.with(
headers: stub_headers(whatsapp_channel),
body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {})

result = service.send_read_messages(test_send_phone_number, [message])

expect(result).to be(true)
end
end

describe '#toggle_typing_status' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }

Expand Down
0