about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorFire Demon <firedemon@creature.cafe>2020-07-27 07:45:50 -0500
committerFire Demon <firedemon@creature.cafe>2020-08-30 05:45:16 -0500
commit22326c3c028ff9e542ba4cbc8422076c420287fc (patch)
tree0a80a481b7f4be48e0c75ed174b7dbd47a9ce262 /app/models
parentba45644591b7626e2acffd6f21de6988456a008b (diff)
[Privacy] Improve handling of mixed permissions
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb8
-rw-r--r--app/models/status.rb70
2 files changed, 72 insertions, 6 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 301dc6c45..f7de58776 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -383,6 +383,14 @@ class Account < ApplicationRecord
     end
   end
 
+  def public_domain_permissions?
+    domain_permissions.where(visibility: [:public, :unlisted]).exists?
+  end
+
+  def private_domain_permissions?
+    domain_permissions.where(visibility: [:private, :direct, :limited]).exists?
+  end
+
   class Field < ActiveModelSerializers::Model
     attributes :name, :value, :verified_at, :account, :errors
 
diff --git a/app/models/status.rb b/app/models/status.rb
index 8bb830c9d..c92d00935 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -160,6 +160,13 @@ class Status < ApplicationRecord
   delegate :max_visibility_for_domain, to: :account
 
   REAL_TIME_WINDOW = 6.hours
+  SORTED_VISIBILITY = {
+    direct: 0,
+    limited: 1,
+    private: 2,
+    unlisted: 3,
+    public: 4,
+  }.with_indifferent_access.freeze
 
   def searchable_by(preloaded = nil)
     ids = []
@@ -284,6 +291,18 @@ class Status < ApplicationRecord
     update_status_stat!(key => [public_send(key) - 1, 0].max)
   end
 
+  def less_private_than?(other_visibility)
+    return false if other_visibility.blank?
+
+    SORTED_VISIBILITY[visibility] > SORTED_VISIBILITY[other_visibility]
+  end
+
+  def more_private_than?(other_visibility)
+    return false if other_visibility.blank?
+
+    SORTED_VISIBILITY[visibility] < SORTED_VISIBILITY[other_visibility]
+  end
+
   def visibility_for_domain(domain)
     v = domain_permissions.find_by(domain: domain)&.visibility || visibility.to_s
 
@@ -301,6 +320,31 @@ class Status < ApplicationRecord
     end
   end
 
+  def public_domain_permissions?
+    return @public_permissions if defined?(@public_permissions)
+    return @public_permissions = false unless account.local?
+
+    @public_permissions = domain_permissions.where(visibility: [:public, :unlisted]).exists?
+  end
+
+  def private_domain_permissions?
+    return @private_permissions if defined?(@private_permissions)
+    return @private_permissions = false unless account.local?
+
+    @private_permissions = domain_permissions.where(visibility: [:private, :direct, :limited]).exists?
+  end
+
+  def should_be_semiprivate?
+    return @should_be_semiprivate if defined?(@should_be_semiprivate)
+    return @should_be_semiprivate = true if distributable? && (!conversation.public? || private_domain_permissions? || account.private_domain_permissions?)
+
+    @should_be_semiprivate = !distributable? && (conversation.public? || public_domain_permissions? || account.public_domain_permissions?)
+  end
+
+  def should_limit_visibility?
+    less_private_than?(thread&.visibility)
+  end
+
   after_create_commit  :increment_counter_caches
   after_destroy_commit :decrement_counter_caches
 
@@ -316,10 +360,12 @@ class Status < ApplicationRecord
   before_validation :set_reblog
   before_validation :set_conversation_perms
   before_validation :set_local
-  before_validation :set_semiprivate, if: :local?
 
   after_create :set_poll_id
 
+  after_save :set_domain_permissions, if: :local?
+  after_save :set_semiprivate, if: :local?
+
   class << self
     def selectable_visibilities
       visibilities.keys - %w(direct limited)
@@ -575,16 +621,16 @@ class Status < ApplicationRecord
     self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
     self.visibility = reblog.visibility if reblog? && visibility.nil?
     self.visibility = (account.locked? ? :private : :public) if visibility.nil?
-    self.visibility = thread.visibility unless thread.nil? || %w(public unlisted).include?(thread.visibility) || ['direct', 'limited', thread.visibility].include?(visibility.to_s)
+    self.visibility = thread.visibility if should_limit_visibility?
     self.sensitive  = false if sensitive.nil?
 
     if reply? && !thread.nil?
       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 = reply? ? Conversation.new(account_id: nil, public: false) : Conversation.new(account_id: account_id, public: %w(public unlisted).include?(visibility.to_s))
-    elsif !reply? && account_id != conversation.account_id
-      conversation.update!(account_id: account_id, public: %w(public unlisted).include?(visibility.to_s))
+      self.conversation = reply? ? Conversation.new(account_id: nil, public: false) : Conversation.new(account_id: account_id, public: distributable?)
+    elsif !reply? && account_id != conversation.account_id || conversation.public? != distributable?
+      conversation.update!(account_id: account_id, public: distributable?)
     end
   end
 
@@ -610,8 +656,20 @@ class Status < ApplicationRecord
                       end
   end
 
+  def set_domain_permissions
+    return unless saved_change_to_visibility?
+
+    domain_permissions.transaction do
+      existing_domains = domain_permissions.select(:domain)
+      permissions = account.domain_permissions.where.not(domain: existing_domains)
+      permissions.find_each do |permission|
+        domain_permissions.create!(domain: permission.domain, visibility: permission.visibility) if less_private_than?(permission.visibility)
+      end
+    end
+  end
+
   def set_semiprivate
-    self.semiprivate = domain_permissions.exists? || account.followers.where(domain: account.domain_permissions.select(:domain)).exists?
+    update_column(:semiprivate, should_be_semiprivate?) if semiprivate != should_be_semiprivate?
   end
 
   def update_statistics