about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb2
-rw-r--r--app/controllers/statuses_controller.rb8
-rw-r--r--app/lib/activitypub/tag_manager.rb12
-rw-r--r--app/models/account.rb23
-rw-r--r--app/models/account_domain_permission.rb61
-rw-r--r--app/models/concerns/account_associations.rb3
-rw-r--r--app/models/status.rb21
-rw-r--r--app/models/status_domain_permission.rb61
-rw-r--r--app/policies/status_policy.rb8
-rw-r--r--app/serializers/activitypub/note_serializer.rb4
-rw-r--r--app/serializers/rest/account_domain_permission_serializer.rb9
-rw-r--r--app/serializers/rest/status_domain_permission_serializer.rb10
-rw-r--r--app/services/process_mentions_service.rb8
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/services/remove_status_service.rb4
-rw-r--r--app/workers/activitypub/distribution_worker.rb9
-rw-r--r--app/workers/activitypub/reply_distribution_worker.rb7
17 files changed, 225 insertions, 27 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