From 84b4e4bddfa6bb9bf9059a8531f229f55790ddb7 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Tue, 9 Apr 2024 04:21:31 +0300 Subject: [PATCH] FEATURE: Add 'Create topic' automation script (#26552) This commit adds a new automation script for creating topics. It's very similar to the existing 'create a post' automation, except that it posts new topics in a specific category and with optional tags. Internal topic: t/125829. --- .../admin/components/placeholders-list.gjs | 2 +- .../automation/config/locales/client.en.yml | 13 ++ .../automation/config/locales/server.en.yml | 3 + .../lib/discourse_automation/scripts.rb | 1 + .../lib/discourse_automation/scripts/topic.rb | 76 ++++++++ plugins/automation/plugin.rb | 1 + plugins/automation/spec/scripts/topic_spec.rb | 171 ++++++++++++++++++ 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 plugins/automation/lib/discourse_automation/scripts/topic.rb create mode 100644 plugins/automation/spec/scripts/topic_spec.rb diff --git a/plugins/automation/admin/assets/javascripts/admin/components/placeholders-list.gjs b/plugins/automation/admin/assets/javascripts/admin/components/placeholders-list.gjs index 110a5dc2dac..2fca8d7679c 100644 --- a/plugins/automation/admin/assets/javascripts/admin/components/placeholders-list.gjs +++ b/plugins/automation/admin/assets/javascripts/admin/components/placeholders-list.gjs @@ -18,6 +18,6 @@ export default class PlaceholdersList extends Component { @action copyPlaceholder(placeholder) { - this.args.onCopy(`${this.args.currentValue}{{${placeholder}}}`); + this.args.onCopy(`${this.args.currentValue || ""}{{${placeholder}}}`); } } diff --git a/plugins/automation/config/locales/client.en.yml b/plugins/automation/config/locales/client.en.yml index 27d7c793205..8e68eef23b7 100644 --- a/plugins/automation/config/locales/client.en.yml +++ b/plugins/automation/config/locales/client.en.yml @@ -244,6 +244,19 @@ en: label: Topic ID post: label: Post content + topic: + fields: + creator: + label: Creator + updated_user_context: The updated user + body: + label: Topic body + title: + label: Topic title + category: + label: Topic category + tags: + label: Topic tags group_category_notification_default: fields: group: diff --git a/plugins/automation/config/locales/server.en.yml b/plugins/automation/config/locales/server.en.yml index 220b1a04796..98c6cb25577 100644 --- a/plugins/automation/config/locales/server.en.yml +++ b/plugins/automation/config/locales/server.en.yml @@ -63,6 +63,9 @@ en: post: title: Create a post description: Create a post on a specified topic + topic: + title: Create a topic + description: Create a topic as a specific user flag_post_on_words: title: Flag post on words description: Flags a post if it contains specified words diff --git a/plugins/automation/lib/discourse_automation/scripts.rb b/plugins/automation/lib/discourse_automation/scripts.rb index 944ab47aca8..208f4b31926 100644 --- a/plugins/automation/lib/discourse_automation/scripts.rb +++ b/plugins/automation/lib/discourse_automation/scripts.rb @@ -16,6 +16,7 @@ module DiscourseAutomation POST = "post" SEND_PMS = "send_pms" SUSPEND_USER_BY_EMAIL = "suspend_user_by_email" + TOPIC = "topic" TOPIC_REQUIRED_WORDS = "topic_required_words" USER_GLOBAL_NOTICE = "user_global_notice" USER_GROUP_MEMBERSHIP_THROUGH_BADGE = "user_group_membership_through_badge" diff --git a/plugins/automation/lib/discourse_automation/scripts/topic.rb b/plugins/automation/lib/discourse_automation/scripts/topic.rb new file mode 100644 index 00000000000..40701e90478 --- /dev/null +++ b/plugins/automation/lib/discourse_automation/scripts/topic.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::TOPIC) do + version 1 + + field :creator, component: :user + field :creator, component: :user, triggerable: :user_updated, accepted_contexts: [:updated_user] + + field :body, component: :post, required: true, accepts_placeholders: true + field :title, component: :text, required: true, accepts_placeholders: true + field :category, component: :category, required: true + field :tags, component: :tags + + 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 + + placeholders = { creator_username: creator_username }.merge(context["placeholders"] || {}) + + 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 + + topic_raw = fields.dig("body", "value") + topic_raw = utils.apply_placeholders(topic_raw, placeholders) + + title = fields.dig("title", "value") + title = utils.apply_placeholders(title, placeholders) + + creator = User.find_by(username: creator_username) + if !creator + Rails.logger.warn "[discourse-automation] creator with username: `#{creator_username}` was not found" + next + end + + category_id = fields.dig("category", "value") + category = Category.find_by(id: category_id) + if !category + Rails.logger.warn "[discourse-automation] category of id: `#{category_id}` was not found" + next + end + + tags = fields.dig("tags", "value") || [] + new_post = + PostCreator.new( + creator, + raw: topic_raw, + title: title, + category: category.id, + tags: tags, + ).create! + + if context["kind"] == DiscourseAutomation::Triggers::USER_UPDATED && new_post.persisted? + user.user_custom_fields.create(name: automation.name, value: "true") + end + end +end diff --git a/plugins/automation/plugin.rb b/plugins/automation/plugin.rb index 8d7026ac937..ed8f9d08f76 100644 --- a/plugins/automation/plugin.rb +++ b/plugins/automation/plugin.rb @@ -59,6 +59,7 @@ after_initialize do lib/discourse_automation/scripts/group_category_notification_default lib/discourse_automation/scripts/pin_topic lib/discourse_automation/scripts/post + lib/discourse_automation/scripts/topic lib/discourse_automation/scripts/send_pms lib/discourse_automation/scripts/suspend_user_by_email lib/discourse_automation/scripts/topic_required_words diff --git a/plugins/automation/spec/scripts/topic_spec.rb b/plugins/automation/spec/scripts/topic_spec.rb new file mode 100644 index 00000000000..54913a2b4e5 --- /dev/null +++ b/plugins/automation/spec/scripts/topic_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require_relative "../discourse_automation_helper" + +describe "Topic" do + let!(:raw) { "this is me testing a new topic by automation" } + let!(:title) { "This is a new topic created by automation" } + fab!(:category) { Fabricate(:category) } + fab!(:tag1) { Fabricate(:tag) } + fab!(:tag2) { Fabricate(:tag) } + + before { SiteSetting.discourse_automation_enabled = true } + + context "when using point_in_time trigger" do + fab!(:automation) do + Fabricate( + :automation, + script: DiscourseAutomation::Scripts::TOPIC, + trigger: DiscourseAutomation::Triggers::POINT_IN_TIME, + ) + end + + before do + automation.upsert_field!( + "execute_at", + "date_time", + { value: 3.hours.from_now }, + target: "trigger", + ) + automation.upsert_field!("title", "text", { value: title }, target: "script") + automation.upsert_field!("body", "post", { value: raw }, target: "script") + automation.upsert_field!( + "category", + "category", + { value: category.id.to_s }, + target: "script", + ) + end + + it "creates expected topic" do + freeze_time 6.hours.from_now do + expect { + Jobs::DiscourseAutomationTracker.new.execute + + topic = Topic.last + expect(topic.category.id).to eq(category.id) + expect(topic.title).to eq(title) + expect(topic.posts.first.raw).to eq(raw) + }.to change { Topic.count }.by(1) + end + end + end + + context "when using recurring trigger" do + fab!(:automation) do + Fabricate( + :automation, + script: DiscourseAutomation::Scripts::TOPIC, + trigger: DiscourseAutomation::Triggers::RECURRING, + ) + end + + before do + automation.upsert_field!("title", "text", { value: title }, target: "script") + automation.upsert_field!("body", "post", { value: raw }, target: "script") + automation.upsert_field!( + "category", + "category", + { value: category.id.to_s }, + target: "script", + ) + end + + it "creates expected topic" do + expect { + automation.trigger! + + topic = Topic.last + expect(topic.category.id).to eq(category.id) + expect(topic.title).to eq(title) + expect(topic.posts.first.raw).to eq(raw) + }.to change { Topic.count }.by(1) + end + end + + context "when using user_updated trigger" do + fab!(:user_field_1) { Fabricate(:user_field, name: "custom field 1") } + fab!(:user_field_2) { Fabricate(:user_field, name: "custom field 2") } + + fab!(:user) do + user = Fabricate(:user, trust_level: TrustLevel[0]) + user.set_user_field(user_field_1.id, "Answer custom 1") + user.set_user_field(user_field_2.id, "Answer custom 2") + user.user_profile.location = "Japan" + user.user_profile.save + user.save + user + end + + fab!(:automation) do + automation = + Fabricate( + :automation, + script: DiscourseAutomation::Scripts::TOPIC, + trigger: DiscourseAutomation::Triggers::USER_UPDATED, + ) + automation.upsert_field!( + "custom_fields", + "custom_fields", + { value: ["custom field 1", "custom field 2"] }, + target: "trigger", + ) + automation.upsert_field!( + "user_profile", + "user_profile", + { value: ["location"] }, + target: "trigger", + ) + automation + end + let!(:user_raw_post) do + "This is a raw test post for user custom field 1: {{custom_field_1}}, custom field 2: {{custom_field_2}} and location: {{location}}" + end + let!(:placeholder_applied_user_raw_post) do + "This is a raw test post for user custom field 1: #{user.custom_fields["user_field_#{user_field_1.id}"]}, custom field 2: #{user.custom_fields["user_field_#{user_field_2.id}"]} and location: #{user.user_profile.location}" + end + + before do + automation.upsert_field!( + "title", + "text", + { value: "{{custom_field_1}} {{location}} this is a title" }, + target: "script", + ) + automation.upsert_field!("body", "post", { value: user_raw_post }, target: "script") + automation.upsert_field!( + "category", + "category", + { value: category.id.to_s }, + target: "script", + ) + automation.upsert_field!("tags", "tags", { value: %w[feedback automation] }, target: "script") + end + + it "creates a topic correctly" do + expect { + UserUpdater.new(user, user).update(location: "Japan") + + topic = Topic.last + expect(topic.category.id).to eq(category.id) + expect(topic.title).to eq( + "#{user.custom_fields["user_field_#{user_field_1.id}"]} #{user.user_profile.location} this is a title", + ) + expect(topic.posts.first.raw).to eq(placeholder_applied_user_raw_post) + expect(topic.tags.pluck(:name)).to contain_exactly("feedback", "automation") + }.to change { Topic.count }.by(1) + end + + context "when creator is one of accepted context" do + before do + automation.upsert_field!("creator", "user", { value: "updated_user" }, target: "script") + end + + it "sets the creator to the topic creator" do + expect { UserUpdater.new(user, user).update(location: "Japan") }.to change { + Topic.where(user_id: user.id).count + }.by(1) + end + end + end +end