mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-06 11:58:15 +08:00
I want to use the AI llm_tagger script on https://discover.discourse.com, but in order to do this the automation needs to be allowed to run on posts created by the system user. To accomplish this, I've added the ability for scripts to add a `allow_system_posts` setting. Existing scripts will still ignore system posts, but this allows script authors to add a setting to opt-in to processing them.
434 lines
16 KiB
Ruby
434 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseAutomation
|
|
module EventHandlers
|
|
def self.handle_post_created_edited(post, action)
|
|
return if post.post_type != Post.types[:regular]
|
|
|
|
topic = post.topic
|
|
return if topic.blank?
|
|
|
|
name = DiscourseAutomation::Triggers::POST_CREATED_EDITED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
# allow scripts to opt-in to system posts via allow_system_posts setting
|
|
next if post.user_id < 0 && !automation.script_field("allow_system_posts")&.dig("value")
|
|
action_type = automation.trigger_field("action_type")
|
|
selected_action = action_type["value"]&.to_sym
|
|
|
|
if selected_action
|
|
next if selected_action == :created && action != :create
|
|
next if selected_action == :edited && action != :edit
|
|
end
|
|
|
|
restricted_archetype = automation.trigger_field("restricted_archetype")["value"]
|
|
if restricted_archetype.present?
|
|
if restricted_archetype == "public"
|
|
next if topic.archetype != Archetype.default
|
|
next if !topic.category
|
|
next if topic.category.read_restricted?
|
|
else
|
|
topic_archetype = topic.archetype
|
|
next if restricted_archetype != topic_archetype
|
|
end
|
|
end
|
|
|
|
original_post_only = automation.trigger_field("original_post_only")
|
|
if original_post_only["value"]
|
|
next if post.post_number != 1
|
|
end
|
|
|
|
first_post_only = automation.trigger_field("first_post_only")
|
|
if first_post_only["value"]
|
|
next if post.user.user_stat.post_count != 1
|
|
end
|
|
|
|
first_topic_only = automation.trigger_field("first_topic_only")
|
|
if first_topic_only["value"]
|
|
next if post.post_number != 1
|
|
next if post.user.user_stat.topic_count != 1
|
|
end
|
|
|
|
skip_via_email = automation.trigger_field("skip_via_email")
|
|
if skip_via_email["value"]
|
|
next if post.via_email?
|
|
end
|
|
|
|
valid_trust_levels = automation.trigger_field("valid_trust_levels")
|
|
if valid_trust_levels["value"]
|
|
next if valid_trust_levels["value"].exclude?(post.user.trust_level)
|
|
end
|
|
|
|
restricted_categories = automation.trigger_field("restricted_categories").dup
|
|
if restricted_category_ids = restricted_categories["value"]
|
|
exclude_subcategories = automation.trigger_field("exclude_subcategories")["value"]
|
|
|
|
if !exclude_subcategories
|
|
# core api is odd, we only support nesting of 3 anyway, this is efficient
|
|
restricted_category_ids = restricted_category_ids.to_set
|
|
# for nesting of 3
|
|
restricted_category_ids +=
|
|
Category.where(parent_category_id: restricted_category_ids).pluck(:id)
|
|
restricted_category_ids +=
|
|
Category.where(parent_category_id: restricted_category_ids).pluck(:id)
|
|
end
|
|
|
|
next if !restricted_category_ids.include?(topic.category_id)
|
|
end
|
|
|
|
restricted_tags = automation.trigger_field("restricted_tags")
|
|
if restricted_tags["value"]
|
|
next if (restricted_tags["value"] & topic.tags.map(&:name)).empty?
|
|
end
|
|
|
|
restricted_inbox_group_ids = automation.trigger_field("restricted_inbox_groups")["value"]
|
|
if restricted_inbox_group_ids.present?
|
|
next if !topic.private_message?
|
|
|
|
target_group_ids = topic.allowed_groups.pluck(:id)
|
|
next if (restricted_inbox_group_ids & target_group_ids).empty?
|
|
end
|
|
|
|
user_group_ids = automation.trigger_field("restricted_groups")["value"]
|
|
next if user_group_ids.present? && !post.user.in_any_groups?(user_group_ids)
|
|
|
|
excluded_group_ids = automation.trigger_field("excluded_groups")["value"]
|
|
next if excluded_group_ids.present? && post.user.in_any_groups?(excluded_group_ids)
|
|
|
|
ignore_automated = automation.trigger_field("ignore_automated")
|
|
next if ignore_automated["value"] && post.incoming_email&.is_auto_generated?
|
|
|
|
post_features = automation.trigger_field("post_features")["value"]
|
|
if post_features.present?
|
|
cooked = post.cooked
|
|
# note the only 100% correct way is to lean on an actual HTML parser
|
|
# however triggers may pop up during the post creation process, we can not afford a full parse
|
|
if post_features.include?("with_images") &&
|
|
!cooked.match?(/<img(?![^>]*class=["'](emoji|avatar))[^>]*>/i)
|
|
next
|
|
end
|
|
next if post_features.include?("with_links") && !cooked.match?(/<a\s+[^>]*>/i)
|
|
next if post_features.include?("with_code") && !cooked.match?(/<pre[^>]*>/i)
|
|
if post_features.include?("with_uploads") &&
|
|
!cooked.match?(/<a\s+[^>]*class=["']attachment[^>]*>/i)
|
|
next
|
|
end
|
|
end
|
|
|
|
automation.trigger!(
|
|
"kind" => name,
|
|
"action" => action,
|
|
"post" => post,
|
|
"placeholders" => {
|
|
"topic_url" => topic.relative_url,
|
|
"topic_title" => topic.title,
|
|
},
|
|
)
|
|
end
|
|
end
|
|
|
|
def self.handle_user_updated(user, new_user: false)
|
|
return if user.id < 0
|
|
|
|
name = DiscourseAutomation::Triggers::USER_UPDATED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
once_per_user = automation.trigger_field("once_per_user")["value"]
|
|
if once_per_user &&
|
|
user.custom_fields[
|
|
DiscourseAutomation::AUTOMATION_IDS_CUSTOM_FIELD
|
|
].presence&.include?(automation.id)
|
|
next
|
|
end
|
|
|
|
new_users_only = automation.trigger_field("new_users_only")["value"]
|
|
|
|
new_user_custom_field = automation.new_user_custom_field_name
|
|
new_user ||= user.custom_fields[new_user_custom_field].present?
|
|
|
|
next if new_users_only && !new_user
|
|
|
|
required_custom_fields = automation.trigger_field("custom_fields")
|
|
user_data = {}
|
|
user_custom_fields_data = DB.query <<-SQL
|
|
SELECT uf.name AS field_name, ucf.value AS field_value
|
|
FROM user_fields uf
|
|
JOIN user_custom_fields ucf ON CONCAT('user_field_', uf.id) = ucf.name
|
|
WHERE ucf.user_id = #{user.id};
|
|
SQL
|
|
|
|
user_custom_fields_data =
|
|
user_custom_fields_data.each_with_object({}) do |obj, hash|
|
|
field_name = obj.field_name
|
|
field_value = obj.field_value
|
|
hash[field_name] = field_value
|
|
end
|
|
|
|
if required_custom_fields["value"]
|
|
if required_custom_fields["value"].any? { |field|
|
|
user_custom_fields_data[field].blank?
|
|
}
|
|
if new_users_only
|
|
user.custom_fields[new_user_custom_field] = "1"
|
|
user.save_custom_fields
|
|
end
|
|
next
|
|
end
|
|
user_data[:custom_fields] = user_custom_fields_data
|
|
end
|
|
|
|
required_user_profile_fields = automation.trigger_field("user_profile")
|
|
user_profile_data = UserProfile.find(user.id).attributes
|
|
if required_user_profile_fields["value"]
|
|
if required_user_profile_fields["value"].any? { |field|
|
|
user_profile_data[field].blank?
|
|
}
|
|
if new_users_only
|
|
user.custom_fields[new_user_custom_field] = "1"
|
|
user.save_custom_fields
|
|
end
|
|
next
|
|
end
|
|
user_data[:profile_data] = user_profile_data
|
|
end
|
|
|
|
if new_users_only && once_per_user
|
|
user.custom_fields.delete(new_user_custom_field)
|
|
user.save_custom_fields
|
|
end
|
|
|
|
automation.add_id_to_custom_field(user, DiscourseAutomation::AUTOMATION_IDS_CUSTOM_FIELD)
|
|
|
|
automation.trigger!("kind" => name, "user" => user, "user_data" => user_data)
|
|
end
|
|
end
|
|
|
|
def self.handle_category_created_edited(category, action)
|
|
name = DiscourseAutomation::Triggers::CATEGORY_CREATED_EDITED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
restricted_category = automation.trigger_field("restricted_category")
|
|
if restricted_category["value"].present?
|
|
next if restricted_category["value"] != category.parent_category_id
|
|
end
|
|
|
|
automation.trigger!("kind" => name, "action" => action, "category" => category)
|
|
end
|
|
end
|
|
|
|
def self.handle_pm_created(topic)
|
|
return if topic.user_id < 0
|
|
|
|
user = topic.user
|
|
target_usernames = topic.allowed_users.pluck(:username) - [user.username]
|
|
target_group_ids = topic.allowed_groups.pluck(:id)
|
|
return if (target_usernames.length + target_group_ids.length) > 1
|
|
|
|
name = DiscourseAutomation::Triggers::PM_CREATED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
restricted_username = automation.trigger_field("restricted_user")["value"]
|
|
next if restricted_username.present? && restricted_username != target_usernames.first
|
|
|
|
restricted_group_id = automation.trigger_field("restricted_group")["value"]
|
|
next if restricted_group_id.present? && restricted_group_id != target_group_ids.first
|
|
|
|
ignore_staff = automation.trigger_field("ignore_staff")
|
|
next if ignore_staff["value"] && user.staff?
|
|
|
|
ignore_group_members = automation.trigger_field("ignore_group_members")
|
|
next if ignore_group_members["value"] && user.in_any_groups?([restricted_group_id])
|
|
|
|
ignore_automated = automation.trigger_field("ignore_automated")
|
|
next if ignore_automated["value"] && topic.first_post.incoming_email&.is_auto_generated?
|
|
|
|
valid_trust_levels = automation.trigger_field("valid_trust_levels")
|
|
if valid_trust_levels["value"]
|
|
next if !valid_trust_levels["value"].include?(user.trust_level)
|
|
end
|
|
|
|
automation.trigger!("kind" => name, "post" => topic.first_post)
|
|
end
|
|
end
|
|
|
|
def self.handle_topic_tags_changed(topic, old_tag_names, new_tag_names, user)
|
|
name = DiscourseAutomation::Triggers::TOPIC_TAGS_CHANGED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
if topic.private_message?
|
|
next if !automation.trigger_field("trigger_with_pms")["value"]
|
|
end
|
|
|
|
watching_categories = automation.trigger_field("watching_categories")
|
|
if watching_categories["value"]
|
|
next if !watching_categories["value"].include?(topic.category_id)
|
|
end
|
|
|
|
removed_tags = old_tag_names - new_tag_names
|
|
added_tags = new_tag_names - old_tag_names
|
|
|
|
watching_tags = automation.trigger_field("watching_tags")
|
|
|
|
if watching_tags["value"]
|
|
changed_tags = (removed_tags | added_tags)
|
|
next if (changed_tags & watching_tags["value"]).empty?
|
|
end
|
|
|
|
trigger_on = automation.trigger_field("trigger_on")["value"]
|
|
|
|
if trigger_on == Triggers::TopicTagsChanged::TriggerOn::TAGS_ADDED && added_tags.empty?
|
|
next
|
|
end
|
|
|
|
if trigger_on == Triggers::TopicTagsChanged::TriggerOn::TAGS_REMOVED &&
|
|
removed_tags.empty?
|
|
next
|
|
end
|
|
|
|
automation.trigger!(
|
|
"kind" => name,
|
|
"topic" => topic,
|
|
"removed_tags" => removed_tags,
|
|
"added_tags" => added_tags,
|
|
"user" => user,
|
|
"placeholders" => {
|
|
"topic_url" => topic.relative_url,
|
|
"topic_title" => topic.title,
|
|
},
|
|
)
|
|
end
|
|
end
|
|
|
|
def self.handle_topic_closed(topic)
|
|
name = DiscourseAutomation::Triggers::TOPIC_CLOSED
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
automation.trigger!(
|
|
"kind" => name,
|
|
"topic" => topic,
|
|
"placeholders" => {
|
|
"topic_url" => topic.relative_url,
|
|
"topic_title" => topic.title,
|
|
},
|
|
)
|
|
end
|
|
end
|
|
|
|
def self.handle_after_post_cook(post, cooked)
|
|
return cooked if post.post_type != Post.types[:regular] || post.post_number > 1
|
|
|
|
name = DiscourseAutomation::Triggers::AFTER_POST_COOK
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: name, enabled: true)
|
|
.find_each do |automation|
|
|
valid_trust_levels = automation.trigger_field("valid_trust_levels")
|
|
if valid_trust_levels["value"]
|
|
next if valid_trust_levels["value"].exclude?(post.user.trust_level)
|
|
end
|
|
|
|
restricted_category = automation.trigger_field("restricted_category")
|
|
if restricted_category["value"]
|
|
category_ids = [post.topic&.category&.parent_category&.id, post.topic&.category&.id]
|
|
next if !category_ids.compact.include?(restricted_category["value"])
|
|
end
|
|
|
|
restricted_tags = automation.trigger_field("restricted_tags")
|
|
if tag_names = restricted_tags["value"]
|
|
found = false
|
|
next if !post.topic
|
|
|
|
post.topic.tags.each do |tag|
|
|
found ||= tag_names.include?(tag.name)
|
|
break if found
|
|
end
|
|
|
|
next if !found
|
|
end
|
|
|
|
if new_cooked = automation.trigger!("kind" => name, "post" => post, "cooked" => cooked)
|
|
cooked = new_cooked
|
|
end
|
|
end
|
|
|
|
cooked
|
|
end
|
|
|
|
def self.handle_user_promoted(user_id, new_trust_level, old_trust_level)
|
|
trigger = DiscourseAutomation::Triggers::USER_PROMOTED
|
|
user = User.find_by(id: user_id)
|
|
return if user.blank?
|
|
|
|
# don't want to do anything if the user is demoted. this should probably
|
|
# be a separate event in core
|
|
return if new_trust_level < old_trust_level
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: trigger, enabled: true)
|
|
.find_each do |automation|
|
|
trust_level_code_all = DiscourseAutomation::USER_PROMOTED_TRUST_LEVEL_CHOICES.first[:id]
|
|
|
|
restricted_group_id = automation.trigger_field("restricted_group")["value"]
|
|
trust_level_transition = automation.trigger_field("trust_level_transition")["value"]
|
|
trust_level_transition = trust_level_transition || trust_level_code_all
|
|
|
|
if restricted_group_id.present? &&
|
|
!GroupUser.exists?(user_id: user_id, group_id: restricted_group_id)
|
|
next
|
|
end
|
|
|
|
transition_code = "TL#{old_trust_level}#{new_trust_level}"
|
|
if trust_level_transition == trust_level_code_all ||
|
|
trust_level_transition == transition_code
|
|
automation.trigger!(
|
|
"kind" => trigger,
|
|
"usernames" => [user.username],
|
|
"placeholders" => {
|
|
"trust_level_transition" =>
|
|
I18n.t(
|
|
"discourse_automation.triggerables.user_promoted.transition_placeholder",
|
|
from_level_name: TrustLevel.name(old_trust_level),
|
|
to_level_name: TrustLevel.name(new_trust_level),
|
|
),
|
|
},
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.handle_stalled_topic(post)
|
|
return if post.topic.blank?
|
|
return if post.user_id != post.topic.user_id
|
|
|
|
DiscourseAutomation::Automation
|
|
.where(trigger: DiscourseAutomation::Triggers::STALLED_TOPIC)
|
|
.where(enabled: true)
|
|
.find_each do |automation|
|
|
fields = automation.serialized_fields
|
|
|
|
categories = fields.dig("categories", "value")
|
|
next if categories && !categories.include?(post.topic.category_id)
|
|
|
|
tags = fields.dig("tags", "value")
|
|
next if tags&.any? && (tags & post.topic.tags.map(&:name)).empty?
|
|
|
|
DiscourseAutomation::UserGlobalNotice
|
|
.where(identifier: automation.id)
|
|
.where(user_id: post.user_id)
|
|
.destroy_all
|
|
end
|
|
end
|
|
end
|
|
end
|