about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-10-04 17:36:11 +0200
committerGitHub <noreply@github.com>2018-10-04 17:36:11 +0200
commita46ab86adfc9e4ea182af9a555237f17071e194c (patch)
treeed5d073badf675fdc0661c54467cc52de85c90b2
parent186024a058d4b8765a10d87ff3d7f3bdcd2fbb3c (diff)
Limit the number of people that can be followed from one account (#8807)
Configurable soft limit of 7,500, and above that, configurable
ratio of 1.1 * followers, controlled by:

- MAX_FOLLOWS_THRESHOLD
- MAX_FOLLOWS_RATIO

Fix #2311
-rw-r--r--app/models/follow.rb1
-rw-r--r--app/models/follow_request.rb1
-rw-r--r--app/validators/follow_limit_validator.rb27
-rw-r--r--app/workers/import_worker.rb4
-rw-r--r--config/locales/en.yml1
-rw-r--r--spec/models/follow_spec.rb14
6 files changed, 47 insertions, 1 deletions
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 714f4e898..7ad56eb78 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -25,6 +25,7 @@ class Follow < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
 
   validates :account_id, uniqueness: { scope: :target_account_id }
+  validates_with FollowLimitValidator, on: :create
 
   scope :recent, -> { reorder(id: :desc) }
 
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 9c4875564..c5451a050 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -22,6 +22,7 @@ class FollowRequest < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
 
   validates :account_id, uniqueness: { scope: :target_account_id }
+  validates_with FollowLimitValidator, on: :create
 
   def authorize!
     account.follow!(target_account, reblogs: show_reblogs, uri: uri)
diff --git a/app/validators/follow_limit_validator.rb b/app/validators/follow_limit_validator.rb
new file mode 100644
index 000000000..eb083ed85
--- /dev/null
+++ b/app/validators/follow_limit_validator.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class FollowLimitValidator < ActiveModel::Validator
+  LIMIT = ENV.fetch('MAX_FOLLOWS_THRESHOLD', 7_500).to_i
+  RATIO = ENV.fetch('MAX_FOLLOWS_RATIO', 1.1).to_f
+
+  def validate(follow)
+    return if follow.account.nil? || !follow.account.local?
+    follow.errors.add(:base, I18n.t('users.follow_limit_reached', limit: self.class.limit_for_account(follow.account))) if limit_reached?(follow.account)
+  end
+
+  class << self
+    def limit_for_account(account)
+      if account.following_count < LIMIT
+        LIMIT
+      else
+        account.followers_count * RATIO
+      end
+    end
+  end
+
+  private
+
+  def limit_reached?(account)
+    account.following_count >= self.class.limit_for_account(account)
+  end
+end
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
index d7c126f75..aeb221cf6 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -37,6 +37,8 @@ class ImportWorker
   end
 
   def import_rows
-    CSV.new(import_contents).reject(&:blank?)
+    rows = CSV.new(import_contents).reject(&:blank?)
+    rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following'
+    rows
   end
 end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f883b17a1..439c5627b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -917,6 +917,7 @@ en:
       tips: Tips
       title: Welcome aboard, %{name}!
   users:
+    follow_limit_reached: You cannot follow more than %{limit} people
     invalid_email: The e-mail address is invalid
     invalid_otp_token: Invalid two-factor code
     otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index f221973b6..0c84e5e7b 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -23,6 +23,20 @@ RSpec.describe Follow, type: :model do
       follow.valid?
       expect(follow).to model_have_error_on_field(:target_account)
     end
+
+    it 'is invalid if account already follows too many people' do
+      alice.update(following_count: FollowLimitValidator::LIMIT)
+
+      expect(subject).to_not be_valid
+      expect(subject).to model_have_error_on_field(:base)
+    end
+
+    it 'is valid if account is only on the brink of following too many people' do
+      alice.update(following_count: FollowLimitValidator::LIMIT - 1)
+
+      expect(subject).to be_valid
+      expect(subject).to_not model_have_error_on_field(:base)
+    end
   end
 
   describe 'recent' do