Add support for FEP-2c59 (#38239)

This commit is contained in:
Claire
2026-03-26 14:33:16 +01:00
committed by GitHub
parent abd29109c5
commit e81a4e258c
5 changed files with 128 additions and 5 deletions

View File

@@ -4,6 +4,7 @@ module ContextHelper
NAMED_CONTEXT_MAP = { NAMED_CONTEXT_MAP = {
activitystreams: 'https://www.w3.org/ns/activitystreams', activitystreams: 'https://www.w3.org/ns/activitystreams',
security: 'https://w3id.org/security/v1', security: 'https://w3id.org/security/v1',
webfinger: 'https://purl.archive.org/socialweb/webfinger',
}.freeze }.freeze
CONTEXT_EXTENSION_MAP = { CONTEXT_EXTENSION_MAP = {

View File

@@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
include RoutingHelper include RoutingHelper
include FormattingHelper include FormattingHelper
context :security context :security, :webfinger
context_extensions :manually_approves_followers, :featured, :also_known_as, context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :discoverable, :suspended, :moved_to, :property_value, :discoverable, :suspended,
@@ -55,6 +55,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
ActivityPub::TagManager.instance.uri_for(object) ActivityPub::TagManager.instance.uri_for(object)
end end
def webfinger
object.local_username_and_domain
end
def type def type
if object.instance_actor? if object.instance_actor?
'Application' 'Application'

View File

@@ -27,11 +27,23 @@ class ActivityPub::FetchRemoteActorService < BaseService
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context? raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type? raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present? raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
raise Error, "Actor #{uri} has no 'preferredUsername', which is a requirement for Mastodon compatibility" if @json['preferredUsername'].blank? raise Error, "Actor #{uri} has neither 'preferredUsername' nor `webfinger`, which is a requirement for Mastodon compatibility" if @json['preferredUsername'].blank? && @json['webfinger'].blank?
@uri = @json['id'] @uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host # FEP-2c59 defines a `webfinger` attribute that makes things more explicit and spares an extra request in some cases.
# It supersedes `preferredUsername`.
if @json['webfinger'].present? && @json['webfinger'].is_a?(String)
@username, @domain = split_acct(@json['webfinger'])
Rails.logger.debug { "Actor #{uri} has an invalid `webfinger` value, falling back to `preferredUsername`" }
end
if @username.blank? || @domain.blank?
raise "Actor #{uri} has no `preferredUsername`, and either a bogus or missing `webfinger`, which is a requirement for Mastodon compatibility" if @json['preferredUsername'].blank?
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
end
check_webfinger! unless only_key check_webfinger! unless only_key

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'json/ld'
class JSON::LD::Context
add_preloaded("http://purl.archive.org/socialweb/webfinger") do
new(processingMode: "json-ld-1.0", term_definitions: {
"webfinger" => TermDefinition.new("webfinger", id: "https://purl.archive.org/socialweb/webfinger#webfinger", type_mapping: "http://www.w3.org/2001/XMLSchema#string"),
"wf" => TermDefinition.new("wf", id: "https://purl.archive.org/socialweb/webfinger#", simple: true, prefix: true),
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
})
end
alias_preloaded("https://purl.archive.org/socialweb/webfinger", "http://purl.archive.org/socialweb/webfinger")
end

View File

@@ -133,5 +133,97 @@ RSpec.describe ActivityPub::FetchRemoteActorService do
expect(subject.call('https://fake.address/@foo', prefetched_body: actor.to_json)).to be_nil expect(subject.call('https://fake.address/@foo', prefetched_body: actor.to_json)).to be_nil
end end
end end
context 'when the actor uses the webfinger propery from FEP-2c59' do
before do
actor[:webfinger] = acct
end
context 'when URI and WebFinger share the same host' do
let(:acct) { 'alice@example.com' }
let!(:webfinger) { { subject: "acct:#{acct}", links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{acct}").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource and looks up webfinger and sets values' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
expect(a_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{acct}")).to have_been_made.once
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'example.com'
end
it_behaves_like 'sets profile data'
end
context 'when WebFinger returns a different URI' do
let(:acct) { 'alice@example.com' }
let!(:webfinger) { { subject: "acct:#{acct}", links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{acct}").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource and looks up webfinger and does not create account' do
expect(account).to be_nil
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
expect(a_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{acct}")).to have_been_made.once
end
end
context 'when WebFinger is at another domain' do
let(:acct) { 'alice@iscool.af' }
let!(:webfinger) { { subject: "acct:#{acct}", links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, "https://iscool.af/.well-known/webfinger?resource=acct:#{acct}").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource and looks up webfinger and follows redirect and sets values' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to_not have_been_made
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'iscool.af'
end
it_behaves_like 'sets profile data'
end
context 'when WebFinger is at another domain and redirects back' do
let(:acct) { 'alice@iscool.af' }
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, "https://iscool.af/.well-known/webfinger?resource=acct:#{acct}").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource and looks up webfinger and follows redirect and sets values' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'example.com'
end
it_behaves_like 'sets profile data'
end
end
end end
end end