Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/activitypub/collections_controller.rb`:
  Conflict caused because we have additional code to make sure pinned
  local-only toots don't get rendered on the ActivityPub endpoints.
  Ported upstream changes.
rebase/4.0.0rc2
Thibaut Girka 2020-09-07 09:21:38 +02:00
commit e5f934ddf0
18 changed files with 128 additions and 138 deletions

View File

@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def show def show
expires_in 3.minutes, public: public_fetch_mode? expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end end
private private
@ -20,17 +20,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_items def set_items
case params[:id] case params[:id]
when 'featured' when 'featured'
@items = begin @items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) }
# Because in public fetch mode we cache the response, there would be no when 'tags'
# benefit from performing the check below, since a blocked account or domain @items = for_signed_account { @account.featured_tags }
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
cache_collection(@account.pinned_statuses.not_local_only, Status)
end
end
when 'devices' when 'devices'
@items = @account.devices @items = @account.devices
else else
@ -40,7 +32,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size def set_size
case params[:id] case params[:id]
when 'featured', 'devices' when 'featured', 'devices', 'tags'
@size = @items.size @size = @items.size
else else
not_found not_found
@ -51,7 +43,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id] case params[:id]
when 'featured' when 'featured'
@type = :ordered @type = :ordered
when 'devices' when 'devices', 'tags'
@type = :unordered @type = :unordered
else else
not_found not_found
@ -66,4 +58,16 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
items: @items items: @items
) )
end end
def for_signed_account
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
yield
end
end
end end

View File

@ -20,9 +20,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def outbox_presenter def outbox_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account, page_params), id: outbox_url(page_params),
type: :ordered, type: :ordered,
part_of: account_outbox_url(@account), part_of: outbox_url,
prev: prev_page, prev: prev_page,
next: next_page, next: next_page,
items: @statuses items: @statuses
@ -32,12 +32,20 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
id: account_outbox_url(@account), id: account_outbox_url(@account),
type: :ordered, type: :ordered,
size: @account.statuses_count, size: @account.statuses_count,
first: account_outbox_url(@account, page: true), first: outbox_url(page: true),
last: account_outbox_url(@account, page: true, min_id: 0) last: outbox_url(page: true, min_id: 0)
) )
end end
end end
def outbox_url(**kwargs)
if params[:account_username].present?
account_outbox_url(@account, **kwargs)
else
instance_actor_outbox_url(**kwargs)
end
end
def next_page def next_page
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
end end
@ -65,4 +73,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def page_params def page_params
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact { page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
end end
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
end end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
before_action :set_account
before_action :set_featured_tags
respond_to :json
def index
render json: @featured_tags, each_serializer: REST::AccountFeaturedTagSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_featured_tags
@featured_tags = @account.featured_tags
end
end

View File

@ -17,6 +17,6 @@ class InstanceActorsController < ApplicationController
end end
def restrict_fields_to def restrict_fields_to
%i(id type preferred_username inbox public_key endpoints url manually_approves_followers) %i(id type preferred_username inbox outbox public_key endpoints url manually_approves_followers)
end end
end end

View File

@ -1,6 +1,5 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import openDB from '../storage/db'; import { importFetchedAccount, importFetchedAccounts } from './importer';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@ -74,45 +73,13 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
function getFromDB(dispatch, getState, index, id) {
return new Promise((resolve, reject) => {
const request = index.get(id);
request.onerror = reject;
request.onsuccess = () => {
if (!request.result) {
reject();
return;
}
dispatch(importAccount(request.result));
resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved));
};
});
}
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchRelationships([id])); dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id)); dispatch(fetchAccountRequest(id));
openDB().then(db => getFromDB( api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch,
getState,
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
id,
).then(() => db.close(), error => {
db.close();
throw error;
})).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data)); dispatch(importFetchedAccount(response.data));
})).then(() => {
dispatch(fetchAccountSuccess()); dispatch(fetchAccountSuccess());
}).catch(error => { }).catch(error => {
dispatch(fetchAccountFail(id, error)); dispatch(fetchAccountFail(id, error));

View File

@ -1,9 +1,7 @@
import api from '../api'; import api from '../api';
import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { ensureComposeIsVisible } from './compose'; import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@ -40,48 +38,6 @@ export function fetchStatusRequest(id, skipLoading) {
}; };
}; };
function getFromDB(dispatch, getState, accountIndex, index, id) {
return new Promise((resolve, reject) => {
const request = index.get(id);
request.onerror = reject;
request.onsuccess = () => {
const promises = [];
if (!request.result) {
reject();
return;
}
dispatch(importStatus(request.result));
if (getState().getIn(['accounts', request.result.account], null) === null) {
promises.push(new Promise((accountResolve, accountReject) => {
const accountRequest = accountIndex.get(request.result.account);
accountRequest.onerror = accountReject;
accountRequest.onsuccess = () => {
if (!request.result) {
accountReject();
return;
}
dispatch(importAccount(accountRequest.result));
accountResolve();
};
}));
}
if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) {
promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog));
}
resolve(Promise.all(promises));
};
});
}
export function fetchStatus(id) { export function fetchStatus(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const skipLoading = getState().getIn(['statuses', id], null) !== null; const skipLoading = getState().getIn(['statuses', id], null) !== null;
@ -94,23 +50,10 @@ export function fetchStatus(id) {
dispatch(fetchStatusRequest(id, skipLoading)); dispatch(fetchStatusRequest(id, skipLoading));
openDB().then(db => { api(getState).get(`/api/v1/statuses/${id}`).then(response => {
const transaction = db.transaction(['accounts', 'statuses'], 'read');
const accountIndex = transaction.objectStore('accounts').index('id');
const index = transaction.objectStore('statuses').index('id');
return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
db.close();
}, error => {
db.close();
throw error;
});
}).then(() => {
dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(importFetchedStatus(response.data)); dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading)); dispatch(fetchStatusSuccess(skipLoading));
})).catch(error => { }).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading)); dispatch(fetchStatusFail(id, error, skipLoading));
}); });
}; };
@ -152,7 +95,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(response => { api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
evictStatus(id);
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account)); dispatch(importFetchedAccount(response.data.account));

View File

@ -256,14 +256,6 @@ html {
background: $ui-base-color; background: $ui-base-color;
} }
.status.status-direct {
background: lighten($ui-base-color, 4%);
}
.focusable:focus .status.status-direct {
background: lighten($ui-base-color, 8%);
}
.detailed-status, .detailed-status,
.detailed-status__action-bar { .detailed-status__action-bar {
background: $white; background: $white;

View File

@ -14,7 +14,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } }, moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } }, also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' }, emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' } }, featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' }, property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' }, atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },

View File

@ -10,7 +10,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
:discoverable, :olm :discoverable, :olm
attributes :id, :type, :following, :followers, attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :inbox, :outbox, :featured, :featured_tags,
:preferred_username, :name, :summary, :preferred_username, :name, :summary,
:url, :manually_approves_followers, :url, :manually_approves_followers,
:discoverable :discoverable
@ -74,13 +74,17 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
end end
def outbox def outbox
account_outbox_url(object) object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object)
end end
def featured def featured
account_collection_url(object, :featured) account_collection_url(object, :featured)
end end
def featured_tags
account_collection_url(object, :tags)
end
def endpoints def endpoints
object object
end end

View File

@ -16,6 +16,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer
ActivityPub::NoteSerializer ActivityPub::NoteSerializer
when 'Device' when 'Device'
ActivityPub::DeviceSerializer ActivityPub::DeviceSerializer
when 'FeaturedTag'
ActivityPub::HashtagSerializer
when 'ActivityPub::CollectionPresenter' when 'ActivityPub::CollectionPresenter'
ActivityPub::CollectionSerializer ActivityPub::CollectionSerializer
when 'String' when 'String'

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class ActivityPub::HashtagSerializer < ActivityPub::Serializer
include RoutingHelper
attributes :type, :href, :name
def type
'Hashtag'
end
def name
"##{object.name}"
end
def href
if object.class.name == 'FeaturedTag'
short_account_tag_url(object.account, object.tag)
else
tag_url(object)
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class REST::AccountFeaturedTagSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :name, :url
def id
object.tag.id.to_s
end
def name
"##{object.name}"
end
def url
short_account_tag_url(object.account, object.tag)
end
end

View File

@ -37,6 +37,7 @@ Rails.application.routes.draw do
resource :instance_actor, path: 'actor', only: [:show] do resource :instance_actor, path: 'actor', only: [:show] do
resource :inbox, only: [:create], module: :activitypub resource :inbox, only: [:create], module: :activitypub
resource :outbox, only: [:show], module: :activitypub
end end
devise_scope :user do devise_scope :user do
@ -438,6 +439,7 @@ Rails.application.routes.draw do
resources :following, only: :index, controller: 'accounts/following_accounts' resources :following, only: :index, controller: 'accounts/following_accounts'
resources :lists, only: :index, controller: 'accounts/lists' resources :lists, only: :index, controller: 'accounts/lists'
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
resources :featured_tags, only: :index, controller: 'accounts/featured_tags'
member do member do
post :follow post :follow

View File

@ -67,6 +67,7 @@ module Mastodon
when :s3 when :s3
paperclip_instance = MediaAttachment.new.file paperclip_instance = MediaAttachment.new.file
s3_interface = paperclip_instance.s3_interface s3_interface = paperclip_instance.s3_interface
s3_permissions = Paperclip::Attachment.default_options[:s3_permissions]
bucket = s3_interface.bucket(Paperclip::Attachment.default_options[:s3_credentials][:bucket]) bucket = s3_interface.bucket(Paperclip::Attachment.default_options[:s3_credentials][:bucket])
last_key = options[:start_after] last_key = options[:start_after]
@ -87,7 +88,7 @@ module Mastodon
record_map = preload_records_from_mixed_objects(objects) record_map = preload_records_from_mixed_objects(objects)
objects.each do |object| objects.each do |object|
object.acl.put(acl: 'public-read') if options[:fix_permissions] && !options[:dry_run] object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !options[:dry_run]
path_segments = object.key.split('/') path_segments = object.key.split('/')
path_segments.delete('cache') path_segments.delete('cache')

View File

@ -448,7 +448,7 @@ RSpec.describe FeedManager do
FeedManager.instance.push_to_home(receiver, another_status) FeedManager.instance.push_to_home(receiver, another_status)
# We should have a tracking set and an entry in reblogs. # We should have a tracking set and an entry in reblogs.
expect(Redis.current.exists(reblog_set_key)).to be true expect(Redis.current.exists?(reblog_set_key)).to be true
expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s] expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s]
# Push everything off the end of the feed. # Push everything off the end of the feed.
@ -461,7 +461,7 @@ RSpec.describe FeedManager do
FeedManager.instance.trim('home', receiver.id) FeedManager.instance.trim('home', receiver.id)
# We should not have any reblog tracking data. # We should not have any reblog tracking data.
expect(Redis.current.exists(reblog_set_key)).to be false expect(Redis.current.exists?(reblog_set_key)).to be false
expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty
end end
end end

View File

@ -150,9 +150,9 @@ RSpec.describe SpamCheck do
let(:redis_key) { spam_check.send(:redis_key) } let(:redis_key) { spam_check.send(:redis_key) }
it 'remembers' do it 'remembers' do
expect(Redis.current.exists(redis_key)).to be true expect(Redis.current.exists?(redis_key)).to be true
spam_check.remember! spam_check.remember!
expect(Redis.current.exists(redis_key)).to be true expect(Redis.current.exists?(redis_key)).to be true
end end
end end
@ -166,9 +166,9 @@ RSpec.describe SpamCheck do
end end
it 'resets' do it 'resets' do
expect(Redis.current.exists(redis_key)).to be true expect(Redis.current.exists?(redis_key)).to be true
spam_check.reset! spam_check.reset!
expect(Redis.current.exists(redis_key)).to be false expect(Redis.current.exists?(redis_key)).to be false
end end
end end

View File

@ -55,9 +55,9 @@ RSpec.describe UnallowDomainService, type: :service do
end end
it 'removes the remote accounts\'s statuses and media attachments' do it 'removes the remote accounts\'s statuses and media attachments' do
expect { bad_status1.reload }.to_not raise_exception ActiveRecord::RecordNotFound expect { bad_status1.reload }.to_not raise_error
expect { bad_status2.reload }.to_not raise_exception ActiveRecord::RecordNotFound expect { bad_status2.reload }.to_not raise_error
expect { bad_attachment.reload }.to_not raise_exception ActiveRecord::RecordNotFound expect { bad_attachment.reload }.to_not raise_error
end end
end end
end end

View File

@ -16,8 +16,8 @@ describe Scheduler::FeedCleanupScheduler do
expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0 expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0
expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1 expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1
expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs'))).to be false expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false
expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs:2'))).to be false expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false
end end
def feed_key_for(user, subtype = nil) def feed_key_for(user, subtype = nil)