feat: Add support for typing events in webhooks (#11423)
Added support for typing events in webhooks. Two new events are now available: `conversation_typing_on` and `conversation_typing_off`. <img width="746" alt="Screenshot 2025-05-08 at 4 50 24 PM" src="https://github.com/user-attachments/assets/62da7b38-de0f-42c5-84f4-066e653da331" /> --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -41,7 +41,9 @@
|
||||
"MESSAGE_UPDATED": "Message updated",
|
||||
"WEBWIDGET_TRIGGERED": "Live chat widget opened by the user",
|
||||
"CONTACT_CREATED": "Contact created",
|
||||
"CONTACT_UPDATED": "Contact updated"
|
||||
"CONTACT_UPDATED": "Contact updated",
|
||||
"CONVERSATION_TYPING_ON": "Conversation Typing On",
|
||||
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
|
||||
}
|
||||
},
|
||||
"END_POINT": {
|
||||
|
||||
@@ -16,6 +16,8 @@ const SUPPORTED_WEBHOOK_EVENTS = [
|
||||
'webwidget_triggered',
|
||||
'contact_created',
|
||||
'contact_updated',
|
||||
'conversation_typing_on',
|
||||
'conversation_typing_off',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@@ -83,8 +83,30 @@ class WebhookListener < BaseListener
|
||||
deliver_account_webhooks(payload, account)
|
||||
end
|
||||
|
||||
def conversation_typing_on(event)
|
||||
handle_typing_status(__method__.to_s, event)
|
||||
end
|
||||
|
||||
def conversation_typing_off(event)
|
||||
handle_typing_status(__method__.to_s, event)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_typing_status(event_name, event)
|
||||
conversation = event.data[:conversation]
|
||||
user = event.data[:user]
|
||||
inbox = conversation.inbox
|
||||
|
||||
payload = {
|
||||
event: event_name,
|
||||
user: user.webhook_data,
|
||||
conversation: conversation.webhook_data,
|
||||
is_private: event.data[:is_private] || false
|
||||
}
|
||||
deliver_webhook_payloads(payload, inbox)
|
||||
end
|
||||
|
||||
def deliver_account_webhooks(payload, account)
|
||||
account.webhooks.account_type.each do |webhook|
|
||||
next unless webhook.subscriptions.include?(payload[:event])
|
||||
|
||||
@@ -26,7 +26,8 @@ class Webhook < ApplicationRecord
|
||||
enum webhook_type: { account_type: 0, inbox_type: 1 }
|
||||
|
||||
ALLOWED_WEBHOOK_EVENTS = %w[conversation_status_changed conversation_updated conversation_created contact_created contact_updated
|
||||
message_created message_updated webwidget_triggered inbox_created inbox_updated].freeze
|
||||
message_created message_updated webwidget_triggered inbox_created inbox_updated
|
||||
conversation_typing_on conversation_typing_off].freeze
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@ class Rack::Attack
|
||||
#
|
||||
# Example: RACK_ATTACK_ALLOWED_IPS="127.0.0.1,::1,192.168.0.10"
|
||||
|
||||
Rack::Attack.safelist('trusted IPs') do |req|
|
||||
req.allowed_ip?
|
||||
end
|
||||
Rack::Attack.safelist('trusted IPs', &:allowed_ip?)
|
||||
|
||||
### Throttle Spammy Clients ###
|
||||
|
||||
|
||||
@@ -279,4 +279,79 @@ describe WebhookListener do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_typing_on' do
|
||||
let(:event_name) { :'conversation.typing_on' }
|
||||
let!(:typing_event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: user) }
|
||||
|
||||
context 'when webhook is not configured' do
|
||||
it 'does not trigger webhook' do
|
||||
expect(WebhookJob).not_to receive(:perform_later)
|
||||
listener.conversation_typing_on(typing_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook is configured' do
|
||||
it 'triggers webhook' do
|
||||
webhook = create(:webhook, inbox: inbox, account: account, subscriptions: ['conversation_typing_on'])
|
||||
|
||||
payload = {
|
||||
event: 'conversation_typing_on',
|
||||
user: user.webhook_data,
|
||||
conversation: conversation.webhook_data,
|
||||
is_private: false
|
||||
}
|
||||
|
||||
expect(WebhookJob).to receive(:perform_later).with(webhook.url, payload).once
|
||||
listener.conversation_typing_on(typing_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox is an API Channel' do
|
||||
it 'triggers webhook if webhook_url is present' do
|
||||
channel_api = create(:channel_api, account: account)
|
||||
api_inbox = channel_api.inbox
|
||||
api_conversation = create(:conversation, account: account, inbox: api_inbox, assignee: user)
|
||||
api_event = Events::Base.new(event_name, Time.zone.now, conversation: api_conversation, user: user, is_private: false)
|
||||
|
||||
payload = {
|
||||
event: 'conversation_typing_on',
|
||||
user: user.webhook_data,
|
||||
conversation: api_conversation.webhook_data,
|
||||
is_private: false
|
||||
}
|
||||
|
||||
expect(WebhookJob).to receive(:perform_later).with(channel_api.webhook_url, payload, :api_inbox_webhook).once
|
||||
listener.conversation_typing_on(api_event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_typing_off' do
|
||||
let(:event_name) { :'conversation.typing_off' }
|
||||
let!(:typing_event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: user, is_private: false) }
|
||||
|
||||
context 'when webhook is not configured' do
|
||||
it 'does not trigger webhook' do
|
||||
expect(WebhookJob).not_to receive(:perform_later)
|
||||
listener.conversation_typing_off(typing_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook is configured' do
|
||||
it 'triggers webhook' do
|
||||
webhook = create(:webhook, inbox: inbox, account: account, subscriptions: ['conversation_typing_off'])
|
||||
|
||||
payload = {
|
||||
event: 'conversation_typing_off',
|
||||
user: user.webhook_data,
|
||||
conversation: conversation.webhook_data,
|
||||
is_private: false
|
||||
}
|
||||
|
||||
expect(WebhookJob).to receive(:perform_later).with(webhook.url, payload).once
|
||||
listener.conversation_typing_off(typing_event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user