diff --git a/app/controllers/activitypub/contexts_controller.rb b/app/controllers/activitypub/contexts_controller.rb new file mode 100644 index 0000000000..0d30349899 --- /dev/null +++ b/app/controllers/activitypub/contexts_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ActivityPub::ContextsController < ActivityPub::BaseController + before_action :set_conversation + + def show + expires_in 3.minutes, public: public_fetch_mode? + render_with_cache json: @conversation, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + end + + private + + def set_conversation + @conversation = Conversation.local.find(params[:id]) + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 189b920126..ec117f1449 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -25,7 +25,7 @@ module CacheConcern end def set_cache_headers - response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' + response.headers['Vary'] = public_fetch_mode? ? 'Accept, Authorization' : 'Accept, Signature, Authorization' end def cache_collection(raw, klass) diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 17ddd31fbb..63bd82bd75 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -66,7 +66,12 @@ class StatusesController < ApplicationController def set_status @status = @account.statuses.find(params[:id]) - authorize @status, :show? + + if request.authorization.present? && request.authorization.match(/^Bearer /i) + raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, '')) + else + authorize @status, :show? + end rescue Mastodon::NotPermittedError not_found end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 1c473efa3f..8bda1548c9 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -49,13 +49,12 @@ module JsonLdHelper !uri.start_with?('http://', 'https://') end + def same_origin?(url_a, url_b) + Addressable::URI.parse(url_a).host.casecmp(Addressable::URI.parse(url_b).host).zero? + end + def invalid_origin?(url) - return true if unsupported_uri_scheme?(url) - - needle = Addressable::URI.parse(url).host - haystack = Addressable::URI.parse(@account.uri).host - - !haystack.casecmp(needle).zero? + unsupported_uri_scheme?(url) || !same_origin?(url, @account.uri) end def canonicalize(json) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index f275feefc5..1ab239757d 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -90,6 +90,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity fetch_replies(@status) check_for_spam distribute(@status) + forward_for_conversation forward_for_reply end @@ -114,7 +115,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity sensitive: @object['sensitive'] || false, visibility: visibility_from_audience, thread: replied_to_status, - conversation: conversation_from_uri(@object['conversation']), + conversation: conversation_from_context, media_attachment_ids: process_attachments.take(4).map(&:id), poll: process_poll, } @@ -122,8 +123,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_audience + conversation_uri = value_or_id(@object['context']) + (audience_to + audience_cc).uniq.each do |audience| - next if audience == ActivityPub::TagManager::COLLECTIONS[:public] + next if audience == ActivityPub::TagManager::COLLECTIONS[:public] || audience == conversation_uri # Unlike with tags, there is no point in resolving accounts we don't already # know here, because silent mentions would only be used for local access @@ -340,15 +343,45 @@ class ActivityPub::Activity::Create < ActivityPub::Activity ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil? end - def conversation_from_uri(uri) - return nil if uri.nil? - return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) + def conversation_from_context + atom_uri = @object['conversation'] - begin - Conversation.find_or_create_by!(uri: uri) - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique - retry + conversation = begin + if atom_uri.present? && OStatus::TagManager.instance.local_id?(atom_uri) + Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(atom_uri, 'Conversation')) + elsif atom_uri.present? && @object['context'].present? + Conversation.find_by(uri: atom_uri) + elsif atom_uri.present? + begin + Conversation.find_or_create_by!(uri: atom_uri) + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique + retry + end + end end + + return conversation if @object['context'].nil? + + uri = value_or_id(@object['context']) + conversation ||= ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) + + return conversation if (conversation.present? && conversation.uri == uri) || !uri.start_with?('https://') + + conversation_json = begin + if @object['context'].is_a?(Hash) && !invalid_origin?(uri) + @object['context'] + else + fetch_resource(uri, true) + end + end + + return conversation if conversation_json.blank? + + conversation ||= Conversation.new + conversation.uri = uri + conversation.inbox_url = conversation_json['inbox'] + conversation.save! if conversation.changed? + conversation end def visibility_from_audience @@ -492,6 +525,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity SpamCheck.perform(@status) end + def forward_for_conversation + return unless audience_to.include?(value_or_id(@object['context'])) && @json['signature'].present? && @status.conversation.local? + + ActivityPub::ForwardDistributionWorker.perform_async(@status.conversation_id, Oj.dump(@json)) + end + def forward_for_reply return unless @status.distributable? && @json['signature'].present? && reply_to_local? diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 3f98dad2eb..b29c606e9d 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -21,8 +21,11 @@ class ActivityPub::TagManager when :person target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target) when :note, :comment, :activity - return activity_account_status_url(target.account, target) if target.reblog? - short_account_status_url(target.account, target) + if target.reblog? + activity_account_status_url(target.account, target) + else + short_account_status_url(target.account, target) + end end end @@ -33,10 +36,15 @@ class ActivityPub::TagManager when :person target.instance_actor? ? instance_actor_url : account_url(target) when :note, :comment, :activity - return activity_account_status_url(target.account, target) if target.reblog? - account_status_url(target.account, target) + if target.reblog? + activity_account_status_url(target.account, target) + else + account_status_url(target.account, target) + end when :emoji emoji_url(target) + when :conversation + context_url(target) end end @@ -66,7 +74,9 @@ class ActivityPub::TagManager [COLLECTIONS[:public]] when 'unlisted', 'private' [account_followers_url(status.account)] - when 'direct', 'limited' + when 'limited' + status.conversation_id.present? ? [uri_for(status.conversation)] : [] + when 'direct' if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) @@ -104,7 +114,7 @@ class ActivityPub::TagManager cc << COLLECTIONS[:public] end - unless status.direct_visibility? || status.limited_visibility? + unless status.direct_visibility? if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 4dfaea889d..873600b0d0 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -3,18 +3,44 @@ # # Table name: conversations # -# id :bigint(8) not null, primary key -# uri :string -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# uri :string +# created_at :datetime not null +# updated_at :datetime not null +# parent_status_id :bigint(8) +# parent_account_id :bigint(8) +# inbox_url :string # class Conversation < ApplicationRecord validates :uri, uniqueness: true, if: :uri? - has_many :statuses + belongs_to :parent_status, class_name: 'Status', optional: true, inverse_of: :conversation + belongs_to :parent_account, class_name: 'Account', optional: true + + has_many :statuses, inverse_of: :conversation + + scope :local, -> { where(uri: nil) } + + before_validation :set_parent_account, on: :create + + after_create :set_conversation_on_parent_status def local? uri.nil? end + + def object_type + :conversation + end + + private + + def set_parent_account + self.parent_account = parent_status.account if parent_status.present? + end + + def set_conversation_on_parent_status + parent_status.update_column(:conversation_id, id) if parent_status.present? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 71596ec2f8..28ae80e090 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -50,9 +50,11 @@ class Status < ApplicationRecord belongs_to :account, inverse_of: :statuses belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true - belongs_to :conversation, optional: true + belongs_to :conversation, optional: true, inverse_of: :statuses belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true + has_one :owned_conversation, class_name: 'Conversation', foreign_key: 'parent_status_id', inverse_of: :parent_status + belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true @@ -63,6 +65,7 @@ class Status < ApplicationRecord has_many :mentions, dependent: :destroy, inverse_of: :status has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status has_many :media_attachments, dependent: :nullify + has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy has_and_belongs_to_many :tags has_and_belongs_to_many :preview_cards @@ -205,7 +208,9 @@ class Status < ApplicationRecord public_visibility? || unlisted_visibility? end - alias sign? distributable? + def sign? + distributable? || limited_visibility? + end def with_media? media_attachments.any? @@ -264,11 +269,11 @@ class Status < ApplicationRecord around_create Mastodon::Snowflake::Callbacks - before_validation :prepare_contents, if: :local? - before_validation :set_reblog - before_validation :set_visibility - before_validation :set_conversation - before_validation :set_local + before_validation :prepare_contents, on: :create, if: :local? + before_validation :set_reblog, on: :create + before_validation :set_visibility, on: :create + before_validation :set_conversation, on: :create + before_validation :set_local, on: :create after_create :set_poll_id @@ -464,7 +469,7 @@ class Status < ApplicationRecord self.in_reply_to_account_id = carried_over_reply_to_account_id self.conversation_id = thread.conversation_id if conversation_id.nil? elsif conversation_id.nil? - self.conversation = Conversation.new + build_owned_conversation end end diff --git a/app/models/status_capability_token.rb b/app/models/status_capability_token.rb new file mode 100644 index 0000000000..1613569ded --- /dev/null +++ b/app/models/status_capability_token.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: status_capability_tokens +# +# id :bigint(8) not null, primary key +# status_id :bigint(8) +# token :string +# created_at :datetime not null +# updated_at :datetime not null +# +class StatusCapabilityToken < ApplicationRecord + belongs_to :status + + validates :token, presence: true + + before_validation :generate_token, on: :create + + private + + def generate_token + self.token = Doorkeeper::OAuth::Helpers::UniqueToken.generate + end +end diff --git a/app/presenters/activitypub/activity_presenter.rb b/app/presenters/activitypub/activity_presenter.rb index 5d174767fe..1198375b48 100644 --- a/app/presenters/activitypub/activity_presenter.rb +++ b/app/presenters/activitypub/activity_presenter.rb @@ -20,6 +20,8 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model else ActivityPub::TagManager.instance.uri_for(status.proper) end + elsif status.limited_visibility? + "bear:?#{{ u: ActivityPub::TagManager.instance.uri_for(status.proper), t: status.capability_tokens.first.token }.to_query}" else status.proper end diff --git a/app/serializers/activitypub/context_serializer.rb b/app/serializers/activitypub/context_serializer.rb new file mode 100644 index 0000000000..99ef9a73b9 --- /dev/null +++ b/app/serializers/activitypub/context_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ActivityPub::ContextSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :id, :type, :inbox + + def id + ActivityPub::TagManager.instance.uri_for(object) + end + + def type + 'Group' + end + + def inbox + account_inbox_url(object.parent_account) + end +end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index f26fd93a42..b0e87efe16 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -7,7 +7,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, :atom_uri, :in_reply_to_atom_uri, - :conversation + :conversation, :context attribute :content attribute :content_map, if: :language? @@ -121,6 +121,12 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end end + def context + return if object.conversation.nil? + + ActivityPub::TagManager.instance.uri_for(object.conversation) + end + def local? object.account.local? end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 0a383d6a39..f44a02849d 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -52,6 +52,7 @@ class PostStatusService < BaseService @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? + @visibility = :limited if @visibility&.to_sym != :direct && @in_reply_to&.limited_visibility? @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError @@ -64,10 +65,11 @@ class PostStatusService < BaseService ApplicationRecord.transaction do @status = @account.statuses.create!(status_attributes) + @status.capability_tokens.create! if @status.limited_visibility? end - process_hashtags_service.call(@status) - process_mentions_service.call(@status) + ProcessHashtagsService.new.call(@status) + ProcessMentionsService.new.call(@status) end def schedule_status! @@ -109,14 +111,6 @@ class PostStatusService < BaseService ISO_639.find(str)&.alpha2 end - def process_mentions_service - ProcessMentionsService.new - end - - def process_hashtags_service - ProcessHashtagsService.new - end - def scheduled? @scheduled_at.present? end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 79af3fc546..3dd46edf9a 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -42,9 +42,21 @@ class ProcessMentionsService < BaseService "@#{mentioned_account.acct}" end + if status.limited_visibility? && status.thread&.limited_visibility? + # If we are replying to a local status, then we'll have the complete + # audience copied here, both local and remote. If we are replying + # to a remote status, only local audience will be copied. Then we + # need to send our reply to the remote author's inbox for distribution + + status.thread.mentions.includes(:account).find_each do |mention| + status.mentions.create(silent: true, account: mention.account) + end + end + status.save! check_for_spam(status) + # Silent mentions need to be delivered separately mentions.each { |mention| create_notification(mention) } end diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index e4997ba0ea..3f6d7408ad 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -12,8 +12,10 @@ class ActivityPub::DistributionWorker return if skip_distribution? - ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| - [payload, @account.id, inbox_url] + if delegate_distribution? + deliver_to_parent! + else + deliver_to_inboxes! end relay! if relayable? @@ -24,22 +26,44 @@ class ActivityPub::DistributionWorker private def skip_distribution? - @status.direct_visibility? || @status.limited_visibility? + @status.direct_visibility? + end + + def delegate_distribution? + @status.limited_visibility? && @status.reply? && !@status.conversation.local? end def relayable? @status.public_visibility? end + def deliver_to_parent! + return if @status.conversation.inbox_url.blank? + + ActivityPub::DeliveryWorker.perform_async(payload, @account.id, @status.conversation.inbox_url) + end + + def deliver_to_inboxes! + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [payload, @account.id, inbox_url] + end + end + def inboxes - # Deliver the status to all followers. - # If the status is a reply to another local status, also forward it to that - # status' authors' followers. - @inboxes ||= if @status.reply? && @status.thread.account.local? && @status.distributable? - @account.followers.or(@status.thread.account.followers).inboxes - else - @account.followers.inboxes - end + # Deliver the status to all followers. If the status is a reply + # to another local status, also forward it to that status' + # authors' followers. If the status has limited visibility, + # deliver it to inboxes of people mentioned (no shared ones) + + @inboxes ||= begin + if @status.limited_visibility? + DeliveryFailureTracker.without_unavailable(Account.remote.joins(:mentions).merge(@status.mentions).pluck(:inbox_url)) + elsif @status.reply? && @status.thread.account.local? && @status.distributable? + @account.followers.or(@status.thread.account.followers).inboxes + else + @account.followers.inboxes + end + end end def payload diff --git a/app/workers/activitypub/forward_distribution_worker.rb b/app/workers/activitypub/forward_distribution_worker.rb new file mode 100644 index 0000000000..994da978be --- /dev/null +++ b/app/workers/activitypub/forward_distribution_worker.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ActivityPub::ForwardDistributionWorker < ActivityPub::DistributionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push' + + def perform(conversation_id, json) + conversation = Conversation.find(conversation_id) + + @status = conversation.parent_status + @account = conversation.parent_account + @json = json + + return if @status.nil? || @account.nil? + + deliver_to_inboxes! + rescue ActiveRecord::RecordNotFound + true + end + + private + + def payload + @json + end +end diff --git a/config/routes.rb b/config/routes.rb index 2c39b36ed3..6504b73f35 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,6 +85,7 @@ Rails.application.routes.draw do end resource :inbox, only: [:create], module: :activitypub + resources :contexts, only: [:show], module: :activitypub get '/@:username', to: 'accounts#show', as: :short_account get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies diff --git a/db/migrate/20200825232828_create_status_capability_tokens.rb b/db/migrate/20200825232828_create_status_capability_tokens.rb new file mode 100644 index 0000000000..926e75f1d8 --- /dev/null +++ b/db/migrate/20200825232828_create_status_capability_tokens.rb @@ -0,0 +1,10 @@ +class CreateStatusCapabilityTokens < ActiveRecord::Migration[5.2] + def change + create_table :status_capability_tokens do |t| + t.belongs_to :status, foreign_key: true + t.string :token + + t.timestamps + end + end +end diff --git a/db/migrate/20200827204602_add_inbox_url_to_conversations.rb b/db/migrate/20200827204602_add_inbox_url_to_conversations.rb new file mode 100644 index 0000000000..111cdf8310 --- /dev/null +++ b/db/migrate/20200827204602_add_inbox_url_to_conversations.rb @@ -0,0 +1,7 @@ +class AddInboxUrlToConversations < ActiveRecord::Migration[5.2] + def change + add_column :conversations, :parent_status_id, :bigint, null: true, default: nil + add_column :conversations, :parent_account_id, :bigint, null: true, default: nil + add_column :conversations, :inbox_url, :string, null: true, default: nil + end +end diff --git a/db/migrate/20200827205543_conversation_ids_to_timestamp_ids.rb b/db/migrate/20200827205543_conversation_ids_to_timestamp_ids.rb new file mode 100644 index 0000000000..32eae1f4f9 --- /dev/null +++ b/db/migrate/20200827205543_conversation_ids_to_timestamp_ids.rb @@ -0,0 +1,15 @@ +class ConversationIdsToTimestampIds < ActiveRecord::Migration[5.2] + def up + safety_assured do + execute("ALTER TABLE conversations ALTER COLUMN id SET DEFAULT timestamp_id('conversations')") + end + + Mastodon::Snowflake.ensure_id_sequences_exist + end + + def down + execute("LOCK conversations") + execute("SELECT setval('conversations_id_seq', (SELECT MAX(id) FROM conversations))") + execute("ALTER TABLE conversations ALTER COLUMN id SET DEFAULT nextval('conversations_id_seq')") + end +end diff --git a/db/schema.rb b/db/schema.rb index 9e1c2748b4..c04d7047ee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_30_190544) do +ActiveRecord::Schema.define(version: 2020_08_27_205543) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -278,10 +278,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.index ["account_id", "conversation_id"], name: "index_conversation_mutes_on_account_id_and_conversation_id", unique: true end - create_table "conversations", force: :cascade do |t| + create_table "conversations", id: :bigint, default: -> { "timestamp_id('conversations'::text)" }, force: :cascade do |t| t.string "uri" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "parent_status_id" + t.bigint "parent_account_id" + t.string "inbox_url" t.index ["uri"], name: "index_conversations_on_uri", unique: true end @@ -743,6 +746,14 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.index ["var"], name: "index_site_uploads_on_var", unique: true end + create_table "status_capability_tokens", force: :cascade do |t| + t.bigint "status_id" + t.string "token" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status_id"], name: "index_status_capability_tokens_on_status_id" + end + create_table "status_pins", force: :cascade do |t| t.bigint "account_id", null: false t.bigint "status_id", null: false @@ -1003,6 +1014,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade + add_foreign_key "status_capability_tokens", "statuses" add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "statuses", on_delete: :cascade add_foreign_key "status_stats", "statuses", on_delete: :cascade diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index cd6e1e6074..d5da493cf6 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -67,7 +67,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns public Cache-Control header' do @@ -92,7 +92,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it_behaves_like 'cachable response' @@ -191,7 +191,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -216,7 +216,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns public Cache-Control header' do @@ -255,7 +255,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -280,7 +280,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns private Cache-Control header' do @@ -342,7 +342,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -367,7 +367,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns private Cache-Control header' do @@ -455,7 +455,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -480,7 +480,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it_behaves_like 'cachable response' @@ -517,7 +517,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -542,7 +542,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns private Cache-Control header' do @@ -604,7 +604,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns no Cache-Control header' do @@ -629,7 +629,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns private Cache-Control header' do @@ -797,7 +797,7 @@ describe StatusesController do end it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept' + expect(response.headers['Vary']).to eq 'Accept, Authorization' end it 'returns public Cache-Control header' do diff --git a/spec/fabricators/status_capability_token_fabricator.rb b/spec/fabricators/status_capability_token_fabricator.rb new file mode 100644 index 0000000000..51c1d41205 --- /dev/null +++ b/spec/fabricators/status_capability_token_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:status_capability_token) do +end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index d2e9fe33ce..e20eb0a0d5 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -13,17 +13,22 @@ RSpec.describe ActivityPub::Activity::Create do }.with_indifferent_access end + let(:delivered_to_account_id) { nil } + + let(:dereferenced_object_json) { nil } + before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' }) + stub_request(:get, 'http://example.com/object/123').to_return(body: Oj.dump(dereferenced_object_json), headers: { 'Content-Type' => 'application/activitypub+json' }) end describe '#perform' do context 'when fetching' do - subject { described_class.new(json, sender) } + subject { described_class.new(json, sender, delivered_to_account_id: delivered_to_account_id) } before do subject.perform @@ -43,6 +48,54 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'when object is a URI' do + let(:object_json) { 'http://example.com/object/123' } + + let(:dereferenced_object_json) do + { + id: 'http://example.com/object/123', + type: 'Note', + content: 'Lorem ipsum', + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'dereferences object from URI' do + expect(a_request(:get, 'http://example.com/object/123')).to have_been_made.once + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'public' + end + end + + context 'when object is a bearcap' do + let(:object_json) { 'bear:?u=http://example.com/object/123&t=hoge' } + + let(:dereferenced_object_json) do + { + id: 'http://example.com/object/123', + type: 'Note', + content: 'Lorem ipsum', + } + end + + it 'dereferences object from URI' do + expect(a_request(:get, 'http://example.com/object/123').with(headers: { 'Authorization' => 'Bearer hoge' })).to have_been_made.once + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.uri).to eq 'http://example.com/object/123' + expect(status.visibility).to eq 'direct' + end + end + context 'standalone' do let(:object_json) do { @@ -146,12 +199,15 @@ RSpec.describe ActivityPub::Activity::Create do context 'limited' do let(:recipient) { Fabricate(:account) } + let(:delivered_to_account_id) { recipient.id } + let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - to: ActivityPub::TagManager.instance.uri_for(recipient), + to: [], + cc: [], } end @@ -164,7 +220,7 @@ RSpec.describe ActivityPub::Activity::Create do it 'creates silent mention' do status = sender.statuses.first - expect(status.mentions.first).to be_silent + expect(status.mentions.find_by(account: recipient)).to be_silent end end diff --git a/spec/models/status_capability_token_spec.rb b/spec/models/status_capability_token_spec.rb new file mode 100644 index 0000000000..50657ef271 --- /dev/null +++ b/spec/models/status_capability_token_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe StatusCapabilityToken, type: :model do +end diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb index 368ca025a0..4701f75ce9 100644 --- a/spec/workers/activitypub/distribution_worker_spec.rb +++ b/spec/workers/activitypub/distribution_worker_spec.rb @@ -9,6 +9,8 @@ describe ActivityPub::DistributionWorker do describe '#perform' do before do allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) + allow(ActivityPub::DeliveryWorker).to receive(:perform_async) + follower.follow!(status.account) end @@ -34,6 +36,40 @@ describe ActivityPub::DistributionWorker do end end + context 'with limited status' do + before do + status.update(visibility: :limited) + status.capability_tokens.create! + end + + context 'standalone' do + before do + 2.times do |i| + status.mentions.create!(silent: true, account: Fabricate(:account, username: "bob#{i}", domain: "example#{i}.com", inbox_url: "https://example#{i}.com/inbox")) + end + end + + it 'delivers to personal inboxes' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['https://example0.com/inbox', 'https://example1.com/inbox']) + end + end + + context 'when it\'s a reply' do + let(:conversation) { Fabricate(:conversation, uri: 'https://example.com/123', inbox_url: 'https://example.com/123/inbox') } + let(:parent) { Fabricate(:status, visibility: :limited, account: Fabricate(:account, username: 'alice', domain: 'example.com', inbox_url: 'https://example.com/inbox'), conversation: conversation) } + + before do + status.update(thread: parent, conversation: conversation) + end + + it 'delivers to inbox of conversation only' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).once + end + end + end + context 'with direct status' do before do status.update(visibility: :direct)