diff --git a/app/jobs/scheduled/ignored_users_summary.rb b/app/jobs/scheduled/ignored_users_summary.rb new file mode 100644 index 00000000000..137109113ae --- /dev/null +++ b/app/jobs/scheduled/ignored_users_summary.rb @@ -0,0 +1,42 @@ +module Jobs + class IgnoredUsersSummary < Jobs::Scheduled + every 1.day + + def execute(args) + return unless SiteSetting.ignore_user_enabled + + params = { + threshold: SiteSetting.ignored_users_count_message_threshold, + gap_days: SiteSetting.ignored_users_message_gap_days, + coalesced_gap_days: SiteSetting.ignored_users_message_gap_days + 1, + } + user_ids = DB.query_single(<<~SQL, params) + SELECT ignored_user_id + FROM ignored_users + WHERE COALESCE(summarized_at, CURRENT_TIMESTAMP + ':coalesced_gap_days DAYS'::INTERVAL) - ':gap_days DAYS'::INTERVAL > CURRENT_TIMESTAMP + GROUP BY ignored_user_id + HAVING COUNT(ignored_user_id) >= :threshold + SQL + + User.where(id: user_ids).find_each { |user| notify_user(user) } + end + + private + + def notify_user(user) + params = SystemMessage.new(user).defaults.merge(ignores_threshold: SiteSetting.ignored_users_count_message_threshold) + title = I18n.t("system_messages.ignored_users_summary.subject_template") + raw = I18n.t("system_messages.ignored_users_summary.text_body_template", params) + + PostCreator.create( + Discourse.system_user, + target_group_names: Group[:moderators].name, + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + title: title, + raw: raw, + skip_validations: true) + IgnoredUser.where(ignored_user_id: user.id).update_all(summarized_at: Time.zone.now) + end + end +end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 224d94f446f..b43594a9be6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1823,6 +1823,10 @@ en: ignore_user_enabled: "[Beta] Allow ignoring users." + ignored_users_count_message_threshold: "Notify moderators if a particular user is ignored by this many other users." + + ignored_users_message_gap_days: "How long wait before notifying moderators again about a user who has been ignored by many others" + user_website_domains_whitelist: "User website will be verified against these domains. Pipe-delimited list." allow_profile_backgrounds: "Allow users to upload profile backgrounds." @@ -2829,6 +2833,18 @@ en: %{raw} ``` + ignored_users_summary: + title: "Ignored User passed threshold" + subject_template: "A user is being ignored by many other users" + text_body_template: | + Hello, + + This is an automated message from %{site_name} to inform you about a potentially problematic user who has been ignored by %{ignores_threshold} users. It would be smart to review their forum activity. + + Please check the [user's profile](%{base_url}/u/%{username}/summary). + + For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). + too_many_spam_flags: title: "Too Many Spam Flags" subject_template: "New account on hold" diff --git a/config/site_settings.yml b/config/site_settings.yml index 123da3ea49b..a2c4f3148b7 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -532,6 +532,14 @@ users: ignore_user_enabled: default: false client: true + ignored_users_count_message_threshold: + default: 5 + client: true + min: 1 + ignored_users_message_gap_days: + default: 365 + client: true + min: 1 groups: enable_group_directory: diff --git a/db/migrate/20190314144755_add_summarized_at_column_to_ignored_users_table.rb b/db/migrate/20190314144755_add_summarized_at_column_to_ignored_users_table.rb new file mode 100644 index 00000000000..6acb32cd796 --- /dev/null +++ b/db/migrate/20190314144755_add_summarized_at_column_to_ignored_users_table.rb @@ -0,0 +1,5 @@ +class AddSummarizedAtColumnToIgnoredUsersTable < ActiveRecord::Migration[5.2] + def change + add_column :ignored_users, :summarized_at, :datetime + end +end diff --git a/spec/fabricators/post_custom_field_fabricator.rb b/spec/fabricators/post_custom_field_fabricator.rb new file mode 100644 index 00000000000..1c764dc871e --- /dev/null +++ b/spec/fabricators/post_custom_field_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:post_custom_field) do + post + name { sequence(:key) { |i| "key#{i}" } } + value "test value" +end diff --git a/spec/jobs/ignored_users_summary_spec.rb b/spec/jobs/ignored_users_summary_spec.rb new file mode 100644 index 00000000000..683e10c4db2 --- /dev/null +++ b/spec/jobs/ignored_users_summary_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +require_dependency 'jobs/scheduled/ignored_users_summary' + +describe Jobs::IgnoredUsersSummary do + before do + SiteSetting.ignore_user_enabled = true + SiteSetting.ignored_users_count_message_threshold = 1 + SiteSetting.ignored_users_message_gap_days = 365 + end + + subject { Jobs::IgnoredUsersSummary.new.execute({}) } + + context "with no ignored users" do + it "does nothing" do + subject + expect { subject }.to_not change { Post.count } + end + end + + context "when some ignored users exist" do + let(:tarek) { Fabricate(:user, username: "tarek") } + let(:matt) { Fabricate(:user, username: "matt") } + let(:john) { Fabricate(:user, username: "john") } + + before do + Fabricate(:ignored_user, user: tarek, ignored_user: matt) + Fabricate(:ignored_user, user_id: tarek.id, ignored_user_id: john.id) + end + + context "when no system message exists for the ignored users" do + context "when threshold is not hit" do + before do + SiteSetting.ignored_users_count_message_threshold = 5 + end + + it "does nothing" do + subject + expect { subject }.to_not change { Post.count } + end + end + + context "when threshold is hit" do + it "creates a system message" do + subject + posts = Post.joins(:topic).where(topics: { + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message + }) + expect(posts.count).to eq(2) + expect(posts[0].raw).to include(matt.username) + expect(posts[1].raw).to include(john.username) + end + end + end + + context "when a system message already exists for the ignored users" do + context "when threshold is not hit" do + before do + SiteSetting.ignored_users_count_message_threshold = 5 + end + + it "does nothing" do + subject + expect { subject }.to_not change { Post.count } + end + end + + context "when threshold is hit" do + it "does nothing" do + subject + expect { subject }.to_not change { Post.count } + end + end + end + end +end