diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index e62fba748..00f3d3cba 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def show 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 private @@ -20,17 +20,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_items case params[:id] when 'featured' - @items = begin - # 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 - cache_collection(@account.pinned_statuses.not_local_only, Status) - end - end + @items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) } + when 'tags' + @items = for_signed_account { @account.featured_tags } when 'devices' @items = @account.devices else @@ -40,7 +32,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_size case params[:id] - when 'featured', 'devices' + when 'featured', 'devices', 'tags' @size = @items.size else not_found @@ -51,7 +43,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController case params[:id] when 'featured' @type = :ordered - when 'devices' + when 'devices', 'tags' @type = :unordered else not_found @@ -66,4 +58,16 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController items: @items ) 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 diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index c33c15255..e066860bf 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -20,9 +20,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def outbox_presenter if page_requested? ActivityPub::CollectionPresenter.new( - id: account_outbox_url(@account, page_params), + id: outbox_url(page_params), type: :ordered, - part_of: account_outbox_url(@account), + part_of: outbox_url, prev: prev_page, next: next_page, items: @statuses @@ -32,12 +32,20 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController id: account_outbox_url(@account), type: :ordered, size: @account.statuses_count, - first: account_outbox_url(@account, page: true), - last: account_outbox_url(@account, page: true, min_id: 0) + first: outbox_url(page: true), + last: outbox_url(page: true, min_id: 0) ) 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 account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT end @@ -65,4 +73,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def page_params { page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact end + + def set_account + @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative + end end diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb new file mode 100644 index 000000000..d6277261d --- /dev/null +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -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 diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 6f02d6a35..4b074ca19 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -17,6 +17,6 @@ class InstanceActorsController < ApplicationController end 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 diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index cb2c682a4..d28f7dad8 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -1,6 +1,5 @@ import api, { getLinks } from '../api'; -import openDB from '../storage/db'; -import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; +import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; 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_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) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); - - if (getState().getIn(['accounts', id], null) !== null) { - return; - } - dispatch(fetchAccountRequest(id)); - openDB().then(db => getFromDB( - 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 => { + api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(importFetchedAccount(response.data)); - })).then(() => { dispatch(fetchAccountSuccess()); }).catch(error => { dispatch(fetchAccountFail(id, error)); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index e565e0b0a..3fc7c0702 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -1,9 +1,7 @@ import api from '../api'; -import openDB from '../storage/db'; -import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; -import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer'; +import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; import { ensureComposeIsVisible } from './compose'; 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) { return (dispatch, getState) => { const skipLoading = getState().getIn(['statuses', id], null) !== null; @@ -94,23 +50,10 @@ export function fetchStatus(id) { dispatch(fetchStatusRequest(id, skipLoading)); - openDB().then(db => { - 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 => { + api(getState).get(`/api/v1/statuses/${id}`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(fetchStatusSuccess(skipLoading)); - })).catch(error => { + }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; @@ -152,7 +95,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusRequest(id)); api(getState).delete(`/api/v1/statuses/${id}`).then(response => { - evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 8c8d69fc4..6b81e7623 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -256,14 +256,6 @@ html { 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__action-bar { background: $white; diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 712c48823..4e406b41d 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -14,7 +14,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } }, also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } }, 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' }, atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' }, conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 627d4446b..5d2741b17 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -10,7 +10,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :discoverable, :olm attributes :id, :type, :following, :followers, - :inbox, :outbox, :featured, + :inbox, :outbox, :featured, :featured_tags, :preferred_username, :name, :summary, :url, :manually_approves_followers, :discoverable @@ -74,13 +74,17 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def outbox - account_outbox_url(object) + object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object) end def featured account_collection_url(object, :featured) end + def featured_tags + account_collection_url(object, :tags) + end + def endpoints object end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index ea7af5433..34026a6b5 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -16,6 +16,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer ActivityPub::NoteSerializer when 'Device' ActivityPub::DeviceSerializer + when 'FeaturedTag' + ActivityPub::HashtagSerializer when 'ActivityPub::CollectionPresenter' ActivityPub::CollectionSerializer when 'String' diff --git a/app/serializers/activitypub/hashtag_serializer.rb b/app/serializers/activitypub/hashtag_serializer.rb new file mode 100644 index 000000000..1a56e4dfe --- /dev/null +++ b/app/serializers/activitypub/hashtag_serializer.rb @@ -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 diff --git a/app/serializers/rest/account_featured_tag_serializer.rb b/app/serializers/rest/account_featured_tag_serializer.rb new file mode 100644 index 000000000..d8d5fd68c --- /dev/null +++ b/app/serializers/rest/account_featured_tag_serializer.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index ce8057031..48d2718c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,6 +37,7 @@ Rails.application.routes.draw do resource :instance_actor, path: 'actor', only: [:show] do resource :inbox, only: [:create], module: :activitypub + resource :outbox, only: [:show], module: :activitypub end devise_scope :user do @@ -438,6 +439,7 @@ Rails.application.routes.draw do resources :following, only: :index, controller: 'accounts/following_accounts' resources :lists, only: :index, controller: 'accounts/lists' resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' + resources :featured_tags, only: :index, controller: 'accounts/featured_tags' member do post :follow diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index 31135f7fc..5f4a414b1 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -67,6 +67,7 @@ module Mastodon when :s3 paperclip_instance = MediaAttachment.new.file 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]) last_key = options[:start_after] @@ -87,7 +88,7 @@ module Mastodon record_map = preload_records_from_mixed_objects(objects) 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.delete('cache') diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 40e8214b6..bb5bdfdc5 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -448,7 +448,7 @@ RSpec.describe FeedManager do FeedManager.instance.push_to_home(receiver, another_status) # 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] # Push everything off the end of the feed. @@ -461,7 +461,7 @@ RSpec.describe FeedManager do FeedManager.instance.trim('home', receiver.id) # 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 end end diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb index d4d66a499..159d83257 100644 --- a/spec/lib/spam_check_spec.rb +++ b/spec/lib/spam_check_spec.rb @@ -150,9 +150,9 @@ RSpec.describe SpamCheck do let(:redis_key) { spam_check.send(:redis_key) } it 'remembers' do - expect(Redis.current.exists(redis_key)).to be true + expect(Redis.current.exists?(redis_key)).to be true spam_check.remember! - expect(Redis.current.exists(redis_key)).to be true + expect(Redis.current.exists?(redis_key)).to be true end end @@ -166,9 +166,9 @@ RSpec.describe SpamCheck do end it 'resets' do - expect(Redis.current.exists(redis_key)).to be true + expect(Redis.current.exists?(redis_key)).to be true spam_check.reset! - expect(Redis.current.exists(redis_key)).to be false + expect(Redis.current.exists?(redis_key)).to be false end end diff --git a/spec/services/unallow_domain_service_spec.rb b/spec/services/unallow_domain_service_spec.rb index 559e152fb..b93945b9a 100644 --- a/spec/services/unallow_domain_service_spec.rb +++ b/spec/services/unallow_domain_service_spec.rb @@ -55,9 +55,9 @@ RSpec.describe UnallowDomainService, type: :service do end it 'removes the remote accounts\'s statuses and media attachments' do - expect { bad_status1.reload }.to_not raise_exception ActiveRecord::RecordNotFound - expect { bad_status2.reload }.to_not raise_exception ActiveRecord::RecordNotFound - expect { bad_attachment.reload }.to_not raise_exception ActiveRecord::RecordNotFound + expect { bad_status1.reload }.to_not raise_error + expect { bad_status2.reload }.to_not raise_error + expect { bad_attachment.reload }.to_not raise_error end end end diff --git a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb index 7fae680ba..914eed829 100644 --- a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb @@ -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(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:2'))).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 end def feed_key_for(user, subtype = nil)