mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
When a customer responds to a bot's interactive prompt (input_select, input_csat, form, input_email) from the widget, the response shows up in the Chatwoot agent UI but is not reflected in the linked Slack channel — Slack only ever shows the original question. This happens because the widget submits the answer as an UPDATE to the original message (writing `content_attributes.submitted_values` or `submitted_email`), but the Slack hook only listened to `message.created`, so updates were ignored. Closes https://linear.app/chatwoot/issue/PLA-147 ### Preview <img width="1290" height="1106" alt="CleanShot 2026-04-21 at 13 19 19@2x" src="https://github.com/user-attachments/assets/cd2a9d3f-89d3-4e81-9230-5b078e1b7b44" /> ### How to test 1. Connect a web widget inbox to a Slack channel. 2. Trigger each bot message type (input_select, form, input_csat, input_email) in a conversation. 3. Submit responses from the widget. 4. Verify each response now appears in the Slack thread, appended to the original bot question. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
153 lines
4.2 KiB
Ruby
153 lines
4.2 KiB
Ruby
class Integrations::Slack::UpdateSlackMessageService
|
|
include RegexHelper
|
|
|
|
SUPPORTED_CONTENT_TYPES = %w[input_select form input_csat input_email].freeze
|
|
|
|
pattr_initialize [:message!, :hook!]
|
|
|
|
def perform
|
|
return unless updateable_message?
|
|
|
|
slack_client.chat_update(
|
|
channel: hook.reference_id,
|
|
ts: slack_message_ts,
|
|
text: updated_message_content
|
|
)
|
|
rescue Slack::Web::Api::Errors::MessageNotFound => e
|
|
# Original Slack message no longer exists (e.g. channel was reconfigured), skip gracefully.
|
|
Rails.logger.error "[Slack] chat_update failed (account=#{message.account_id}, hook=#{hook.id}): #{e.message}"
|
|
rescue Slack::Web::Api::Errors::IsArchived, Slack::Web::Api::Errors::AccountInactive, Slack::Web::Api::Errors::MissingScope,
|
|
Slack::Web::Api::Errors::InvalidAuth,
|
|
Slack::Web::Api::Errors::ChannelNotFound, Slack::Web::Api::Errors::NotInChannel => e
|
|
Rails.logger.error "[Slack] chat_update failed (account=#{message.account_id}, hook=#{hook.id}): #{e.message}"
|
|
hook.prompt_reauthorization!
|
|
hook.disable
|
|
end
|
|
|
|
private
|
|
|
|
def updateable_message?
|
|
hook&.reference_id.present? &&
|
|
slack_message_ts.present? &&
|
|
message.content_type.in?(SUPPORTED_CONTENT_TYPES) &&
|
|
(message.submitted_values.present? || message.submitted_email.present?)
|
|
end
|
|
|
|
def slack_message_ts
|
|
source_id = message.external_source_id_slack.to_s
|
|
return unless source_id.start_with?('cw-origin-')
|
|
|
|
source_id.delete_prefix('cw-origin-').presence
|
|
end
|
|
|
|
def updated_message_content
|
|
question = sanitized_content(message_text).presence
|
|
response = formatted_response
|
|
|
|
return question.to_s if response.blank?
|
|
|
|
[question, response].compact.join("\n\n")
|
|
end
|
|
|
|
def formatted_response
|
|
case message.content_type
|
|
when 'input_select'
|
|
format_input_select_response
|
|
when 'form'
|
|
format_form_response
|
|
when 'input_csat'
|
|
format_csat_response
|
|
when 'input_email'
|
|
format_email_response
|
|
end
|
|
end
|
|
|
|
def format_input_select_response
|
|
item = Array(message.submitted_values).first
|
|
return if item.blank?
|
|
|
|
value = item['title'] || item[:title] || item['value'] || item[:value]
|
|
value = sanitized_content(value)
|
|
return if value.blank?
|
|
|
|
"*Response:* #{value}"
|
|
end
|
|
|
|
def format_email_response
|
|
email = sanitized_content(message.submitted_email)
|
|
return if email.blank?
|
|
|
|
"*Email:* #{email}"
|
|
end
|
|
|
|
def format_form_response
|
|
submitted_values = Array(message.submitted_values)
|
|
return if submitted_values.blank?
|
|
|
|
items_by_name = Array(message.items).index_by { |i| flex_value(i, 'name') }
|
|
|
|
lines = submitted_values.filter_map do |sv|
|
|
format_form_line(sv, items_by_name)
|
|
end
|
|
|
|
return if lines.blank?
|
|
|
|
"*Responses:*\n#{lines.join("\n")}"
|
|
end
|
|
|
|
def format_csat_response
|
|
csat_response = flex_value(message.submitted_values, 'csat_survey_response', 'csatSurveyResponse')
|
|
return if csat_response.blank?
|
|
|
|
rating = flex_value(csat_response, 'rating')
|
|
feedback = flex_value(csat_response, 'feedback_message', 'feedbackMessage')
|
|
|
|
lines = []
|
|
lines << "• Rating: #{rating}" if rating.present?
|
|
lines << "• Feedback: #{sanitized_content(feedback)}" if feedback.present?
|
|
|
|
return if lines.blank?
|
|
|
|
"*CSAT:*\n#{lines.join("\n")}"
|
|
end
|
|
|
|
def format_form_line(submitted_value, items_by_name)
|
|
name = flex_value(submitted_value, 'name')
|
|
value = sanitized_content(flex_value(submitted_value, 'value'))
|
|
return if value.blank?
|
|
|
|
label = sanitized_content(flex_value(items_by_name[name], 'label') || name)
|
|
return if label.blank?
|
|
|
|
"• #{label}: #{value}"
|
|
end
|
|
|
|
def flex_value(hash, *keys)
|
|
return if hash.blank?
|
|
|
|
keys.each do |key|
|
|
value = hash[key.to_sym] || hash[key.to_s]
|
|
return value if value.present?
|
|
end
|
|
nil
|
|
end
|
|
|
|
def message_text
|
|
content = message.processed_message_content || message.content
|
|
|
|
if content.present?
|
|
content.to_s.gsub(MENTION_REGEX, '\1')
|
|
else
|
|
content
|
|
end
|
|
end
|
|
|
|
def sanitized_content(text)
|
|
ActionView::Base.full_sanitizer.sanitize(text.to_s).strip
|
|
end
|
|
|
|
def slack_client
|
|
@slack_client ||= Slack::Web::Client.new(token: hook.access_token)
|
|
end
|
|
end
|