FEATURE: Autotag watched words (#12244)
New topics with be matched against a set of watched words and be tagged accordingly.
This commit is contained in:
parent
8d38438725
commit
b49b455e47
|
@ -6,6 +6,7 @@ import Component from "@ember/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import WatchedWord from "admin/models/watched-word";
|
import WatchedWord from "admin/models/watched-word";
|
||||||
import bootbox from "bootbox";
|
import bootbox from "bootbox";
|
||||||
|
import { equal } from "@ember/object/computed";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
|
|
||||||
|
@ -15,10 +16,8 @@ export default Component.extend({
|
||||||
actionKey: null,
|
actionKey: null,
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
|
|
||||||
@discourseComputed("actionKey")
|
canReplace: equal("actionKey", "replace"),
|
||||||
canReplace(actionKey) {
|
canTag: equal("actionKey", "tag"),
|
||||||
return actionKey === "replace";
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("regularExpressions")
|
@discourseComputed("regularExpressions")
|
||||||
placeholderKey(regularExpressions) {
|
placeholderKey(regularExpressions) {
|
||||||
|
@ -61,7 +60,7 @@ export default Component.extend({
|
||||||
|
|
||||||
const watchedWord = WatchedWord.create({
|
const watchedWord = WatchedWord.create({
|
||||||
word: this.word,
|
word: this.word,
|
||||||
replacement: this.canReplace ? this.replacement : null,
|
replacement: this.canReplace || this.canTag ? this.replacement : null,
|
||||||
action: this.actionKey,
|
action: this.actionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,13 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canTag}}
|
||||||
|
<div class="watched-word-input">
|
||||||
|
<label for="watched-tag">{{i18n "admin.watched_words.form.tag_label"}}</label>
|
||||||
|
{{text-field id="watched-tag" value=replacement disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.tag_placeholder"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{d-button class="btn-default" action=(action "submit") disabled=formSubmitted label="admin.watched_words.form.add"}}
|
{{d-button class="btn-default" action=(action "submit") disabled=formSubmitted label="admin.watched_words.form.add"}}
|
||||||
|
|
||||||
{{#if showMessage}}
|
{{#if showMessage}}
|
||||||
|
|
|
@ -29,13 +29,13 @@ module Jobs
|
||||||
cooked = cp.html
|
cooked = cp.html
|
||||||
|
|
||||||
if cooked != (recooked || orig_cooked)
|
if cooked != (recooked || orig_cooked)
|
||||||
|
|
||||||
if orig_cooked.present? && cooked.blank?
|
if orig_cooked.present? && cooked.blank?
|
||||||
# TODO stop/restart the worker if needed, let's gather a few here first
|
# TODO stop/restart the worker if needed, let's gather a few here first
|
||||||
Rails.logger.warn("Cooked post processor in FATAL state, bypassing. You need to urgently restart sidekiq\norig: #{orig_cooked}\nrecooked: #{recooked}\ncooked: #{cooked}\npost id: #{post.id}")
|
Rails.logger.warn("Cooked post processor in FATAL state, bypassing. You need to urgently restart sidekiq\norig: #{orig_cooked}\nrecooked: #{recooked}\ncooked: #{cooked}\npost id: #{post.id}")
|
||||||
else
|
else
|
||||||
post.update_column(:cooked, cp.html)
|
post.update_column(:cooked, cp.html)
|
||||||
extract_links(post)
|
extract_links(post)
|
||||||
|
auto_tag(post) if SiteSetting.tagging_enabled? && post.post_number == 1
|
||||||
post.publish_change_to_clients! :revised
|
post.publish_change_to_clients! :revised
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -60,6 +60,25 @@ module Jobs
|
||||||
TopicLink.extract_from(post)
|
TopicLink.extract_from(post)
|
||||||
QuotedPost.extract_from(post)
|
QuotedPost.extract_from(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_tag(post)
|
||||||
|
word_watcher = WordWatcher.new(post.raw)
|
||||||
|
|
||||||
|
old_tags = post.topic.tags.pluck(:name).to_set
|
||||||
|
new_tags = old_tags.dup
|
||||||
|
|
||||||
|
WordWatcher.words_for_action(:tag).each do |word, tags|
|
||||||
|
new_tags += tags.split(",") if word_watcher.matches?(word)
|
||||||
|
end
|
||||||
|
|
||||||
|
if old_tags != new_tags
|
||||||
|
post.revise(
|
||||||
|
Discourse.system_user,
|
||||||
|
tags: new_tags.to_a,
|
||||||
|
edit_reason: I18n.t(:watched_words_auto_tag)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,8 @@ class WatchedWord < ActiveRecord::Base
|
||||||
censor: 2,
|
censor: 2,
|
||||||
require_approval: 3,
|
require_approval: 3,
|
||||||
flag: 4,
|
flag: 4,
|
||||||
replace: 5
|
replace: 5,
|
||||||
|
tag: 6,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,6 @@ class WatchedWordSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_replacement?
|
def include_replacement?
|
||||||
action == :replace
|
action == :replace || action == :tag
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class WordWatcher
|
||||||
|
|
||||||
def self.words_for_action(action)
|
def self.words_for_action(action)
|
||||||
words = WatchedWord.where(action: WatchedWord.actions[action.to_sym]).limit(1000)
|
words = WatchedWord.where(action: WatchedWord.actions[action.to_sym]).limit(1000)
|
||||||
if action.to_sym == :replace
|
if action.to_sym == :replace || action.to_sym == :tag
|
||||||
words.pluck(:word, :replacement).to_h
|
words.pluck(:word, :replacement).to_h
|
||||||
else
|
else
|
||||||
words.pluck(:word)
|
words.pluck(:word)
|
||||||
|
@ -31,7 +31,7 @@ class WordWatcher
|
||||||
def self.word_matcher_regexp(action, raise_errors: false)
|
def self.word_matcher_regexp(action, raise_errors: false)
|
||||||
words = get_cached_words(action)
|
words = get_cached_words(action)
|
||||||
if words
|
if words
|
||||||
if action.to_sym == :replace
|
if action.to_sym == :replace || action.to_sym == :tag
|
||||||
words = words.keys
|
words = words.keys
|
||||||
end
|
end
|
||||||
words = words.map do |w|
|
words = words.map do |w|
|
||||||
|
@ -110,4 +110,12 @@ class WordWatcher
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def matches?(word)
|
||||||
|
if SiteSetting.watched_words_regular_expressions?
|
||||||
|
Regexp.new(word).match?(@raw)
|
||||||
|
else
|
||||||
|
@raw.include?(word)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4621,18 +4621,22 @@ en:
|
||||||
require_approval: "Require Approval"
|
require_approval: "Require Approval"
|
||||||
flag: "Flag"
|
flag: "Flag"
|
||||||
replace: "Replace"
|
replace: "Replace"
|
||||||
|
tag: "Auto-tag"
|
||||||
action_descriptions:
|
action_descriptions:
|
||||||
block: "Prevent posts containing these words from being posted. The user will see an error message when they try to submit their post."
|
block: "Prevent posts containing these words from being posted. The user will see an error message when they try to submit their post."
|
||||||
censor: "Allow posts containing these words, but replace them with characters that hide the censored words."
|
censor: "Allow posts containing these words, but replace them with characters that hide the censored words."
|
||||||
require_approval: "Posts containing these words will require approval by staff before they can be seen."
|
require_approval: "Posts containing these words will require approval by staff before they can be seen."
|
||||||
flag: "Allow posts containing these words, but flag them as inappropriate so moderators can review them."
|
flag: "Allow posts containing these words, but flag them as inappropriate so moderators can review them."
|
||||||
replace: "Replace words in posts with other words or links"
|
replace: "Replace words in posts with other words or links"
|
||||||
|
tag: "Automatically tag posts with these words"
|
||||||
form:
|
form:
|
||||||
label: "New Word"
|
label: "New Word"
|
||||||
placeholder: "full word or * as wildcard"
|
placeholder: "full word or * as wildcard"
|
||||||
placeholder_regexp: "regular expression"
|
placeholder_regexp: "regular expression"
|
||||||
replacement_label: "Replacement"
|
replacement_label: "Replacement"
|
||||||
replacement_placeholder: "example or https://example.com"
|
replacement_placeholder: "example or https://example.com"
|
||||||
|
tag_label: "Tag"
|
||||||
|
tag_placeholder: "tag1,tag2,tag3"
|
||||||
add: "Add"
|
add: "Add"
|
||||||
success: "Success"
|
success: "Success"
|
||||||
exists: "Already exists"
|
exists: "Already exists"
|
||||||
|
|
|
@ -361,6 +361,7 @@ en:
|
||||||
max_pm_recipients: "Sorry, you can send a message to maximum %{recipients_limit} recipients."
|
max_pm_recipients: "Sorry, you can send a message to maximum %{recipients_limit} recipients."
|
||||||
pm_reached_recipients_limit: "Sorry, you can't have more than %{recipients_limit} recipients in a message."
|
pm_reached_recipients_limit: "Sorry, you can't have more than %{recipients_limit} recipients in a message."
|
||||||
removed_direct_reply_full_quotes: "Automatically removed quote of whole previous post."
|
removed_direct_reply_full_quotes: "Automatically removed quote of whole previous post."
|
||||||
|
watched_words_auto_tag: "Automatically tagged topic"
|
||||||
secure_upload_not_allowed_in_public_topic: "Sorry, the following secure upload(s) cannot be used in a public topic: %{upload_filenames}."
|
secure_upload_not_allowed_in_public_topic: "Sorry, the following secure upload(s) cannot be used in a public topic: %{upload_filenames}."
|
||||||
create_pm_on_existing_topic: "Sorry, you can't create a PM on an existing topic."
|
create_pm_on_existing_topic: "Sorry, you can't create a PM on an existing topic."
|
||||||
slow_mode_enabled: "This topic is in slow mode."
|
slow_mode_enabled: "This topic is in slow mode."
|
||||||
|
|
|
@ -77,6 +77,43 @@ describe Jobs::ProcessPost do
|
||||||
post.reload
|
post.reload
|
||||||
expect(post.cooked).to eq(cooked)
|
expect(post.cooked).to eq(cooked)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "automatically tags first posts" do
|
||||||
|
SiteSetting.tagging_enabled = true
|
||||||
|
|
||||||
|
Fabricate(:watched_word, action: WatchedWord.actions[:tag], word: "Greetings?", replacement: "hello , world")
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greeting", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly()
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greetings", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly()
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greetings?", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly("hello", "world")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "automatically tags first posts (regex)" do
|
||||||
|
SiteSetting.tagging_enabled = true
|
||||||
|
SiteSetting.watched_words_regular_expressions = true
|
||||||
|
|
||||||
|
Fabricate(:watched_word, action: WatchedWord.actions[:tag], word: "Greetings?", replacement: "hello , world")
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greeting", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly("hello", "world")
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greetings", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly("hello", "world")
|
||||||
|
|
||||||
|
post = Fabricate(:post, raw: "Greetings?", cooked: "")
|
||||||
|
Jobs::ProcessPost.new.execute(post_id: post.id)
|
||||||
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly("hello", "world")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue