FEATURE: Clean up inactive users. (#7172)

FEATURE: Clean up inactive users. (#7172)

diff --git a/app/jobs/scheduled/clean_up_inactive_users.rb b/app/jobs/scheduled/clean_up_inactive_users.rb
new file mode 100644
index 0000000..801f05c
--- /dev/null
+++ b/app/jobs/scheduled/clean_up_inactive_users.rb
@@ -0,0 +1,29 @@
+module Jobs
+
+  class CleanUpInactiveUsers < Jobs::Scheduled
+    every 1.day
+
+    def execute(args)
+      return if SiteSetting.clean_up_inactive_users_after_days <= 0
+
+      destroyer = UserDestroyer.new(Discourse.system_user)
+
+      User.joins("LEFT JOIN posts ON posts.user_id = users.id")
+        .where(last_posted_at: nil)
+        .where("posts.user_id IS NULL")
+        .where("users.last_seen_at < ?", SiteSetting.clean_up_inactive_users_after_days.days.ago)
+        .where(trust_level: 0)
+        .find_each do |user|
+
+        begin
+          destroyer.destroy(user, context: I18n.t("user.destroy_reasons.inactive_user"))
+        rescue => e
+          Discourse.handle_job_exception(e,
+            message: "Cleaning up inactive users",
+            extra: { user_id: user.id }
+          )
+        end
+      end
+    end
+  end
+end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index f050e03..8b830a6 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1829,6 +1829,8 @@ en:
 
     ignored_users_message_gap_days: "How long to wait before notifying moderators again about a user who has been ignored by many others."
 
+    clean_up_inactive_users_after_days: "Number of days before an inactive user (trust level 0 without any posts) is removed."
+
     user_website_domains_whitelist: "User website will be verified against these domains. Pipe-delimited list."
 
     allow_profile_backgrounds: "Allow users to upload profile backgrounds."
@@ -2201,6 +2203,7 @@ en:
       unused_staged_user: "Unused staged user"
       fixed_primary_email: "Fixed primary email for staged user"
       same_ip_address: "Same IP address (%{ip_address}) as other users"
+      inactive_user: "Inactive user"
 
   flags_reminder:
     flags_were_submitted:
diff --git a/config/site_settings.yml b/config/site_settings.yml
index aa87006..6ead1d3 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -541,6 +541,8 @@ users:
     default: 365
     client: true
     min: 1
+  clean_up_inactive_users_after_days:
+    default: 730
 
 groups:
   enable_group_directory:
diff --git a/spec/jobs/clean_up_inactive_users_spec.rb b/spec/jobs/clean_up_inactive_users_spec.rb
new file mode 100644
index 0000000..5d01233
--- /dev/null
+++ b/spec/jobs/clean_up_inactive_users_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe Jobs::CleanUpInactiveUsers do
+  context 'when user is inactive' do
+    let(:user) { Fabricate(:user) }
+    let(:active_user) { Fabricate(:active_user) }
+
+    it 'should clean up the user' do
+      user.update!(last_seen_at: 3.years.ago, trust_level: 0)
+      active_user
+
+      expect { described_class.new.execute({}) }.to change { User.count }.by(-1)
+      expect(User.find_by(id: user.id)).to eq(nil)
+    end
+  end
+
+  context 'when user is not inactive' do
+
+    let!(:active_user_1) { Fabricate(:post, user: Fabricate(:user, trust_level: 0)).user }
+    let!(:active_user_2) { Fabricate(:user, trust_level: 0, last_seen_at: 2.days.ago) }
+    let!(:active_user_3) { Fabricate(:user, trust_level: 1) }
+
+    it 'should not clean up active users' do
+      expect { described_class.new.execute({}) }.to_not change { User.count }
+      expect(User.find_by(id: active_user_1.id)).to_not eq(nil)
+      expect(User.find_by(id: active_user_2.id)).to_not eq(nil)
+      expect(User.find_by(id: active_user_3.id)).to_not eq(nil)
+    end
+  end
+end

GitHub sha: 2347661a