mirror of
https://github.com/discourse/discourse.git
synced 2025-05-21 18:12:32 +08:00
FEATURE: Merge discourse-automation (#26432)
Automation (previously known as discourse-automation) is now a core plugin.
This commit is contained in:
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
66
plugins/automation/lib/discourse_automation/scripts/post.rb
Normal file
66
plugins/automation/lib/discourse_automation/scripts/post.rb
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Reference in New Issue
Block a user