diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index e2c4b8acf..974f57820 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -97,8 +97,8 @@ module AccountInteractions has_many :announcement_mutes, dependent: :destroy end - def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false) - rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit) + def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false) + rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit) .find_or_create_by!(target_account: other_account) rel.show_reblogs = reblogs unless reblogs.nil? @@ -111,8 +111,8 @@ module AccountInteractions rel end - def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false) - rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit) + def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false) + rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit) .find_or_create_by!(target_account: other_account) rel.show_reblogs = reblogs unless reblogs.nil? diff --git a/app/models/concerns/follow_limitable.rb b/app/models/concerns/follow_limitable.rb new file mode 100644 index 000000000..c64060d6e --- /dev/null +++ b/app/models/concerns/follow_limitable.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module FollowLimitable + extend ActiveSupport::Concern + + included do + validates_with FollowLimitValidator, on: :create, unless: :bypass_follow_limit? + end + + def bypass_follow_limit=(value) + @bypass_follow_limit = value + end + + def bypass_follow_limit? + @bypass_follow_limit + end +end diff --git a/app/models/follow.rb b/app/models/follow.rb index 69a1722b3..a5e3fe809 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -17,6 +17,7 @@ class Follow < ApplicationRecord include Paginable include RelationshipCacheable include RateLimitable + include FollowLimitable rate_limit by: :account, family: :follows @@ -26,7 +27,6 @@ class Follow < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy validates :account_id, uniqueness: { scope: :target_account_id } - validates_with FollowLimitValidator, on: :create, if: :rate_limit? scope :recent, -> { reorder(id: :desc) } diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 2d2a77b59..59fefcdf6 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -17,6 +17,7 @@ class FollowRequest < ApplicationRecord include Paginable include RelationshipCacheable include RateLimitable + include FollowLimitable rate_limit by: :account, family: :follows @@ -26,7 +27,6 @@ class FollowRequest < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy validates :account_id, uniqueness: { scope: :target_account_id } - validates_with FollowLimitValidator, on: :create, if: :rate_limit? def authorize! account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 962572851..b98f7011d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -11,11 +11,12 @@ class FollowService < BaseService # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false # @option [Boolean] :bypass_locked + # @option [Boolean] :bypass_limit Allow following past the total follow number # @option [Boolean] :with_rate_limit def call(source_account, target_account, options = {}) @source_account = source_account @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) - @options = { bypass_locked: false, with_rate_limit: false }.merge(options) + @options = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options) raise ActiveRecord::RecordNotFound if following_not_possible? raise Mastodon::NotPermittedError if following_not_allowed? @@ -54,7 +55,7 @@ class FollowService < BaseService end def request_follow! - follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit]) + follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]) if @target_account.local? LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request) @@ -66,7 +67,7 @@ class FollowService < BaseService end def direct_follow! - follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit]) + follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]) LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow) MergeWorker.perform_async(@target_account.id, @source_account.id) diff --git a/app/workers/authorize_follow_worker.rb b/app/workers/authorize_follow_worker.rb index 0d5014624..f57900fa5 100644 --- a/app/workers/authorize_follow_worker.rb +++ b/app/workers/authorize_follow_worker.rb @@ -7,7 +7,7 @@ class AuthorizeFollowWorker source_account = Account.find(source_account_id) target_account = Account.find(target_account_id) - AuthorizeFollowService.new.call(source_account, target_account) + AuthorizeFollowService.new.call(source_account, target_account, bypass_limit: true) rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index 4a455f3ae..4a7100435 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -15,7 +15,11 @@ class Import::RelationshipWorker case relationship when 'follow' - FollowService.new.call(from_account, target_account, options) + begin + FollowService.new.call(from_account, target_account, options) + rescue ActiveRecord::RecordInvalid + raise if FollowLimitValidator.limit_for_account(from_account) < from_account.following_count + end when 'unfollow' UnfollowService.new.call(from_account, target_account) when 'block' diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb index 98940680d..319b00109 100644 --- a/app/workers/refollow_worker.rb +++ b/app/workers/refollow_worker.rb @@ -19,7 +19,7 @@ class RefollowWorker # Schedule re-follow begin - FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify) + FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify, bypass_limit: true) rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError next end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index 71b5a0e3f..0bd5ff472 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -14,7 +14,7 @@ class UnfollowFollowWorker reblogs = follow&.show_reblogs? notify = follow&.notify? - FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked) + FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked, bypass_limit: true) UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index cdd1db995..653bfca30 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -384,7 +384,7 @@ module Mastodon end processed, = parallelize_with_progress(Account.local.without_suspended) do |account| - FollowService.new.call(account, target_account) + FollowService.new.call(account, target_account, bypass_limit: true) end say("OK, followed target from #{processed} accounts", :green) diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb index 6b4c04291..df6731b64 100644 --- a/spec/workers/refollow_worker_spec.rb +++ b/spec/workers/refollow_worker_spec.rb @@ -23,8 +23,8 @@ describe RefollowWorker do result = subject.perform(account.id) expect(result).to be_nil - expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false) - expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false) + expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false, bypass_limit: true) + expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false, bypass_limit: true) end end end