FEATURE: Merge discourse-automation (#26432)

Automation (previously known as discourse-automation) is now a core plugin.
This commit is contained in:
Osama Sayegh
2024-04-03 18:20:43 +03:00
committed by GitHub
parent 2190c9b957
commit 3d4faf3272
314 changed files with 21182 additions and 10 deletions

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
# This script takes the name of a User Custom Field containing a group name.
# On each run, it ensures that each user belongs to the group name given by that UCF (NOTE: group full_name, not name).
#
# In other words, it designates a certain User Custom Field to act as
# a "pointer" to a group that the user should belong to, and adds users as needed.
DiscourseAutomation::Scriptable.add(
DiscourseAutomation::Scripts::ADD_USER_TO_GROUP_THROUGH_CUSTOM_FIELD,
) do
field :custom_field_name, component: :custom_field, required: true
version 1
triggerables %i[recurring user_first_logged_in]
script do |trigger, fields|
custom_field_name = fields.dig("custom_field_name", "value")
case trigger["kind"]
when DiscourseAutomation::Triggers::API_CALL, DiscourseAutomation::Triggers::RECURRING
query =
DB.query(<<-SQL, prefix: ::User::USER_FIELD_PREFIX, custom_field_name: custom_field_name)
SELECT u.id as user_id, g.id as group_id
FROM users u
JOIN user_custom_fields ucf
ON u.id = ucf.user_id
AND ucf.name = CONCAT(:prefix, :custom_field_name)
JOIN groups g
on g.full_name ilike ucf.value
FULL OUTER JOIN group_users gu
ON gu.user_id = u.id
AND gu.group_id = g.id
WHERE gu.id is null
AND u.active = true
ORDER BY 1, 2
SQL
groups_by_id = {}
User
.where(id: query.map(&:user_id))
.order(:id)
.zip(query) do |user, query_row|
group_id = query_row.group_id
group = groups_by_id[group_id] ||= Group.find(group_id)
group.add(user)
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)
end
when DiscourseAutomation::Triggers::USER_FIRST_LOGGED_IN
group_name =
DB.query_single(
<<-SQL,
SELECT value
FROM user_custom_fields ucf
WHERE ucf.user_id = :user_id AND ucf.name = CONCAT(:prefix, :custom_field_name)
SQL
prefix: ::User::USER_FIELD_PREFIX,
custom_field_name: custom_field_name,
user_id: trigger["user"].id,
).first
next if !group_name
group = Group.find_by(full_name: group_name)
next if !group
user = trigger["user"]
group.add(user)
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::APPEND_LAST_CHECKED_BY) do
version 1
triggerables [:after_post_cook]
script do |context|
post = context["post"]
topic = post.topic
cooked = context["cooked"]
doc = Loofah.fragment(cooked)
node = doc.css("blockquote.discourse-automation").first
if node.blank?
node = doc.document.create_element("blockquote")
node["class"] = "discourse-automation"
doc.add_child(node)
end
username = topic.custom_fields[DiscourseAutomation::TOPIC_LAST_CHECKED_BY]
checked_at = topic.custom_fields[DiscourseAutomation::TOPIC_LAST_CHECKED_AT]
if username.present? && checked_at.present?
checked_at = DateTime.parse(checked_at)
date_time =
"[date=#{checked_at.to_date} time=#{checked_at.strftime("%H:%M:%S")} timezone=UTC]"
node.inner_html +=
PrettyText.cook(
I18n.t(
"discourse_automation.scriptables.append_last_checked_by.text",
username: username,
date_time: date_time,
),
).html_safe
end
summary_tag =
"<summary>#{I18n.t("discourse_automation.scriptables.append_last_checked_by.summary")}</summary>"
button_tag =
"<input type=\"button\" value=\"#{I18n.t("discourse_automation.scriptables.append_last_checked_by.button_text")}\" class=\"btn btn-checked\" />"
node.inner_html +=
"<details>#{summary_tag}#{I18n.t("discourse_automation.scriptables.append_last_checked_by.details")}#{button_tag}</details>"
doc.try(:to_html)
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::APPEND_LAST_EDITED_BY) do
version 1
triggerables [:after_post_cook]
script do |context|
post = context["post"]
post_revision = post.revisions.where("user_id > 0").last
username = post_revision&.user&.username || post.username
updated_at = post_revision&.updated_at || post.updated_at
cooked = context["cooked"]
doc = Loofah.fragment(cooked)
node = doc.css("blockquote.discourse-automation").first
if node.blank?
node = doc.document.create_element("blockquote")
node["class"] = "discourse-automation"
doc.add_child(node)
end
date_time = "[date=#{updated_at.to_date} time=#{updated_at.strftime("%H:%M:%S")} timezone=UTC]"
node.inner_html +=
PrettyText.cook(
I18n.t(
"discourse_automation.scriptables.append_last_edited_by.text",
username: username,
date_time: date_time,
),
).html_safe
doc.try(:to_html)
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::AUTO_RESPONDER) do
field :word_answer_list, component: :"key-value", accepts_placeholders: true
field :answering_user, component: :user
field :once, component: :boolean
version 1
triggerables %i[post_created_edited pm_created]
placeholder :sender_username
placeholder :word
script do |context, fields, automation|
key = DiscourseAutomation::AUTO_RESPONDER_TRIGGERED_IDS
answering_username = fields.dig("answering_user", "value") || Discourse.system_user.username
placeholders = { sender_username: answering_username }
post = context["post"]
next if !post.topic
next if fields.dig("once", "value") && post.topic.custom_fields[key]&.include?(automation.id)
answers = Set.new
word_answer_list_json = fields.dig("word_answer_list", "value")
next if word_answer_list_json.blank?
word_answer_list = JSON.parse(word_answer_list_json)
next if word_answer_list.blank?
word_answer_list.each do |word_answer_pair|
if word_answer_pair["key"].blank?
answers.add(word_answer_pair)
next
end
if post.is_first_post?
if match = post.topic.title.match(/\b(#{word_answer_pair["key"]})\b/i)
word_answer_pair["key"] = match.captures.first
answers.add(word_answer_pair)
next
end
end
if match = post.raw.match(/\b(#{word_answer_pair["key"]})\b/i)
word_answer_pair["key"] = match.captures.first
answers.add(word_answer_pair)
end
end
next if answers.blank?
answering_user = User.find_by(username: answering_username)
next if post.user == answering_user
replies =
post
.replies
.where(user_id: answering_user.id, deleted_at: nil)
.secured(Guardian.new(post.user))
next if replies.present?
answers =
answers
.to_a
.map do |answer|
utils.apply_placeholders(answer["value"], placeholders.merge(key: answer["key"]))
end
.join("\n\n")
value = (Array(post.topic.custom_fields[key]) << automation.id).compact.uniq
post.topic.custom_fields[key] = value
post.topic.save_custom_fields
PostCreator.create!(
answering_user,
topic_id: post.topic.id,
reply_to_post_number: post.post_number,
raw: answers,
skip_validations: true,
)
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::AUTO_TAG_TOPIC) do
field :tags, component: :tags, required: true
version 1
triggerables %i[post_created_edited pm_created]
script do |context, fields|
post = context["post"]
next if !post.is_first_post?
next if !post.topic
next unless topic = Topic.find_by(id: post.topic.id)
tags = fields.dig("tags", "value")
DiscourseTagging.tag_topic_by_names(topic, Guardian.new(post.user), tags, append: true)
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::BANNER_TOPIC) do
field :topic_id, component: :text, required: true
field :banner_until, component: :date_time
field :user, component: :user
version 1
triggerables [:point_in_time]
script do |_, fields|
next unless topic_id = fields.dig("topic_id", "value")
next unless topic = Topic.find_by(id: topic_id)
banner_until = fields.dig("banner_until", "value") || nil
username = fields.dig("user", "value") || Discourse.system_user.username
next unless user = User.find_by(username: username)
topic.make_banner!(user, banner_until)
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::CLOSE_TOPIC) do
field :topic, component: :text, required: true, triggerable: :point_in_time
field :message, component: :text
field :user, component: :user
version 1
triggerables %i[point_in_time stalled_wiki]
script do |context, fields|
message = fields.dig("message", "value")
username = fields.dig("user", "value") || Discourse.system_user.username
topic_id = fields.dig("topic", "value") || context.dig("topic", "id")
next unless topic_id
next unless topic = Topic.find_by(id: topic_id)
user = User.find_by_username(username)
next unless user
next unless Guardian.new(user).can_moderate?(topic)
topic.update_status("closed", true, user)
if message.present?
topic_closed_post = topic.posts.where(action_code: "closed.enabled").last
topic_closed_post.raw = message
# FIXME: when there is proper error handling and logging in automation,
# remove this and allow validations to take place
topic_closed_post.skip_validation = true
topic_closed_post.save!
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::FLAG_POST_ON_WORDS) do
field :words, component: :text_list, required: true
version 1
triggerables %i[post_created_edited]
script do |trigger, fields|
post = trigger["post"]
Array(fields.dig("words", "value")).each do |list|
words = list.split(",")
count = words.inject(0) { |acc, word| post.raw.match?(/#{word}/i) ? acc + 1 : acc }
next if count < words.length
has_trust_level = post.user.has_trust_level?(TrustLevel[2])
trusted_user =
has_trust_level ||
ReviewableFlaggedPost.where(
status: Reviewable.statuses[:rejected],
target_created_by: post.user,
).exists?
next if trusted_user
message =
I18n.t("discourse_automation.scriptables.flag_post_on_words.flag_message", words: list)
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:spam],
message: message,
queue_for_review: true,
).perform
end
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::GIFT_EXCHANGE) do
placeholder :year
placeholder :giftee_username
placeholder :gifter_username
field :giftee_assignment_messages, component: :pms, accepts_placeholders: true, required: true
field :gift_exchangers_group, component: :group, required: true
version 17
triggerables %i[point_in_time]
script do |_, fields, automation|
now = Time.zone.now
group_id = fields.dig("gift_exchangers_group", "value")
unless group = Group.find_by(id: group_id)
Rails.logger.warn "[discourse-automation] Couldn’t find group with id #{group_id}"
next
end
cf_name = "#{group.name}-gifts-were-exchanged-#{automation.id}-#{version}-#{now.year}"
if group.custom_fields[cf_name].present?
Rails.logger.warn "[discourse-automation] Gift exchange script has already been run on #{cf_name} this year #{now.year} for this script version #{version}"
next
end
usernames = group.users.pluck(:username)
if usernames.size < 3
Rails.logger.warn "[discourse-automation] Gift exchange needs at least 3 users in a group"
next
end
usernames.shuffle!
usernames << usernames[0]
# shuffle the pairs to prevent prying eyes to identify matches by looking at the timestamps of the topics
pairs = usernames.each_cons(2).to_a.shuffle
pairs.each do |gifter, giftee|
placeholders = { year: now.year.to_s, gifter_username: gifter, giftee_username: giftee }
Array(fields.dig("giftee_assignment_messages", "value")).each do |giftee_assignment_message|
if giftee_assignment_message["title"].blank?
Rails.logger.warn "[discourse-automation] Gift exchange requires a title for the PM"
next
end
if giftee_assignment_message["raw"].blank?
Rails.logger.warn "[discourse-automation] Gift exchange requires a raw for the PM"
next
end
raw = utils.apply_placeholders(giftee_assignment_message["raw"], placeholders)
title = utils.apply_placeholders(giftee_assignment_message["title"], placeholders)
utils.send_pm(
{ target_usernames: Array(gifter), title: title, raw: raw },
delay: giftee_assignment_message["delay"],
prefers_encrypt: giftee_assignment_message["prefers_encrypt"],
automation_id: automation.id,
)
end
end
group.custom_fields[cf_name] = true
group.save_custom_fields
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(
DiscourseAutomation::Scripts::GROUP_CATEGORY_NOTIFICATION_DEFAULT,
) do
version 1
field :group, component: :group
field :notification_level, component: :category_notification_level
field :update_existing_members, component: :boolean
triggerables %i[category_created_edited]
script do |context, fields|
category_id = context["category"].id
group_id = fields.dig("group", "value")
notification_level = fields.dig("notification_level", "value")
unless group = Group.find_by(id: group_id)
Rails.logger.warn "[discourse-automation] Couldn’t find group with id #{group_id}"
next
end
GroupCategoryNotificationDefault
.find_or_initialize_by(group_id: group_id, category_id: category_id)
.tap do |gc|
gc.notification_level = notification_level
gc.save!
end
if fields.dig("update_existing_members", "value")
group
.users
.select(:id, :user_id)
.find_in_batches do |batch|
user_ids = batch.pluck(:user_id)
category_users = []
existing_users =
CategoryUser.where(category_id: category_id, user_id: user_ids).where(
"notification_level IS NOT NULL",
)
skip_user_ids = existing_users.pluck(:user_id)
batch.each do |group_user|
next if skip_user_ids.include?(group_user.user_id)
category_users << {
category_id: category_id,
user_id: group_user.user_id,
notification_level: notification_level,
}
end
next if category_users.blank?
CategoryUser.insert_all!(category_users)
end
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::PIN_TOPIC) do
field :pinnable_topic, component: :text, required: true
field :pinned_until, component: :date_time
field :pinned_globally, component: :boolean
version 1
triggerables [:point_in_time]
script do |_context, fields|
next unless topic_id = fields.dig("pinnable_topic", "value")
next unless topic = Topic.find_by(id: topic_id)
pinned_globally = fields.dig("pinned_globally", "value") || false
pinned_until = fields.dig("pinned_until", "value") || nil
topic.update_pinned(true, pinned_globally, pinned_until)
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::POST) do
version 1
placeholder :creator_username
field :creator, component: :user
field :creator, component: :user, triggerable: :user_updated, accepted_contexts: [:updated_user]
field :topic, component: :text, required: true
field :post, component: :post, required: true, accepts_placeholders: true
placeholder :creator_username
placeholder :updated_user_username, triggerable: :user_updated
placeholder :updated_user_name, triggerable: :user_updated
triggerables %i[recurring point_in_time user_updated]
script do |context, fields, automation|
creator_username = fields.dig("creator", "value")
creator_username = context["user"]&.username if creator_username == "updated_user"
creator_username ||= Discourse.system_user.username
topic_id = fields.dig("topic", "value")
post_raw = fields.dig("post", "value")
placeholders = { creator_username: creator_username }.merge(context["placeholders"] || {})
creator = User.find_by(username: creator_username)
topic = Topic.find_by(id: topic_id)
if !topic || topic.closed? || topic.archived?
Rails.logger.warn "[discourse-automation] topic with id: `#{topic_id}` was not found"
next
end
if context["kind"] == DiscourseAutomation::Triggers::USER_UPDATED
user = context["user"]
user_data = context["user_data"]
user_profile_data = user_data[:profile_data] || {}
user_custom_fields = {}
user_data[:custom_fields]&.each do |k, v|
user_custom_fields[k.gsub(/\s+/, "_").underscore] = v
end
user = User.find(context["user"].id)
placeholders["username"] = user.username
placeholders["name"] = user.name
placeholders["updated_user_username"] = user.username
placeholders["updated_user_name"] = user.name
placeholders = placeholders.merge(user_profile_data, user_custom_fields)
end
post_raw = utils.apply_placeholders(post_raw, placeholders)
if !creator
Rails.logger.warn "[discourse-automation] creator with username: `#{creator_username}` was not found"
next
end
new_post = PostCreator.new(creator, topic_id: topic_id, raw: post_raw).create!
if context["kind"] == DiscourseAutomation::Triggers::USER_UPDATED && new_post.persisted?
user.user_custom_fields.create(name: automation.name, value: "true")
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::SEND_PMS) do
version 1
placeholder :sender_username
placeholder :receiver_username
field :sender, component: :user
field :receiver, component: :user, triggerable: :recurring
field :sendable_pms, component: :pms, accepts_placeholders: true, required: true
triggerables %i[
user_badge_granted
user_added_to_group
stalled_wiki
recurring
user_promoted
api_call
user_removed_from_group
]
script do |context, fields, automation|
sender_username = fields.dig("sender", "value") || Discourse.system_user.username
placeholders = { sender_username: sender_username }.merge(context["placeholders"] || {})
usernames = context["usernames"] || []
# optional field when using recurring triggerable
if u = fields.dig("receiver", "value")
usernames << u
end
usernames.compact.uniq.each do |username|
placeholders[:receiver_username] = username
Array(fields.dig("sendable_pms", "value")).each do |sendable|
next if !sendable["title"] || !sendable["raw"]
pm = {}
pm["title"] = utils.apply_placeholders(sendable["title"], placeholders)
pm["raw"] = utils.apply_placeholders(sendable["raw"], placeholders)
pm["target_usernames"] = Array(username)
utils.send_pm(
pm,
sender: sender_username,
automation_id: automation.id,
delay: sendable["delay"],
prefers_encrypt: !!sendable["prefers_encrypt"],
)
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::SUSPEND_USER_BY_EMAIL) do
version 1
triggerables %i[api_call]
field :reason, component: :text, required: true
field :suspend_until, component: :date_time, required: true
field :actor, component: :user
script do |context, fields|
email = context["email"]
next unless target = UserEmail.find_by(email: email)&.user
next if target.suspended?
unless actor =
User.find_by(username: fields.dig("actor", "value") || Discourse.system_user.username)
next
end
guardian = Guardian.new(actor)
guardian.ensure_can_suspend!(target)
suspend_until = context["suspend_until"].presence || fields.dig("suspend_until", "value")
reason = context["reason"].presence || fields.dig("reason", "value")
User.transaction do
target.suspended_till = suspend_until
target.suspended_at = DateTime.now
target.save!
StaffActionLogger.new(actor).log_user_suspend(target, reason)
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::TOPIC_REQUIRED_WORDS) do
field :words, component: :text_list
version 1
triggerables %i[topic]
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::USER_GLOBAL_NOTICE) do
field :notice, component: :message, required: true, accepts_placeholders: true
field :level,
component: :choices,
extra: {
content:
%w[success error warning info].map do |level|
{
id: level,
name: "discourse_automation.scriptables.user_global_notice.levels.#{level}",
}
end,
}
version 1
triggerables [:stalled_topic]
triggerables [:first_accepted_solution] if defined?(DiscourseSolved)
placeholder :username
script do |context, fields, automation|
placeholders = {}.merge(context["placeholders"] || {})
if context["kind"] == DiscourseAutomation::Triggers::STALLED_TOPIC
user = context["topic"].user
placeholders["username"] = user.username
elsif context["kind"] == "first_accepted_solution"
username = context["usernames"][0]
user = User.find_by(username: username)
placeholders["username"] = username
end
notice = utils.apply_placeholders(fields.dig("notice", "value") || "", placeholders)
level = fields.dig("level", "value")
begin
DiscourseAutomation::UserGlobalNotice.upsert(
{
identifier: automation.id,
notice: notice,
user_id: user.id,
level: level,
created_at: Time.now,
updated_at: Time.now,
},
unique_by: "idx_discourse_automation_user_global_notices",
)
rescue ActiveRecord::RecordNotUnique
# do nothing
end
end
on_reset do |automation|
DiscourseAutomation::UserGlobalNotice.where(identifier: automation.id).destroy_all
end
end

View File

@ -0,0 +1,116 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(
DiscourseAutomation::Scripts::USER_GROUP_MEMBERSHIP_THROUGH_BADGE,
) do
version 2
field :badge,
component: :choices,
extra: {
content:
Badge.order(:name).select(:id, :name).map { |b| { id: b.id, translated_name: b.name } },
},
required: true
field :group, component: :group, required: true, extra: { ignore_automatic: true }
field :update_user_title_and_flair, component: :boolean
field :remove_members_without_badge, component: :boolean
triggerables %i[recurring user_first_logged_in]
script do |context, fields|
badge_id = fields.dig("badge", "value")
group_id = fields.dig("group", "value")
update_user_title_and_flair = fields.dig("update_user_title_and_flair", "value")
remove_members_without_badge = fields.dig("remove_members_without_badge", "value")
current_user = context["user"]
bulk_modify_start_count =
DiscourseAutomation::USER_GROUP_MEMBERSHIP_THROUGH_BADGE_BULK_MODIFY_START_COUNT
badge = Badge.find_by(id: badge_id)
unless badge
Rails.logger.warn("[discourse-automation] Couldn’t find badge with id #{badge_id}")
next
end
group = Group.find_by(id: group_id)
unless group
Rails.logger.warn("[discourse-automation] Couldn’t find group with id #{group_id}")
next
end
query_options = { group_id: group.id, badge_id: badge.id }
# IDs of users who currently have badge but not members of target group
user_ids_to_add_query = +<<~SQL
SELECT u.id AS user_id
FROM users u
JOIN user_badges ub ON u.id = ub.user_id
LEFT JOIN group_users gu ON u.id = gu.user_id AND gu.group_id = :group_id
WHERE ub.badge_id = :badge_id AND gu.user_id IS NULL
SQL
if current_user
user_ids_to_add_query << " AND u.id = :user_id"
query_options[:user_id] = current_user.id
end
user_ids_to_add = DB.query_single(user_ids_to_add_query, query_options)
if user_ids_to_add.count < bulk_modify_start_count
User
.where(id: user_ids_to_add)
.each do |user|
group.add(user)
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)
end
else
group.bulk_add(user_ids_to_add)
end
if update_user_title_and_flair && user_ids_to_add.present?
DB.exec(<<~SQL, ids: user_ids_to_add, title: group.title, flair_group_id: group.id)
UPDATE users
SET title = :title, flair_group_id = :flair_group_id
WHERE id IN (:ids)
SQL
end
next unless remove_members_without_badge
# IDs of users who are currently target group members without the badge
user_ids_to_remove_query = +<<~SQL
SELECT u.id AS user_id
FROM users u
JOIN group_users gu ON u.id = gu.user_id
LEFT JOIN user_badges ub ON u.id = ub.user_id AND ub.badge_id = :badge_id
WHERE gu.group_id = :group_id AND ub.user_id IS NULL
SQL
if current_user
user_ids_to_remove_query << " AND u.id = :user_id"
query_options[:user_id] ||= current_user.id
end
user_ids_to_remove = DB.query_single(user_ids_to_remove_query, query_options)
if user_ids_to_remove.count < bulk_modify_start_count
User
.where(id: user_ids_to_remove)
.each do |user|
group.remove(user)
GroupActionLogger.new(Discourse.system_user, group).log_remove_user_from_group(user)
end
else
group.bulk_remove(user_ids_to_remove)
end
if update_user_title_and_flair && user_ids_to_remove.present?
DB.exec(<<~SQL, ids: user_ids_to_remove, title: group.title, flair_group_id: group.id)
UPDATE users
SET title = NULL, flair_group_id = NULL
WHERE id IN (:ids) AND (flair_group_id = :flair_group_id OR title = :title)
SQL
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::ZAPIER_WEBHOOK) do
field :webhook_url, component: :text, required: true
version 1
triggerables %i[user_promoted user_added_to_group user_badge_granted user_removed_from_group]
script do |context, fields|
webhook_url = fields.dig("webhook_url", "value")
unless webhook_url&.start_with?("https://hooks.zapier.com/hooks/catch/")
Rails.logger.warn "[discourse-automation] #{webhook_url} is not a valid Zapier webhook URL, expecting an URL starting with https://hooks.zapier.com/hooks/catch/"
next
end
Jobs.enqueue(
:discourse_automation_call_zapier_webhook,
webhook_url: webhook_url,
context: context,
)
end
end