FEATURE: granular webhooks (#23070)

Before this change, webhooks could be only configured for specific groups like for example, all topic events.

We would like to have more granular control like for example topic_created or topic_destroyed.

Test are failing because plugins changed has to be merged as well:
discourse/discourse-assign#498
discourse/discourse-solved#248
discourse/discourse-topic-voting#159
This commit is contained in:
Krzysztof Kotlarek 2023-10-09 14:35:31 +11:00 committed by GitHub
parent 1d3b2d6bd4
commit c468110929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 705 additions and 198 deletions

View File

@ -1,7 +1,4 @@
<label class="hook-event">
<Input @type="checkbox" @checked={{this.enabled}} name="event-choice" />
{{this.name}}
<p>{{this.details}}</p>
{{this.details}}
</label>

View File

@ -2,12 +2,10 @@ import Component from "@glimmer/component";
import I18n from "I18n";
export default class WebhookEventChooser extends Component {
get name() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.name`);
}
get details() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.details`);
return I18n.t(
`admin.web_hooks.${this.args.group}_event.${this.args.type.name}`
);
}
get eventTypeExists() {

View File

@ -14,7 +14,7 @@ export default class AdminWebHooksEditController extends Controller {
@controller adminWebHooks;
@alias("adminWebHooks.eventTypes") eventTypes;
@alias("adminWebHooks.groupedEventTypes") groupedEventTypes;
@alias("adminWebHooks.defaultEventTypes") defaultEventTypes;
@alias("adminWebHooks.contentTypes") contentTypes;

View File

@ -8,7 +8,7 @@ export default class AdminWebHooksRoute extends Route {
setupController(controller, model) {
controller.setProperties({
model,
eventTypes: model.extras.event_types,
groupedEventTypes: model.extras.grouped_event_types,
defaultEventTypes: model.extras.default_event_types,
contentTypes: model.extras.content_types,
deliveryStatuses: model.extras.delivery_statuses,

View File

@ -43,8 +43,9 @@
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="individual"
@selection={{this.model.webhookType}}
@onChange={{action (mut this.model.wildcard_web_hook) false}}
@value={{false}}
@selection={{this.model.wildcard_web_hook}}
/>
{{i18n "admin.web_hooks.individual_event"}}
<InputTip @validation={{this.eventTypeValidation}} />
@ -52,20 +53,27 @@
{{#unless this.model.wildcard_web_hook}}
<div class="event-selector">
{{#each this.eventTypes as |type|}}
<WebhookEventChooser
@type={{type}}
@eventTypes={{this.model.web_hook_event_types}}
/>
{{/each}}
{{#each-in this.groupedEventTypes as |group eventTypes|}}
<div class="event-group">
{{i18n (concat "admin.web_hooks." group "_event.group_name")}}
{{#each eventTypes as |type|}}
<WebhookEventChooser
@type={{type}}
@group={{group}}
@eventTypes={{this.model.web_hook_event_types}}
/>
{{/each}}
</div>
{{/each-in}}
</div>
{{/unless}}
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="wildcard"
@selection={{this.model.webhookType}}
@onChange={{action (mut this.model.wildcard_web_hook) true}}
@value={{true}}
@selection={{this.model.wildcard_web_hook}}
/>
{{i18n "admin.web_hooks.wildcard_event"}}
</label>

View File

@ -232,9 +232,15 @@ table.api-keys {
}
.event-selector {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: auto auto;
margin: 0.5em 0;
margin-left: 1.5em;
.event-group {
display: inline-block;
margin-bottom: 1em;
}
.hook-event {
margin-bottom: 0.5em;
@ -344,8 +350,7 @@ table.api-keys {
.hook-event {
display: inline-block;
width: 40%;
margin-left: 20px;
width: 100%;
label {
display: inline-block;
}

View File

@ -18,7 +18,7 @@ class Admin::WebHooksController < Admin::AdminController
json = {
web_hooks: serialize_data(web_hooks, AdminWebHookSerializer),
extras: {
event_types: WebHookEventType.active,
grouped_event_types: WebHookEventType.active_grouped,
default_event_types: WebHook.default_event_types,
content_types: WebHook.content_types.map { |name, id| { id: id, name: name } },
delivery_statuses:

View File

@ -8,7 +8,7 @@ module HasDestroyedWebHook
def enqueue_destroyed_web_hook
type = self.class.name.underscore.to_sym
if WebHook.active_web_hooks(type).exists?
if WebHook.active_web_hooks("#{type}_destroyed").exists?
payload = WebHook.generate_payload(type, self)
yield
WebHook.enqueue_hooks(type, "#{type}_destroyed".to_sym, id: id, payload: payload)

View File

@ -32,23 +32,49 @@ class WebHook < ActiveRecord::Base
end
def self.default_event_types
[WebHookEventType.find(WebHookEventType::POST)]
WebHookEventType.where(
id: [
WebHookEventType::TYPES[:post_created],
WebHookEventType::TYPES[:post_edited],
WebHookEventType::TYPES[:post_destroyed],
WebHookEventType::TYPES[:post_recovered],
],
)
end
def strip_url
self.payload_url = (payload_url || "").strip.presence
end
def self.active_web_hooks(type)
EVENT_NAME_TO_EVENT_TYPE_MAP = {
/\Atopic_\w+_status_updated\z/ => "topic_edited",
"reviewable_score_updated" => "reviewable_updated",
"reviewable_transitioned_to" => "reviewable_updated",
}
def self.translate_event_name_to_type(event_name)
EVENT_NAME_TO_EVENT_TYPE_MAP.each do |key, value|
if key.is_a?(Regexp)
return value if event_name.to_s =~ key
else
return value if event_name.to_s == key
end
end
event_name.to_s
end
def self.active_web_hooks(event)
event_type = translate_event_name_to_type(event)
WebHook
.where(active: true)
.joins(:web_hook_event_types)
.where("web_hooks.wildcard_web_hook = ? OR web_hook_event_types.name = ?", true, type.to_s)
.where("web_hooks.wildcard_web_hook = ? OR web_hook_event_types.name = ?", true, event_type)
.distinct
end
def self.enqueue_hooks(type, event, opts = {})
active_web_hooks(type).each do |web_hook|
active_web_hooks(event).each do |web_hook|
Jobs.enqueue(
:emit_web_hook_event,
opts.merge(web_hook_id: web_hook.id, event_name: event.to_s, event_type: type.to_s),
@ -57,7 +83,7 @@ class WebHook < ActiveRecord::Base
end
def self.enqueue_object_hooks(type, object, event, serializer = nil, opts = {})
if active_web_hooks(type).exists?
if active_web_hooks(event).exists?
payload = WebHook.generate_payload(type, object, serializer)
WebHook.enqueue_hooks(type, event, opts.merge(id: object.id, payload: payload))
@ -65,7 +91,7 @@ class WebHook < ActiveRecord::Base
end
def self.enqueue_topic_hooks(event, topic, payload = nil)
if active_web_hooks("topic").exists? && topic.present?
if active_web_hooks(event).exists? && topic.present?
payload ||=
begin
topic_view = TopicView.new(topic.id, Discourse.system_user, skip_staff_action: true)
@ -84,7 +110,7 @@ class WebHook < ActiveRecord::Base
end
def self.enqueue_post_hooks(event, post, payload = nil)
if active_web_hooks("post").exists? && post.present?
if active_web_hooks(event).exists? && post.present?
payload ||= WebHook.generate_payload(:post, post)
WebHook.enqueue_hooks(

View File

@ -18,27 +18,97 @@ class WebHookEventType < ActiveRecord::Base
TOPIC_VOTING = 17
CHAT_MESSAGE = 18
enum group: {
topic: 0,
post: 1,
user: 2,
group: 3,
category: 4,
tag: 5,
reviewable: 6,
notification: 7,
solved: 8,
assign: 9,
user_badge: 10,
group_user: 11,
like: 12,
user_promoted: 13,
voting: 14,
chat: 15,
},
_scopes: false
TYPES = {
topic_created: 101,
topic_revised: 102,
topic_edited: 103,
topic_destroyed: 104,
topic_recovered: 105,
post_created: 201,
post_edited: 202,
post_destroyed: 203,
post_recovered: 204,
user_logged_in: 301,
user_logged_out: 302,
user_confirmed_email: 303,
user_created: 304,
user_approved: 305,
user_updated: 306,
user_destroyed: 307,
user_suspended: 308,
user_unsuspended: 309,
group_created: 401,
group_updated: 402,
group_destroyed: 403,
category_created: 501,
category_updated: 502,
category_destroyed: 503,
tag_created: 601,
tag_updated: 602,
tag_destroyed: 603,
reviewable_created: 901,
reviewable_updated: 902,
notification_created: 1001,
solved_accepted_solution: 1101,
solved_unaccepted_solution: 1102,
assign_assigned: 1201,
assign_unassigned: 1202,
user_badge_granted: 1301,
user_badge_revoked: 1302,
group_user_added: 1401,
group_user_removed: 1402,
like_created: 1501,
user_promoted_created: 1601,
voting_topic_upvote: 1701,
voting_topic_unvote: 1702,
chat_message_created: 1801,
chat_message_edited: 1802,
chat_message_trashed: 1803,
chat_message_restored: 1804,
}
has_and_belongs_to_many :web_hooks
default_scope { order("id ASC") }
validates :name, presence: true, uniqueness: true
scope :active_grouped, -> { active.where.not(group: nil).group_by(&:group) }
def self.active
ids_to_exclude = []
unless defined?(SiteSetting.solved_enabled) && SiteSetting.solved_enabled
ids_to_exclude << SOLVED
ids_to_exclude << TYPES[:solved_accept_unaccept]
end
unless defined?(SiteSetting.assign_enabled) && SiteSetting.assign_enabled
ids_to_exclude << ASSIGN
ids_to_exclude << TYPES[:assign_assign_unassign]
end
unless defined?(SiteSetting.voting_enabled) && SiteSetting.voting_enabled
ids_to_exclude << TOPIC_VOTING
ids_to_exclude << TYPES[:voting_voted_unvoted]
end
unless defined?(SiteSetting.chat_enabled) && SiteSetting.chat_enabled
ids_to_exclude << CHAT_MESSAGE
ids_to_exclude << TYPES[:chat_message]
end
self.where.not(id: ids_to_exclude)
end
end
@ -47,6 +117,7 @@ end
#
# Table name: web_hook_event_types
#
# id :integer not null, primary key
# name :string not null
# id :integer not null, primary key
# name :string not null
# group :integer
#

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class WebHookEventTypesHook < ActiveRecord::Base
belongs_to :web_hook_event_type
belongs_to :web_hook
end
# == Schema Information
#
# Table name: web_hook_event_types_hooks
#
# web_hook_id :integer not null
# web_hook_event_type_id :integer not null
#
# Indexes
#
# idx_web_hook_event_types_hooks_on_ids (web_hook_event_type_id,web_hook_id) UNIQUE
#

View File

@ -4939,41 +4939,65 @@ en:
groups_filter: "Triggered Groups"
delete_confirm: "Delete this webhook?"
topic_event:
name: "Topic Event"
details: "When there is a new topic, revised, changed or deleted."
group_name: "Topic Events"
topic_created: "Topic is created"
topic_revised: "Topic is revised"
topic_edited: "Topic is updated"
topic_destroyed: "Topic is deleted"
topic_recovered: "Topic is recovered"
post_event:
name: "Post Event"
details: "When there is a new reply, edit, deleted or recovered."
user_event:
name: "User Event"
details: "When a user logs in, logs out, confirms their email, is created, approved, updated, suspended or unsuspended."
group_name: "Post Events"
post_created: "Post is created"
post_edited: "Post is updated"
post_destroyed: "Post is deleted"
post_recovered: "Post is recovered"
group_event:
name: "Group Event"
details: "When a group is created, updated or destroyed."
category_event:
name: "Category Event"
details: "When a category is created, updated or destroyed."
group_name: "Group Events"
group_created: "Group is created"
group_updated: "Group is updated"
group_destroyed: "Group is deleted"
tag_event:
name: "Tag Event"
details: "When a tag is created, updated or destroyed."
group_name: "Tag Events"
tag_created: "Tag is created"
tag_updated: "Tag is updated"
tag_destroyed: "Tag is deleted"
category_event:
group_name: "Category Events"
category_created: "Category is created"
category_updated: "Category is updated"
category_destroyed: "Category is deleted"
user_event:
group_name: "User Events"
user_logged_in: "User logged in"
user_logged_out: "User logged out"
user_confirmed_email: "User confirmed e-mail"
user_created: "User is created"
user_approved: "User is approved"
user_updated: "User is updated"
user_destroyed: "User is deleted"
user_suspended: "User is suspended"
user_unsuspended: "User is unsuspended"
reviewable_event:
name: "Reviewable Event"
details: "When a new item is ready for review and when its status is updated."
notification_event:
name: "Notification Event"
details: "When a user receives a notification in their feed."
user_promoted_event:
name: "User Promoted Event"
details: "When a user is promoted from one trust level to another."
group_name: "Reviewable Events"
reviewable_created: "Reviewable item is ready"
reviewable_updated: "Reviewable item is updated"
user_badge_event:
name: "Badge Event"
details: "When a badge is granted or revoked."
group_user_event:
name: "Group User Event"
details: "When a user is added or removed in a group."
group_name: "Badge Events"
user_badge_granted: "User badge is granted"
user_badge_revoked: "User badge is revoked"
like_event:
name: "Like Event"
details: "When a user likes a post."
group_name: "Like Events"
post_liked: "When a user likes a post."
notification_event:
group_name: "Notification Events"
notification_created: "An user receives a notification in their feed"
group_user_event:
group_name: "Group User Events"
user_added_to_group: "An user is added to a group"
user_removed_from_group: "An user is removed from a group"
user_promoted_event:
group_name: "User Promoted Events"
user_promoted: "An user is promoted"
delivery_status:
title: "Delivery Status"
inactive: "Inactive"

View File

@ -1,81 +1,232 @@
# frozen_string_literal: true
WebHookEventType.seed do |b|
b.id = WebHookEventType::TOPIC
b.name = "topic"
b.id = WebHookEventType::TYPES[:topic_created]
b.name = "topic_created"
b.group = WebHookEventType.groups[:topic]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::POST
b.name = "post"
b.id = WebHookEventType::TYPES[:topic_revised]
b.name = "topic_revised"
b.group = WebHookEventType.groups[:topic]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::USER
b.name = "user"
b.id = WebHookEventType::TYPES[:topic_edited]
b.name = "topic_edited"
b.group = WebHookEventType.groups[:topic]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::GROUP
b.name = "group"
b.id = WebHookEventType::TYPES[:topic_destroyed]
b.name = "topic_destroyed"
b.group = WebHookEventType.groups[:topic]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::CATEGORY
b.name = "category"
b.id = WebHookEventType::TYPES[:topic_recovered]
b.name = "topic_recovered"
b.group = WebHookEventType.groups[:topic]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TAG
b.name = "tag"
b.id = WebHookEventType::TYPES[:post_created]
b.name = "post_created"
b.group = WebHookEventType.groups[:post]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::REVIEWABLE
b.name = "reviewable"
b.id = WebHookEventType::TYPES[:post_edited]
b.name = "post_edited"
b.group = WebHookEventType.groups[:post]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::NOTIFICATION
b.name = "notification"
b.id = WebHookEventType::TYPES[:post_destroyed]
b.name = "post_destroyed"
b.group = WebHookEventType.groups[:post]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::SOLVED
b.name = "solved"
b.id = WebHookEventType::TYPES[:post_recovered]
b.name = "post_recovered"
b.group = WebHookEventType.groups[:post]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::ASSIGN
b.name = "assign"
b.id = WebHookEventType::TYPES[:user_logged_in]
b.name = "user_logged_in"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::USER_BADGE
b.name = "user_badge"
b.id = WebHookEventType::TYPES[:user_logged_out]
b.name = "user_logged_out"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::GROUP_USER
b.name = "group_user"
b.id = WebHookEventType::TYPES[:user_confirmed_email]
b.name = "user_confirmed_email"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::LIKE
b.name = "like"
b.id = WebHookEventType::TYPES[:user_created]
b.name = "user_created"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::USER_PROMOTED
b.id = WebHookEventType::TYPES[:user_approved]
b.name = "user_approved"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_updated]
b.name = "user_updated"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_destroyed]
b.name = "user_destroyed"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_suspended]
b.name = "user_suspended"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_unsuspended]
b.name = "user_unsuspended"
b.group = WebHookEventType.groups[:user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:group_created]
b.name = "group_created"
b.group = WebHookEventType.groups[:group]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:group_updated]
b.name = "group_updated"
b.group = WebHookEventType.groups[:group]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:group_destroyed]
b.name = "group_destroyed"
b.group = WebHookEventType.groups[:group]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:category_created]
b.name = "category_created"
b.group = WebHookEventType.groups[:category]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:category_updated]
b.name = "category_updated"
b.group = WebHookEventType.groups[:category]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:category_destroyed]
b.name = "category_destroyed"
b.group = WebHookEventType.groups[:category]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:tag_created]
b.name = "tag_created"
b.group = WebHookEventType.groups[:tag]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:tag_updated]
b.name = "tag_updated"
b.group = WebHookEventType.groups[:tag]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:tag_destroyed]
b.name = "tag_destroyed"
b.group = WebHookEventType.groups[:tag]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:reviewable_created]
b.name = "reviewable_created"
b.group = WebHookEventType.groups[:reviewable]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:reviewable_updated]
b.name = "reviewable_updated"
b.group = WebHookEventType.groups[:reviewable]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:notification_created]
b.name = "notification_created"
b.group = WebHookEventType.groups[:notification]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:solved_accepted_solution]
b.name = "accepted_solution"
b.group = WebHookEventType.groups[:solved]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:solved_unaccepted_solution]
b.name = "unaccepted_solution"
b.group = WebHookEventType.groups[:solved]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:assign_assigned]
b.name = "assigned"
b.group = WebHookEventType.groups[:assign]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:assign_unassigned]
b.name = "unassigned"
b.group = WebHookEventType.groups[:assign]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_badge_granted]
b.name = "user_badge_granted"
b.group = WebHookEventType.groups[:user_badge]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_badge_revoked]
b.name = "user_badge_revoked"
b.group = WebHookEventType.groups[:user_badge]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:group_user_added]
b.name = "user_added_to_group"
b.group = WebHookEventType.groups[:group_user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:group_user_removed]
b.name = "user_removed_from_group"
b.group = WebHookEventType.groups[:group_user]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:like_created]
b.name = "post_liked"
b.group = WebHookEventType.groups[:like]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:user_promoted_created]
b.name = "user_promoted"
b.group = WebHookEventType.groups[:user_promoted]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TOPIC_VOTING
b.name = "topic_voting"
b.id = WebHookEventType::TYPES[:voting_topic_upvote]
b.name = "topic_upvote"
b.group = WebHookEventType.groups[:voting]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::CHAT_MESSAGE
b.name = "chat_message"
b.id = WebHookEventType::TYPES[:voting_topic_unvote]
b.name = "topic_unvote"
b.group = WebHookEventType.groups[:voting]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:chat_message_created]
b.name = "chat_message_created"
b.group = WebHookEventType.groups[:chat]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:chat_message_edited]
b.name = "chat_message_edited"
b.group = WebHookEventType.groups[:chat]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:chat_message_trashed]
b.name = "chat_message_trashed"
b.group = WebHookEventType.groups[:chat]
end
WebHookEventType.seed do |b|
b.id = WebHookEventType::TYPES[:chat_message_restored]
b.name = "chat_message_restored"
b.group = WebHookEventType.groups[:chat]
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddGroupToWebHookEventType < ActiveRecord::Migration[7.0]
def change
add_column :web_hook_event_types, :group, :integer
end
end

View File

@ -0,0 +1,153 @@
# frozen_string_literal: true
class MoveWebHooksToNewEventIds < ActiveRecord::Migration[7.0]
def up
execute <<~SQL
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 101, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 1;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 102, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 1;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 103, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 1;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 104, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 1;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 105, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 1;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 201, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 2;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 202, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 2;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 203, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 2;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 204, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 2;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 301, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 302, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 303, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 304, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 305, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 306, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 307, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 308, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 309, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 3;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 401, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 4;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 402, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 4;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 403, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 4;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 501, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 5;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 502, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 5;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 503, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 5;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 601, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 6;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 602, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 6;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 603, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 6;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 901, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 9;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 902, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 9;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1001, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 10;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1101, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 11;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1102, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 11;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1201, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 12;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1202, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 12;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1301, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 13;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1302, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 13;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1401, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 14;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1402, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 14;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1501, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 15;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1601, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 16;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1701, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 17;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1702, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 17;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1801, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 18;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1802, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 18;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1803, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 18;
INSERT INTO web_hook_event_types_hooks(web_hook_event_type_id, web_hook_id)
SELECT 1804, web_hook_id FROM web_hook_event_types_hooks WHERE web_hook_event_types_hooks.web_hook_event_type_id = 18;
DELETE FROM web_hook_event_types WHERE id < 100;
SQL
end
def down
execute <<~SQL
DELETE FROM web_hook_event_types_hooks WHERE web_hook_event_type_id > 100
SQL
end
end

View File

@ -60,9 +60,11 @@ class PostDestroyer
end
def destroy
payload = WebHook.generate_payload(:post, @post) if WebHook.active_web_hooks(:post).exists?
payload = WebHook.generate_payload(:post, @post) if WebHook.active_web_hooks(
:post_destroyed,
).exists?
is_first_post = @post.is_first_post? && @topic
has_topic_web_hooks = is_first_post && WebHook.active_web_hooks(:topic).exists?
has_topic_web_hooks = is_first_post && WebHook.active_web_hooks(:topic_destroyed).exists?
if has_topic_web_hooks
topic_view = TopicView.new(@topic.id, Discourse.system_user, skip_staff_action: true)

View File

@ -20,9 +20,12 @@ en:
chat:
create_message: "Create a chat message in a specified channel."
web_hooks:
chat_message_event:
name: "Chat message event"
details: "When a chat message is created, edited, trashed or restored."
chat_event:
group_name: "Chat events"
chat_message_created: "Message is created"
chat_message_edited: "Message is edited"
chat_message_trashed: "Message is trashed"
chat_message_restored: "Message is restored"
about:
chat_messages_count: "Chat messages"
chat_channels_count: "Chat channels"

View File

@ -4,7 +4,7 @@ module Chat
module OutgoingWebHookExtension
def self.prepended(base)
def base.enqueue_chat_message_hooks(event, payload, opts = {})
if active_web_hooks("chat_message").exists?
if active_web_hooks(event).exists?
WebHook.enqueue_hooks(:chat_message, event, payload: payload, **opts)
end
end

View File

@ -1,9 +1,15 @@
# frozen_string_literal: true
Fabricator(:outgoing_chat_message_web_hook, from: :web_hook) do
transient chat_message_hook: WebHookEventType.find_by(name: "chat_message")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:chat_message_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(
name: %w[
chat_message_created
chat_message_edited
chat_message_trashed
chat_message_restored
],
)
end
end

View File

@ -8,9 +8,10 @@ Fabricator(:web_hook) do
verify_certificate true
active true
transient post_hook: WebHookEventType.find_by(name: "post")
after_build { |web_hook, transients| web_hook.web_hook_event_types << transients[:post_hook] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[post_created post_edited post_destroyed post_recovered])
end
end
Fabricator(:inactive_web_hook, from: :web_hook) { active false }
@ -18,85 +19,96 @@ Fabricator(:inactive_web_hook, from: :web_hook) { active false }
Fabricator(:wildcard_web_hook, from: :web_hook) { wildcard_web_hook true }
Fabricator(:topic_web_hook, from: :web_hook) do
transient topic_hook: WebHookEventType.find_by(name: "topic")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:topic_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(
name: %w[topic_created topic_revised topic_edited topic_destroyed topic_recovered],
)
end
end
Fabricator(:post_web_hook, from: :web_hook) do
transient topic_hook: WebHookEventType.find_by(name: "post")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:post_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[post_created post_edited post_destroyed post_recovered])
end
end
Fabricator(:user_web_hook, from: :web_hook) do
transient user_hook: WebHookEventType.find_by(name: "user")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:user_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(
name: %w[
user_logged_in
user_logged_out
user_confirmed_email
user_created
user_approved
user_updated
user_destroyed
user_suspended
user_unsuspended
],
)
end
end
Fabricator(:group_web_hook, from: :web_hook) do
transient group_hook: WebHookEventType.find_by(name: "group")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:group_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[group_created group_updated group_destroyed])
end
end
Fabricator(:category_web_hook, from: :web_hook) do
transient category_hook: WebHookEventType.find_by(name: "category")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:category_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[category_created category_updated category_destroyed])
end
end
Fabricator(:tag_web_hook, from: :web_hook) do
transient tag_hook: WebHookEventType.find_by(name: "tag")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:tag_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[tag_created tag_updated tag_destroyed])
end
end
Fabricator(:reviewable_web_hook, from: :web_hook) do
transient reviewable_hook: WebHookEventType.find_by(name: "reviewable")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:reviewable_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[reviewable_created reviewable_updated])
end
end
Fabricator(:notification_web_hook, from: :web_hook) do
transient notification_hook: WebHookEventType.find_by(name: "notification")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:notification_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types = WebHookEventType.where(name: "notification_created")
end
end
Fabricator(:user_badge_web_hook, from: :web_hook) do
transient user_badge_hook: WebHookEventType.find_by(name: "user_badge")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:user_badge_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[user_badge_granted user_badge_revoked])
end
end
Fabricator(:group_user_web_hook, from: :web_hook) do
transient group_user_hook: WebHookEventType.find_by(name: "group_user")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:group_user_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types =
WebHookEventType.where(name: %w[user_added_to_group user_removed_from_group])
end
end
Fabricator(:like_web_hook, from: :web_hook) do
transient like_hook: WebHookEventType.find_by(name: "like")
after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:like_hook]] }
after_build do |web_hook|
web_hook.web_hook_event_types = WebHookEventType.where(name: "post_liked")
end
end
Fabricator(:user_promoted_web_hook, from: :web_hook) do
transient user_promoted_hook: WebHookEventType.find_by(name: "user_promoted")
after_build do |web_hook, transients|
web_hook.web_hook_event_types = [transients[:user_promoted_hook]]
after_build do |web_hook|
web_hook.web_hook_event_types = WebHookEventType.where(name: "user_promoted")
end
end

View File

@ -34,7 +34,7 @@ RSpec.describe Jobs::EmitWebHookEvent do
job.execute(
web_hook_id: post_hook.id,
payload: { id: post.id }.to_json,
event_type: WebHookEventType::POST,
event_type: WebHookEventType::TYPES[:post_created],
)
expect(WebHookEvent.last.web_hook_id).to eq(post_hook.id)
@ -280,7 +280,7 @@ RSpec.describe Jobs::EmitWebHookEvent do
stub_request(:post, post_hook.payload_url).to_return(body: "OK", status: 200)
topic_event_type = WebHookEventType.all.first
web_hook_id = Fabricate("#{topic_event_type.name}_web_hook").id
web_hook_id = Fabricate("#{topic_event_type.name.gsub("_created", "")}_web_hook").id
expect do
job.execute(

View File

@ -43,51 +43,53 @@ RSpec.describe WebHook do
end
it "excludes disabled plugin web_hooks" do
web_hook_event_types = WebHookEventType.active.find_by(name: "solved")
expect(web_hook_event_types).to eq(nil)
web_hook_event_types = WebHookEventType.active.where(name: "solved_accept_unaccept")
expect(web_hook_event_types).to be_empty
end
it "includes non-plugin web_hooks" do
web_hook_event_types = WebHookEventType.active.where(name: "topic")
expect(web_hook_event_types.count).to eq(1)
web_hook_event_types = WebHookEventType.active.where(group: "topic")
expect(web_hook_event_types.count).to eq(5)
end
it "includes enabled plugin web_hooks" do
SiteSetting.stubs(:solved_enabled).returns(true)
solved_event_types = WebHookEventType.active.where(name: "solved")
expect(solved_event_types.count).to eq(1)
SiteSetting.stubs(:assign_enabled).returns(true)
assign_event_types = WebHookEventType.active.where(name: "assign")
expect(assign_event_types.count).to eq(1)
assign_event_types = WebHookEventType.active.where(group: "assign").pluck(:name)
expect(assign_event_types).to eq(%w[assigned unassigned])
SiteSetting.stubs(:voting_enabled).returns(true)
voting_event_types = WebHookEventType.active.where(name: "topic_voting")
expect(voting_event_types.count).to eq(1)
voting_event_types = WebHookEventType.active.where(group: "voting").pluck(:name)
expect(voting_event_types).to eq(%w[topic_upvote topic_unvote])
#
SiteSetting.stubs(:solved_enabled).returns(true)
solved_event_types = WebHookEventType.active.where(group: "solved").pluck(:name)
expect(solved_event_types).to eq(%w[accepted_solution unaccepted_solution])
#
SiteSetting.stubs(:chat_enabled).returns(true)
chat_enabled_types = WebHookEventType.active.where("name LIKE 'chat_%'")
expect(chat_enabled_types.count).to eq(1)
chat_event_types = WebHookEventType.active.where(group: "chat").pluck(:name)
expect(chat_event_types).to eq(
%w[chat_message_created chat_message_edited chat_message_trashed chat_message_restored],
)
end
describe "#active_web_hooks" do
it "returns unique hooks" do
post_hook.web_hook_event_types << WebHookEventType.find_by(name: "topic")
post_hook.web_hook_event_types << WebHookEventType.find_by(group: "topic")
post_hook.update!(wildcard_web_hook: true)
expect(WebHook.active_web_hooks(:post)).to eq([post_hook])
expect(WebHook.active_web_hooks(:post_created)).to eq([post_hook])
end
it "find relevant hooks" do
expect(WebHook.active_web_hooks(:post)).to eq([post_hook])
expect(WebHook.active_web_hooks(:topic)).to eq([topic_hook])
expect(WebHook.active_web_hooks(:post_created)).to eq([post_hook])
expect(WebHook.active_web_hooks(:topic_created)).to eq([topic_hook])
end
it "excludes inactive hooks" do
post_hook.update!(active: false)
expect(WebHook.active_web_hooks(:post)).to eq([])
expect(WebHook.active_web_hooks(:topic)).to eq([topic_hook])
expect(WebHook.active_web_hooks(:post_created)).to eq([])
expect(WebHook.active_web_hooks(:topic_created)).to eq([topic_hook])
end
describe "wildcard web hooks" do
@ -96,9 +98,15 @@ RSpec.describe WebHook do
it "should include wildcard hooks" do
expect(WebHook.active_web_hooks(:wildcard)).to eq([wildcard_hook])
expect(WebHook.active_web_hooks(:post)).to contain_exactly(post_hook, wildcard_hook)
expect(WebHook.active_web_hooks(:post_created)).to contain_exactly(
post_hook,
wildcard_hook,
)
expect(WebHook.active_web_hooks(:topic)).to contain_exactly(topic_hook, wildcard_hook)
expect(WebHook.active_web_hooks(:topic_created)).to contain_exactly(
topic_hook,
wildcard_hook,
)
end
end
end
@ -231,6 +239,24 @@ RSpec.describe WebHook do
expect(payload["tags"]).to contain_exactly(tag.name)
end
it "should enqueue granular hooks for topic" do
topic_web_hook.web_hook_event_types.delete(
WebHookEventType.where(name: "topic_destroyed").last,
)
post = PostCreator.create(user, raw: "post", title: "topic", skip_validations: true)
topic_id = post.topic.id
job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first
expect(job_args["event_name"]).to eq("topic_created")
payload = JSON.parse(job_args["payload"])
expect(payload["id"]).to eq(topic_id)
expect { PostDestroyer.new(user, post).destroy }.not_to change {
Jobs::EmitWebHookEvent.jobs.count
}
end
it "should not log a personal message view when processing new topic" do
SiteSetting.log_personal_messages_views = true
Fabricate(:topic_web_hook)

View File

@ -20,7 +20,7 @@ RSpec.describe Admin::WebHooksController do
wildcard_web_hook: false,
active: true,
verify_certificate: true,
web_hook_event_type_ids: [1],
web_hook_event_type_ids: [WebHookEventType::TYPES[:topic_created]],
group_ids: [],
category_ids: [],
},
@ -47,7 +47,7 @@ RSpec.describe Admin::WebHooksController do
wildcard_web_hook: false,
active: true,
verify_certificate: true,
web_hook_event_type_ids: [1],
web_hook_event_type_ids: [WebHookEventType::TYPES[:topic_created]],
group_ids: [],
category_ids: [],
},