0
0
Fork 0

Add ActivityPub inbox (#4216)

* Add ActivityPub inbox

* Handle ActivityPub deletes

* Handle ActivityPub creates

* Handle ActivityPub announces

* Stubs for handling all activities that need to be handled

* Add ActivityPub actor resolving

* Handle conversation URI passing in ActivityPub

* Handle content language in ActivityPub

* Send accept header when fetching actor, handle JSON parse errors

* Test for ActivityPub::FetchRemoteAccountService

* Handle public key and icon/image when embedded/as array/as resolvable URI

* Implement ActivityPub::FetchRemoteStatusService

* Add stubs for more interactions

* Undo activities implemented

* Handle out of order activities

* Hook up ActivityPub to ResolveRemoteAccountService, handle
Update Account activities

* Add fragment IDs to all transient activity serializers

* Add tests and fixes

* Add stubs for missing tests

* Add more tests

* Add more tests
This commit is contained in:
Eugen Rochko 2017-08-08 21:52:15 +02:00 committed by GitHub
parent dcbc1af38a
commit dd7ef0dc41
50 changed files with 1652 additions and 21 deletions

View file

@ -0,0 +1,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::InboxesController, type: :controller do
describe 'POST #create' do
pending
end
end

View file

@ -0,0 +1,19 @@
require 'rails_helper'
RSpec.describe ActivityPub::OutboxesController, type: :controller do
let!(:account) { Fabricate(:account) }
before do
Fabricate(:status, account: account)
end
describe 'GET #show' do
before do
get :show, params: { account_username: account.username }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
end

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'rails_helper'
describe JsonLdHelper do
describe '#equals_or_includes?' do
it 'returns true when value equals' do
expect(helper.equals_or_includes?('foo', 'foo')).to be true
end
it 'returns false when value does not equal' do
expect(helper.equals_or_includes?('foo', 'bar')).to be false
end
it 'returns true when value is included' do
expect(helper.equals_or_includes?(%w(foo baz), 'foo')).to be true
end
it 'returns false when value is not included' do
expect(helper.equals_or_includes?(%w(foo baz), 'bar')).to be false
end
end
describe '#first_of_value' do
pending
end
describe '#supported_context?' do
pending
end
describe '#fetch_resource' do
pending
end
end

View file

@ -0,0 +1,29 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Announce do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: recipient) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Announce',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a reblog by sender of status' do
expect(sender.reblogged?(status)).to be true
end
end
end

View file

@ -0,0 +1,28 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Block do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Block',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(recipient),
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a block from sender to recipient' do
expect(sender.blocking?(recipient)).to be true
end
end
end

View file

@ -0,0 +1,221 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Create do
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Create',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: object_json,
}.with_indifferent_access
end
subject { described_class.new(json, sender) }
before do
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
end
describe '#perform' do
before do
subject.perform
end
context 'standalone' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
it 'missing to/cc defaults to direct privacy' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'direct'
end
end
context 'public' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
to: 'https://www.w3.org/ns/activitystreams#Public',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'public'
end
end
context 'unlisted' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
cc: 'https://www.w3.org/ns/activitystreams#Public',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'unlisted'
end
end
context 'private' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
to: 'http://example.com/followers',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'private'
end
end
context 'direct' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'direct'
end
end
context 'as a reply' do
let(:original_status) { Fabricate(:status) }
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.thread).to eq original_status
expect(status.reply?).to be true
expect(status.in_reply_to_account).to eq original_status.account
expect(status.conversation).to eq original_status.conversation
end
end
context 'with mentions' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
tag: [
{
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(recipient),
},
],
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.mentions.map(&:account)).to include(recipient)
end
end
context 'with media attachments' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
attachment: [
{
type: 'Document',
mime_type: 'image/png',
url: 'http://example.com/attachment.png',
},
],
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
end
end
context 'with hashtags' do
let(:object_json) do
{
id: 'bar',
type: 'Note',
content: 'Lorem ipsum',
tag: [
{
type: 'Hashtag',
href: 'http://example.com/blah',
name: '#test',
},
],
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.tags.map(&:name)).to include('test')
end
end
end
end

View file

@ -0,0 +1,28 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Delete do
let(:sender) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: sender, uri: 'foobar') }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Delete',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'deletes sender\'s status' do
expect(Status.find_by(id: status.id)).to be_nil
end
end
end

View file

@ -0,0 +1,28 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Follow do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(recipient),
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a follow from sender to recipient' do
expect(sender.following?(recipient)).to be true
end
end
end

View file

@ -0,0 +1,29 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Like do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: recipient) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Like',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a favourite from sender to status' do
expect(sender.favourited?(status)).to be true
end
end
end

View file

@ -0,0 +1,107 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Undo do
let(:sender) { Fabricate(:account) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Undo',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: object_json,
}.with_indifferent_access
end
subject { described_class.new(json, sender) }
describe '#perform' do
context 'with Announce' do
let(:status) { Fabricate(:status) }
let(:object_json) do
{
id: 'bar',
type: 'Announce',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
}
end
before do
Fabricate(:status, reblog: status, account: sender, uri: 'bar')
end
it 'deletes the reblog' do
subject.perform
expect(sender.reblogged?(status)).to be false
end
end
context 'with Block' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Block',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(recipient),
}
end
before do
sender.block!(recipient)
end
it 'deletes block from sender to recipient' do
subject.perform
expect(sender.blocking?(recipient)).to be false
end
end
context 'with Follow' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(recipient),
}
end
before do
sender.follow!(recipient)
end
it 'deletes follow from sender to recipient' do
subject.perform
expect(sender.following?(recipient)).to be false
end
end
context 'with Like' do
let(:status) { Fabricate(:status) }
let(:object_json) do
{
id: 'bar',
type: 'Like',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
}
end
before do
Fabricate(:favourite, account: sender, status: status)
end
it 'deletes favourite from sender to status' do
subject.perform
expect(sender.favourited?(status)).to be false
end
end
end
end

View file

@ -0,0 +1,41 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Update do
let!(:sender) { Fabricate(:account) }
before do
sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
end
let(:modified_sender) do
sender.dup.tap do |modified_sender|
modified_sender.display_name = 'Totally modified now'
end
end
let(:actor_json) do
ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
end
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Update',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: actor_json,
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'updates profile' do
expect(sender.reload.display_name).to eq 'Totally modified now'
end
end
end

View file

@ -0,0 +1,99 @@
require 'rails_helper'
RSpec.describe ActivityPub::TagManager do
include RoutingHelper
subject { described_class.instance }
describe '#url_for' do
it 'returns a string' do
account = Fabricate(:account)
expect(subject.url_for(account)).to be_a String
end
end
describe '#uri_for' do
it 'returns a string' do
account = Fabricate(:account)
expect(subject.uri_for(account)).to be_a String
end
end
describe '#to' do
it 'returns public collection for public status' do
status = Fabricate(:status, visibility: :public)
expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
end
it 'returns followers collection for unlisted status' do
status = Fabricate(:status, visibility: :unlisted)
expect(subject.to(status)).to eq [account_followers_url(status.account)]
end
it 'returns followers collection for private status' do
status = Fabricate(:status, visibility: :private)
expect(subject.to(status)).to eq [account_followers_url(status.account)]
end
it 'returns URIs of mentions for direct status' do
status = Fabricate(:status, visibility: :direct)
mentioned = Fabricate(:account)
status.mentions.create(account: mentioned)
expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
end
end
describe '#cc' do
it 'returns followers collection for public status' do
status = Fabricate(:status, visibility: :public)
expect(subject.cc(status)).to eq [account_followers_url(status.account)]
end
it 'returns public collection for unlisted status' do
status = Fabricate(:status, visibility: :unlisted)
expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public']
end
it 'returns empty array for private status' do
status = Fabricate(:status, visibility: :private)
expect(subject.cc(status)).to eq []
end
it 'returns empty array for direct status' do
status = Fabricate(:status, visibility: :direct)
expect(subject.cc(status)).to eq []
end
it 'returns URIs of mentions for non-direct status' do
status = Fabricate(:status, visibility: :public)
mentioned = Fabricate(:account)
status.mentions.create(account: mentioned)
expect(subject.cc(status)).to include(subject.uri_for(mentioned))
end
end
describe '#local_uri?' do
it 'returns false for non-local URI' do
expect(subject.local_uri?('http://example.com/123')).to be false
end
it 'returns true for local URIs' do
account = Fabricate(:account)
expect(subject.local_uri?(subject.uri_for(account))).to be true
end
end
describe '#uri_to_local_id' do
it 'returns the local ID' do
account = Fabricate(:account)
expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
end
end
describe '#uri_to_resource' do
it 'returns the local resource' do
account = Fabricate(:account)
expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account
end
end
end

View file

@ -0,0 +1,96 @@
require 'rails_helper'
RSpec.describe ActivityPub::FetchRemoteAccountService do
subject { ActivityPub::FetchRemoteAccountService.new }
let!(:actor) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/alice',
type: 'Person',
preferredUsername: 'alice',
name: 'Alice',
summary: 'Foo bar',
}
end
describe '#call' do
let(:account) { subject.call('https://example.com/alice') }
shared_examples 'sets profile data' do
it 'returns an account' do
expect(account).to be_an Account
end
it 'sets display name' do
expect(account.display_name).to eq 'Alice'
end
it 'sets note' do
expect(account.note).to eq 'Foo bar'
end
it 'sets URL' do
expect(account.url).to eq 'https://example.com/alice'
end
end
context 'when URI and WebFinger share the same host' do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'sets username and domain from webfinger' do
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'example.com'
end
include_examples 'sets profile data'
end
context 'when WebFinger presents different domain than URI' do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'fetches resource' do
account
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
end
it 'looks up webfinger' do
account
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
end
it 'looks up "redirected" webfinger' do
account
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
end
it 'sets username and domain from final webfinger' do
expect(account.username).to eq 'alice'
expect(account.domain).to eq 'iscool.af'
end
include_examples 'sets profile data'
end
end
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe ActivityPub::FetchRemoteStatusService do
pending
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe ActivityPub::ProcessAccountService do
pending
end

View file

@ -0,0 +1,9 @@
require 'rails_helper'
RSpec.describe ActivityPub::ProcessCollectionService do
subject { ActivityPub::ProcessCollectionService.new }
describe '#call' do
pending
end
end