Implement FEP 7888: Part 1 - publish conversation context (#35959)

This commit is contained in:
Jesse Karmani 2025-09-05 12:28:29 -07:00 committed by GitHub
parent 9463a31107
commit 65b4a0a6f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 309 additions and 12 deletions

View file

@ -0,0 +1,81 @@
# frozen_string_literal: true
class ActivityPub::ContextsController < ActivityPub::BaseController
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_conversation
before_action :set_items
DESCENDANTS_LIMIT = 60
def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: context_presenter, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
def items
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: items_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def account_required?
false
end
def set_conversation
@conversation = Conversation.local.find(params[:id])
end
def set_items
@items = @conversation.statuses.distributable_visibility.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def context_presenter
first_page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)
ActivityPub::ContextPresenter.from_conversation(@conversation).tap do |presenter|
presenter.first = first_page
end
end
def items_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation),
type: :unordered,
first: page
)
end
def page_requested?
truthy_param?(:page)
end
def next_page
return nil if @items.size < DESCENDANTS_LIMIT
items_context_url(@conversation, page: true, min_id: @items.last.id)
end
def page_params
params.permit(:page, :min_id)
end
end

View file

@ -40,6 +40,8 @@ class ActivityPub::TagManager
case target.object_type
when :person
target.instance_actor? ? instance_actor_url : account_url(target)
when :conversation
context_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
@ -76,6 +78,12 @@ class ActivityPub::TagManager
activity_account_status_url(target.account, target)
end
def context_uri_for(target, page_params = nil)
raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
items_context_url(target.conversation, page_params)
end
def replies_uri_for(target, page_params = nil)
raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?

View file

@ -4,10 +4,12 @@
#
# Table name: conversations
#
# id :bigint(8) not null, primary key
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint(8) not null, primary key
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
# parent_account_id :bigint(8)
# parent_status_id :bigint(8)
#
class Conversation < ApplicationRecord
@ -15,7 +17,24 @@ class Conversation < ApplicationRecord
has_many :statuses, dependent: nil
belongs_to :parent_status, class_name: 'Status', optional: true, inverse_of: :conversation
belongs_to :parent_account, class_name: 'Account', optional: true
scope :local, -> { where(uri: nil) }
before_validation :set_parent_account, on: :create
def local?
uri.nil?
end
def object_type
:conversation
end
private
def set_parent_account
self.parent_account = parent_status.account if parent_status.present?
end
end

View file

@ -70,6 +70,8 @@ class Status < ApplicationRecord
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
end
has_one :owned_conversation, class_name: 'Conversation', foreign_key: 'parent_status_id', inverse_of: :parent_status, dependent: nil
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
@ -442,7 +444,8 @@ class Status < ApplicationRecord
self.in_reply_to_account_id = carried_over_reply_to_account_id
self.conversation_id = thread.conversation_id if conversation_id.nil?
elsif conversation_id.nil?
self.conversation = Conversation.new
conversation = build_owned_conversation
self.conversation = conversation
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
class ActivityPub::ContextPresenter < ActiveModelSerializers::Model
attributes :id, :type, :attributed_to, :first, :object_type
class << self
def from_conversation(conversation)
new.tap do |presenter|
presenter.id = ActivityPub::TagManager.instance.uri_for(conversation)
presenter.attributed_to = ActivityPub::TagManager.instance.uri_for(conversation.parent_account)
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class ActivityPub::ContextSerializer < ActivityPub::Serializer
include RoutingHelper
attributes :id, :type, :attributed_to, :first
def type
'Collection'
end
end

View file

@ -9,7 +9,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:in_reply_to, :published, :url,
:attributed_to, :to, :cc, :sensitive,
:atom_uri, :in_reply_to_atom_uri,
:conversation
:conversation, :context
attribute :content
attribute :content_map, if: :language?
@ -163,6 +163,12 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
end
def context
return if object.conversation.nil?
ActivityPub::TagManager.instance.uri_for(object.conversation)
end
def local?
object.account.local?
end