mirror of https://github.com/mastodon/mastodon
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.rbpull/6618/head
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