Federate pinned statuses over ActivityPub (#6610)
* Federate pinned statuses over ActivityPub * Display pinned toots in web UI Fix #6117 * Fix migration * Fix tests * Update outbox_serializer.rb * Update remove_serializer.rb * Update add_serializer.rb * Update fetch_featured_collection_service.rbmaster
parent
45feb439bd
commit
9110db41c5
@ -0,0 +1,57 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::CollectionsController < Api::BaseController |
||||
include SignatureVerification |
||||
|
||||
before_action :set_account |
||||
before_action :set_size |
||||
before_action :set_statuses |
||||
|
||||
def show |
||||
render json: collection_presenter, |
||||
serializer: ActivityPub::CollectionSerializer, |
||||
adapter: ActivityPub::Adapter, |
||||
content_type: 'application/activity+json', |
||||
skip_activities: true |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_account |
||||
@account = Account.find_local!(params[:account_username]) |
||||
end |
||||
|
||||
def set_statuses |
||||
@statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id]) |
||||
@statuses = cache_collection(@statuses, Status) |
||||
end |
||||
|
||||
def set_size |
||||
case params[:id] |
||||
when 'featured' |
||||
@account.pinned_statuses.count |
||||
else |
||||
raise ActiveRecord::NotFound |
||||
end |
||||
end |
||||
|
||||
def scope_for_collection |
||||
case params[:id] |
||||
when 'featured' |
||||
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope| |
||||
scope.merge!(@account.pinned_statuses) |
||||
end |
||||
else |
||||
raise ActiveRecord::NotFound |
||||
end |
||||
end |
||||
|
||||
def collection_presenter |
||||
ActivityPub::CollectionPresenter.new( |
||||
id: account_collection_url(@account, params[:id]), |
||||
type: :ordered, |
||||
size: @size, |
||||
items: @statuses |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::Activity::Add < ActivityPub::Activity |
||||
def perform |
||||
return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url |
||||
|
||||
status = status_from_uri(object_uri) |
||||
|
||||
return unless status.account_id == @account.id && !@account.pinned?(status) |
||||
|
||||
StatusPin.create!(account: @account, status: status) |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::Activity::Remove < ActivityPub::Activity |
||||
def perform |
||||
return unless @json['origin'].present? && value_or_id(@json['origin']) == @account.featured_collection_url |
||||
|
||||
status = status_from_uri(object_uri) |
||||
|
||||
return unless status.account_id == @account.id |
||||
|
||||
pin = StatusPin.find_by(account: @account, status: status) |
||||
pin&.destroy! |
||||
end |
||||
end |
@ -0,0 +1,24 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::AddSerializer < ActiveModel::Serializer |
||||
include RoutingHelper |
||||
|
||||
attributes :type, :actor, :target |
||||
attribute :proper_object, key: :object |
||||
|
||||
def type |
||||
'Add' |
||||
end |
||||
|
||||
def actor |
||||
ActivityPub::TagManager.instance.uri_for(object.account) |
||||
end |
||||
|
||||
def proper_object |
||||
ActivityPub::TagManager.instance.uri_for(object) |
||||
end |
||||
|
||||
def target |
||||
account_collection_url(object, :featured) |
||||
end |
||||
end |
@ -0,0 +1,8 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer |
||||
def self.serializer_for(model, options) |
||||
return ActivityPub::ActivitySerializer if model.is_a?(Status) |
||||
super |
||||
end |
||||
end |
@ -0,0 +1,24 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::RemoveSerializer < ActiveModel::Serializer |
||||
include RoutingHelper |
||||
|
||||
attributes :type, :actor, :origin |
||||
attribute :proper_object, key: :object |
||||
|
||||
def type |
||||
'Remove' |
||||
end |
||||
|
||||
def actor |
||||
ActivityPub::TagManager.instance.uri_for(object.account) |
||||
end |
||||
|
||||
def proper_object |
||||
ActivityPub::TagManager.instance.uri_for(object) |
||||
end |
||||
|
||||
def origin |
||||
account_collection_url(object, :featured) |
||||
end |
||||
end |
@ -0,0 +1,52 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::FetchFeaturedCollectionService < BaseService |
||||
include JsonLdHelper |
||||
|
||||
def call(account) |
||||
@account = account |
||||
@json = fetch_resource(@account.featured_collection_url, true) |
||||
|
||||
return unless supported_context? |
||||
return if @account.suspended? || @account.local? |
||||
|
||||
case @json['type'] |
||||
when 'Collection', 'CollectionPage' |
||||
process_items @json['items'] |
||||
when 'OrderedCollection', 'OrderedCollectionPage' |
||||
process_items @json['orderedItems'] |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def process_items(items) |
||||
status_ids = items.map { |item| value_or_id(item) } |
||||
.reject { |uri| ActivityPub::TagManager.instance.local_uri?(uri) } |
||||
.map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) } |
||||
.compact |
||||
.select { |status| status.account_id == @account.id } |
||||
.map(&:id) |
||||
|
||||
to_remove = [] |
||||
to_add = status_ids |
||||
|
||||
StatusPin.where(account: @account).pluck(:status_id).each do |status_id| |
||||
if status_ids.include?(status_id) |
||||
to_add.delete(status_id) |
||||
else |
||||
to_remove << status_id |
||||
end |
||||
end |
||||
|
||||
StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty? |
||||
|
||||
to_add.each do |status_id| |
||||
StatusPin.create!(account: @account, status_id: status_id) |
||||
end |
||||
end |
||||
|
||||
def supported_context? |
||||
super(@json) |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::SynchronizeFeaturedCollectionWorker |
||||
include Sidekiq::Worker |
||||
|
||||
sidekiq_options queue: 'pull' |
||||
|
||||
def perform(account_id) |
||||
ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id)) |
||||
rescue ActiveRecord::RecordNotFound |
||||
true |
||||
end |
||||
end |
@ -0,0 +1,5 @@ |
||||
class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1] |
||||
def change |
||||
add_column :accounts, :featured_collection_url, :string |
||||
end |
||||
end |
@ -0,0 +1,29 @@ |
||||
require 'rails_helper' |
||||
|
||||
RSpec.describe ActivityPub::Activity::Add do |
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } |
||||
let(:status) { Fabricate(:status, account: sender) } |
||||
|
||||
let(:json) do |
||||
{ |
||||
'@context': 'https://www.w3.org/ns/activitystreams', |
||||
id: 'foo', |
||||
type: 'Add', |
||||
actor: ActivityPub::TagManager.instance.uri_for(sender), |
||||
object: ActivityPub::TagManager.instance.uri_for(status), |
||||
target: sender.featured_collection_url, |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
describe '#perform' do |
||||
subject { described_class.new(json, sender) } |
||||
|
||||
before do |
||||
subject.perform |
||||
end |
||||
|
||||
it 'creates a pin' do |
||||
expect(sender.pinned?(status)).to be true |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
require 'rails_helper' |
||||
|
||||
RSpec.describe ActivityPub::Activity::Remove do |
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } |
||||
let(:status) { Fabricate(:status, account: sender) } |
||||
|
||||
let(:json) do |
||||
{ |
||||
'@context': 'https://www.w3.org/ns/activitystreams', |
||||
id: 'foo', |
||||
type: 'Add', |
||||
actor: ActivityPub::TagManager.instance.uri_for(sender), |
||||
object: ActivityPub::TagManager.instance.uri_for(status), |
||||
origin: sender.featured_collection_url, |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
describe '#perform' do |
||||
subject { described_class.new(json, sender) } |
||||
|
||||
before do |
||||
StatusPin.create!(account: sender, status: status) |
||||
subject.perform |
||||
end |
||||
|
||||
it 'removes a pin' do |
||||
expect(sender.pinned?(status)).to be false |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue