Refactor controllers for statuses, accounts, and more (#11249)

This commit is contained in:
Eugen Rochko 2019-07-08 12:03:45 +02:00 committed by GitHub
parent f14776475d
commit 63c7fe8e48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 299 additions and 289 deletions

View file

@ -3,11 +3,11 @@
class AboutController < ApplicationController class AboutController < ApplicationController
layout 'public' layout 'public'
before_action :set_instance_presenter, only: [:show, :more, :terms] before_action :set_body_classes, only: :show
before_action :set_instance_presenter
before_action :set_expires_in
def show def show; end
@hide_navbar = true
end
def more; end def more; end
@ -27,4 +27,12 @@ class AboutController < ApplicationController
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def set_body_classes
@hide_navbar = true
end
def set_expires_in
expires_in 0, public: true
end
end end

View file

@ -6,13 +6,13 @@ class AccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_body_classes
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in? expires_in 0, public: true unless user_signed_in?
@body_classes = 'with-modals'
@pinned_statuses = [] @pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@ -32,22 +32,25 @@ class AccountsController < ApplicationController
end end
format.rss do format.rss do
mark_cacheable! expires_in 0, public: true
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses) render xml: RSS::AccountSerializer.render(@account, @statuses)
end end
format.json do format.json do
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do expires_in 3.minutes, public: true
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end
end end
end end
end end
private private
def set_body_classes
@body_classes = 'with-modals'
end
def show_pinned_statuses? def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end

View file

@ -2,29 +2,19 @@
class ActivityPub::CollectionsController < Api::BaseController class ActivityPub::CollectionsController < Api::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :set_size before_action :set_size
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers before_action :set_cache_headers
def show def show
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do expires_in 3.minutes, public: true
ActiveModelSerializers::SerializableResource.new( render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
skip_activities: true
)
end
end end
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses def set_statuses
@statuses = scope_for_collection @statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)

View file

@ -3,8 +3,7 @@
class ActivityPub::InboxesController < Api::BaseController class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification include SignatureVerification
include JsonLdHelper include JsonLdHelper
include AccountOwnedConcern
before_action :set_account
def create def create
if unknown_deleted_account? if unknown_deleted_account?
@ -27,8 +26,8 @@ class ActivityPub::InboxesController < Api::BaseController
false false
end end
def set_account def account_required?
@account = Account.find_local!(params[:account_username]) if params[:account_username] params[:account_username].present?
end end
def body def body

View file

@ -4,8 +4,8 @@ class ActivityPub::OutboxesController < Api::BaseController
LIMIT = 20 LIMIT = 20
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers before_action :set_cache_headers
@ -17,10 +17,6 @@ class ActivityPub::OutboxesController < Api::BaseController
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter def outbox_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(

View file

@ -0,0 +1,68 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < Api::BaseController
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
def index
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end
private
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status, page_params),
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status),
type: :unordered,
first: page
)
end
def page_requested?
params[:page] == 'true'
end
def next_page
account_status_replies_url(
@account,
@status,
page: true,
min_id: @replies&.last&.id,
other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
)
end
def page_params
params_slice(:other_accounts, :min_id).merge(page: true)
end
end

View file

@ -1,10 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::ProofsController < Api::BaseController class Api::ProofsController < Api::BaseController
before_action :set_account include AccountOwnedConcern
before_action :set_provider before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension
def index def index
render json: @account, serializer: @provider.serializer_class render json: @account, serializer: @provider.serializer_class
@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end end
def set_account def username_param
@account = Account.find_local!(params[:username]) params[:username]
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
gone if @account.suspended?
end end
end end

View file

@ -154,8 +154,4 @@ class ApplicationController < ActionController::Base
def set_cache_headers def set_cache_headers
response.headers['Vary'] = 'Accept' response.headers['Vary'] = 'Accept'
end end
def mark_cacheable!
expires_in 0, public: true
end
end end

View file

@ -3,24 +3,19 @@
module AccountControllerConcern module AccountControllerConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12 FOLLOW_PER_PAGE = 12
included do included do
layout 'public' layout 'public'
before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
end end
private private
def set_account
@account = Account.find_local!(username_param)
end
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
@ -29,27 +24,15 @@ module AccountControllerConcern
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [
webfinger_account_link, webfinger_account_link,
atom_account_url_link,
actor_url_link, actor_url_link,
] ]
) )
end end
def username_param
params[:account_username]
end
def webfinger_account_link def webfinger_account_link
[ [
webfinger_account_url, webfinger_account_url,
[%w(rel lrdd), %w(type application/xrd+xml)], [%w(rel lrdd), %w(type application/jrd+json)],
]
end
def atom_account_url_link
[
account_url(@account, format: 'atom'),
[%w(rel alternate), %w(type application/atom+xml)],
] ]
end end
@ -63,15 +46,4 @@ module AccountControllerConcern
def webfinger_account_url def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s) webfinger_url(resource: @account.to_webfinger_s)
end end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
if @account.suspended?
expires_in(3.minutes, public: true)
gone
end
end
end end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
module AccountOwnedConcern
extend ActiveSupport::Concern
included do
before_action :set_account, if: :account_required?
before_action :check_account_approval, if: :account_required?
before_action :check_account_suspension, if: :account_required?
end
private
def account_required?
true
end
def set_account
@account = Account.find_local!(username_param)
end
def username_param
params[:account_username]
end
def check_account_approval
not_found if @account.local? && @account.user_pending?
end
def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
end
end

View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
module StatusControllerConcern
extend ActiveSupport::Concern
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{
statuses: statuses,
starting_depth: starting_depth,
}
else
next_status = statuses.pop
{
statuses: statuses,
starting_depth: starting_depth,
next_status: next_status,
}
end
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
end
end

View file

@ -6,6 +6,7 @@ class CustomCssController < ApplicationController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
expires 3.minutes, public: true
render plain: Setting.custom_css || '', content_type: 'text/css' render plain: Setting.custom_css || '', content_type: 'text/css'
end end
end end

View file

@ -7,9 +7,8 @@ class EmojisController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do expires_in 3.minutes, public: true
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end
end end
end end
end end

View file

@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in? expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network? next if @account.user_hides_network?

View file

@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in? expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network? next if @account.user_hides_network?

View file

@ -21,7 +21,7 @@ class HomeController < ApplicationController
when 'statuses' when 'statuses'
status = Status.find_by(id: matches[2]) status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?) if status&.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status)) redirect_to(ActivityPub::TagManager.instance.url_for(status))
return return
end end

View file

@ -2,6 +2,7 @@
class IntentsController < ApplicationController class IntentsController < ApplicationController
before_action :check_uri before_action :check_uri
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show def show

View file

@ -4,6 +4,7 @@ class ManifestsController < ApplicationController
skip_before_action :store_current_location skip_before_action :store_current_location
def show def show
expires_in 3.minutes, public: true
render json: InstancePresenter.new, serializer: ManifestSerializer render json: InstancePresenter.new, serializer: ManifestSerializer
end end
end end

View file

@ -31,7 +31,6 @@ class MediaController < ApplicationController
def verify_permitted_status! def verify_permitted_status!
authorize @media_attachment.status, :show? authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end

View file

@ -8,20 +8,16 @@ class PublicTimelinesController < ApplicationController
before_action :set_instance_presenter before_action :set_instance_presenter
def show def show
respond_to do |format| @initial_state_json = ActiveModelSerializers::SerializableResource.new(
format.html do InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
@initial_state_json = ActiveModelSerializers::SerializableResource.new( serializer: InitialStateSerializer
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), ).to_json
serializer: InitialStateSerializer
).to_json
end
end
end end
private private
def check_enabled def check_enabled
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview not_found unless Setting.timeline_preview
end end
def set_body_classes def set_body_classes

View file

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class RemoteFollowController < ApplicationController class RemoteFollowController < ApplicationController
include AccountOwnedConcern
layout 'modal' layout 'modal'
before_action :set_account
before_action :gone, if: :suspended_account?
before_action :set_body_classes before_action :set_body_classes
def new def new
@ -32,14 +32,6 @@ class RemoteFollowController < ApplicationController
{ acct: session[:remote_follow] } { acct: session[:remote_follow] }
end end
def set_account
@account = Account.find_local!(params[:account_username])
end
def suspended_account?
@account.suspended?
end
def set_body_classes def set_body_classes
@body_classes = 'modal-layout' @body_classes = 'modal-layout'
@hide_header = true @hide_header = true

View file

@ -1,24 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusesController < ApplicationController class StatusesController < ApplicationController
include StatusControllerConcern
include SignatureAuthentication include SignatureAuthentication
include Authorization include Authorization
include AccountOwnedConcern
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
layout 'public' layout 'public'
before_action :set_account
before_action :set_status before_action :set_status
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show] before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_replies, only: [:replies] before_action :set_body_classes
before_action :set_autoplay, only: :embed
content_security_policy only: :embed do |p| content_security_policy only: :embed do |p|
p.frame_ancestors(false) p.frame_ancestors(false)
@ -28,25 +25,20 @@ class StatusesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 10.seconds, public: true if current_account.nil? expires_in 10.seconds, public: true if current_account.nil?
@body_classes = 'with-modals'
set_ancestors set_ancestors
set_descendants set_descendants
end end
format.json do format.json do
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do expires_in 3.minutes, public: @status.distributable?
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end end
end end
end end
def activity def activity
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do expires_in 3.minutes, public: @status.distributable?
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
end end
def embed def embed
@ -54,120 +46,14 @@ class StatusesController < ApplicationController
expires_in 180, public: true expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
render layout: 'embedded' render layout: 'embedded'
end end
def replies
render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end
private private
def replies_collection_presenter def set_body_classes
page = ActivityPub::CollectionPresenter.new( @body_classes = 'with-modals'
id: replies_account_status_url(@account, @status, page_params),
type: :unordered,
part_of: replies_account_status_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
if page_requested?
page
else
ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status),
type: :unordered,
first: page
)
end
end
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{
statuses: statuses,
starting_depth: starting_depth,
}
else
next_status = statuses.pop
{
statuses: statuses,
starting_depth: starting_depth,
next_status: next_status,
}
end
end
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
end end
def set_link_headers def set_link_headers
@ -185,39 +71,15 @@ class StatusesController < ApplicationController
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def check_account_suspension
gone if @account.suspended?
end
def redirect_to_original def redirect_to_original
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end end
def set_referrer_policy_header def set_referrer_policy_header
return if @status.public_visibility? || @status.unlisted_visibility? response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
response.headers['Referrer-Policy'] = 'origin'
end end
def page_requested? def set_autoplay
params[:page] == 'true' @autoplay = truthy_param?(:autoplay)
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def next_page
last_reply = @replies.last
return if last_reply.nil?
same_account = last_reply.account_id == @account.id
return unless same_account || @replies.size == DESCENDANTS_LIMIT
same_account = false unless @replies.size == DESCENDANTS_LIMIT
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
end
def page_params
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
end end
end end

View file

@ -5,14 +5,15 @@ class TagsController < ApplicationController
layout 'public' layout 'public'
before_action :set_tag
before_action :set_body_classes before_action :set_body_classes
before_action :set_instance_presenter before_action :set_instance_presenter
def show def show
@tag = Tag.find_normalized!(params[:id])
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true
@initial_state_json = ActiveModelSerializers::SerializableResource.new( @initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: {}, token: current_session&.token), InitialStatePresenter.new(settings: {}, token: current_session&.token),
serializer: InitialStateSerializer serializer: InitialStateSerializer
@ -20,6 +21,8 @@ class TagsController < ApplicationController
end end
format.rss do format.rss do
expires_in 0, public: true
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE) @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@ -27,19 +30,22 @@ class TagsController < ApplicationController
end end
format.json do format.json do
expires_in 3.minutes, public: true
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render json: collection_presenter, render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
end end
end end
private private
def set_tag
@tag = Tag.find_normalized!(params[:id])
end
def set_body_classes def set_body_classes
@body_classes = 'with-modals' @body_classes = 'with-modals'
end end

View file

@ -13,7 +13,7 @@ module WellKnown
format.xml { render content_type: 'application/xrd+xml' } format.xml { render content_type: 'application/xrd+xml' }
end end
expires_in(3.days, public: true) expires_in 3.days, public: true
end end
end end
end end

View file

@ -19,7 +19,7 @@ module WellKnown
end end
end end
expires_in(3.days, public: true) expires_in 3.days, public: true
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
head 404 head 404
end end
@ -27,12 +27,9 @@ module WellKnown
private private
def username_from_resource def username_from_resource
resource_user = resource_param resource_user = resource_param
username, domain = resource_user.split('@') username, domain = resource_user.split('@')
if Rails.configuration.x.alternate_domains.include?(domain) resource_user = "#{username}@#{Rails.configuration.x.local_domain}" if Rails.configuration.x.alternate_domains.include?(domain)
resource_user = "#{username}@#{Rails.configuration.x.local_domain}"
end
WebfingerResource.new(resource_user).username WebfingerResource.new(resource_user).username
end end

View file

@ -40,7 +40,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
end end
def announceable?(status) def announceable?(status)
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? status.account_id == @account.id || status.distributable?
end end
def related_to_local_activity? def related_to_local_activity?

View file

@ -42,7 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
resolve_thread(@status) resolve_thread(@status)
fetch_replies(@status) fetch_replies(@status)
distribute(@status) distribute(@status)
forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? forward_for_reply if @status.distributable?
end end
def find_existing_status def find_existing_status

View file

@ -31,7 +31,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
return if @status.nil? return if @status.nil?
if @status.public_visibility? || @status.unlisted_visibility? if @status.distributable?
forward_for_reply forward_for_reply
forward_for_reblogs forward_for_reblogs
end end

View file

@ -51,7 +51,7 @@ class ActivityPub::TagManager
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?
replies_account_status_url(target.account, target, page_params) account_status_replies_url(target.account, target, page_params)
end end
# Primary audience of a status # Primary audience of a status

View file

@ -193,7 +193,7 @@ class Status < ApplicationRecord
end end
def hidden? def hidden?
private_visibility? || direct_visibility? || limited_visibility? !distributable?
end end
def distributable? def distributable?
@ -446,7 +446,8 @@ class Status < ApplicationRecord
end end
def update_statistics def update_statistics
return unless public_visibility? || unlisted_visibility? return unless distributable?
ActivityTracker.increment('activity:statuses:local') ActivityTracker.increment('activity:statuses:local')
end end
@ -455,7 +456,7 @@ class Status < ApplicationRecord
account&.increment_count!(:statuses_count) account&.increment_count!(:statuses_count)
reblog&.increment_count!(:reblogs_count) if reblog? reblog&.increment_count!(:reblogs_count) if reblog?
thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) thread&.increment_count!(:replies_count) if in_reply_to_id.present? && distributable?
end end
def decrement_counter_caches def decrement_counter_caches
@ -463,7 +464,7 @@ class Status < ApplicationRecord
account&.decrement_count!(:statuses_count) account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog? reblog&.decrement_count!(:reblogs_count) if reblog?
thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && distributable?
end end
def unlink_from_conversations def unlink_from_conversations

View file

@ -1,9 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::ActivitySerializer < ActivityPub::Serializer class ActivityPub::ActivitySerializer < ActivityPub::Serializer
cache key: 'activity', expires_in: 3.minutes
attributes :id, :type, :actor, :published, :to, :cc attributes :id, :type, :actor, :published, :to, :cc
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object?
attribute :proper_uri, key: :object, unless: :serialize_object? attribute :proper_uri, key: :object, unless: :serialize_object?
attribute :atom_uri, if: :announce? attribute :atom_uri, if: :announce?

View file

@ -3,6 +3,8 @@
class ActivityPub::ActorSerializer < ActivityPub::Serializer class ActivityPub::ActorSerializer < ActivityPub::Serializer
include RoutingHelper include RoutingHelper
cache key: 'actor', expires_in: 3.minutes
context :security context :security
context_extensions :manually_approves_followers, :featured, :also_known_as, context_extensions :manually_approves_followers, :featured, :also_known_as,

View file

@ -7,6 +7,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer
super super
end end
cache key: 'collection', expires_in: 3.minutes
attribute :id, if: -> { object.id.present? } attribute :id, if: -> { object.id.present? }
attribute :type attribute :type
attribute :total_items, if: -> { object.size.present? } attribute :total_items, if: -> { object.size.present? }

View file

@ -3,6 +3,8 @@
class ActivityPub::EmojiSerializer < ActivityPub::Serializer class ActivityPub::EmojiSerializer < ActivityPub::Serializer
include RoutingHelper include RoutingHelper
cache key: 'emoji', expires_in: 3.minutes
context_extensions :emoji context_extensions :emoji
attributes :id, :type, :name, :updated attributes :id, :type, :name, :updated

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::NoteSerializer < ActivityPub::Serializer class ActivityPub::NoteSerializer < ActivityPub::Serializer
cache key: 'note', expires_in: 3.minutes
context_extensions :atom_uri, :conversation, :sensitive, context_extensions :atom_uri, :conversation, :sensitive,
:hashtag, :emoji, :focal_point, :blurhash :hashtag, :emoji, :focal_point, :blurhash

View file

@ -14,7 +14,7 @@ class ProcessHashtagsService < BaseService
TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
end end
return unless status.public_visibility? || status.unlisted_visibility? return unless status.distributable?
status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
featured_tag.increment(status.created_at) featured_tag.increment(status.created_at)

View file

@ -50,9 +50,9 @@
= fa_icon 'reply-all fw' = fa_icon 'reply-all fw'
.status__action-bar__counter__label= obscured_counter status.replies_count .status__action-bar__counter__label= obscured_counter status.replies_count
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
- if status.public_visibility? || status.unlisted_visibility? - if status.distributable?
= fa_icon 'retweet fw' = fa_icon 'retweet fw'
- elsif status.private_visibility? - elsif status.private_visibility? || status.limited_visibility?
= fa_icon 'lock fw' = fa_icon 'lock fw'
- else - else
= fa_icon 'envelope fw' = fa_icon 'envelope fw'

View file

@ -52,8 +52,9 @@ Rails.application.routes.draw do
member do member do
get :activity get :activity
get :embed get :embed
get :replies
end end
resources :replies, only: [:index], module: :activitypub
end end
resources :followers, only: [:index], controller: :follower_accounts resources :followers, only: [:index], controller: :follower_accounts

View file

@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do
it 'sets link headers' do it 'sets link headers' do
account = Fabricate(:account, username: 'username', user: Fabricate(:user)) account = Fabricate(:account, username: 'username', user: Fabricate(:user))
get 'success', params: { account_username: 'username' } get 'success', params: { account_username: 'username' }
expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/xrd+xml", <http://test.host/users/username.atom>; rel="alternate"; type="application/atom+xml", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"' expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/jrd+json", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
end end
it 'returns http success' do it 'returns http success' do

View file

@ -92,7 +92,7 @@ describe StatusesController do
end end
it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
stub_const 'StatusesController::DESCENDANTS_LIMIT', 1 stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1
status = Fabricate(:status) status = Fabricate(:status)
child = Fabricate(:status, in_reply_to_id: status.id) child = Fabricate(:status, in_reply_to_id: status.id)
@ -103,7 +103,7 @@ describe StatusesController do
end end
it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 2 stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2
status = Fabricate(:status) status = Fabricate(:status)
child0 = Fabricate(:status, in_reply_to_id: status.id) child0 = Fabricate(:status, in_reply_to_id: status.id)
child1 = Fabricate(:status, in_reply_to_id: child0.id) child1 = Fabricate(:status, in_reply_to_id: child0.id)

View file

@ -11,16 +11,16 @@ describe 'Link headers' do
end end
it 'contains webfinger url in link header' do it 'contains webfinger url in link header' do
link_header = link_header_with_type('application/xrd+xml') link_header = link_header_with_type('application/jrd+json')
expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
expect(link_header.attr_pairs.first).to eq %w(rel lrdd) expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
end end
it 'contains atom url in link header' do it 'contains activitypub url in link header' do
link_header = link_header_with_type('application/atom+xml') link_header = link_header_with_type('application/activity+json')
expect(link_header.href).to eq 'http://www.example.com/users/test.atom' expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test'
expect(link_header.attr_pairs.first).to eq %w(rel alternate) expect(link_header.attr_pairs.first).to eq %w(rel alternate)
end end