mirror of
https://github.com/discourse/discourse.git
synced 2025-04-02 06:31:34 +08:00

This commit starts from a simple observation: cooking messages on the hot path can be slow. Especially with a lot of mentions. To move cooking from the hot path, this commit has made the following changes: - updating cooked, inserting mentions and notifying user of new mentions has been moved inside the `process_message` job. It happens right after the `Chat::MessageProcessor` run, which is where the cooking happens. - the similar existing code in `rebake!` has also been moved to rely on the `process_message`job only - refactored `create_mentions` and `update_mentions` into one single `upsert_mentions` which can be called invariably - allows services to decide if their job is ran inline or later. It avoids to need to know you have to use `Jobs.run_immediately!` in this case, in tests it will be inline per default - made various frontend changes to make the chat-channel component lifecycle clearer. we had to handle `did-update @channel` which was super awkward and creating bugs with listeners which the changes of the PR made clear in failing specs - adds a new `-processed` (and `-not-processed`) class on the chat message, this is made to have a good lifecyle hook in system specs
128 lines
4.4 KiB
Ruby
128 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Chat
|
|
class IncomingWebhooksController < ::ApplicationController
|
|
include Chat::WithServiceHelper
|
|
|
|
requires_plugin Chat::PLUGIN_NAME
|
|
|
|
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT = 10
|
|
|
|
skip_before_action :verify_authenticity_token, :redirect_to_login_if_required
|
|
|
|
before_action :validate_payload
|
|
|
|
def create_message
|
|
debug_payload
|
|
|
|
process_webhook_payload(text: params[:text], key: params[:key])
|
|
end
|
|
|
|
# See https://api.slack.com/reference/messaging/payload for the
|
|
# slack message payload format. For now we only support the
|
|
# text param, which we preprocess lightly to remove the slack-isms
|
|
# in the formatting.
|
|
def create_message_slack_compatible
|
|
debug_payload
|
|
|
|
# See note in validate_payload on why this is needed
|
|
attachments =
|
|
if params[:payload].present?
|
|
payload = params[:payload]
|
|
if String === payload
|
|
payload = JSON.parse(payload)
|
|
payload.deep_symbolize_keys!
|
|
end
|
|
payload[:attachments]
|
|
else
|
|
params[:attachments]
|
|
end
|
|
|
|
if params[:text].present?
|
|
text = Chat::SlackCompatibility.process_text(params[:text])
|
|
else
|
|
text = Chat::SlackCompatibility.process_legacy_attachments(attachments)
|
|
end
|
|
|
|
process_webhook_payload(text: text, key: params[:key])
|
|
rescue JSON::ParserError
|
|
raise Discourse::InvalidParameters
|
|
end
|
|
|
|
private
|
|
|
|
def process_webhook_payload(text:, key:)
|
|
webhook = find_and_rate_limit_webhook(key)
|
|
webhook.chat_channel.add(Discourse.system_user)
|
|
|
|
with_service(
|
|
Chat::CreateMessage,
|
|
chat_channel_id: webhook.chat_channel_id,
|
|
guardian: Discourse.system_user.guardian,
|
|
message: text,
|
|
incoming_chat_webhook: webhook,
|
|
) do
|
|
on_success { render json: success_json }
|
|
on_failed_contract do |contract|
|
|
raise Discourse::InvalidParameters.new(contract.errors.full_messages)
|
|
end
|
|
on_failed_policy(:no_silenced_user) { raise Discourse::InvalidAccess }
|
|
on_model_not_found(:channel) { raise Discourse::NotFound }
|
|
on_failed_policy(:allowed_to_join_channel) { raise Discourse::InvalidAccess }
|
|
on_model_not_found(:channel_membership) { raise Discourse::InvalidAccess }
|
|
on_failed_policy(:ensure_reply_consistency) { raise Discourse::NotFound }
|
|
on_failed_policy(:allowed_to_create_message_in_channel) do |policy|
|
|
render_json_error(policy.reason)
|
|
end
|
|
on_failed_policy(:ensure_valid_thread_for_channel) do
|
|
render_json_error(I18n.t("chat.errors.thread_invalid_for_channel"))
|
|
end
|
|
on_failed_policy(:ensure_thread_matches_parent) do
|
|
render_json_error(I18n.t("chat.errors.thread_does_not_match_parent"))
|
|
end
|
|
on_model_errors(:message_instance) do |model|
|
|
render_json_error(model.errors.map(&:full_message).join(", "))
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_and_rate_limit_webhook(key)
|
|
webhook = Chat::IncomingWebhook.includes(:chat_channel).find_by(key: key)
|
|
raise Discourse::NotFound unless webhook
|
|
|
|
# Rate limit to 10 messages per-minute. We can move to a site setting in the future if needed.
|
|
RateLimiter.new(
|
|
nil,
|
|
"incoming_chat_webhook_#{webhook.id}",
|
|
WEBHOOK_MESSAGES_PER_MINUTE_LIMIT,
|
|
1.minute,
|
|
).performed!
|
|
webhook
|
|
end
|
|
|
|
# The webhook POST body can be in 3 different formats:
|
|
#
|
|
# * { text: "message text" }, which is the most basic method, and also mirrors Slack payloads
|
|
# * { attachments: [ text: "message text" ] }, which is a variant of Slack payloads using legacy attachments
|
|
# * { payload: "<JSON STRING>", attachments: null, text: null }, where JSON STRING can look
|
|
# like the `attachments` example above (along with other attributes), which is fired by OpsGenie
|
|
def validate_payload
|
|
params.require(:key)
|
|
|
|
if !params[:text] && !params[:payload] && !params[:attachments]
|
|
raise Discourse::InvalidParameters
|
|
end
|
|
end
|
|
|
|
def debug_payload
|
|
return if !SiteSetting.chat_debug_webhook_payloads
|
|
Rails.logger.warn(
|
|
"Debugging chat webhook payload for endpoint #{params[:key]}: " +
|
|
JSON.dump(
|
|
{ payload: params[:payload], attachments: params[:attachments], text: params[:text] },
|
|
),
|
|
)
|
|
end
|
|
end
|
|
end
|