From 48533e2a5dffe70c8802eeef85d9fff968534dcd Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:19:35 +0530 Subject: [PATCH] fix: strip markdown hard-break backslashes from webhook payloads (#13950) --- app/models/concerns/push_data_helper.rb | 2 +- app/models/message.rb | 7 ++++++ .../conversations/event_data_presenter.rb | 9 +++++++ app/presenters/message_content_presenter.rb | 2 +- .../messages/webhook_content_normalizer.rb | 10 ++++++++ .../event_data_presenter_spec.rb | 25 +++++++++++++++++++ .../message_content_presenter_spec.rb | 20 +++++++++++++++ 7 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 app/services/messages/webhook_content_normalizer.rb diff --git a/app/models/concerns/push_data_helper.rb b/app/models/concerns/push_data_helper.rb index 47a1e4e84..3bd35f1fe 100644 --- a/app/models/concerns/push_data_helper.rb +++ b/app/models/concerns/push_data_helper.rb @@ -10,6 +10,6 @@ module PushDataHelper end def webhook_data - Conversations::EventDataPresenter.new(self).push_data + Conversations::EventDataPresenter.new(self).webhook_data end end diff --git a/app/models/message.rb b/app/models/message.rb index ccbb250c3..f25d2e112 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -170,6 +170,13 @@ class Message < ApplicationRecord data end + def webhook_push_event_data + push_event_data.merge( + content: Messages::WebhookContentNormalizer.normalize(content), + processed_message_content: Messages::WebhookContentNormalizer.normalize(processed_message_content) + ) + end + def webhook_data data = { account: account.webhook_data, diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index 0c04455a1..ae0e69608 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -21,12 +21,21 @@ class Conversations::EventDataPresenter < SimpleDelegator } end + # Like #push_data but with message text normalized for external integrations (webhooks). + def webhook_data + push_data.merge(messages: webhook_push_messages) + end + private def push_messages [messages.where(account_id: account_id).chat.last&.push_event_data].compact end + def webhook_push_messages + [messages.where(account_id: account_id).chat.last&.webhook_push_event_data].compact + end + def push_meta { sender: contact.push_event_data, diff --git a/app/presenters/message_content_presenter.rb b/app/presenters/message_content_presenter.rb index 3832e6a10..75ed34854 100644 --- a/app/presenters/message_content_presenter.rb +++ b/app/presenters/message_content_presenter.rb @@ -8,7 +8,7 @@ class MessageContentPresenter < SimpleDelegator end def webhook_content - content_with_survey_link + Messages::WebhookContentNormalizer.normalize(content_with_survey_link) end private diff --git a/app/services/messages/webhook_content_normalizer.rb b/app/services/messages/webhook_content_normalizer.rb new file mode 100644 index 000000000..b45f83ad0 --- /dev/null +++ b/app/services/messages/webhook_content_normalizer.rb @@ -0,0 +1,10 @@ +# Strips CommonMark hard line breaks from stored markdown source (backslash before newline). +# ProseMirror / the dashboard editor emits this form so soft breaks survive as markdown; +# webhook consumers expect plain newlines without a visible backslash (e.g. WhatsApp gateways). +class Messages::WebhookContentNormalizer + def self.normalize(text) + return text if text.blank? + + text.gsub(/\\\r?\n/, "\n") + end +end diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb index bf0eaf6f6..76fd8f8a8 100644 --- a/spec/presenters/conversations/event_data_presenter_spec.rb +++ b/spec/presenters/conversations/event_data_presenter_spec.rb @@ -44,4 +44,29 @@ RSpec.describe Conversations::EventDataPresenter do expect(presenter.push_data.except(:applied_sla, :sla_events)).to include(expected_data) end end + + describe '#webhook_data' do + it 'normalizes hard-break backslashes in message content' do + message = create(:message, conversation: conversation, account: conversation.account, + message_type: :outgoing, content: "Hello\\\nWorld") + data = presenter.webhook_data + webhook_message = data[:messages].first + + expect(webhook_message).to be_present + expect(webhook_message[:content]).to eq("Hello\nWorld") + expect(webhook_message[:id]).to eq(message.id) + end + + it 'preserves normal newlines in message content' do + create(:message, conversation: conversation, account: conversation.account, + message_type: :outgoing, content: "Line one\n\nLine two") + webhook_message = presenter.webhook_data[:messages].first + + expect(webhook_message[:content]).to eq("Line one\n\nLine two") + end + + it 'returns empty messages when conversation has no chat messages' do + expect(presenter.webhook_data[:messages]).to eq([]) + end + end end diff --git a/spec/presenters/message_content_presenter_spec.rb b/spec/presenters/message_content_presenter_spec.rb index d12e0d4d0..8b999ec80 100644 --- a/spec/presenters/message_content_presenter_spec.rb +++ b/spec/presenters/message_content_presenter_spec.rb @@ -63,6 +63,26 @@ RSpec.describe MessageContentPresenter do it 'returns raw content without markdown rendering' do expect(presenter.webhook_content).to eq('Regular **bold** message') end + + it 'strips CommonMark hard-break backslashes before newlines' do + message.update!(content: "First\\\nSecond\\\nThird\\\nFourth") + expect(presenter.webhook_content).to eq("First\nSecond\nThird\nFourth") + end + + it 'preserves backslashes that are not followed by a newline' do + message.update!(content: "path\\to\\file\\\nNext line") + expect(presenter.webhook_content).to eq("path\\to\\file\nNext line") + end + + it 'handles carriage return and newline pairs' do + message.update!(content: "Line one\\\r\nLine two\\\r\nLine three") + expect(presenter.webhook_content).to eq("Line one\nLine two\nLine three") + end + + it 'preserves normal newlines and only strips hard-break newlines' do + message.update!(content: "line one\\\nline two\n\nline three") + expect(presenter.webhook_content).to eq("line one\nline two\n\nline three") + end end context 'when message is input_csat and inbox is not web widget' do