mirror of
https://github.com/discourse/discourse.git
synced 2026-07-31 16:48:26 -05:00
FEATURE: New automation for emailing specific users when a post is flagged (#36580)
This commit adds a new "Email on flagged post" automation which can be configured to send emails to users when a post is flagged. The automation can be configured to trigger only when posts are flagged in certain categories or only when certain tags are present on the post's topic. The body of the email sent can also be configured with the following placeholders available: * topic_url * topic_title * post_url * post_number * flagger_username * flagged_username * flag_type * category * tags * post_excerpt
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
module DiscourseAutomation
|
||||
class SendFlagEmail < ::Jobs::Base
|
||||
def execute(args)
|
||||
post_action = PostAction.find_by(id: args[:post_action_id])
|
||||
return if post_action.blank?
|
||||
|
||||
post = post_action&.post
|
||||
return if post.blank?
|
||||
|
||||
email = args[:email]
|
||||
raise Discourse::InvalidParameters.new(:email) if email.blank?
|
||||
|
||||
email_template =
|
||||
::DiscourseAutomation::Field.where(id: args[:email_template_automation_field_id]).pick(
|
||||
:metadata,
|
||||
)[
|
||||
"value"
|
||||
]
|
||||
|
||||
if email_template.blank?
|
||||
raise Discourse::InvalidParameters.new(:email_template_automation_field_id)
|
||||
end
|
||||
|
||||
placeholders = build_placeholders(post_action, post)
|
||||
|
||||
subject =
|
||||
I18n.t(
|
||||
"discourse_automation.scriptables.email_on_flagged_post.subject",
|
||||
topic_title: placeholders[:topic_title],
|
||||
flagger_username: placeholders[:flagger_username],
|
||||
)
|
||||
|
||||
body =
|
||||
::DiscourseAutomation::Scriptable::Utils.apply_placeholders(email_template, placeholders)
|
||||
|
||||
::DiscourseAutomation::FlagMailer.send_flag_email(email, subject:, body:).deliver_now
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_placeholders(post_action, post)
|
||||
topic = post.topic
|
||||
flagger = post_action.user
|
||||
target_user = post.user
|
||||
|
||||
{
|
||||
topic_url: topic.url,
|
||||
post_url: post.full_url,
|
||||
topic_title: topic&.title,
|
||||
post_number: post.post_number,
|
||||
flagger_username: flagger.username,
|
||||
flagged_username: target_user.username,
|
||||
flag_type: PostActionTypeView.new.names[post_action.post_action_type_id],
|
||||
category: topic.category&.name,
|
||||
tags: topic.tags&.map(&:name)&.join(", "),
|
||||
site_title: SiteSetting.title,
|
||||
post_excerpt: post.excerpt_for_topic,
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAutomation
|
||||
class FlagMailer < ActionMailer::Base
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
def send_flag_email(to_address, subject:, body:)
|
||||
build_email(
|
||||
to_address,
|
||||
subject: subject,
|
||||
body: body,
|
||||
add_unsubscribe_link: false,
|
||||
allow_reply_by_email: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -100,6 +100,7 @@ module DiscourseAutomation
|
||||
def upsert_field!(name, component, metadata, target: "script")
|
||||
field = fields.find_or_initialize_by(name: name, component: component, target: target)
|
||||
field.update!(metadata: metadata)
|
||||
field
|
||||
end
|
||||
|
||||
def self.deserialize_context(context)
|
||||
|
||||
@@ -61,6 +61,14 @@ en:
|
||||
text:
|
||||
label: Text
|
||||
triggerables:
|
||||
post_flag_created:
|
||||
fields:
|
||||
categories:
|
||||
label: Categories
|
||||
description: Only trigger when the flagged topic is in these categories
|
||||
tags:
|
||||
label: Tags
|
||||
description: Only trigger when the flagged topic has any of these tags
|
||||
not_found: Couldn’t find trigger `%{trigger}` for automation `%{automation}`, ensure the associated plugin is installed
|
||||
user_badge_granted:
|
||||
fields:
|
||||
@@ -271,6 +279,14 @@ en:
|
||||
label: Tags
|
||||
description: Optional, will trigger only if the post has any of these tags
|
||||
scriptables:
|
||||
email_on_flagged_post:
|
||||
fields:
|
||||
email_template:
|
||||
label: Email template
|
||||
description: Supports automation placeholders
|
||||
recipients:
|
||||
label: Recipients
|
||||
description: Users or email addresses to notify
|
||||
not_found: Couldn’t find script `%{script}` for automation `%{automation}`, ensure the associated plugin is installed
|
||||
zapier_webhook:
|
||||
fields:
|
||||
|
||||
@@ -16,6 +16,9 @@ en:
|
||||
invalid_field: Field’s component `%{component}` is not usable on `%{target}:%{target_name}`.
|
||||
invalid_metadata: Data for `%{field}` is invalid or component `%{component}` is unknown.
|
||||
triggerables:
|
||||
post_flag_created:
|
||||
title: Post flag created
|
||||
description: Triggers an automation when a post is flagged
|
||||
errors:
|
||||
custom_fields_or_user_profile_required: "At least one of 'custom_fields' or 'user_profile' must be provided."
|
||||
user_badge_granted:
|
||||
@@ -74,6 +77,24 @@ en:
|
||||
title: After user update
|
||||
description: When a user updates any information the automation will be triggered
|
||||
scriptables:
|
||||
email_on_flagged_post:
|
||||
title: Email on flagged post
|
||||
description: Send a custom email whenever a flag is created on a post
|
||||
subject: "Flagged post: %{topic_title} (%{flagger_username})"
|
||||
default_template: |-
|
||||
A post has been flagged.
|
||||
|
||||
Topic: {{topic_title}} ({{topic_url}})
|
||||
Post: {{post_url}} (#{{post_number}})
|
||||
Flag type: {{flag_type}}
|
||||
Flagged user: {{flagged_username}}
|
||||
Flagged by: {{flagger_username}}
|
||||
Category: {{category}}
|
||||
Tags: {{tags}}
|
||||
|
||||
--------
|
||||
Post excerpt:
|
||||
{{post_excerpt}}
|
||||
post:
|
||||
title: Create a post
|
||||
description: Create a post on a specified topic
|
||||
|
||||
@@ -430,5 +430,31 @@ module DiscourseAutomation
|
||||
.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
def self.handle_post_flag_created(post_action)
|
||||
name = DiscourseAutomation::Triggers::POST_FLAG_CREATED
|
||||
|
||||
post = post_action.post
|
||||
topic = post.topic
|
||||
|
||||
DiscourseAutomation::Automation
|
||||
.where(trigger: name, enabled: true)
|
||||
.find_each do |automation|
|
||||
flag_type_field = automation.trigger_field("flag_type")
|
||||
flag_type_id = flag_type_field["value"]
|
||||
|
||||
next if flag_type_id.present? && flag_type_id != post_action.post_action_type_id
|
||||
|
||||
categories = automation.trigger_field("categories")["value"]
|
||||
next if categories.present? && !categories.include?(topic.category_id)
|
||||
|
||||
if SiteSetting.tagging_enabled?
|
||||
tags = automation.trigger_field("tags")["value"]
|
||||
next if tags.present? && (topic.tags.map(&:name) & tags).empty?
|
||||
end
|
||||
|
||||
automation.trigger!("kind" => name, "post_action_id" => post_action.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ module DiscourseAutomation
|
||||
AUTO_TAG_TOPIC = "auto_tag_topic"
|
||||
BANNER_TOPIC = "banner_topic"
|
||||
CLOSE_TOPIC = "close_topic"
|
||||
EMAIL_ON_FLAGGED_POST = "email_on_flagged_post"
|
||||
FLAG_POST_ON_WORDS = "flag_post_on_words"
|
||||
GIFT_EXCHANGE = "gift_exchange"
|
||||
GROUP_CATEGORY_NOTIFICATION_DEFAULT = "group_category_notification_default"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
DiscourseAutomation::Scriptable.add(DiscourseAutomation::Scripts::EMAIL_ON_FLAGGED_POST) do
|
||||
version 1
|
||||
run_in_background if !Rails.env.test?
|
||||
|
||||
triggerables [DiscourseAutomation::Triggers::POST_FLAG_CREATED]
|
||||
|
||||
field :email_template,
|
||||
component: :message,
|
||||
required: true,
|
||||
accepts_placeholders: true,
|
||||
default_value:
|
||||
I18n.t("discourse_automation.scriptables.email_on_flagged_post.default_template")
|
||||
|
||||
field :recipients, component: :users, required: true
|
||||
|
||||
script do |context, fields, automation|
|
||||
recipients = fields.dig("recipients", "value").uniq
|
||||
next if recipients.blank?
|
||||
|
||||
to_emails = []
|
||||
to_emails = to_emails.concat(recipients.select { |r| Email.is_valid?(r) })
|
||||
to_users = recipients - to_emails
|
||||
|
||||
if to_users.present?
|
||||
primary_emails = User.includes(:primary_email).filter_by_username(to_users).map(&:email)
|
||||
to_emails.concat(primary_emails)
|
||||
end
|
||||
|
||||
to_emails.uniq!
|
||||
|
||||
automation_email_template_field_id = automation.fields.where(name: "email_template").pick(:id)
|
||||
|
||||
to_emails.each do |email|
|
||||
Jobs.enqueue(
|
||||
Jobs::DiscourseAutomation::SendFlagEmail,
|
||||
email:,
|
||||
email_template_automation_field_id: automation_email_template_field_id,
|
||||
post_action_id: context["post_action_id"],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ module DiscourseAutomation
|
||||
AFTER_POST_COOK = "after_post_cook"
|
||||
API_CALL = "api_call"
|
||||
CATEGORY_CREATED_EDITED = "category_created_edited"
|
||||
POST_FLAG_CREATED = "post_flag_created"
|
||||
PM_CREATED = "pm_created"
|
||||
TOPIC_TAGS_CHANGED = "topic_tags_changed"
|
||||
POINT_IN_TIME = "point_in_time"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
DiscourseAutomation::Triggerable.add(DiscourseAutomation::Triggers::POST_FLAG_CREATED) do
|
||||
field :categories, component: :categories
|
||||
field :tags, component: :tags
|
||||
|
||||
placeholder :topic_url
|
||||
placeholder :topic_title
|
||||
placeholder :post_url
|
||||
placeholder :post_number
|
||||
placeholder :flagger_username
|
||||
placeholder :flagged_username
|
||||
placeholder :flag_type
|
||||
placeholder :category
|
||||
placeholder :tags
|
||||
placeholder :post_excerpt
|
||||
end
|
||||
@@ -52,6 +52,7 @@ after_initialize do
|
||||
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
|
||||
@@ -71,6 +72,7 @@ after_initialize do
|
||||
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
|
||||
@@ -206,6 +208,10 @@ after_initialize do
|
||||
DiscourseAutomation::EventHandlers.handle_post_created_edited(post, :edit)
|
||||
end
|
||||
|
||||
on(:flag_created) do |post_action|
|
||||
DiscourseAutomation::EventHandlers.handle_post_flag_created(post_action) if post_action.post
|
||||
end
|
||||
|
||||
on(:category_created) do |category|
|
||||
DiscourseAutomation::EventHandlers.handle_category_created_edited(category, :create)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Jobs::DiscourseAutomation::SendFlagEmail do
|
||||
fab!(:topic)
|
||||
fab!(:post) { Fabricate(:post, topic:) }
|
||||
|
||||
fab!(:email_template_automation_field) do
|
||||
field = nil
|
||||
|
||||
Fabricate(
|
||||
:automation,
|
||||
script: DiscourseAutomation::Scripts::EMAIL_ON_FLAGGED_POST,
|
||||
trigger: DiscourseAutomation::Triggers::POST_FLAG_CREATED,
|
||||
).tap do |automation|
|
||||
field =
|
||||
automation.upsert_field!(
|
||||
"email_template",
|
||||
"message",
|
||||
{
|
||||
value:
|
||||
I18n.t("discourse_automation.scriptables.email_on_flagged_post.default_template"),
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
field
|
||||
end
|
||||
|
||||
fab!(:post_action) do
|
||||
Fabricate(:post_action, post:, post_action_type_id: PostActionType.types[:spam])
|
||||
end
|
||||
|
||||
describe "#execute" do
|
||||
it "delivers email to the right recipient with the right subject and body" do
|
||||
expect do
|
||||
described_class.new.execute(
|
||||
email: "recipient@example.com",
|
||||
email_template_automation_field_id: email_template_automation_field.id,
|
||||
post_action_id: post_action.id,
|
||||
)
|
||||
end.to change { ActionMailer::Base.deliveries.size }.by(1)
|
||||
|
||||
mail = ActionMailer::Base.deliveries.last
|
||||
|
||||
expect(mail.subject).to eq(
|
||||
I18n.t(
|
||||
"discourse_automation.scriptables.email_on_flagged_post.subject",
|
||||
topic_title: topic.title,
|
||||
flagger_username: post_action.user.username,
|
||||
),
|
||||
)
|
||||
|
||||
expect(mail.body.to_s).to eq(<<~BODY.chomp)
|
||||
A post has been flagged.
|
||||
|
||||
Topic: #{topic.title} (#{topic.url})
|
||||
Post: #{post.full_url} (##{post.post_number})
|
||||
Flag type: #{PostActionTypeView.new.names[post_action.post_action_type_id]}
|
||||
Flagged user: #{post.user.username}
|
||||
Flagged by: #{post_action.user.username}
|
||||
Category: #{post.topic.category.name}
|
||||
Tags: #{post.topic.tags.pluck(:name).join(", ")}
|
||||
|
||||
--------
|
||||
Post excerpt:
|
||||
#{post.excerpt_for_topic}
|
||||
BODY
|
||||
end
|
||||
|
||||
it "does not deliver email if `post_action_id` is invalid" do
|
||||
expect do
|
||||
described_class.new.execute(
|
||||
email: "recipient@example.com",
|
||||
email_template_automation_field_id: email_template_automation_field.id,
|
||||
post_action_id: -999_999,
|
||||
)
|
||||
end.not_to change { ActionMailer::Base.deliveries.size }
|
||||
end
|
||||
|
||||
it "does not deliver email if post associated with the post action has been destroyed" do
|
||||
post_action.post.destroy!
|
||||
|
||||
expect do
|
||||
described_class.new.execute(
|
||||
email: "recipient@example.com",
|
||||
email_template_automation_field_id: email_template_automation_field.id,
|
||||
post_action_id: post_action.id,
|
||||
)
|
||||
end.not_to change { ActionMailer::Base.deliveries.size }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "EmailOnFlaggedPost" do
|
||||
fab!(:recipient) { Fabricate(:user, email: "recipient@example.com") }
|
||||
fab!(:user_2, :user)
|
||||
fab!(:flagger) { Fabricate(:user, trust_level: TrustLevel[2]) }
|
||||
fab!(:second_flagger) { Fabricate(:user, trust_level: TrustLevel[2]) }
|
||||
fab!(:topic)
|
||||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||||
|
||||
fab!(:automation) do
|
||||
Fabricate(
|
||||
:automation,
|
||||
script: DiscourseAutomation::Scripts::EMAIL_ON_FLAGGED_POST,
|
||||
trigger: DiscourseAutomation::Triggers::POST_FLAG_CREATED,
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.discourse_automation_enabled = true
|
||||
SiteSetting.disable_emails = "no"
|
||||
end
|
||||
|
||||
it "rejects email addresses that are invalid" do
|
||||
automation.upsert_field!("recipients", "users", { value: ["@invalid-email"] })
|
||||
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
end.not_to change { Jobs::DiscourseAutomation::SendFlagEmail.jobs.length }
|
||||
end
|
||||
|
||||
it "rejects usernames that do not exist" do
|
||||
automation.upsert_field!("recipients", "users", { value: ["nonexistent_user"] })
|
||||
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
end.not_to change { Jobs::DiscourseAutomation::SendFlagEmail.jobs.length }
|
||||
end
|
||||
|
||||
it "deduplicates email addresses" do
|
||||
automation.upsert_field!("recipients", "users", { value: [recipient.email, recipient.email] })
|
||||
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
end.to change { Jobs::DiscourseAutomation::SendFlagEmail.jobs.length }.by(1)
|
||||
end
|
||||
|
||||
it "enqueues a job per recipient with placeholders applied" do
|
||||
automation.upsert_field!("recipients", "users", { value: [recipient.email, user_2.username] })
|
||||
|
||||
field =
|
||||
automation.upsert_field!(
|
||||
"email_template",
|
||||
"message",
|
||||
{
|
||||
value: I18n.t("discourse_automation.scriptables.email_on_flagged_post.default_template"),
|
||||
},
|
||||
)
|
||||
|
||||
result = nil
|
||||
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
end.to change { Jobs::DiscourseAutomation::SendFlagEmail.jobs.length }.by(2)
|
||||
|
||||
expect_job_enqueued(
|
||||
job: Jobs::DiscourseAutomation::SendFlagEmail,
|
||||
args: {
|
||||
email: recipient.email,
|
||||
email_template_automation_field_id: field.id,
|
||||
post_action_id: result.post_action.id,
|
||||
},
|
||||
)
|
||||
|
||||
expect_job_enqueued(
|
||||
job: Jobs::DiscourseAutomation::SendFlagEmail,
|
||||
args: {
|
||||
email: user_2.primary_email.email,
|
||||
email_template_automation_field_id: field.id,
|
||||
post_action_id: result.post_action.id,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe DiscourseAutomation::Triggers::POST_FLAG_CREATED do
|
||||
fab!(:flagger) { Fabricate(:user, trust_level: TrustLevel[2]) }
|
||||
fab!(:second_flagger) { Fabricate(:user, trust_level: TrustLevel[2]) }
|
||||
fab!(:category)
|
||||
fab!(:tag)
|
||||
fab!(:topic) { Fabricate(:topic, category: category, tags: [tag]) }
|
||||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||||
|
||||
fab!(:automation) do
|
||||
Fabricate(:automation, trigger: DiscourseAutomation::Triggers::POST_FLAG_CREATED)
|
||||
end
|
||||
|
||||
before { SiteSetting.discourse_automation_enabled = true }
|
||||
|
||||
it "ignores the tags field if tagging is disabled" do
|
||||
SiteSetting.tagging_enabled = false
|
||||
|
||||
automation.upsert_field!("tags", "tags", { value: [tag.name] }, target: "trigger")
|
||||
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, Fabricate(:post, topic: Fabricate(:topic)))
|
||||
expect(result.success).to eq(true)
|
||||
end.to change { automation.reload.stats.count }.by(1)
|
||||
end
|
||||
|
||||
it "only triggers the automation if flagged post is in a topic with the specified tags defined by the tags field" do
|
||||
automation.upsert_field!("tags", "tags", { value: [tag.name] }, target: "trigger")
|
||||
|
||||
post_action_id = nil
|
||||
|
||||
triggered_automations =
|
||||
capture_contexts do
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
post_action_id = result.post_action.id
|
||||
|
||||
result = PostActionCreator.spam(flagger, Fabricate(:post, topic: Fabricate(:topic)))
|
||||
expect(result.success).to eq(true)
|
||||
end.to change { automation.reload.stats.count }.by(1)
|
||||
end
|
||||
|
||||
expect(triggered_automations.length).to eq(1)
|
||||
|
||||
triggered_automation = triggered_automations.first
|
||||
|
||||
expect(triggered_automation["kind"]).to eq(DiscourseAutomation::Triggers::POST_FLAG_CREATED)
|
||||
expect(triggered_automation["post_action_id"]).to eq(post_action_id)
|
||||
end
|
||||
|
||||
it "only triggers the automation if flagged post is in a topic with the specified categories defined by the categories field" do
|
||||
automation.upsert_field!(
|
||||
"categories",
|
||||
"categories",
|
||||
{ value: [category.id] },
|
||||
target: "trigger",
|
||||
)
|
||||
|
||||
post_action_id = nil
|
||||
|
||||
triggered_automations =
|
||||
capture_contexts do
|
||||
expect do
|
||||
result = PostActionCreator.spam(flagger, post)
|
||||
expect(result.success).to eq(true)
|
||||
post_action_id = result.post_action.id
|
||||
|
||||
result = PostActionCreator.spam(flagger, Fabricate(:post, topic: Fabricate(:topic)))
|
||||
expect(result.success).to eq(true)
|
||||
end.to change { automation.reload.stats.count }.by(1)
|
||||
end
|
||||
|
||||
expect(triggered_automations.length).to eq(1)
|
||||
|
||||
triggered_automation = triggered_automations.first
|
||||
|
||||
expect(triggered_automation["kind"]).to eq(DiscourseAutomation::Triggers::POST_FLAG_CREATED)
|
||||
expect(triggered_automation["post_action_id"]).to eq(post_action_id)
|
||||
end
|
||||
end
|
||||
+3
-2
@@ -56,8 +56,9 @@ class LocaleFileValidator
|
||||
PLURALIZATION_KEYS = %w[zero one two few many other]
|
||||
ENGLISH_KEYS = %w[one other]
|
||||
|
||||
EXEMPTED_DOUBLE_CURLY_BRACKET_KEYS = [
|
||||
"js.discourse_automation.scriptables.auto_responder.fields.word_answer_list.description",
|
||||
EXEMPTED_DOUBLE_CURLY_BRACKET_KEYS = %w[
|
||||
js.discourse_automation.scriptables.auto_responder.fields.word_answer_list.description
|
||||
discourse_automation.scriptables.email_on_flagged_post.default_template
|
||||
]
|
||||
|
||||
def initialize(filename)
|
||||
|
||||
Reference in New Issue
Block a user