diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 76dd56e49..9767df3ad 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -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": { diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue index f3c724bb2..430658329 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue @@ -16,6 +16,8 @@ const SUPPORTED_WEBHOOK_EVENTS = [ 'webwidget_triggered', 'contact_created', 'contact_updated', + 'conversation_typing_on', + 'conversation_typing_off', ]; export default { diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 3fdd823b8..82a9fc711 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -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]) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 540dbd9b7..105ffdc91 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -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 diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index f147cbf57..fdbd47008 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -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 ### diff --git a/spec/listeners/webhook_listener_spec.rb b/spec/listeners/webhook_listener_spec.rb index 736db07d0..5062b11bc 100644 --- a/spec/listeners/webhook_listener_spec.rb +++ b/spec/listeners/webhook_listener_spec.rb @@ -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