discourse/app/controllers/admin/web_hooks_controller.rb
Natalie Tay 69af7d62ae
FIX: Allow selecting tags when creating or editing webhooks (#37942)
Commit 9e99066b07 changed TagChooser to pass tag objects instead of tag
name strings, but the webhook model sends these objects directly to the
backend, where `permit(tag_names: [])` silently stripped the non-scalar
values, so no tags were saved.

We'll still be taking in `tag_names` on the API, with a deprecation
note. The frontend should start sending `tag_ids` which is the new way
forward.

<img width="351" height="156" alt="Screenshot 2026-02-20 at 4 10 58 PM"
src="https://github.com/user-attachments/assets/9afb6fbe-d676-43d4-a0ff-29cbc1bcd377"
/>

meta:
https://meta.discourse.org/t/webhook-not-accepting-private-tags/396521
2026-02-20 23:00:50 +08:00

214 lines
5.8 KiB
Ruby

# frozen_string_literal: true
class Admin::WebHooksController < Admin::AdminController
before_action :fetch_web_hook,
only: %i[
show
update
destroy
list_events
bulk_events
ping
redeliver_event
redeliver_failed_events
]
def index
limit = 50
offset = params[:offset].to_i
web_hooks =
WebHook
.limit(limit)
.offset(offset)
.includes(:web_hook_event_types)
.includes(:categories)
.includes(:groups)
data = serialize_data(web_hooks, AdminWebHookSerializer, root: "web_hooks")
serialized_grouped_event_types =
WebHookEventType.active_grouped.transform_values do |array|
serialize_data(array, WebHookEventTypeSerializer)
end
json = {
web_hooks: data.delete("web_hooks"),
extras:
data.merge(
grouped_event_types: serialized_grouped_event_types,
default_event_types:
serialize_data(WebHook.default_event_types, WebHookEventTypeSerializer),
content_types: WebHook.content_types.map { |name, id| { id: id, name: name } },
delivery_statuses:
WebHook.last_delivery_statuses.map { |name, id| { id: id, name: name.to_s } },
),
total_rows_web_hooks: WebHook.count,
load_more_web_hooks:
admin_web_hooks_path(limit: limit, offset: offset + limit, format: :json),
}
render json: MultiJson.dump(json), status: :ok
end
def show
data = serialize_data(@web_hook, AdminWebHookSerializer, root: "web_hook")
web_hook = data.delete("web_hook")
data = { "extras" => data, "web_hook" => web_hook }
render json: MultiJson.dump(data), status: :ok
end
def edit
data = serialize_data(@web_hook, AdminWebHookSerializer, root: "web_hook")
data["extras"] = { "categories" => data.delete(:categories) }
render json: MultiJson.dump(data), status: :ok
end
def create
web_hook = WebHook.new(web_hook_params)
if web_hook.save
StaffActionLogger.new(current_user).log_web_hook(
web_hook,
UserHistory.actions[:web_hook_create],
)
render_serialized(web_hook, AdminWebHookSerializer, root: "web_hook")
else
render_json_error web_hook.errors.full_messages
end
end
def update
if @web_hook.update(web_hook_params)
StaffActionLogger.new(current_user).log_web_hook(
@web_hook,
UserHistory.actions[:web_hook_update],
changes: @web_hook.saved_changes,
)
render_serialized(@web_hook, AdminWebHookSerializer, root: "web_hook")
else
render_json_error @web_hook.errors.full_messages
end
end
def destroy
@web_hook.destroy!
StaffActionLogger.new(current_user).log_web_hook(
@web_hook,
UserHistory.actions[:web_hook_destroy],
)
render json: success_json
end
def list_events
limit = 50
offset = params[:offset].to_i
events = @web_hook.web_hook_events.includes(:redelivering_webhook_event)
status = params[:status]
if status == "successful"
events = events.successful
elsif status == "failed"
events = events.failed
end
total = events.count
events = events.limit(limit).offset(offset)
json = {
web_hook_events: serialize_data(events, AdminWebHookEventSerializer),
total_rows_web_hook_events: total,
load_more_web_hook_events:
web_hook_events_admin_api_index_path(
limit: limit,
offset: offset + limit,
status: status,
format: :json,
),
extras: {
web_hook_id: @web_hook.id,
},
}
render json: MultiJson.dump(json), status: :ok
end
def bulk_events
params.require(:ids)
web_hook_events = @web_hook.web_hook_events.where(id: params[:ids])
render_serialized(web_hook_events, AdminWebHookEventSerializer)
end
def redeliver_event
web_hook_event = @web_hook.web_hook_events.find_by(id: params[:event_id])
return render_json_error(I18n.t("not_found"), status: 404) if web_hook_event.blank?
emitter = WebHookEmitter.new(@web_hook, web_hook_event)
emitter.emit!(headers: MultiJson.load(web_hook_event.headers), body: web_hook_event.payload)
render_serialized(web_hook_event, AdminWebHookEventSerializer, root: "web_hook_event")
end
def redeliver_failed_events
web_hook_events =
@web_hook
.web_hook_events
.includes(:redelivering_webhook_event)
.not_ping
.where(id: params[:event_ids])
raise Discourse::InvalidParameters if web_hook_events.count.zero?
web_hook_events.each do |web_hook_event|
if !web_hook_event.redelivering_webhook_event
RedeliveringWebhookEvent.create!(web_hook_event: web_hook_event)
end
end
render json: { event_ids: web_hook_events.map(&:id) }
end
def ping
Jobs.enqueue(
:emit_web_hook_event,
web_hook_id: @web_hook.id,
event_type: "ping",
event_name: "ping",
)
render json: success_json
end
private
def web_hook_params
permitted =
params.require(:web_hook).permit(
:payload_url,
:content_type,
:secret,
:wildcard_web_hook,
:active,
:verify_certificate,
web_hook_event_type_ids: [],
group_ids: [],
tag_names: [],
tag_ids: [],
category_ids: [],
)
if permitted[:tag_names].present?
Discourse.deprecate(
"The `tag_names` param for webhooks is deprecated, use `tag_ids` instead.",
since: "3.5.0.beta1",
drop_from: "3.6.0.beta1",
)
permitted.delete(:tag_ids)
else
permitted.delete(:tag_names)
end
permitted
end
def fetch_web_hook
@web_hook = WebHook.find(params[:id])
end
end