Fix followers synchronization mechanism not working when URI has empty path (#16510)
* Fix followers synchronization mechanism not working when URI has empty path To my knowledge, there is no current implementation on the fediverse that can use bare domains (e.g., actor is at https://example.org instead of something like https://example.org/actor) that also plans to support the followers synchronization mechanism. However, Mastodon's current implementation would exclude such accounts from followers list. Also adds tests and rename them to reflect the proper method names. * Move url prefix regexp to its own constant
This commit is contained in:
parent
13b08610a0
commit
5efb1ff337
5 changed files with 49 additions and 38 deletions
|
@ -58,8 +58,9 @@ class Account < ApplicationRecord
|
||||||
hub_url
|
hub_url
|
||||||
)
|
)
|
||||||
|
|
||||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
|
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
|
||||||
|
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
|
||||||
|
|
||||||
include AccountAssociations
|
include AccountAssociations
|
||||||
include AccountAvatar
|
include AccountAvatar
|
||||||
|
@ -379,7 +380,7 @@ class Account < ApplicationRecord
|
||||||
def synchronization_uri_prefix
|
def synchronization_uri_prefix
|
||||||
return 'local' if local?
|
return 'local' if local?
|
||||||
|
|
||||||
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
|
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
|
||||||
end
|
end
|
||||||
|
|
||||||
class Field < ActiveModelSerializers::Model
|
class Field < ActiveModelSerializers::Model
|
||||||
|
|
|
@ -254,10 +254,13 @@ module AccountInteractions
|
||||||
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_followers_hash(url_prefix)
|
def remote_followers_hash(url)
|
||||||
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
|
url_prefix = url[Account::URL_PREFIX_RE]
|
||||||
|
return if url_prefix.blank?
|
||||||
|
|
||||||
|
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
|
||||||
digest = "\x00" * 32
|
digest = "\x00" * 32
|
||||||
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
|
followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
|
||||||
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
||||||
end
|
end
|
||||||
digest.unpack('H*')[0]
|
digest.unpack('H*')[0]
|
||||||
|
|
|
@ -44,11 +44,7 @@ class ActivityPub::DeliveryWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def synchronization_header
|
def synchronization_header
|
||||||
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(inbox_url_prefix)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
||||||
end
|
|
||||||
|
|
||||||
def inbox_url_prefix
|
|
||||||
@inbox_url[/http(s?):\/\/[^\/]+\//]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform_request
|
def perform_request
|
||||||
|
|
|
@ -539,46 +539,57 @@ describe AccountInteractions do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#followers_hash' do
|
describe '#remote_followers_hash' do
|
||||||
let(:me) { Fabricate(:account, username: 'Me') }
|
let(:me) { Fabricate(:account, username: 'Me') }
|
||||||
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
||||||
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
|
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
|
||||||
let(:remote_3) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
|
let(:remote_3) { Fabricate(:account, username: 'instance-actor', domain: 'example.org', uri: 'https://example.org') }
|
||||||
|
let(:remote_4) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
remote_1.follow!(me)
|
remote_1.follow!(me)
|
||||||
remote_2.follow!(me)
|
remote_2.follow!(me)
|
||||||
remote_3.follow!(me)
|
remote_3.follow!(me)
|
||||||
|
remote_4.follow!(me)
|
||||||
me.follow!(remote_1)
|
me.follow!(remote_1)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on a local user' do
|
it 'returns correct hash for remote domains' do
|
||||||
it 'returns correct hash for remote domains' do
|
expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
|
||||||
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
|
expect(me.remote_followers_hash('https://foo.org.evil.com/')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
end
|
expect(me.remote_followers_hash('https://foo')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
|
||||||
it 'invalidates cache as needed when removing or adding followers' do
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
|
||||||
remote_1.unfollow!(me)
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
|
|
||||||
remote_1.follow!(me)
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on a remote user' do
|
it 'invalidates cache as needed when removing or adding followers' do
|
||||||
it 'returns correct hash for remote domains' do
|
expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
remote_3.unfollow!(me)
|
||||||
end
|
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||||
|
remote_1.unfollow!(me)
|
||||||
|
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
|
||||||
|
remote_1.follow!(me)
|
||||||
|
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'invalidates cache as needed when removing or adding followers' do
|
describe '#local_followers_hash' do
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
let(:me) { Fabricate(:account, username: 'Me') }
|
||||||
me.unfollow!(remote_1)
|
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
||||||
expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
|
||||||
me.follow!(remote_1)
|
before do
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
me.follow!(remote_1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns correct hash for local users' do
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'invalidates cache as needed when removing or adding followers' do
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
|
me.unfollow!(remote_1)
|
||||||
|
expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
me.follow!(remote_1)
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe ActivityPub::DeliveryWorker do
|
||||||
let(:payload) { 'test' }
|
let(:payload) { 'test' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/').and_return('somehash')
|
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
|
|
Loading…
Reference in a new issue