feat: mark message as read (#43)
* feat: implement send_read_messages method for WhatsApp channel * feat: implement messages_read event handling and dispatch for conversations * feat: enhance messages_read handling to include last_seen_at and conversation context * feat: update last_seen handling to reference agent_last_seen_at for messages read event * chore: fix rebase * feat: update error handling * feat: update send_read_messages to mark received messages as not sent by the user * test: controller spec * test: channel listener * test: channel and provider --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
parent
19ad42a580
commit
c6e505e924
@ -110,6 +110,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
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
|
||||
|
||||
|
||||
@ -22,6 +22,19 @@ class ChannelListener < BaseListener
|
||||
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)
|
||||
|
||||
@ -91,6 +91,12 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -104,6 +104,27 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
||||
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
|
||||
@ -190,5 +211,10 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
||||
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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -687,6 +687,20 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
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
|
||||
|
||||
|
||||
@ -91,6 +91,31 @@ describe ChannelListener do
|
||||
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)
|
||||
Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: create(:user), is_private: is_private)
|
||||
end
|
||||
|
||||
@ -68,6 +68,8 @@ RSpec.describe Channel::Whatsapp do
|
||||
|
||||
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)
|
||||
@ -75,7 +77,6 @@ RSpec.describe Channel::Whatsapp do
|
||||
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
|
||||
@ -96,13 +97,14 @@ RSpec.describe Channel::Whatsapp do
|
||||
|
||||
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
|
||||
@ -118,6 +120,36 @@ RSpec.describe Channel::Whatsapp do
|
||||
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)
|
||||
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
|
||||
|
||||
@ -4,7 +4,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
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' }
|
||||
@ -29,7 +29,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
|
||||
response = service.setup_channel_provider
|
||||
|
||||
expect(response).to be true
|
||||
expect(response).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
@ -68,7 +68,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
|
||||
response = service.disconnect_channel_provider
|
||||
|
||||
expect(response).to be true
|
||||
expect(response).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
@ -312,10 +312,8 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
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
|
||||
|
||||
@ -326,7 +324,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
.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
|
||||
|
||||
@ -337,7 +335,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
.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
|
||||
|
||||
@ -363,6 +361,20 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
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" }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user