mirror of
https://github.com/discourse/discourse.git
synced 2025-05-25 18:12:10 +08:00
FEATURE: Display the Watched Words that caused a post to be flagged. (#31435)
When a post is flagged due to matching watched words, it can be difficult to know what you're looking for, particularly if you have a lot of watched words built up over a long period of time. This change stores the list of matched words, and later displays them in the review queue, listing which Watched Words were responsible for the flag. Because watched words can change, this is recorded at the time the post is flagged. For posts that were flagged prior to this feature landing, it tries to guess the relevant words based on the current Watched Words set.
This commit is contained in:
@ -23,6 +23,9 @@
|
||||
{{#if rs.reason}}
|
||||
<div class="reviewable-score-reason">{{html-safe rs.reason}}</div>
|
||||
{{/if}}
|
||||
{{#if rs.context}}
|
||||
<div class="reviewable-score-context">{{html-safe rs.context}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if rs.reviewable_conversation}}
|
||||
<div class="reviewable-conversation">
|
||||
|
@ -49,12 +49,15 @@ module Jobs
|
||||
if !post.user&.staff? && !post.user&.staged?
|
||||
s = post.raw
|
||||
s << " #{post.topic.title}" if post.post_number == 1
|
||||
if !args[:bypass_bump] && WordWatcher.new(s).should_flag?
|
||||
word_watcher = WordWatcher.new(s)
|
||||
if !args[:bypass_bump] && word_watcher.should_flag?
|
||||
words = word_watcher.word_matches_for_action?(:flag, all_matches: true)
|
||||
PostActionCreator.create(
|
||||
Discourse.system_user,
|
||||
post,
|
||||
:inappropriate,
|
||||
reason: :watched_word,
|
||||
context: words.join(","),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -199,7 +199,8 @@ class Reviewable < ActiveRecord::Base
|
||||
created_at: nil,
|
||||
take_action: false,
|
||||
meta_topic_id: nil,
|
||||
force_review: false
|
||||
force_review: false,
|
||||
context: nil
|
||||
)
|
||||
type_bonus = PostActionType.where(id: reviewable_score_type).pluck(:score_bonus)[0] || 0
|
||||
take_action_bonus = take_action ? 5.0 : 0.0
|
||||
@ -216,6 +217,7 @@ class Reviewable < ActiveRecord::Base
|
||||
meta_topic_id: meta_topic_id,
|
||||
take_action_bonus: take_action_bonus,
|
||||
created_at: created_at || Time.zone.now,
|
||||
context: context,
|
||||
)
|
||||
rs.reason = reason.to_s if reason
|
||||
rs.save!
|
||||
|
@ -117,6 +117,7 @@ end
|
||||
# updated_at :datetime not null
|
||||
# reason :string
|
||||
# user_accuracy_bonus :float default(0.0), not null
|
||||
# context :string
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
@ -42,7 +42,12 @@ class ReviewableScoreSerializer < ApplicationSerializer
|
||||
|
||||
if link_text
|
||||
link = build_link_for(object.reason, link_text)
|
||||
|
||||
if object.reason == "watched_word"
|
||||
text = watched_word_reason(link)
|
||||
else
|
||||
text = I18n.t("reviewables.reasons.#{object.reason}", link: link, default: object.reason)
|
||||
end
|
||||
else
|
||||
text = I18n.t("reviewables.reasons.#{object.reason}", default: object.reason)
|
||||
end
|
||||
@ -69,6 +74,35 @@ class ReviewableScoreSerializer < ApplicationSerializer
|
||||
|
||||
private
|
||||
|
||||
def watched_word_reason(link)
|
||||
if object.context.nil?
|
||||
# If the words weren't recorded, try to guess them based on current settings.
|
||||
words =
|
||||
WordWatcher.new(object.reviewable.post.raw).word_matches_for_action?(
|
||||
:flag,
|
||||
all_matches: true,
|
||||
)
|
||||
else
|
||||
words = object.context.split(",")
|
||||
end
|
||||
|
||||
if words.nil?
|
||||
text =
|
||||
I18n.t("reviewables.reasons.no_context.watched_word", link: link, default: "watched_word")
|
||||
else
|
||||
text =
|
||||
I18n.t(
|
||||
"reviewables.reasons.watched_word",
|
||||
link: link,
|
||||
words: words.join(", "),
|
||||
count: words.length,
|
||||
default: "watched_word",
|
||||
)
|
||||
end
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
def url_for(reason, text)
|
||||
case reason
|
||||
when "watched_word"
|
||||
|
@ -5584,7 +5584,9 @@ en:
|
||||
new_topics_unless_trust_level: "Users at low trust levels must have topics approved by staff. See %{link}."
|
||||
fast_typer: "New user typed their first post suspiciously fast, suspected bot or spammer behavior. See %{link}."
|
||||
auto_silence_regex: "New user whose first post matches the %{link} setting."
|
||||
watched_word: "This post included a Watched Word. See your %{link}."
|
||||
watched_word:
|
||||
one: "This post included a Watched Word, the flagged word is: %{words}. Check the %{link} for more."
|
||||
other: "This post included Watched Words, the flagged words are: %{words}. Check the %{link} for more."
|
||||
staged: "New topics and posts for staged users must be approved by staff. See %{link}."
|
||||
category: "Posts in this category require manual approval by staff. See the %{link}."
|
||||
must_approve_users: "All new users must be approved by staff. See %{link}."
|
||||
@ -5595,8 +5597,10 @@ en:
|
||||
contains_media: "This post includes embedded media. See %{link}."
|
||||
queued_by_staff: "A staff member thinks this post needs review. It'll remain hidden until then."
|
||||
links:
|
||||
watched_word: list of watched words
|
||||
watched_word: watched word settings
|
||||
category: category settings
|
||||
no_context:
|
||||
watched_word: "This post included a Watched Word. Check the %{link} for more."
|
||||
|
||||
actions:
|
||||
agree:
|
||||
|
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
class AddContextToReviewableScores < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :reviewable_scores, :context, :string
|
||||
end
|
||||
end
|
@ -14,7 +14,8 @@ class PostActionCreator
|
||||
message: nil,
|
||||
created_at: nil,
|
||||
reason: nil,
|
||||
silent: false
|
||||
silent: false,
|
||||
context: nil
|
||||
)
|
||||
new(
|
||||
created_by,
|
||||
@ -24,6 +25,7 @@ class PostActionCreator
|
||||
created_at: created_at,
|
||||
reason: reason,
|
||||
silent: silent,
|
||||
context: context,
|
||||
).perform
|
||||
end
|
||||
|
||||
@ -50,7 +52,8 @@ class PostActionCreator
|
||||
created_at: nil,
|
||||
queue_for_review: false,
|
||||
reason: nil,
|
||||
silent: false
|
||||
silent: false,
|
||||
context: nil
|
||||
)
|
||||
@created_by = created_by
|
||||
@created_at = created_at || Time.zone.now
|
||||
@ -73,6 +76,8 @@ class PostActionCreator
|
||||
@reason = "queued_by_staff" if reason.nil? && @queue_for_review
|
||||
|
||||
@silent = silent
|
||||
|
||||
@context = context
|
||||
end
|
||||
|
||||
def post_can_act?
|
||||
@ -403,6 +408,7 @@ class PostActionCreator
|
||||
meta_topic_id: @meta_post&.topic_id,
|
||||
reason: @reason,
|
||||
force_review: trusted_spam_flagger?,
|
||||
context: @context,
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -12,6 +12,7 @@ RSpec.describe WatchedWord do
|
||||
Fabricate(:watched_word, action: WatchedWord.actions[:require_approval])
|
||||
end
|
||||
let(:flag_word) { Fabricate(:watched_word, action: WatchedWord.actions[:flag]) }
|
||||
let(:another_flag_word) { Fabricate(:watched_word, action: WatchedWord.actions[:flag]) }
|
||||
let(:block_word) { Fabricate(:watched_word, action: WatchedWord.actions[:block]) }
|
||||
let(:another_block_word) { Fabricate(:watched_word, action: WatchedWord.actions[:block]) }
|
||||
|
||||
@ -232,7 +233,43 @@ RSpec.describe WatchedWord do
|
||||
).to eq(true)
|
||||
reviewable = ReviewableFlaggedPost.where(target: post)
|
||||
expect(reviewable).to be_present
|
||||
expect(ReviewableScore.where(reviewable: reviewable, reason: "watched_word")).to be_present
|
||||
expect(
|
||||
ReviewableScore.where(
|
||||
reviewable: reviewable,
|
||||
reason: "watched_word",
|
||||
context: flag_word.word,
|
||||
),
|
||||
).to be_present
|
||||
end
|
||||
|
||||
it "should flag the post if it contains multiple flagged words" do
|
||||
topic = Fabricate(:topic, user: tl2_user)
|
||||
post =
|
||||
Fabricate(
|
||||
:post,
|
||||
raw:
|
||||
"I said.... #{flag_word.word} and #{another_flag_word.word} and #{flag_word.word} again",
|
||||
topic: topic,
|
||||
user: tl2_user,
|
||||
)
|
||||
expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to change { PostAction.count }.by(
|
||||
1,
|
||||
)
|
||||
expect(
|
||||
PostAction.where(
|
||||
post_id: post.id,
|
||||
post_action_type_id: PostActionType.types[:inappropriate],
|
||||
).exists?,
|
||||
).to eq(true)
|
||||
reviewable = ReviewableFlaggedPost.where(target: post)
|
||||
expect(reviewable).to be_present
|
||||
expect(
|
||||
ReviewableScore.where(
|
||||
reviewable: reviewable,
|
||||
reason: "watched_word",
|
||||
context: "#{flag_word.word},#{another_flag_word.word}",
|
||||
),
|
||||
).to be_present
|
||||
end
|
||||
|
||||
it "should look at the title too" do
|
||||
|
@ -63,6 +63,39 @@ RSpec.describe ReviewableScoreSerializer do
|
||||
expect(serialized.reason).to eq(custom)
|
||||
end
|
||||
end
|
||||
|
||||
context "with watched words" do
|
||||
let(:link) do
|
||||
"<a href=\"#{Discourse.base_url}/admin/customize/watched_words\">#{I18n.t("reviewables.reasons.links.watched_word")}</a>"
|
||||
end
|
||||
it "tries to guess the watched words if they weren't recorded at the time of flagging" do
|
||||
reviewable.target =
|
||||
Fabricate(:post, raw: "I'm a post with some bad words like 'bad' and 'words'.")
|
||||
|
||||
score = serialized_score("watched_word")
|
||||
|
||||
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "bad")
|
||||
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "words")
|
||||
|
||||
expect(score.reason).to include("bad, words")
|
||||
end
|
||||
|
||||
it "uses the no-context message if the post has no watched words" do
|
||||
reviewable.target = Fabricate(:post, raw: "This post contains no bad words.")
|
||||
|
||||
score = serialized_score("watched_word")
|
||||
|
||||
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "superbad")
|
||||
|
||||
expect(score.reason).to eq(
|
||||
I18n.t(
|
||||
"reviewables.reasons.no_context.watched_word",
|
||||
link: link,
|
||||
default: "watched_word",
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#setting_name_for_reason" do
|
||||
|
Reference in New Issue
Block a user