Convert activitypub/*
controller specs to request specs (#33992)
This commit is contained in:
parent
445aa4ac72
commit
ef879a532f
4 changed files with 197 additions and 129 deletions
146
spec/requests/activitypub/collections_spec.rb
Normal file
146
spec/requests/activitypub/collections_spec.rb
Normal file
|
@ -0,0 +1,146 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'ActivityPub Collections' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
|
||||
let(:remote_account) { nil }
|
||||
|
||||
before do
|
||||
Fabricate.times(2, :status_pin, account: account)
|
||||
Fabricate(:status_pin, account: account, status: private_pinned)
|
||||
Fabricate(:status, account: account, visibility: :private)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account }
|
||||
|
||||
context 'when id is "featured"' do
|
||||
let(:id) { 'featured' }
|
||||
|
||||
context 'without signature' do
|
||||
let(:remote_account) { nil }
|
||||
|
||||
it 'returns http success and correct media type and correct items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
|
||||
expect(response.parsed_body[:orderedItems])
|
||||
.to be_an(Array)
|
||||
.and have_attributes(size: 3)
|
||||
.and include(ActivityPub::TagManager.instance.uri_for(private_pinned))
|
||||
.and not_include(private_pinned.text)
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature' do
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
context 'when getting a featured resource' do
|
||||
it 'returns http success and correct media type and expected items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers
|
||||
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
|
||||
expect(response.parsed_body[:orderedItems])
|
||||
.to be_an(Array)
|
||||
.and have_attributes(size: 3)
|
||||
.and include(ActivityPub::TagManager.instance.uri_for(private_pinned))
|
||||
.and not_include(private_pinned.text)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with authorized fetch mode' do
|
||||
before { Setting.authorized_fetch = true }
|
||||
|
||||
context 'when signed request account is blocked' do
|
||||
before { account.block!(remote_account) }
|
||||
|
||||
it 'returns http success and correct media type and cache headers and empty items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq('application/activity+json')
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to include('private')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array).and(be_empty)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed request account is domain blocked' do
|
||||
before { account.block_domain!(remote_account.domain) }
|
||||
|
||||
it 'returns http success and correct media type and cache headers and empty items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq('application/activity+json')
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to include('private')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array).and(be_empty)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when id is not "featured"' do
|
||||
let(:id) { 'hoge' }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
81
spec/requests/activitypub/followers_synchronizations_spec.rb
Normal file
81
spec/requests/activitypub/followers_synchronizations_spec.rb
Normal file
|
@ -0,0 +1,81 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'ActivityPub Follower Synchronizations' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:follower_example_com_user_a) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
|
||||
let!(:follower_example_com_user_b) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }
|
||||
let!(:follower_foo_com_user_a) { Fabricate(:account, domain: 'foo.com', uri: 'https://foo.com/users/a') }
|
||||
let!(:follower_example_com_instance_actor) { Fabricate(:account, username: 'instance-actor', domain: 'example.com', uri: 'https://example.com') }
|
||||
|
||||
before do
|
||||
follower_example_com_user_a.follow!(account)
|
||||
follower_example_com_user_b.follow!(account)
|
||||
follower_foo_com_user_a.follow!(account)
|
||||
follower_example_com_instance_actor.follow!(account)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'without signature' do
|
||||
subject { get account_followers_synchronization_path(account_username: account.username) }
|
||||
|
||||
it 'returns http not authorized' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature from example.com' do
|
||||
subject { get account_followers_synchronization_path(account_username: account.username), headers: nil, sign_with: remote_account }
|
||||
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
|
||||
|
||||
it 'returns http success and cache control and activity json types and correct items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to eq 'max-age=0, private'
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
|
||||
expect(response.parsed_body[:orderedItems])
|
||||
.to be_an(Array)
|
||||
.and contain_exactly(
|
||||
follower_example_com_instance_actor.uri,
|
||||
follower_example_com_user_a.uri,
|
||||
follower_example_com_user_b.uri
|
||||
)
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
221
spec/requests/activitypub/outboxes_spec.rb
Normal file
221
spec/requests/activitypub/outboxes_spec.rb
Normal file
|
@ -0,0 +1,221 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'ActivityPub Outboxes' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
Fabricate(:status, account: account, visibility: :public)
|
||||
Fabricate(:status, account: account, visibility: :unlisted)
|
||||
Fabricate(:status, account: account, visibility: :private)
|
||||
Fabricate(:status, account: account, visibility: :direct)
|
||||
Fabricate(:status, account: account, visibility: :limited)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'without signature' do
|
||||
subject { get account_outbox_path(account_username: account.username, page: page) }
|
||||
|
||||
let(:remote_account) { nil }
|
||||
|
||||
context 'with page not requested' do
|
||||
let(:page) { nil }
|
||||
|
||||
it 'returns http success and correct media type and headers and items count' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers
|
||||
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Vary'])
|
||||
.to be_nil
|
||||
expect(response.parsed_body[:totalItems])
|
||||
.to eq 4
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with page requested' do
|
||||
let(:page) { 'true' }
|
||||
|
||||
it 'returns http success and correct media type and vary header and items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers
|
||||
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Vary'])
|
||||
.to include 'Signature'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array)
|
||||
.and(have_attributes(size: 2))
|
||||
.and(all(satisfy { |item| targets_public_collection?(item) }))
|
||||
)
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature' do
|
||||
subject { get account_outbox_path(account_username: account.username, page: page), headers: nil, sign_with: remote_account }
|
||||
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:page) { 'true' }
|
||||
|
||||
context 'when signed request account does not follow account' do
|
||||
it 'returns http success and correct media type and headers and items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to eq 'private, no-store'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array)
|
||||
.and(have_attributes(size: 2))
|
||||
.and(all(satisfy { |item| targets_public_collection?(item) }))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed request account follows account' do
|
||||
before { remote_account.follow!(account) }
|
||||
|
||||
it 'returns http success and correct media type and headers and items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to eq 'private, no-store'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array)
|
||||
.and(have_attributes(size: 3))
|
||||
.and(all(satisfy { |item| targets_public_collection?(item) || targets_followers_collection?(item, account) }))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed request account is blocked' do
|
||||
before { account.block!(remote_account) }
|
||||
|
||||
it 'returns http success and correct media type and headers and items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to eq 'private, no-store'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array).and(be_empty)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed request account is domain blocked' do
|
||||
before { account.block_domain!(remote_account.domain) }
|
||||
|
||||
it 'returns http success and correct media type and headers and items' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
expect(response.headers['Cache-Control'])
|
||||
.to eq 'private, no-store'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
orderedItems: be_an(Array).and(be_empty)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ap_public_collection
|
||||
ActivityPub::TagManager::COLLECTIONS[:public]
|
||||
end
|
||||
|
||||
def targets_public_collection?(item)
|
||||
item[:to].include?(ap_public_collection) || item[:cc].include?(ap_public_collection)
|
||||
end
|
||||
|
||||
def targets_followers_collection?(item, account)
|
||||
item[:to].include?(
|
||||
account_followers_url(account, ActionMailer::Base.default_url_options)
|
||||
)
|
||||
end
|
||||
end
|
272
spec/requests/activitypub/replies_spec.rb
Normal file
272
spec/requests/activitypub/replies_spec.rb
Normal file
|
@ -0,0 +1,272 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'ActivityPub Replies' do
|
||||
let(:status) { Fabricate(:status, visibility: parent_visibility) }
|
||||
let(:remote_account) { Fabricate(:account, domain: 'foobar.com') }
|
||||
let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
|
||||
let(:remote_querier) { nil }
|
||||
|
||||
shared_examples 'common behavior' do
|
||||
context 'when status is private' do
|
||||
let(:parent_visibility) { :private }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is direct' do
|
||||
let(:parent_visibility) { :direct }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'disallowed access' do
|
||||
context 'when status is public' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'common behavior'
|
||||
end
|
||||
|
||||
shared_examples 'allowed access' do
|
||||
context 'when account is permanently suspended' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before do
|
||||
status.account.suspend!
|
||||
status.account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before { status.account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is public' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
it 'returns http success and correct media type' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers
|
||||
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
context 'without `only_other_accounts` param' do
|
||||
it "returns items with thread author's replies" do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(
|
||||
include(
|
||||
items: be_an(Array)
|
||||
.and(have_attributes(size: 1))
|
||||
.and(all(satisfy { |item| targets_public_collection?(item) }))
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when there are few self-replies' do
|
||||
it 'points next to replies from other people' do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(
|
||||
include(
|
||||
next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=true page=true)).any? }
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are many self-replies' do
|
||||
before do
|
||||
10.times { Fabricate(:status, account: status.account, thread: status, visibility: :public) }
|
||||
end
|
||||
|
||||
it 'points next to other self-replies' do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(
|
||||
include(
|
||||
next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=false page=true)).any? }
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with `only_other_accounts` param' do
|
||||
let(:only_other_accounts) { 'true' }
|
||||
|
||||
it 'returns items with other public or unlisted replies and correctly inlines replies and uses IDs' do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(
|
||||
include(items: be_an(Array).and(have_attributes(size: 3)))
|
||||
)
|
||||
)
|
||||
|
||||
# Only inline replies that are local and public, or unlisted
|
||||
expect(inlined_replies)
|
||||
.to all(satisfy { |item| targets_public_collection?(item) })
|
||||
.and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) })
|
||||
|
||||
# Use ids for remote replies
|
||||
expect(remote_replies)
|
||||
.to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) })
|
||||
end
|
||||
|
||||
context 'when there are few replies' do
|
||||
it 'does not have a next page' do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(not_include(next: be_present))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are many replies' do
|
||||
before do
|
||||
10.times { Fabricate(:status, thread: status, visibility: :public) }
|
||||
end
|
||||
|
||||
it 'points next to other replies' do
|
||||
subject
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(
|
||||
first: be_a(Hash).and(
|
||||
include(
|
||||
next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=true page=true)).any? }
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'common behavior'
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
|
||||
|
||||
Fabricate.times(2, :status, thread: status, visibility: :public)
|
||||
Fabricate(:status, thread: status, visibility: :private)
|
||||
Fabricate(:status, account: status.account, thread: status, visibility: :public)
|
||||
Fabricate(:status, account: status.account, thread: status, visibility: :private)
|
||||
|
||||
Fabricate(:status, account: remote_account, thread: status, visibility: :public, uri: remote_reply_id)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let(:only_other_accounts) { nil }
|
||||
|
||||
context 'with no signature' do
|
||||
subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts) }
|
||||
|
||||
it_behaves_like 'allowed access'
|
||||
end
|
||||
|
||||
context 'with signature' do
|
||||
subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts), headers: nil, sign_with: remote_querier }
|
||||
|
||||
let(:remote_querier) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
it_behaves_like 'allowed access'
|
||||
|
||||
context 'when signed request account is blocked' do
|
||||
before { status.account.block!(remote_querier) }
|
||||
|
||||
it_behaves_like 'disallowed access'
|
||||
end
|
||||
|
||||
context 'when signed request account is domain blocked' do
|
||||
before { status.account.block_domain!(remote_querier.domain) }
|
||||
|
||||
it_behaves_like 'disallowed access'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inlined_replies
|
||||
response
|
||||
.parsed_body[:first][:items]
|
||||
.select { |x| x.is_a?(Hash) }
|
||||
end
|
||||
|
||||
def remote_replies
|
||||
response
|
||||
.parsed_body[:first][:items]
|
||||
.reject { |x| x.is_a?(Hash) }
|
||||
end
|
||||
|
||||
def parsed_uri_query_values(uri)
|
||||
Addressable::URI
|
||||
.parse(uri)
|
||||
.query
|
||||
.split('&')
|
||||
end
|
||||
|
||||
def ap_public_collection
|
||||
ActivityPub::TagManager::COLLECTIONS[:public]
|
||||
end
|
||||
|
||||
def targets_public_collection?(item)
|
||||
item[:to].include?(ap_public_collection) || item[:cc].include?(ap_public_collection)
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue