mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-05 00:22:42 +00:00
Implement FEP 7888: Part 1 - publish conversation context (#35959)
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
Chromatic / Run Chromatic (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / ImageMagick tests (.ruby-version) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.2) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Bundler Audit / security (push) Has been cancelled
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
Chromatic / Run Chromatic (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Crowdin / Upload translations / upload-translations (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / ImageMagick tests (.ruby-version) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.2) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Bundler Audit / security (push) Has been cancelled
This commit is contained in:
parent
9463a31107
commit
65b4a0a6f1
81
app/controllers/activitypub/contexts_controller.rb
Normal file
81
app/controllers/activitypub/contexts_controller.rb
Normal 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
|
|
@ -40,6 +40,8 @@ class ActivityPub::TagManager
|
||||||
case target.object_type
|
case target.object_type
|
||||||
when :person
|
when :person
|
||||||
target.instance_actor? ? instance_actor_url : account_url(target)
|
target.instance_actor? ? instance_actor_url : account_url(target)
|
||||||
|
when :conversation
|
||||||
|
context_url(target)
|
||||||
when :note, :comment, :activity
|
when :note, :comment, :activity
|
||||||
return activity_account_status_url(target.account, target) if target.reblog?
|
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)
|
activity_account_status_url(target.account, target)
|
||||||
end
|
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)
|
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?
|
raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,12 @@
|
||||||
#
|
#
|
||||||
# Table name: conversations
|
# Table name: conversations
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# uri :string
|
# uri :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# parent_account_id :bigint(8)
|
||||||
|
# parent_status_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Conversation < ApplicationRecord
|
class Conversation < ApplicationRecord
|
||||||
|
@ -15,7 +17,24 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
has_many :statuses, dependent: nil
|
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?
|
def local?
|
||||||
uri.nil?
|
uri.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def object_type
|
||||||
|
:conversation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_parent_account
|
||||||
|
self.parent_account = parent_status.account if parent_status.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,6 +70,8 @@ class Status < ApplicationRecord
|
||||||
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
||||||
end
|
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 :favourites, inverse_of: :status, dependent: :destroy
|
||||||
has_many :bookmarks, 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
|
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.in_reply_to_account_id = carried_over_reply_to_account_id
|
||||||
self.conversation_id = thread.conversation_id if conversation_id.nil?
|
self.conversation_id = thread.conversation_id if conversation_id.nil?
|
||||||
elsif conversation_id.nil?
|
elsif conversation_id.nil?
|
||||||
self.conversation = Conversation.new
|
conversation = build_owned_conversation
|
||||||
|
self.conversation = conversation
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
14
app/presenters/activitypub/context_presenter.rb
Normal file
14
app/presenters/activitypub/context_presenter.rb
Normal 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
|
11
app/serializers/activitypub/context_serializer.rb
Normal file
11
app/serializers/activitypub/context_serializer.rb
Normal 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
|
|
@ -9,7 +9,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
:in_reply_to, :published, :url,
|
:in_reply_to, :published, :url,
|
||||||
:attributed_to, :to, :cc, :sensitive,
|
:attributed_to, :to, :cc, :sensitive,
|
||||||
:atom_uri, :in_reply_to_atom_uri,
|
:atom_uri, :in_reply_to_atom_uri,
|
||||||
:conversation
|
:conversation, :context
|
||||||
|
|
||||||
attribute :content
|
attribute :content
|
||||||
attribute :content_map, if: :language?
|
attribute :content_map, if: :language?
|
||||||
|
@ -163,6 +163,12 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def context
|
||||||
|
return if object.conversation.nil?
|
||||||
|
|
||||||
|
ActivityPub::TagManager.instance.uri_for(object.conversation)
|
||||||
|
end
|
||||||
|
|
||||||
def local?
|
def local?
|
||||||
object.account.local?
|
object.account.local?
|
||||||
end
|
end
|
||||||
|
|
|
@ -120,6 +120,11 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
resources :contexts, only: [:show], module: :activitypub do
|
||||||
|
member do
|
||||||
|
get :items
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
constraints(encoded_path: /%40.*/) do
|
constraints(encoded_path: /%40.*/) do
|
||||||
get '/:encoded_path', to: redirect { |params|
|
get '/:encoded_path', to: redirect { |params|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddParentStatusAndParentAccountToConversations < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :conversations, :parent_status_id, :bigint, null: true, default: nil
|
||||||
|
add_column :conversations, :parent_account_id, :bigint, null: true, default: nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexOnConversationToStatuses < ActiveRecord::Migration[8.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_index :statuses, :conversation_id, algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_08_20_084312) do
|
ActiveRecord::Schema[8.0].define(version: 2025_09_02_221600) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -359,6 +359,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_20_084312) do
|
||||||
t.string "uri"
|
t.string "uri"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.bigint "parent_status_id"
|
||||||
|
t.bigint "parent_account_id"
|
||||||
t.index ["uri"], name: "index_conversations_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
t.index ["uri"], name: "index_conversations_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1145,6 +1147,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_20_084312) do
|
||||||
t.integer "quote_approval_policy", default: 0, null: false
|
t.integer "quote_approval_policy", default: 0, null: false
|
||||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||||
t.index ["account_id"], name: "index_statuses_on_account_id"
|
t.index ["account_id"], name: "index_statuses_on_account_id"
|
||||||
|
t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
|
||||||
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
||||||
t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||||
t.index ["id", "language", "account_id"], name: "index_statuses_public_20250129", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
t.index ["id", "language", "account_id"], name: "index_statuses_public_20250129", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Fabricator(:conversation)
|
Fabricator(:conversation) do
|
||||||
|
parent_account { Fabricate(:account) }
|
||||||
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe AccountConversation do
|
||||||
|
|
||||||
it 'appends to old record when there is a match' do
|
it 'appends to old record when there is a match' do
|
||||||
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
||||||
conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
conversation = described_class.create!(account: alice, conversation_id: last_status.conversation_id, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
||||||
|
|
||||||
status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status)
|
status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status)
|
||||||
status.mentions.create(account: alice)
|
status.mentions.create(account: alice)
|
||||||
|
@ -36,7 +36,7 @@ RSpec.describe AccountConversation do
|
||||||
|
|
||||||
it 'creates new record when new participants are added' do
|
it 'creates new record when new participants are added' do
|
||||||
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
||||||
conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
conversation = described_class.create!(account: alice, conversation_id: last_status.conversation_id, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
||||||
|
|
||||||
status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status)
|
status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status)
|
||||||
status.mentions.create(account: alice)
|
status.mentions.create(account: alice)
|
||||||
|
@ -55,7 +55,7 @@ RSpec.describe AccountConversation do
|
||||||
it 'updates last status to a previous value' do
|
it 'updates last status to a previous value' do
|
||||||
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
||||||
status = Fabricate(:status, account: alice, visibility: :direct)
|
status = Fabricate(:status, account: alice, visibility: :direct)
|
||||||
conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id])
|
conversation = described_class.create!(account: alice, conversation_id: last_status.conversation_id, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id])
|
||||||
last_status.mentions.create(account: bob)
|
last_status.mentions.create(account: bob)
|
||||||
last_status.destroy!
|
last_status.destroy!
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
@ -65,7 +65,7 @@ RSpec.describe AccountConversation do
|
||||||
|
|
||||||
it 'removes the record if no other statuses are referenced' do
|
it 'removes the record if no other statuses are referenced' do
|
||||||
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
last_status = Fabricate(:status, account: alice, visibility: :direct)
|
||||||
conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
conversation = described_class.create!(account: alice, conversation_id: last_status.conversation_id, participant_account_ids: [bob.id], status_ids: [last_status.id])
|
||||||
last_status.mentions.create(account: bob)
|
last_status.mentions.create(account: bob)
|
||||||
last_status.destroy!
|
last_status.destroy!
|
||||||
expect(described_class.where(id: conversation.id).count).to eq 0
|
expect(described_class.where(id: conversation.id).count).to eq 0
|
||||||
|
|
127
spec/requests/activitypub/contexts_spec.rb
Normal file
127
spec/requests/activitypub/contexts_spec.rb
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'ActivityPub Contexts' do
|
||||||
|
let(:conversation) { Fabricate(:conversation) }
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
subject { get context_path(id: conversation.id), headers: nil }
|
||||||
|
|
||||||
|
let!(:status) { Fabricate(:status, conversation: conversation) }
|
||||||
|
let!(:unrelated_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
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[:type])
|
||||||
|
.to eq 'Collection'
|
||||||
|
|
||||||
|
expect(response.parsed_body[:first][:items])
|
||||||
|
.to be_an(Array)
|
||||||
|
.and have_attributes(size: 1)
|
||||||
|
.and include(ActivityPub::TagManager.instance.uri_for(status))
|
||||||
|
.and not_include(ActivityPub::TagManager.instance.uri_for(unrelated_status))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with pagination' do
|
||||||
|
context 'with few statuses' do
|
||||||
|
before do
|
||||||
|
3.times do
|
||||||
|
Fabricate(:status, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include a next page link' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response.parsed_body[:first][:next]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with many statuses' do
|
||||||
|
before do
|
||||||
|
(ActivityPub::ContextsController::DESCENDANTS_LIMIT + 1).times do
|
||||||
|
Fabricate(:status, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a next page link' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response.parsed_body['first']['next']).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #items' do
|
||||||
|
subject { get items_context_path(id: conversation.id, page: 0, min_id: nil), headers: nil }
|
||||||
|
|
||||||
|
context 'with few statuses' do
|
||||||
|
before do
|
||||||
|
3.times do
|
||||||
|
Fabricate(:status, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success and correct media type and correct items' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
|
||||||
|
expect(response.media_type)
|
||||||
|
.to eq 'application/activity+json'
|
||||||
|
|
||||||
|
expect(response.parsed_body[:type])
|
||||||
|
.to eq 'Collection'
|
||||||
|
|
||||||
|
expect(response.parsed_body[:first][:items])
|
||||||
|
.to be_an(Array)
|
||||||
|
.and have_attributes(size: 3)
|
||||||
|
|
||||||
|
expect(response.parsed_body[:first][:next]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with many statuses' do
|
||||||
|
before do
|
||||||
|
(ActivityPub::ContextsController::DESCENDANTS_LIMIT + 1).times do
|
||||||
|
Fabricate(:status, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a next page link' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response.parsed_body['first']['next']).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with page requested' do
|
||||||
|
before do
|
||||||
|
(ActivityPub::ContextsController::DESCENDANTS_LIMIT + 1).times do |_i|
|
||||||
|
Fabricate(:status, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the correct items' do
|
||||||
|
get items_context_path(id: conversation.id, page: 0, min_id: nil), headers: nil
|
||||||
|
next_page = response.parsed_body['first']['next']
|
||||||
|
get next_page, headers: nil
|
||||||
|
|
||||||
|
expect(response.parsed_body['items'])
|
||||||
|
.to be_an(Array)
|
||||||
|
.and have_attributes(size: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -23,6 +23,7 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||||
'zh-TW' => a_kind_of(String),
|
'zh-TW' => a_kind_of(String),
|
||||||
}),
|
}),
|
||||||
'replies' => replies_collection_values,
|
'replies' => replies_collection_values,
|
||||||
|
'context' => ActivityPub::TagManager.instance.uri_for(parent.conversation),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user