@@ -3,11 +3,11 @@ | |||
class AboutController < ApplicationController | |||
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 | |||
@hide_navbar = true | |||
end | |||
def show; end | |||
def more; end | |||
@@ -27,4 +27,12 @@ class AboutController < ApplicationController | |||
def set_instance_presenter | |||
@instance_presenter = InstancePresenter.new | |||
end | |||
def set_body_classes | |||
@hide_navbar = true | |||
end | |||
def set_expires_in | |||
expires_in 0, public: true | |||
end | |||
end |
@@ -6,13 +6,13 @@ class AccountsController < ApplicationController | |||
include AccountControllerConcern | |||
before_action :set_cache_headers | |||
before_action :set_body_classes | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
expires_in 0, public: true unless user_signed_in? | |||
@body_classes = 'with-modals' | |||
@pinned_statuses = [] | |||
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) | |||
@@ -32,22 +32,25 @@ class AccountsController < ApplicationController | |||
end | |||
format.rss do | |||
mark_cacheable! | |||
expires_in 0, public: true | |||
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) | |||
render xml: RSS::AccountSerializer.render(@account, @statuses) | |||
end | |||
format.json do | |||
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do | |||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) | |||
end | |||
expires_in 3.minutes, public: true | |||
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter | |||
end | |||
end | |||
end | |||
private | |||
def set_body_classes | |||
@body_classes = 'with-modals' | |||
end | |||
def show_pinned_statuses? | |||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? | |||
end | |||
@@ -2,29 +2,19 @@ | |||
class ActivityPub::CollectionsController < Api::BaseController | |||
include SignatureVerification | |||
include AccountOwnedConcern | |||
before_action :set_account | |||
before_action :set_size | |||
before_action :set_statuses | |||
before_action :set_cache_headers | |||
def show | |||
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do | |||
ActiveModelSerializers::SerializableResource.new( | |||
collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
skip_activities: true | |||
) | |||
end | |||
expires_in 3.minutes, public: true | |||
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true | |||
end | |||
private | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) | |||
end | |||
def set_statuses | |||
@statuses = scope_for_collection | |||
@statuses = cache_collection(@statuses, Status) | |||
@@ -3,8 +3,7 @@ | |||
class ActivityPub::InboxesController < Api::BaseController | |||
include SignatureVerification | |||
include JsonLdHelper | |||
before_action :set_account | |||
include AccountOwnedConcern | |||
def create | |||
if unknown_deleted_account? | |||
@@ -27,8 +26,8 @@ class ActivityPub::InboxesController < Api::BaseController | |||
false | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) if params[:account_username] | |||
def account_required? | |||
params[:account_username].present? | |||
end | |||
def body | |||
@@ -4,8 +4,8 @@ class ActivityPub::OutboxesController < Api::BaseController | |||
LIMIT = 20 | |||
include SignatureVerification | |||
include AccountOwnedConcern | |||
before_action :set_account | |||
before_action :set_statuses | |||
before_action :set_cache_headers | |||
@@ -17,10 +17,6 @@ class ActivityPub::OutboxesController < Api::BaseController | |||
private | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) | |||
end | |||
def outbox_presenter | |||
if page_requested? | |||
ActivityPub::CollectionPresenter.new( | |||
@@ -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 |
@@ -1,10 +1,9 @@ | |||
# frozen_string_literal: true | |||
class Api::ProofsController < Api::BaseController | |||
before_action :set_account | |||
include AccountOwnedConcern | |||
before_action :set_provider | |||
before_action :check_account_approval | |||
before_action :check_account_suspension | |||
def index | |||
render json: @account, serializer: @provider.serializer_class | |||
@@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController | |||
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:username]) | |||
end | |||
def check_account_approval | |||
not_found if @account.user_pending? | |||
end | |||
def check_account_suspension | |||
gone if @account.suspended? | |||
def username_param | |||
params[:username] | |||
end | |||
end |
@@ -154,8 +154,4 @@ class ApplicationController < ActionController::Base | |||
def set_cache_headers | |||
response.headers['Vary'] = 'Accept' | |||
end | |||
def mark_cacheable! | |||
expires_in 0, public: true | |||
end | |||
end |
@@ -3,24 +3,19 @@ | |||
module AccountControllerConcern | |||
extend ActiveSupport::Concern | |||
include AccountOwnedConcern | |||
FOLLOW_PER_PAGE = 12 | |||
included do | |||
layout 'public' | |||
before_action :set_account | |||
before_action :check_account_approval | |||
before_action :check_account_suspension | |||
before_action :set_instance_presenter | |||
before_action :set_link_headers | |||
end | |||
private | |||
def set_account | |||
@account = Account.find_local!(username_param) | |||
end | |||
def set_instance_presenter | |||
@instance_presenter = InstancePresenter.new | |||
end | |||
@@ -29,27 +24,15 @@ module AccountControllerConcern | |||
response.headers['Link'] = LinkHeader.new( | |||
[ | |||
webfinger_account_link, | |||
atom_account_url_link, | |||
actor_url_link, | |||
] | |||
) | |||
end | |||
def username_param | |||
params[:account_username] | |||
end | |||
def webfinger_account_link | |||
[ | |||
webfinger_account_url, | |||
[%w(rel lrdd), %w(type application/xrd+xml)], | |||
] | |||
end | |||
def atom_account_url_link | |||
[ | |||
account_url(@account, format: 'atom'), | |||
[%w(rel alternate), %w(type application/atom+xml)], | |||
[%w(rel lrdd), %w(type application/jrd+json)], | |||
] | |||
end | |||
@@ -63,15 +46,4 @@ module AccountControllerConcern | |||
def webfinger_account_url | |||
webfinger_url(resource: @account.to_webfinger_s) | |||
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 |
@@ -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 |
@@ -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 |
@@ -6,6 +6,7 @@ class CustomCssController < ApplicationController | |||
before_action :set_cache_headers | |||
def show | |||
expires 3.minutes, public: true | |||
render plain: Setting.custom_css || '', content_type: 'text/css' | |||
end | |||
end |
@@ -7,9 +7,8 @@ class EmojisController < ApplicationController | |||
def show | |||
respond_to do |format| | |||
format.json do | |||
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do | |||
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) | |||
end | |||
expires_in 3.minutes, public: true | |||
render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter | |||
end | |||
end | |||
end | |||
@@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController | |||
def index | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
expires_in 0, public: true unless user_signed_in? | |||
next if @account.user_hides_network? | |||
@@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController | |||
def index | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
expires_in 0, public: true unless user_signed_in? | |||
next if @account.user_hides_network? | |||
@@ -21,7 +21,7 @@ class HomeController < ApplicationController | |||
when 'statuses' | |||
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)) | |||
return | |||
end | |||
@@ -2,6 +2,7 @@ | |||
class IntentsController < ApplicationController | |||
before_action :check_uri | |||
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri | |||
def show | |||
@@ -4,6 +4,7 @@ class ManifestsController < ApplicationController | |||
skip_before_action :store_current_location | |||
def show | |||
expires_in 3.minutes, public: true | |||
render json: InstancePresenter.new, serializer: ManifestSerializer | |||
end | |||
end |
@@ -31,7 +31,6 @@ class MediaController < ApplicationController | |||
def verify_permitted_status! | |||
authorize @media_attachment.status, :show? | |||
rescue Mastodon::NotPermittedError | |||
# Reraise in order to get a 404 instead of a 403 error code | |||
raise ActiveRecord::RecordNotFound | |||
end | |||
@@ -8,20 +8,16 @@ class PublicTimelinesController < ApplicationController | |||
before_action :set_instance_presenter | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
@initial_state_json = ActiveModelSerializers::SerializableResource.new( | |||
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), | |||
serializer: InitialStateSerializer | |||
).to_json | |||
end | |||
end | |||
@initial_state_json = ActiveModelSerializers::SerializableResource.new( | |||
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), | |||
serializer: InitialStateSerializer | |||
).to_json | |||
end | |||
private | |||
def check_enabled | |||
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview | |||
not_found unless Setting.timeline_preview | |||
end | |||
def set_body_classes | |||
@@ -1,10 +1,10 @@ | |||
# frozen_string_literal: true | |||
class RemoteFollowController < ApplicationController | |||
include AccountOwnedConcern | |||
layout 'modal' | |||
before_action :set_account | |||
before_action :gone, if: :suspended_account? | |||
before_action :set_body_classes | |||
def new | |||
@@ -32,14 +32,6 @@ class RemoteFollowController < ApplicationController | |||
{ acct: session[:remote_follow] } | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) | |||
end | |||
def suspended_account? | |||
@account.suspended? | |||
end | |||
def set_body_classes | |||
@body_classes = 'modal-layout' | |||
@hide_header = true | |||
@@ -1,24 +1,21 @@ | |||
# frozen_string_literal: true | |||
class StatusesController < ApplicationController | |||
include StatusControllerConcern | |||
include SignatureAuthentication | |||
include Authorization | |||
ANCESTORS_LIMIT = 40 | |||
DESCENDANTS_LIMIT = 60 | |||
DESCENDANTS_DEPTH_LIMIT = 20 | |||
include AccountOwnedConcern | |||
layout 'public' | |||
before_action :set_account | |||
before_action :set_status | |||
before_action :set_instance_presenter | |||
before_action :set_link_headers | |||
before_action :check_account_suspension | |||
before_action :redirect_to_original, only: [:show] | |||
before_action :set_referrer_policy_header, only: [:show] | |||
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| | |||
p.frame_ancestors(false) | |||
@@ -28,25 +25,20 @@ class StatusesController < ApplicationController | |||
respond_to do |format| | |||
format.html do | |||
expires_in 10.seconds, public: true if current_account.nil? | |||
@body_classes = 'with-modals' | |||
set_ancestors | |||
set_descendants | |||
end | |||
format.json do | |||
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do | |||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) | |||
end | |||
expires_in 3.minutes, public: @status.distributable? | |||
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter | |||
end | |||
end | |||
end | |||
def activity | |||
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do | |||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) | |||
end | |||
expires_in 3.minutes, public: @status.distributable? | |||
render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter | |||
end | |||
def embed | |||
@@ -54,120 +46,14 @@ class StatusesController < ApplicationController | |||
expires_in 180, public: true | |||
response.headers['X-Frame-Options'] = 'ALLOWALL' | |||
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) | |||
render layout: 'embedded' | |||
end | |||
def replies | |||
render json: replies_collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
content_type: 'application/activity+json', | |||
skip_activities: true | |||
end | |||
private | |||
def replies_collection_presenter | |||
page = ActivityPub::CollectionPresenter.new( | |||
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 | |||
def set_body_classes | |||
@body_classes = 'with-modals' | |||
end | |||
def set_link_headers | |||
@@ -185,39 +71,15 @@ class StatusesController < ApplicationController | |||
@instance_presenter = InstancePresenter.new | |||
end | |||
def check_account_suspension | |||
gone if @account.suspended? | |||
end | |||
def redirect_to_original | |||
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? | |||
end | |||
def set_referrer_policy_header | |||
return if @status.public_visibility? || @status.unlisted_visibility? | |||
response.headers['Referrer-Policy'] = 'origin' | |||
end | |||
def page_requested? | |||
params[:page] == 'true' | |||
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) | |||
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? | |||
end | |||
def page_params | |||
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact | |||
def set_autoplay | |||
@autoplay = truthy_param?(:autoplay) | |||
end | |||
end |
@@ -5,14 +5,15 @@ class TagsController < ApplicationController | |||
layout 'public' | |||
before_action :set_tag | |||
before_action :set_body_classes | |||
before_action :set_instance_presenter | |||
def show | |||
@tag = Tag.find_normalized!(params[:id]) | |||
respond_to do |format| | |||
format.html do | |||
expires_in 0, public: true | |||
@initial_state_json = ActiveModelSerializers::SerializableResource.new( | |||
InitialStatePresenter.new(settings: {}, token: current_session&.token), | |||
serializer: InitialStateSerializer | |||
@@ -20,6 +21,8 @@ class TagsController < ApplicationController | |||
end | |||
format.rss do | |||
expires_in 0, public: true | |||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE) | |||
@statuses = cache_collection(@statuses, Status) | |||
@@ -27,19 +30,22 @@ class TagsController < ApplicationController | |||
end | |||
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 = cache_collection(@statuses, Status) | |||
render json: collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
content_type: 'application/activity+json' | |||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' | |||
end | |||
end | |||
end | |||
private | |||
def set_tag | |||
@tag = Tag.find_normalized!(params[:id]) | |||
end | |||
def set_body_classes | |||
@body_classes = 'with-modals' | |||
end | |||
@@ -13,7 +13,7 @@ module WellKnown | |||
format.xml { render content_type: 'application/xrd+xml' } | |||
end | |||
expires_in(3.days, public: true) | |||
expires_in 3.days, public: true | |||
end | |||
end | |||
end |
@@ -19,7 +19,7 @@ module WellKnown | |||
end | |||
end | |||
expires_in(3.days, public: true) | |||
expires_in 3.days, public: true | |||
rescue ActiveRecord::RecordNotFound | |||
head 404 | |||
end | |||
@@ -27,12 +27,9 @@ module WellKnown | |||
private | |||
def username_from_resource | |||
resource_user = resource_param | |||
resource_user = resource_param | |||
username, domain = resource_user.split('@') | |||
if Rails.configuration.x.alternate_domains.include?(domain) | |||
resource_user = "#{username}@#{Rails.configuration.x.local_domain}" | |||
end | |||
resource_user = "#{username}@#{Rails.configuration.x.local_domain}" if Rails.configuration.x.alternate_domains.include?(domain) | |||
WebfingerResource.new(resource_user).username | |||
end | |||
@@ -40,7 +40,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity | |||
end | |||
def announceable?(status) | |||
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? | |||
status.account_id == @account.id || status.distributable? | |||
end | |||
def related_to_local_activity? | |||
@@ -42,7 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | |||
resolve_thread(@status) | |||
fetch_replies(@status) | |||
distribute(@status) | |||
forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? | |||
forward_for_reply if @status.distributable? | |||
end | |||
def find_existing_status | |||
@@ -31,7 +31,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity | |||
return if @status.nil? | |||
if @status.public_visibility? || @status.unlisted_visibility? | |||
if @status.distributable? | |||
forward_for_reply | |||
forward_for_reblogs | |||
end | |||
@@ -51,7 +51,7 @@ class ActivityPub::TagManager | |||
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? | |||
replies_account_status_url(target.account, target, page_params) | |||
account_status_replies_url(target.account, target, page_params) | |||
end | |||
# Primary audience of a status | |||
@@ -193,7 +193,7 @@ class Status < ApplicationRecord | |||
end | |||
def hidden? | |||
private_visibility? || direct_visibility? || limited_visibility? | |||
!distributable? | |||
end | |||
def distributable? | |||
@@ -446,7 +446,8 @@ class Status < ApplicationRecord | |||
end | |||
def update_statistics | |||
return unless public_visibility? || unlisted_visibility? | |||
return unless distributable? | |||
ActivityTracker.increment('activity:statuses:local') | |||
end | |||
@@ -455,7 +456,7 @@ class Status < ApplicationRecord | |||
account&.increment_count!(:statuses_count) | |||
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 | |||
def decrement_counter_caches | |||
@@ -463,7 +464,7 @@ class Status < ApplicationRecord | |||
account&.decrement_count!(:statuses_count) | |||
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 | |||
def unlink_from_conversations | |||
@@ -1,9 +1,12 @@ | |||
# frozen_string_literal: true | |||
class ActivityPub::ActivitySerializer < ActivityPub::Serializer | |||
cache key: 'activity', expires_in: 3.minutes | |||
attributes :id, :type, :actor, :published, :to, :cc | |||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? | |||
attribute :proper_uri, key: :object, unless: :serialize_object? | |||
attribute :atom_uri, if: :announce? | |||
@@ -3,6 +3,8 @@ | |||
class ActivityPub::ActorSerializer < ActivityPub::Serializer | |||
include RoutingHelper | |||
cache key: 'actor', expires_in: 3.minutes | |||
context :security | |||
context_extensions :manually_approves_followers, :featured, :also_known_as, | |||
@@ -7,6 +7,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer | |||
super | |||
end | |||
cache key: 'collection', expires_in: 3.minutes | |||
attribute :id, if: -> { object.id.present? } | |||
attribute :type | |||
attribute :total_items, if: -> { object.size.present? } | |||
@@ -3,6 +3,8 @@ | |||
class ActivityPub::EmojiSerializer < ActivityPub::Serializer | |||
include RoutingHelper | |||
cache key: 'emoji', expires_in: 3.minutes | |||
context_extensions :emoji | |||
attributes :id, :type, :name, :updated | |||
@@ -1,6 +1,8 @@ | |||
# frozen_string_literal: true | |||
class ActivityPub::NoteSerializer < ActivityPub::Serializer | |||
cache key: 'note', expires_in: 3.minutes | |||
context_extensions :atom_uri, :conversation, :sensitive, | |||
:hashtag, :emoji, :focal_point, :blurhash | |||
@@ -14,7 +14,7 @@ class ProcessHashtagsService < BaseService | |||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? | |||
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| | |||
featured_tag.increment(status.created_at) | |||
@@ -50,9 +50,9 @@ | |||
= fa_icon 'reply-all fw' | |||
.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 | |||
- if status.public_visibility? || status.unlisted_visibility? | |||
- if status.distributable? | |||
= fa_icon 'retweet fw' | |||
- elsif status.private_visibility? | |||
- elsif status.private_visibility? || status.limited_visibility? | |||
= fa_icon 'lock fw' | |||
- else | |||
= fa_icon 'envelope fw' | |||
@@ -52,8 +52,9 @@ Rails.application.routes.draw do | |||
member do | |||
get :activity | |||
get :embed | |||
get :replies | |||
end | |||
resources :replies, only: [:index], module: :activitypub | |||
end | |||
resources :followers, only: [:index], controller: :follower_accounts | |||
@@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do | |||
it 'sets link headers' do | |||
account = Fabricate(:account, username: 'username', user: Fabricate(:user)) | |||
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 | |||
it 'returns http success' do | |||
@@ -92,7 +92,7 @@ describe StatusesController do | |||
end | |||
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) | |||
child = Fabricate(:status, in_reply_to_id: status.id) | |||
@@ -103,7 +103,7 @@ describe StatusesController do | |||
end | |||
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) | |||
child0 = Fabricate(:status, in_reply_to_id: status.id) | |||
child1 = Fabricate(:status, in_reply_to_id: child0.id) | |||
@@ -11,16 +11,16 @@ describe 'Link headers' do | |||
end | |||
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.attr_pairs.first).to eq %w(rel lrdd) | |||
end | |||
it 'contains atom url in link header' do | |||
link_header = link_header_with_type('application/atom+xml') | |||
it 'contains activitypub url in link header' do | |||
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) | |||
end | |||