Skip to content

WIP: Implement FEP-7888 - conversation context #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: freq-main
Choose a base branch
from
48 changes: 48 additions & 0 deletions app/controllers/activitypub/contexts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,63 @@

class ActivityPub::ContextsController < ActivityPub::BaseController
before_action :set_conversation
before_action :set_items, only: :items

DESCENDANTS_LIMIT = 60

def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: @conversation, 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 items_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: context_items_url(@conversation, page_params),
type: :unordered,
part_of: context_items_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? status : status.uri }
)

return page if page_requested?

ActivityPub::CollectionPresenter.new(
id: context_items_url(@conversation),
type: :unordered,
first: page
)
end

def page_requested?
truthy_param?(:page)
end

def next_page
return nil if @items.size < DESCENDANTS_LIMIT

context_items_url(@conversation, page: true, min_id: @items.last.id)
end

def page_params
params.permit(:page, :min_id)
end
end
2 changes: 1 addition & 1 deletion app/controllers/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def set_link_headers
def set_status
@status = @account.statuses.find(params[:id])

if request.authorization.present? && request.authorization.match(/^Bearer /i)
if !@status.distributable? && request.authorization.present? && request.authorization.match(/^Bearer /i)
raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, ''))
else
authorize @status, :show?
Expand Down
8 changes: 8 additions & 0 deletions app/lib/activitypub/tag_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def uri_for(target)
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?

Expand Down Expand Up @@ -68,6 +70,12 @@ def activity_uri_for(target)
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?

context_items_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?

Expand Down
2 changes: 0 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ class User < ApplicationRecord
scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') }

before_validation :sanitize_role
before_validation :sanitize_time_zone
before_validation :sanitize_locale
before_create :set_approved
after_commit :send_pending_devise_notifications
after_create_commit :trigger_webhooks
Expand Down
29 changes: 25 additions & 4 deletions app/serializers/activitypub/context_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,38 @@
class ActivityPub::ContextSerializer < ActivityPub::Serializer
include RoutingHelper

attributes :id, :type, :inbox
attributes :id, :type, :first, :attributed_to

has_one :first, serializer: ActivityPub::CollectionSerializer

def id
ActivityPub::TagManager.instance.uri_for(object)
end

def attributed_to
ActivityPub::TagManager.instance.uri_for(object.parent_account)
end

def type
'Group'
'Collection'
end

def inbox
account_inbox_url(object.parent_account)
def first
conversation_statuses = object.statuses[0..5]
last_status = conversation_statuses.last
conversation_statuses = conversation_statuses.pluck(:id, :uri)
last_id = conversation_statuses.last&.first
has_more = object.statuses.count > ActivityPub::ContextsController::DESCENDANTS_LIMIT

next_page = if has_more
last_id ? ActivityPub::TagManager.instance.context_uri_for(last_status, page: true, min_id: last_id) : ActivityPub::TagManager.instance.context_uri_for(last_status, page: true, only_other_accounts: true)
end

ActivityPub::CollectionPresenter.new(
type: :unordered,
part_of: ActivityPub::TagManager.instance.uri_for(object),
items: conversation_statuses.map(&:second),
next: next_page
)
end
end
2 changes: 2 additions & 0 deletions app/serializers/activitypub/note_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
has_many :virtual_attachments, key: :attachment
has_many :virtual_tags, key: :tag

has_one :context
has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local?
has_one :likes, serializer: ActivityPub::CollectionSerializer, if: :local?
has_one :shares, serializer: ActivityPub::CollectionSerializer, if: :local?
Expand Down Expand Up @@ -158,6 +159,7 @@ def conversation

def context
return if object.conversation.nil?
return if object.conversation.parent_status.nil?

ActivityPub::TagManager.instance.uri_for(object.conversation)
end
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def redirect_with_vary(path)
end

resource :inbox, only: [:create], module: :activitypub
resources :contexts, only: [:show], module: :activitypub
resources :contexts, only: [:show], module: :activitypub do
resources :items, only: [:index], module: :activitypub
end

constraints(encoded_path: /%40.*/) do
get '/:encoded_path', to: redirect { |params|
Expand Down
42 changes: 42 additions & 0 deletions spec/controllers/activitypub/contexts_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::ContextsController do
let(:user) { Fabricate(:user) }
let(:conversation) { Fabricate(:conversation) }

before do
allow(controller).to receive(:signed_request_actor).and_return(nil)
end

describe 'GET #show' do
context 'with few statuses' do
before do
3.times do
Fabricate(:status, visibility: :private, account: user.account, conversation: conversation)
end
end

it 'does not include a next page link' do
get :show, params: { id: conversation.id }
json = JSON.parse(response.body)
expect(json['first']['next']).to be_nil
end
end

context 'with many statuses' do
before do
(ActivityPub::ContextsController::DESCENDANTS_LIMIT + 1).times do
Fabricate(:status, visibility: :private, account: user.account, conversation: conversation)
end
end

it 'includes a next page link' do
get :show, params: { id: conversation.id }
json = JSON.parse(response.body)
expect(json['first']['next']).to_not be_nil
end
end
end
end
4 changes: 3 additions & 1 deletion spec/fabricators/conversation_fabricator.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

Fabricator(:conversation)
Fabricator(:conversation) do
parent_account { Fabricate(:account) }
end
17 changes: 9 additions & 8 deletions spec/serializers/activitypub/note_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

let!(:account) { Fabricate(:account) }
let!(:other) { Fabricate(:account) }
let!(:parent) { Fabricate(:status, account: account, visibility: :public, language: 'zh-TW') }
let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) }
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:parent) { Fabricate(:status, account: account, visibility: :private, language: 'zh-TW') }
let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :private) }
let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :private) }
let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :private) }
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :private) }
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }

it 'has the expected shape and replies collection' do
Expand All @@ -22,16 +22,17 @@
'contentMap' => include({
'zh-TW' => a_kind_of(String),
}),
'context' => ActivityPub::TagManager.instance.uri_for(parent.conversation),
'replies' => replies_collection_values,
})
end

def replies_collection_values
include(
'type' => eql('Collection'),
'type' => 'Collection',
'first' => include(
'type' => eql('CollectionPage'),
'items' => reply_items
'type' => 'CollectionPage',
'items' => []
)
)
end
Expand Down