fix: Send raw content in webhook payloads instead of channel-rendered markdown (#13896)
Webhook payloads (`message_created`, `message_updated`) started sending channel-rendered HTML in the `content` field instead of the original raw message content after PR #12878. This broke downstream agent bots and integrations that expected plain text or markdown. Closes https://linear.app/chatwoot/issue/PLA-109/webhook-payloads-send-channel-rendered-html-instead-of-raw-content ## How to reproduce 1. Connect an agent bot to a WebWidget inbox 2. Send a message with markdown formatting (e.g. `**bold**`) from the widget 3. Observe the agent bot webhook payload — `content` field contains `<p><strong>bold</strong></p>` instead of `**bold**` ## What changed Split `MessageContentPresenter` into two public methods: - `outgoing_content` — renders markdown for the target channel (used by channel delivery services) - `webhook_content` — returns raw content with CSAT survey URL when applicable, no markdown rendering (used by `webhook_data`) Updated `Message#webhook_data` to use `webhook_content` instead of `outgoing_content`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -176,7 +176,7 @@ class Message < ApplicationRecord
|
||||
additional_attributes: additional_attributes,
|
||||
content_attributes: content_attributes,
|
||||
content_type: content_type,
|
||||
content: outgoing_content,
|
||||
content: webhook_content,
|
||||
conversation: conversation.webhook_data,
|
||||
created_at: created_at,
|
||||
id: id,
|
||||
@@ -195,6 +195,11 @@ class Message < ApplicationRecord
|
||||
MessageContentPresenter.new(self).outgoing_content
|
||||
end
|
||||
|
||||
# Raw content with survey URL (no markdown rendering) for webhook consumers
|
||||
def webhook_content
|
||||
MessageContentPresenter.new(self).webhook_content
|
||||
end
|
||||
|
||||
def email_notifiable_message?
|
||||
return false if private?
|
||||
return false if %w[outgoing template].exclude?(message_type)
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
class MessageContentPresenter < SimpleDelegator
|
||||
def outgoing_content
|
||||
content_to_send = if should_append_survey_link?
|
||||
survey_link = survey_url(conversation.uuid)
|
||||
custom_message = inbox.csat_config&.dig('message')
|
||||
custom_message.present? ? "#{custom_message} #{survey_link}" : I18n.t('conversations.survey.response', link: survey_link)
|
||||
else
|
||||
content
|
||||
end
|
||||
|
||||
Messages::MarkdownRendererService.new(
|
||||
content_to_send,
|
||||
content_with_survey_link,
|
||||
conversation.inbox.channel_type,
|
||||
conversation.inbox.channel
|
||||
).render
|
||||
end
|
||||
|
||||
def webhook_content
|
||||
content_with_survey_link
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def content_with_survey_link
|
||||
if should_append_survey_link?
|
||||
survey_link = survey_url(conversation.uuid)
|
||||
custom_message = inbox.csat_config&.dig('message')
|
||||
custom_message.present? ? "#{custom_message} #{survey_link}" : I18n.t('conversations.survey.response', link: survey_link)
|
||||
else
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def should_append_survey_link?
|
||||
input_csat? && !inbox.web_widget?
|
||||
end
|
||||
|
||||
@@ -401,12 +401,11 @@ RSpec.describe Message do
|
||||
expect(message.webhook_data.key?(:attachments)).to be false
|
||||
end
|
||||
|
||||
it 'uses outgoing_content for webhook content' do
|
||||
message = create(:message, content: 'Test content')
|
||||
expect(message).to receive(:outgoing_content).and_return('Outgoing test content')
|
||||
it 'uses raw content without markdown rendering for webhook content' do
|
||||
message = create(:message, content: 'Test **bold** content')
|
||||
|
||||
webhook_data = message.webhook_data
|
||||
expect(webhook_data[:content]).to eq('Outgoing test content')
|
||||
expect(webhook_data[:content]).to eq('Test **bold** content')
|
||||
end
|
||||
|
||||
it 'includes CSAT survey link in webhook content for input_csat messages' do
|
||||
@@ -414,7 +413,6 @@ RSpec.describe Message do
|
||||
conversation = create(:conversation, inbox: inbox)
|
||||
message = create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience')
|
||||
|
||||
expect(message.outgoing_content).to include('survey/responses/')
|
||||
expect(message.webhook_data[:content]).to include('survey/responses/')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,6 +55,34 @@ RSpec.describe MessageContentPresenter do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#webhook_content' do
|
||||
context 'when message is not input_csat' do
|
||||
let(:content_type) { 'text' }
|
||||
let(:content) { 'Regular **bold** message' }
|
||||
|
||||
it 'returns raw content without markdown rendering' do
|
||||
expect(presenter.webhook_content).to eq('Regular **bold** message')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is input_csat and inbox is not web widget' do
|
||||
let(:content_type) { 'input_csat' }
|
||||
let(:content) { 'Rate your experience' }
|
||||
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(false)
|
||||
end
|
||||
|
||||
it 'includes CSAT survey URL without markdown rendering' do
|
||||
with_modified_env 'FRONTEND_URL' => 'https://app.chatwoot.com' do
|
||||
expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
|
||||
expect(presenter.webhook_content).to include(expected_url)
|
||||
expect(presenter.webhook_content).not_to include('<p>')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'delegation' do
|
||||
let(:content_type) { 'text' }
|
||||
let(:content) { 'Test message' }
|
||||
|
||||
Reference in New Issue
Block a user