Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
Fix being able to import more than allowed number of follows (mastodo…
Browse files Browse the repository at this point in the history
…n#15384)

* Fix being able to import more than allowed number of follows

Without this commit, if someone tries importing a second list of accounts to
follow before the first one has been processed, this will queue imports for
the two whole lists, even if they exceed the account's allowed number of
outgoing follows.

This commit changes it so the individual queued imports aren't exempt from
the follow limit check (they remain exempt from the rate-limiting check
though).

* Catch validation errors to not re-queue failed follows

Co-authored-by: Claire <[email protected]>
  • Loading branch information
ClearlyClaire and ClearlyClaire authored Dec 26, 2020
1 parent 4580129 commit f1f96eb
Show file tree
Hide file tree
Showing 11 changed files with 38 additions and 16 deletions.
8 changes: 4 additions & 4 deletions app/models/concerns/account_interactions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ def follow_mapping(query, field)
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?
Expand All @@ -111,8 +111,8 @@ def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: fals
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?
Expand Down
17 changes: 17 additions & 0 deletions app/models/concerns/follow_limitable.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion app/models/follow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Follow < ApplicationRecord
include Paginable
include RelationshipCacheable
include RateLimitable
include FollowLimitable

rate_limit by: :account, family: :follows

Expand All @@ -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) }

Expand Down
2 changes: 1 addition & 1 deletion app/models/follow_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class FollowRequest < ApplicationRecord
include Paginable
include RelationshipCacheable
include RateLimitable
include FollowLimitable

rate_limit by: :account, family: :follows

Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions app/services/follow_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -54,7 +55,7 @@ def change_follow_request_options!
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)
Expand All @@ -66,7 +67,7 @@ def request_follow!
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)
Expand Down
2 changes: 1 addition & 1 deletion app/workers/authorize_follow_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def perform(source_account_id, target_account_id)
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
Expand Down
6 changes: 5 additions & 1 deletion app/workers/import/relationship_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ def perform(account_id, target_account_uri, relationship, options = {})

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'
Expand Down
2 changes: 1 addition & 1 deletion app/workers/refollow_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def perform(target_account_id)

# 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
Expand Down
2 changes: 1 addition & 1 deletion app/workers/unfollow_follow_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def perform(follower_account_id, old_target_account_id, new_target_account_id, b
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
Expand Down
2 changes: 1 addition & 1 deletion lib/mastodon/accounts_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ def follow(username)
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)
Expand Down
4 changes: 2 additions & 2 deletions spec/workers/refollow_worker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
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

0 comments on commit f1f96eb

Please sign in to comment.