diff options
20 files changed, 274 insertions, 28 deletions
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 4d4f5e364..ec123dc5b 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -12,7 +12,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def show expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?)) - render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: signed_request_account&.domain end private diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 0360dc390..23cbb8c37 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -37,14 +37,18 @@ class StatusesController < ApplicationController format.json do expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter + render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, target_domain: signed_request_account&.domain end end end def activity expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter + render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), + content_type: 'application/activity+json', + serializer: ActivityPub::ActivitySerializer, + adapter: ActivityPub::Adapter, + target_domain: signed_request_account&.domain end def embed diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 3f98dad2e..7951c130a 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -60,8 +60,8 @@ class ActivityPub::TagManager # Public statuses go out to primarily the public collection # Unlisted and private statuses go out primarily to the followers collection # Others go out only to the people they mention - def to(status) - case status.visibility + def to(status, target_domain: nil) + case status.visibility_for_domain(target_domain) when 'public' [COLLECTIONS[:public]] when 'unlisted', 'private' @@ -92,19 +92,21 @@ class ActivityPub::TagManager # Unlisted statuses go to the public as well # Both of those and private statuses also go to the people mentioned in them # Direct ones don't have a secondary audience - def cc(status) + def cc(status, target_domain: nil) cc = [] cc << uri_for(status.reblog.account) if status.reblog? - case status.visibility + visibility = status.visibility_for_domain(target_domain) + + case visibility when 'public' cc << account_followers_url(status.account) when 'unlisted' cc << COLLECTIONS[:public] end - unless status.direct_visibility? || status.limited_visibility? + unless %w(direct limited).include?(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/account.rb b/app/models/account.rb index 8b384f212..301dc6c45 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -360,6 +360,29 @@ class Account < ApplicationRecord shared_inbox_url.presence || inbox_url end + def max_visibility_for_domain(domain) + return 'public' if domain.blank? + + domain_permissions.find_by(domain: domain)&.visibility || 'public' + end + + def visibility_for_domain(domain) + v = visibility.to_s + + case max_visibility_for_domain(domain) + when 'public' + v + when 'unlisted' + v == 'public' ? 'unlisted' : v + when 'private' + %w(public unlisted).include?(v) ? 'private' : v + when 'direct' + 'direct' + else + v != 'direct' ? 'limited' : 'direct' + end + end + class Field < ActiveModelSerializers::Model attributes :name, :value, :verified_at, :account, :errors diff --git a/app/models/account_domain_permission.rb b/app/models/account_domain_permission.rb new file mode 100644 index 000000000..ffa9cbbec --- /dev/null +++ b/app/models/account_domain_permission.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: account_domain_permissions +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) not null +# domain :string default(""), not null +# visibility :integer default("public"), not null +# + +class AccountDomainPermission < ApplicationRecord + include Paginable + + belongs_to :account, inverse_of: :domain_permissions + enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility + + class << self + def create_by_domains(permissions_list) + Array(permissions_list).map(&method(:normalize)).map do |permissions| + where(**permissions).first_or_create + end + end + + def create_by_domains!(permissions_list) + Array(permissions_list).map(&method(:normalize)).map do |permissions| + where(**permissions).first_or_create! + end + end + + def create_or_update(domain_permissions) + domain_permissions = normalize(domain_permissions) + permissions = find_by(domain: domain_permissions[:domain]) + if permissions.present? + permissions.update(**domain_permissions) + else + create(**domain_permissions) + end + permissions + end + + def create_or_update!(domain_permissions) + domain_permissions = normalize(domain_permissions) + permissions = find_by(domain: domain_permissions[:domain]) + if permissions.present? + permissions.update!(**domain_permissions) + else + create!(**domain_permissions) + end + permissions + end + + private + + def normalize(hash) + hash.symbolize_keys! + hash[:domain] = hash[:domain].strip.downcase + hash.compact + end + end +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index db7396582..10eaa4874 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -63,5 +63,8 @@ module AccountAssociations # Threads has_many :threads, class_name: 'Conversation', inverse_of: :account, dependent: :nullify + + # Domain permissions + has_many :domain_permissions, class_name: 'AccountDomainPermission', inverse_of: :account, dependent: :destroy end end diff --git a/app/models/status.rb b/app/models/status.rb index e8ea2f70a..4806f81f4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -74,6 +74,7 @@ class Status < ApplicationRecord has_many :inlined_attachments, class_name: 'InlineMediaAttachment', inverse_of: :status, dependent: :destroy has_many :mutes, class_name: 'StatusMute', inverse_of: :status, dependent: :destroy belongs_to :conversation_mute, primary_key: 'conversation_id', foreign_key: 'conversation_id', inverse_of: :conversation, dependent: :destroy, optional: true + has_many :domain_permissions, class_name: 'StatusDomainPermission', inverse_of: :status, dependent: :destroy has_and_belongs_to_many :tags has_and_belongs_to_many :preview_cards @@ -154,6 +155,7 @@ class Status < ApplicationRecord thread: { account: :account_stat } delegate :domain, to: :account, prefix: true + delegate :max_visibility_for_domain, to: :account REAL_TIME_WINDOW = 6.hours @@ -280,6 +282,23 @@ class Status < ApplicationRecord update_status_stat!(key => [public_send(key) - 1, 0].max) end + def visibility_for_domain(domain) + v = domain_permissions.find_by(domain: domain)&.visibility || visibility.to_s + + case max_visibility_for_domain(domain) + when 'public' + v + when 'unlisted' + v == 'public' ? 'unlisted' : v + when 'private' + %w(public unlisted).include?(v) ? 'private' : v + when 'direct' + 'direct' + else + v != 'direct' ? 'limited' : 'direct' + end + end + after_create_commit :increment_counter_caches after_destroy_commit :decrement_counter_caches @@ -544,7 +563,7 @@ class Status < ApplicationRecord if account.domain.nil? && !attribute_changed?(:local_only) self.local_only = true if marked_local_only? end - self.local_only = true if thread&.local_only? && self.local_only.nil? + self.local_only = true if thread&.local_only? && local_only.nil? self.local_only = reblog.local_only if reblog? end diff --git a/app/models/status_domain_permission.rb b/app/models/status_domain_permission.rb new file mode 100644 index 000000000..f9e5dc55e --- /dev/null +++ b/app/models/status_domain_permission.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: status_domain_permissions +# +# id :bigint(8) not null, primary key +# status_id :bigint(8) not null +# domain :string default(""), not null +# visibility :integer default("public"), not null +# + +class StatusDomainPermission < ApplicationRecord + include Paginable + + belongs_to :status, inverse_of: :domain_permissions + enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility + + class << self + def create_by_domains(permissions_list) + Array(permissions_list).map(&method(:normalize)).map do |permissions| + where(**permissions).first_or_create + end + end + + def create_by_domains!(permissions_list) + Array(permissions_list).map(&method(:normalize)).map do |permissions| + where(**permissions).first_or_create! + end + end + + def create_or_update(domain_permissions) + domain_permissions = normalize(domain_permissions) + permissions = find_by(domain: domain_permissions[:domain]) + if permissions.present? + permissions.update(**domain_permissions) + else + create(**domain_permissions) + end + permissions + end + + def create_or_update!(domain_permissions) + domain_permissions = normalize(domain_permissions) + permissions = find_by(domain: domain_permissions[:domain]) + if permissions.present? + permissions.update!(**domain_permissions) + else + create!(**domain_permissions) + end + permissions + end + + private + + def normalize(hash) + hash.symbolize_keys! + hash[:domain] = hash[:domain].strip.downcase + hash.compact + end + end +end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index bec58c39f..69d18c4bf 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -45,7 +45,7 @@ class StatusPolicy < ApplicationPolicy private def requires_mention? - record.direct_visibility? || record.limited_visibility? + %w(direct limited).include?(visibility_for_remote_domain) end def owned? @@ -53,7 +53,7 @@ class StatusPolicy < ApplicationPolicy end def private? - record.private_visibility? || !public_conversation? + visibility_for_remote_domain == 'private' || !public_conversation? end def mention_exists? @@ -164,4 +164,8 @@ class StatusPolicy < ApplicationPolicy def public_conversation? @public_conversation ||= (record.conversation&.public? || false) end + + def visibility_for_remote_domain + @visibility_for_domain ||= record.visibility_for_domain(current_account&.domain) + end end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index fd40d1f50..86dc64590 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -120,11 +120,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def to - ActivityPub::TagManager.instance.to(object) + ActivityPub::TagManager.instance.to(object, target_domain: instance_options[:target_domain]) end def cc - ActivityPub::TagManager.instance.cc(object) + ActivityPub::TagManager.instance.cc(object, target_domain: instance_options[:target_domain]) end def virtual_tags diff --git a/app/serializers/rest/account_domain_permission_serializer.rb b/app/serializers/rest/account_domain_permission_serializer.rb new file mode 100644 index 000000000..8bfbe1473 --- /dev/null +++ b/app/serializers/rest/account_domain_permission_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::AccountDomainPermissionSerializer < ActiveModel::Serializer + attributes :id, :domain, :visibility + + def id + object.id.to_s + end +end diff --git a/app/serializers/rest/status_domain_permission_serializer.rb b/app/serializers/rest/status_domain_permission_serializer.rb new file mode 100644 index 000000000..ecdecdd3b --- /dev/null +++ b/app/serializers/rest/status_domain_permission_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class REST::StatusDomainPermissionSerializer < ActiveModel::Serializer + attributes :id, :domain, :visibility + has_one :status + + def id + object.id.to_s + end +end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 8b4b11617..9d219f3f6 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -21,6 +21,7 @@ class ProcessMentionsService < BaseService check_for_spam(status) + @activitypub_json = {} mentions.each { |mention| create_notification(mention) } end @@ -32,13 +33,12 @@ class ProcessMentionsService < BaseService if mentioned_account.local? LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name) elsif mentioned_account.activitypub? && !@status.local_only? - ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(activitypub_json(mentioned_account.domain), mention.status.account_id, mentioned_account.inbox_url) end end - def activitypub_json - return @activitypub_json if defined?(@activitypub_json) - @activitypub_json = Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, embed: false), ActivityPub::ActivitySerializer, signer: @status.account)) + def activitypub_json(domain) + @activitypub_json[domain] ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, embed: false), ActivityPub::ActivitySerializer, signer: @status.account, target_domain: domain)) end def check_for_spam(status) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 65fc2c40d..f4280e9b2 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -60,6 +60,6 @@ class ReblogService < BaseService end def build_json(reblog) - Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(reblog, embed: false), ActivityPub::ActivitySerializer, signer: reblog.account)) + Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(reblog, embed: false), ActivityPub::ActivitySerializer, signer: reblog.account, target_domain: reblog.account.domain)) end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index a5aafee21..f9604071c 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -107,12 +107,12 @@ class RemoveStatusService < BaseService def relay! ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url| - [signed_activity_json, @account.id, inbox_url] + [signed_activity_json(Addressable::URI.parse(inbox_url).host), @account.id, inbox_url] end end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account)) + @signed_activity_json[domain] ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account, target_domain: domain)) end def remove_reblogs diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 2ee86d496..716d751c4 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -9,11 +9,12 @@ class ActivityPub::DistributionWorker def perform(status_id) @status = Status.find(status_id) @account = @status.account + @payload = {} return if skip_distribution? ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| - [payload, @account.id, inbox_url] + [payload(Addressable::URI.parse(inbox_url).host), @account.id, inbox_url] end relay! if relayable? @@ -42,13 +43,13 @@ class ActivityPub::DistributionWorker end end - def payload - @payload ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, update: true, embed: false), ActivityPub::ActivitySerializer, signer: @account)) + def payload(domain) + @payload[domain] ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, update: true, embed: false), ActivityPub::ActivitySerializer, signer: @account, target_domain: domain)) end def relay! ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url| - [payload, @account.id, inbox_url] + [payload(Addressable::URI.parse(inbox_url).host), @account.id, inbox_url] end end end diff --git a/app/workers/activitypub/reply_distribution_worker.rb b/app/workers/activitypub/reply_distribution_worker.rb index 9089caf86..e8648ffcd 100644 --- a/app/workers/activitypub/reply_distribution_worker.rb +++ b/app/workers/activitypub/reply_distribution_worker.rb @@ -12,11 +12,12 @@ class ActivityPub::ReplyDistributionWorker def perform(status_id) @status = Status.find(status_id) @account = @status.thread&.account + @payload = {} return unless @account.present? && @status.distributable? ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| - [payload, @status.account_id, inbox_url] + [payload(Addressable::URI.parse(inbox_url).host), @status.account_id, inbox_url] end rescue ActiveRecord::RecordNotFound true @@ -28,7 +29,7 @@ class ActivityPub::ReplyDistributionWorker @inboxes ||= @account.followers.inboxes end - def payload - @payload ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, update: true, embed: false), ActivityPub::ActivitySerializer, signer: @status.account)) + def payload(domain) + @payload[domain] ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status, update: true, embed: false), ActivityPub::ActivitySerializer, signer: @status.account, target_domain: domain)) end end diff --git a/db/migrate/20200725071818_create_status_domain_permissions.rb b/db/migrate/20200725071818_create_status_domain_permissions.rb new file mode 100644 index 000000000..e8faf3e00 --- /dev/null +++ b/db/migrate/20200725071818_create_status_domain_permissions.rb @@ -0,0 +1,13 @@ +class CreateStatusDomainPermissions < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + create_table :status_domain_permissions do |t| + t.references :status, null: false, index: { algorithm: :concurrently }, foreign_key: { on_delete: :cascade } + t.string :domain, null: false, default: '', index: { algorithm: :concurrently } + t.integer :visibility, null: false, default: 0, index: { algorithm: :concurrently } + end + + add_index :status_domain_permissions, [:status_id, :domain], unique: true, algorithm: :concurrently + end +end diff --git a/db/migrate/20200725080000_create_account_domain_permissions.rb b/db/migrate/20200725080000_create_account_domain_permissions.rb new file mode 100644 index 000000000..2497eda69 --- /dev/null +++ b/db/migrate/20200725080000_create_account_domain_permissions.rb @@ -0,0 +1,13 @@ +class CreateAccountDomainPermissions < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + create_table :account_domain_permissions do |t| + t.references :account, null: false, index: { algorithm: :concurrently }, foreign_key: { on_delete: :cascade } + t.string :domain, null: false, default: '', index: { algorithm: :concurrently } + t.integer :visibility, null: false, default: 0, index: { algorithm: :concurrently } + end + + add_index :account_domain_permissions, [:account_id, :domain], unique: true, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 111bf086d..0f649b382 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_07_24_045955) do +ActiveRecord::Schema.define(version: 2020_07_25_080000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,6 +44,16 @@ ActiveRecord::Schema.define(version: 2020_07_24_045955) do t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true end + create_table "account_domain_permissions", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "domain", default: "", null: false + t.integer "visibility", default: 0, null: false + t.index ["account_id", "domain"], name: "index_account_domain_permissions_on_account_id_and_domain", unique: true + t.index ["account_id"], name: "index_account_domain_permissions_on_account_id" + t.index ["domain"], name: "index_account_domain_permissions_on_domain" + t.index ["visibility"], name: "index_account_domain_permissions_on_visibility" + end + create_table "account_identity_proofs", force: :cascade do |t| t.bigint "account_id" t.string "provider", default: "", null: false @@ -762,6 +772,16 @@ ActiveRecord::Schema.define(version: 2020_07_24_045955) do t.index ["var"], name: "index_site_uploads_on_var", unique: true end + create_table "status_domain_permissions", force: :cascade do |t| + t.bigint "status_id", null: false + t.string "domain", default: "", null: false + t.integer "visibility", default: 0, null: false + t.index ["domain"], name: "index_status_domain_permissions_on_domain" + t.index ["status_id", "domain"], name: "index_status_domain_permissions_on_status_id_and_domain", unique: true + t.index ["status_id"], name: "index_status_domain_permissions_on_status_id" + t.index ["visibility"], name: "index_status_domain_permissions_on_visibility" + end + create_table "status_mutes", force: :cascade do |t| t.integer "account_id", null: false t.bigint "status_id", null: false @@ -950,6 +970,7 @@ ActiveRecord::Schema.define(version: 2020_07_24_045955) do add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade + add_foreign_key "account_domain_permissions", "accounts", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify add_foreign_key "account_migrations", "accounts", on_delete: :cascade @@ -1030,6 +1051,7 @@ ActiveRecord::Schema.define(version: 2020_07_24_045955) 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_domain_permissions", "statuses", on_delete: :cascade 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 |