about summary refs log tree commit diff
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
parentba45644591b7626e2acffd6f21de6988456a008b (diff)
[Privacy] Improve handling of mixed permissions
-rw-r--r--app/lib/command_tag/commands/status_tools.rb6
-rw-r--r--app/lib/command_tag/processor.rb9
-rw-r--r--app/models/account.rb8
-rw-r--r--app/models/status.rb70
4 files changed, 72 insertions, 21 deletions
diff --git a/app/lib/command_tag/commands/status_tools.rb b/app/lib/command_tag/commands/status_tools.rb
index 0e6524001..23bdcbfc3 100644
--- a/app/lib/command_tag/commands/status_tools.rb
+++ b/app/lib/command_tag/commands/status_tools.rb
@@ -27,10 +27,4 @@ module CommandTag::Commands::StatusTools
 
   alias handle_v_before_save                      handle_visibility_before_save
   alias handle_privacy_before_save                handle_visibility_before_save
-
-  def handle_semiprivate_before_save(args)
-    return unless author_of_status?
-
-    @semiprivate = args.blank? || read_boolean_from(args[0])
-  end
 end
diff --git a/app/lib/command_tag/processor.rb b/app/lib/command_tag/processor.rb
index e9b8f5c05..da6d49160 100644
--- a/app/lib/command_tag/processor.rb
+++ b/app/lib/command_tag/processor.rb
@@ -59,7 +59,6 @@ class CommandTag::Processor
     elsif @status.destroyed?
       %w(after_destroy once_after_destroy).each { |suffix| execute_statements(suffix) }
     else
-      update_semiprivate_attribute
       @status.text = @text
       process_inline_images!
       if @status.save
@@ -175,14 +174,6 @@ class CommandTag::Processor
     @status.destroy unless @status.destroyed?
   end
 
-  def status_still_semiprivate?
-    @status.domain_permissions.exists? || @account.followers.where(domain: @account.domain_permissions.select(:domain)).exists?
-  end
-
-  def update_semiprivate_attribute
-    @status.semiprivate = @semiprivate.presence || status_still_semiprivate?
-  end
-
   def normalize(text)
     text.to_s.strip.downcase
   end
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