FEATURE: Introduce set_topic_timer automation script (#36660)

Allows different types of topic-timer to be automatically configured on
topics. It uses the 'post_created_edited' trigger, which provides all
sorts of filtering options including category/author/etc.

<img width="804" height="270" alt="SCR-20251212-mzrg"
src="https://github.com/user-attachments/assets/90cca3f8-1516-4adf-830a-c2f43125795d"
/>

---------

Co-authored-by: Régis Hanol <regis@hanol.fr>
This commit is contained in:
David Taylor
2025-12-23 09:46:52 +00:00
committed by GitHub
co-authored by Régis Hanol
parent cd6c6f8abc
commit b7a4eadf74
10 changed files with 272 additions and 44 deletions
@@ -16,6 +16,7 @@ import DaMessageField from "./fields/da-message-field";
import DaPeriodField from "./fields/da-period-field";
import DaPmsField from "./fields/da-pms-field";
import DaPostField from "./fields/da-post-field";
import DaRelativeTimeField from "./fields/da-relative-time-field";
import DaTagsField from "./fields/da-tags-field";
import DaTextField from "./fields/da-text-field";
import DaTextListField from "./fields/da-text-list-field";
@@ -48,6 +49,7 @@ const FIELD_COMPONENTS = {
email_group_user: DaEmailGroupUserField,
custom_field: DaCustomField,
custom_fields: DaCustomFields,
relative_time: DaRelativeTimeField,
};
export default class AutomationField extends Component {
@@ -0,0 +1,25 @@
import RelativeTimePicker from "discourse/components/relative-time-picker";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
export default class RelativeTimeField extends BaseField {
<template>
<section class="field text-field">
<div class="control-group">
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<div class="field-wrapper">
<RelativeTimePicker
@durationMinutes={{@field.metadata.value}}
@onChange={{this.mutValue}}
/>
<DAFieldDescription @description={{@description}} />
</div>
</div>
</div>
</section>
</template>
}
@@ -240,6 +240,11 @@ module DiscourseAutomation
},
},
},
"relative_time" => {
"value" => {
"type" => "integer",
},
},
}
private
@@ -434,6 +434,13 @@ en:
fields:
custom_field_name:
label: "User Custom Field name"
set_topic_timer:
title: Set Topic Timer
fields:
type:
label: Timer Type
duration:
label: Timer Duration
unnamed_automation: "Unnamed automation"
models:
@@ -182,3 +182,6 @@ en:
random_assign:
title: "Random Assign"
description: "Randomly assign topics to a group"
set_topic_timer:
title: "Set Topic Timer"
description: "Set a topic timer on a topic"
@@ -17,6 +17,7 @@ module DiscourseAutomation
POST = "post"
REMOVE_UPLOAD_MARKUP_FROM_DELETED_POSTS = "remove_upload_markup_from_deleted_posts"
SEND_PMS = "send_pms"
SET_TOPIC_TIMER = "set_topic_timer"
SUSPEND_USER_BY_EMAIL = "suspend_user_by_email"
TOPIC = "topic"
TOPIC_REQUIRED_WORDS = "topic_required_words"
@@ -0,0 +1,59 @@
# frozen_string_literal: true
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::SET_TOPIC_TIMER) do
field :type,
component: :choices,
extra: {
content: [
{ id: "auto_close", name: "topic.auto_close.title" },
{ id: "auto_close_after_last_post", name: "topic.auto_close_after_last_post.title" },
{ id: "auto_delete", name: "topic.auto_delete.title" },
{ id: "auto_delete_replies", name: "topic.auto_delete_replies.title" },
{ id: "auto_bump", name: "topic.auto_bump.title" },
],
},
required: true
field :duration, component: :relative_time, required: true
version 1
triggerables %i[post_created_edited]
script do |context, fields|
post = context["post"]
timer_type = fields.dig("type", "value")
next if !post.topic
next unless topic = Topic.find_by(id: post.topic_id)
duration_minutes = fields.dig("duration", "value")
case timer_type
when "auto_close"
topic.set_or_create_timer(
TopicTimer.types[:close],
(Time.now + duration_minutes.minutes).iso8601,
)
when "auto_close_after_last_post"
topic.set_or_create_timer(
TopicTimer.types[:close],
nil,
based_on_last_post: true,
duration_minutes:,
)
when "auto_delete"
topic.set_or_create_timer(
TopicTimer.types[:delete],
(Time.now + duration_minutes.minutes).iso8601,
)
when "auto_delete_replies"
topic.set_or_create_timer(TopicTimer.types[:delete_replies], nil, duration_minutes:)
when "auto_bump"
topic.set_or_create_timer(
TopicTimer.types[:bump],
(Time.now + duration_minutes.minutes).iso8601,
)
end
end
end
+6 -44
View File
@@ -43,50 +43,12 @@ end
require_relative "lib/discourse_automation/engine"
after_initialize do
%w[
lib/discourse_automation/scripts
lib/discourse_automation/scripts/add_user_to_group_through_custom_field
lib/discourse_automation/scripts/append_last_checked_by
lib/discourse_automation/scripts/append_last_edited_by
lib/discourse_automation/scripts/auto_responder
lib/discourse_automation/scripts/auto_tag_topic
lib/discourse_automation/scripts/banner_topic
lib/discourse_automation/scripts/close_topic
lib/discourse_automation/scripts/flag_post_on_words
lib/discourse_automation/scripts/email_on_flagged_post
lib/discourse_automation/scripts/gift_exchange
lib/discourse_automation/scripts/group_category_notification_default
lib/discourse_automation/scripts/pin_topic
lib/discourse_automation/scripts/post
lib/discourse_automation/scripts/remove_upload_markup_from_deleted_posts
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
lib/discourse_automation/scripts/user_global_notice
lib/discourse_automation/scripts/user_group_membership_through_badge
lib/discourse_automation/scripts/zapier_webhook
lib/discourse_automation/triggers
lib/discourse_automation/triggers/after_post_cook
lib/discourse_automation/triggers/api_call
lib/discourse_automation/triggers/category_created_edited
lib/discourse_automation/triggers/pm_created
lib/discourse_automation/triggers/point_in_time
lib/discourse_automation/triggers/post_created_edited
lib/discourse_automation/triggers/post_flag_created
lib/discourse_automation/triggers/recurring
lib/discourse_automation/triggers/stalled_topic
lib/discourse_automation/triggers/stalled_wiki
lib/discourse_automation/triggers/topic_tags_changed
lib/discourse_automation/triggers/topic
lib/discourse_automation/triggers/topic_closed
lib/discourse_automation/triggers/user_added_to_group
lib/discourse_automation/triggers/user_badge_granted
lib/discourse_automation/triggers/user_promoted
lib/discourse_automation/triggers/user_removed_from_group
lib/discourse_automation/triggers/user_first_logged_in
lib/discourse_automation/triggers/user_updated
].each { |path| require_relative path }
Dir
.glob("./lib/discourse_automation/scripts/*.rb", base: __dir__)
.each { |file| require_relative file }
Dir
.glob("./lib/discourse_automation/triggers/*.rb", base: __dir__)
.each { |file| require_relative file }
reloadable_patch do
Post.prepend DiscourseAutomation::PostExtension
@@ -0,0 +1,126 @@
# frozen_string_literal: true
describe "SetTopicTimer" do
fab!(:automation) do
Fabricate(
:automation,
script: DiscourseAutomation::Scripts::SET_TOPIC_TIMER,
trigger: DiscourseAutomation::Triggers::POST_CREATED_EDITED,
)
end
def configure_automation(type, duration)
automation.fields.create!(
component: "choices",
name: "type",
metadata: {
value: type,
},
target: "script",
)
automation.fields.create!(
component: "relative_time",
name: "duration",
metadata: {
value: duration,
},
target: "script",
)
end
before { SiteSetting.discourse_automation_enabled = true }
it "handles auto_close timer" do
configure_automation("auto_close", 60)
freeze_time do
post =
PostCreator.new(
Fabricate(:admin),
raw: "my new topic",
title: "Test topic for timer",
).create!
timer = post.topic.reload.topic_timers[0]
expect(timer.status_type).to eq(TopicTimer.types[:close])
expect(timer.execute_at).to be_within(1.second).of(60.minutes.from_now)
expect(timer.based_on_last_post).to eq(false)
end
end
it "handles auto_close_after_last_post timer" do
configure_automation("auto_close_after_last_post", 60)
freeze_time do
post =
PostCreator.new(
Fabricate(:admin),
raw: "my new topic",
title: "Test topic for timer",
).create!
timer = post.topic.reload.topic_timers[0]
expect(timer.status_type).to eq(TopicTimer.types[:close])
expect(timer.execute_at).to be_within(1.second).of(60.minutes.from_now)
expect(timer.duration_minutes).to eq(60)
expect(timer.based_on_last_post).to eq(true)
end
end
it "handles auto_delete timer" do
configure_automation("auto_delete", 60)
freeze_time do
post =
PostCreator.new(
Fabricate(:admin),
raw: "my new topic",
title: "Test topic for timer",
).create!
timer = post.topic.reload.topic_timers[0]
expect(timer.status_type).to eq(TopicTimer.types[:delete])
expect(timer.execute_at).to be_within(1.second).of(60.minutes.from_now)
expect(timer.duration_minutes).to eq(nil)
expect(timer.based_on_last_post).to eq(false)
end
end
it "handles auto_delete_replies timer" do
configure_automation("auto_delete_replies", 60)
freeze_time do
post =
PostCreator.new(
Fabricate(:admin),
raw: "my new topic",
title: "Test topic for timer",
).create!
timer = post.topic.reload.topic_timers[0]
expect(timer.status_type).to eq(TopicTimer.types[:delete_replies])
expect(timer.execute_at).to be_within(1.second).of(60.minutes.from_now)
expect(timer.duration_minutes).to eq(60)
expect(timer.based_on_last_post).to eq(false)
end
end
it "handles auto_bump timer" do
configure_automation("auto_bump", 60)
freeze_time do
post =
PostCreator.new(
Fabricate(:admin),
raw: "my new topic",
title: "Test topic for timer",
).create!
timer = post.topic.reload.topic_timers[0]
expect(timer.status_type).to eq(TopicTimer.types[:bump])
expect(timer.execute_at).to be_within(1.second).of(60.minutes.from_now)
expect(timer.duration_minutes).to eq(nil)
expect(timer.based_on_last_post).to eq(false)
end
end
end
@@ -0,0 +1,38 @@
import { getOwner } from "@ember/owner";
import { fillIn, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import AutomationField from "discourse/plugins/automation/admin/components/automation-field";
import AutomationFabricators from "discourse/plugins/automation/admin/lib/fabricators";
module("Integration | Component | da-relative_time-field", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.automation = new AutomationFabricators(getOwner(this)).automation();
});
test("set value", async function (assert) {
this.field = new AutomationFabricators(getOwner(this)).field({
component: "relative_time",
});
await render(
<template>
<AutomationField
@automation={{this.automation}}
@field={{this.field}}
/>
</template>
);
await fillIn(".relative-time-duration", "4");
assert.strictEqual(this.field.metadata.value, 4);
await selectKit().expand();
await selectKit().selectRowByValue("hours");
assert.strictEqual(this.field.metadata.value, 4 * 60);
});
});