feat: toggle typing status on channel provider (#39)
* feat: toggle typing status on channel provider * refactor: general refactorings * fix: check if channel implements method * test: channel listener specs * test: channel spec * test: baileys service spec
This commit is contained in:
parent
81d62d94f1
commit
62ef2113d5
@ -48,6 +48,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
case permitted_params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
when 'recording'
|
||||
trigger_typing_event(CONVERSATION_RECORDING)
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
|
||||
@ -30,6 +30,8 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
when 'recording'
|
||||
trigger_typing_event(CONVERSATION_RECORDING)
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
|
||||
@ -18,7 +18,8 @@ class AsyncDispatcher < BaseDispatcher
|
||||
NotificationListener.instance,
|
||||
ParticipationListener.instance,
|
||||
ReportingEventListener.instance,
|
||||
WebhookListener.instance
|
||||
WebhookListener.instance,
|
||||
ChannelListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@ -576,7 +576,11 @@ function createEditorView() {
|
||||
handleDOMEvents: {
|
||||
keyup: () => {
|
||||
if (!props.disabled) {
|
||||
if (props.modelValue.length) {
|
||||
typingIndicator.start();
|
||||
} else {
|
||||
typingIndicator.stop();
|
||||
}
|
||||
updateImgToolbarOnDelete();
|
||||
}
|
||||
},
|
||||
|
||||
@ -863,6 +863,9 @@ export default {
|
||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||
if (!this.isRecordingAudio) {
|
||||
this.resetAudioRecorderInput();
|
||||
this.onTypingOff();
|
||||
} else {
|
||||
this.onRecording();
|
||||
}
|
||||
},
|
||||
toggleAudioRecorderPlayPause() {
|
||||
@ -872,6 +875,7 @@ export default {
|
||||
if (!this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopRecording();
|
||||
this.onTypingOff();
|
||||
} else if (this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
@ -887,6 +891,9 @@ export default {
|
||||
onTypingOn() {
|
||||
this.toggleTyping('on');
|
||||
},
|
||||
onRecording() {
|
||||
this.toggleTyping('recording');
|
||||
},
|
||||
onTypingOff() {
|
||||
this.toggleTyping('off');
|
||||
},
|
||||
|
||||
@ -144,7 +144,11 @@ export default {
|
||||
this.resizeTextarea();
|
||||
},
|
||||
onKeyup() {
|
||||
if (this.modelValue.length) {
|
||||
this.typingIndicator.start();
|
||||
} else {
|
||||
this.typingIndicator.stop();
|
||||
}
|
||||
},
|
||||
onBlur() {
|
||||
this.typingIndicator.stop();
|
||||
|
||||
25
app/listeners/channel_listener.rb
Normal file
25
app/listeners/channel_listener.rb
Normal file
@ -0,0 +1,25 @@
|
||||
class ChannelListener < BaseListener
|
||||
def conversation_typing_on(event)
|
||||
handle_typing_event(event)
|
||||
end
|
||||
|
||||
def conversation_recording(event)
|
||||
handle_typing_event(event)
|
||||
end
|
||||
|
||||
def conversation_typing_off(event)
|
||||
handle_typing_event(event)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_typing_event(event)
|
||||
is_private, conversation = event.data.values_at(:is_private, :conversation)
|
||||
return if is_private
|
||||
|
||||
channel = conversation.inbox.channel
|
||||
return unless channel.respond_to?(:toggle_typing_status)
|
||||
|
||||
channel.toggle_typing_status(event.name, conversation: conversation)
|
||||
end
|
||||
end
|
||||
@ -79,6 +79,12 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
data
|
||||
end
|
||||
|
||||
def toggle_typing_status(typing_status, conversation:)
|
||||
return unless provider_service.respond_to?(:toggle_typing_status)
|
||||
|
||||
provider_service.toggle_typing_status(conversation.contact.phone_number, typing_status)
|
||||
end
|
||||
|
||||
delegate :setup_channel_provider, to: :provider_service
|
||||
delegate :disconnect_channel_provider, to: :provider_service
|
||||
delegate :send_message, to: :provider_service
|
||||
|
||||
@ -18,6 +18,8 @@ class Conversations::TypingStatusManager
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
||||
when 'recording'
|
||||
trigger_typing_event(CONVERSATION_RECORDING, params[:is_private])
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
||||
end
|
||||
|
||||
@ -66,6 +66,26 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def toggle_typing_status(phone_number, typing_status)
|
||||
@phone_number = phone_number
|
||||
status_map = {
|
||||
Events::Types::CONVERSATION_TYPING_ON => 'composing',
|
||||
Events::Types::CONVERSATION_RECORDING => 'recording',
|
||||
Events::Types::CONVERSATION_TYPING_OFF => 'paused'
|
||||
}
|
||||
|
||||
response = HTTParty.patch(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/presence",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
toJid: remote_jid,
|
||||
type: status_map[typing_status]
|
||||
}.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_url
|
||||
|
||||
@ -27,6 +27,7 @@ module Events::Types
|
||||
ASSIGNEE_CHANGED = 'assignee.changed'
|
||||
TEAM_CHANGED = 'team.changed'
|
||||
CONVERSATION_TYPING_ON = 'conversation.typing_on'
|
||||
CONVERSATION_RECORDING = 'conversation.recording'
|
||||
CONVERSATION_TYPING_OFF = 'conversation.typing_off'
|
||||
CONVERSATION_MENTIONED = 'conversation.mentioned'
|
||||
|
||||
|
||||
66
spec/listeners/channel_listener_spec.rb
Normal file
66
spec/listeners/channel_listener_spec.rb
Normal file
@ -0,0 +1,66 @@
|
||||
require 'rails_helper'
|
||||
describe ChannelListener do
|
||||
let(:listener) { described_class.instance }
|
||||
|
||||
context 'when handling typing events' do
|
||||
let(:channel) { create(:channel_whatsapp, sync_templates: false, validate_provider_config: false) }
|
||||
let(:conversation) { create(:conversation, inbox: create(:inbox, channel: channel)) }
|
||||
|
||||
it 'skips the event if is_private is true' do
|
||||
allow(channel).to receive(:toggle_typing_status)
|
||||
|
||||
listener.conversation_typing_on(build_typing_event(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation, is_private: true))
|
||||
|
||||
expect(channel).not_to have_received(:toggle_typing_status)
|
||||
end
|
||||
|
||||
it 'skips the event if channel does not respond to toggle_typing_status' do
|
||||
channel = create(:channel_api)
|
||||
conversation = create(:conversation, inbox: create(:inbox, channel: channel))
|
||||
|
||||
expect do
|
||||
listener.conversation_typing_on(build_typing_event(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation))
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
describe '#conversation_typing_on' do
|
||||
let(:event_name) { Events::Types::CONVERSATION_TYPING_ON }
|
||||
|
||||
it 'calls toggle_typing_status on the channel' do
|
||||
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||
|
||||
listener.conversation_typing_on(build_typing_event(event_name, conversation: conversation))
|
||||
|
||||
expect(channel).to have_received(:toggle_typing_status)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_recording' do
|
||||
let(:event_name) { Events::Types::CONVERSATION_RECORDING }
|
||||
|
||||
it 'calls toggle_typing_status on the channel' do
|
||||
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||
|
||||
listener.conversation_recording(build_typing_event(event_name, conversation: conversation))
|
||||
|
||||
expect(channel).to have_received(:toggle_typing_status)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_typing_off' do
|
||||
let(:event_name) { Events::Types::CONVERSATION_TYPING_OFF }
|
||||
|
||||
it 'calls toggle_typing_status on the channel' do
|
||||
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||
|
||||
listener.conversation_typing_off(build_typing_event(event_name, conversation: conversation))
|
||||
|
||||
expect(channel).to have_received(:toggle_typing_status)
|
||||
end
|
||||
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
|
||||
end
|
||||
@ -62,6 +62,35 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#toggle_typing_status' do
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
it 'calls provider service method' do
|
||||
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
|
||||
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
|
||||
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.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
describe '#disconnect_channel_provider' do
|
||||
context 'when provider is baileys' do
|
||||
|
||||
@ -363,6 +363,80 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#toggle_typing_status' do
|
||||
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }
|
||||
|
||||
it 'calls presence endpoint for typing on' do
|
||||
request = stub_request(:patch, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
toJid: test_send_jid,
|
||||
type: 'composing'
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200)
|
||||
|
||||
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
|
||||
it 'calls presence endpoint for recording' do
|
||||
request = stub_request(:patch, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
toJid: test_send_jid,
|
||||
type: 'recording'
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200)
|
||||
|
||||
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_RECORDING)
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
|
||||
it 'calls presence endpoint for typing off' do
|
||||
request = stub_request(:patch, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
toJid: test_send_jid,
|
||||
type: 'paused'
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200)
|
||||
|
||||
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_OFF)
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
|
||||
it 'logs the error and returns false' do
|
||||
stub_request(:patch, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
toJid: test_send_jid,
|
||||
type: 'composing'
|
||||
}.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: 'error message',
|
||||
headers: {}
|
||||
)
|
||||
allow(Rails.logger).to receive(:error).with('error message')
|
||||
|
||||
response = service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
|
||||
|
||||
expect(response).to be(false)
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
|
||||
it 'uses the base url from the environment variable' do
|
||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
|
||||
|
||||
@ -8,7 +8,11 @@ parameters:
|
||||
in: query
|
||||
required: true
|
||||
type: string
|
||||
description: Typing status, either 'on' or 'off'
|
||||
enum:
|
||||
- 'on'
|
||||
- 'recording'
|
||||
- 'off'
|
||||
description: Typing status.
|
||||
responses:
|
||||
200:
|
||||
description: Typing status toggled successfully
|
||||
|
||||
@ -989,7 +989,12 @@
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"description": "Typing status, either 'on' or 'off'"
|
||||
"enum": [
|
||||
"on",
|
||||
"recording",
|
||||
"off"
|
||||
],
|
||||
"description": "Typing status."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user