about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFire Demon <firedemon@creature.cafe>2020-07-21 23:40:01 -0500
committerFire Demon <firedemon@creature.cafe>2020-08-30 05:44:01 -0500
commitc752a46c457759149c14ae0e4d501d5ef2ce478a (patch)
tree5066a3b5cf73ba06d16a8b494bc3e91a094f807b
parent03338243d28df8ecca77785c77214260ca0c32c9 (diff)
[Privacy] Implement thread ownership and visibility
-rw-r--r--app/lib/activitypub/activity/create.rb3
-rw-r--r--app/lib/feed_manager.rb4
-rw-r--r--app/models/concerns/account_associations.rb3
-rw-r--r--app/models/conversation.rb3
-rw-r--r--app/models/status.rb23
-rw-r--r--app/policies/status_policy.rb52
-rw-r--r--db/migrate/20200721202723_add_account_id_to_conversations.rb9
-rw-r--r--db/migrate/20200721212401_backfill_account_id_on_conversations.rb15
-rw-r--r--db/migrate/20200721221427_add_public_to_conversations.rb7
-rw-r--r--db/migrate/20200721221659_backfill_conversation_visibility.rb15
-rw-r--r--db/schema.rb6
11 files changed, 119 insertions, 21 deletions
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index d2bbd26d5..fbb4624e3 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -383,7 +383,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
 
     begin
-      Conversation.find_or_create_by!(uri: uri)
+      conversation = Conversation.find_by(uri: uri)
+      Conversation.create!(uri: uri, account: @account, public: %i(public unlisted).include?(visibility_from_audience)) if conversation.nil?
     rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
       retry
     end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 92c125c0f..49db68119 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -56,6 +56,7 @@ class FeedManager
       should_filter &&= status.account_id == list.account_id
       should_filter &&= !list.show_all_replies?
       should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
+      should_filter &&= !(list.show_list_replies? && status.conversation&.account_id.present? && ListAccount.where(list_id: list.id, account_id: status.conversation.account_id).exists?)
       return false if should_filter
     end
 
@@ -248,11 +249,13 @@ class FeedManager
 
     check_for_blocks = crutches[:active_mentions][status.id] || []
     check_for_blocks.concat([status.account_id])
+    check_for_blocks.concat([status.conversation.account_id]) unless status.conversation&.account_id.nil?
     check_for_blocks.concat([[status.in_reply_to_account_id]]) if status.reply?
 
     if status.reblog?
       check_for_blocks.concat([status.reblog.account_id])
       check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || [])
+      check_for_blocks.concat([status.reblog.conversation.account_id]) unless status.reblog.conversation&.account_id.nil?
       check_for_blocks.concat([[status.reblog.in_reply_to_account_id]]) if status.reblog.reply?
     end
 
@@ -260,6 +263,7 @@ class FeedManager
 
     if status.reply? && !status.in_reply_to_account_id.nil?                                                                      # Filter out if it's a reply
       should_filter   = !crutches[:following][status.in_reply_to_account_id]                                                     # and I'm not following the person it's a reply to
+      should_filter &&= !crutches[:following][status.conversation&.account_id]                                                   # and I'm not following the thread owner
       should_filter &&= receiver_id != status.in_reply_to_account_id                                                             # and it's not a reply to me
       should_filter &&= status.account_id != status.in_reply_to_account_id                                                       # and it's not a self-reply
 
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index cca3a17fa..db7396582 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -60,5 +60,8 @@ module AccountAssociations
     # Hashtags
     has_and_belongs_to_many :tags
     has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
+
+    # Threads
+    has_many :threads, class_name: 'Conversation', inverse_of: :account, dependent: :nullify
   end
 end
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index bbe3ada31..d1674fe4e 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -7,6 +7,8 @@
 #  uri        :string
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
+#  account_id :bigint(8)
+#  public     :boolean          default(FALSE), not null
 #
 
 class Conversation < ApplicationRecord
@@ -14,6 +16,7 @@ class Conversation < ApplicationRecord
 
   has_many :statuses
   has_many :mutes, class_name: 'ConversationMute', inverse_of: :conversation, dependent: :destroy
+  belongs_to :account, inverse_of: :threads
 
   def local?
     uri.nil?
diff --git a/app/models/status.rb b/app/models/status.rb
index 120a69715..657fcaca0 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -290,8 +290,7 @@ class Status < ApplicationRecord
 
   before_validation :prepare_contents, if: :local?
   before_validation :set_reblog
-  before_validation :set_conversation
-  before_validation :set_visibility
+  before_validation :set_conversation_perms
   before_validation :set_local
 
   after_create :set_poll_id
@@ -537,27 +536,25 @@ class Status < ApplicationRecord
     update_column(:poll_id, poll.id) unless poll.nil?
   end
 
-  def set_visibility
-    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.sensitive  = false if sensitive.nil?
-  end
-
   def set_locality
     self.local_only = marked_local_only? if account.domain.nil? && !attribute_changed?(:local_only)
   end
 
-  def set_conversation
+  def set_conversation_perms
     self.thread = thread.reblog if thread&.reblog?
-
     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.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 = Conversation.new
+      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))
     end
   end
 
@@ -616,4 +613,4 @@ class Status < ApplicationRecord
     end
   end
 end
-# rubocop:enable Metrics/ClassLength
\ No newline at end of file
+# rubocop:enable Metrics/ClassLength
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 22e985b03..bec58c39f 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -18,9 +18,9 @@ class StatusPolicy < ApplicationPolicy
     if requires_mention?
       owned? || mention_exists?
     elsif private?
-      owned? || (following_author? && following_parent_author?) || mention_exists?
+      owned? || following_owners? || mention_exists?
     else
-      current_account.nil? || !(author_blocking? || parent_author_blocking? || author_blocking_domain? || parent_author_blocking_domain?)
+      current_account.nil? || !blocked_by_owners?
     end
   end
 
@@ -53,7 +53,7 @@ class StatusPolicy < ApplicationPolicy
   end
 
   def private?
-    record.private_visibility?
+    record.private_visibility? || !public_conversation?
   end
 
   def mention_exists?
@@ -78,6 +78,12 @@ class StatusPolicy < ApplicationPolicy
     parent_author.domain_blocking?(current_account.domain)
   end
 
+  def conversation_author_blocking_domain?
+    return false if current_account.nil? || current_account.domain.nil? || conversation_owner.nil?
+
+    conversation_owner.domain_blocking?(current_account.domain)
+  end
+
   def blocking_author?
     return false if current_account.nil?
 
@@ -96,6 +102,19 @@ class StatusPolicy < ApplicationPolicy
     @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][parent_author.id] : parent_author.blocking?(current_account)
   end
 
+  def conversation_author_blocking?
+    return public_conversation? if conversation_owner.nil?
+
+    @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][conversation_owner.id] : conversation_owner.blocking?(current_account)
+  end
+
+  def blocked_by_owners?
+    return (author_blocking? || author_blocking_domain?) if conversation_owner&.id == author.id && parent_author&.id == author.id
+    return true if conversation_author_blocking? || parent_author_blocking? || author_blocking?
+
+    conversation_author_blocking_domain? || parent_author_blocking_domain? || author_blocking_domain?
+  end
+
   def following_author?
     return false if current_account.nil?
 
@@ -109,14 +128,31 @@ class StatusPolicy < ApplicationPolicy
     @preloaded_relations[:following] ? @preloaded_relations[:following][parent_author.id] : current_account.following?(parent_author)
   end
 
+  def following_conversation_owner?
+    return false if current_account.nil?
+    return public_conversation? if conversation_owner.nil?
+
+    @preloaded_relations[:following] ? @preloaded_relations[:following][conversation_owner.id] : current_account.following?(conversation_owner)
+  end
+
+  def following_owners?
+    return following_author? if conversation_owner&.id == author.id && parent_author&.id == author.id
+
+    following_conversation_owner? && following_parent_author? && following_author?
+  end
+
   def author
-    record.account
+    @author ||= record.account
   end
 
   def parent_author
-    record.in_reply_to_account
+    @parent_author ||= record.in_reply_to_account
   end
-  
+
+  def conversation_owner
+    @conversation_owner ||= record.conversation&.account
+  end
+
   def local_only?
     record.local_only?
   end
@@ -124,4 +160,8 @@ class StatusPolicy < ApplicationPolicy
   def published?
     record.published?
   end
+
+  def public_conversation?
+    @public_conversation ||= (record.conversation&.public? || false)
+  end
 end
diff --git a/db/migrate/20200721202723_add_account_id_to_conversations.rb b/db/migrate/20200721202723_add_account_id_to_conversations.rb
new file mode 100644
index 000000000..afddf4823
--- /dev/null
+++ b/db/migrate/20200721202723_add_account_id_to_conversations.rb
@@ -0,0 +1,9 @@
+class AddAccountIdToConversations < ActiveRecord::Migration[5.2]
+  disable_ddl_transaction!
+
+  def change
+    safety_assured do
+      add_reference :conversations, :account, foreign_key: true, index: {algorithm: :concurrently}
+    end
+  end
+end
diff --git a/db/migrate/20200721212401_backfill_account_id_on_conversations.rb b/db/migrate/20200721212401_backfill_account_id_on_conversations.rb
new file mode 100644
index 000000000..595fd8e52
--- /dev/null
+++ b/db/migrate/20200721212401_backfill_account_id_on_conversations.rb
@@ -0,0 +1,15 @@
+class BackfillAccountIdOnConversations < ActiveRecord::Migration[5.2]
+  disable_ddl_transaction!
+
+  def up
+    Rails.logger.info('Backfilling owners of conversation threads...')
+    safety_assured do
+      Conversation.left_outer_joins(:statuses).where(statuses: { id: nil }).in_batches.destroy_all
+      execute('UPDATE conversations SET account_id = s.account_id FROM (SELECT account_id, conversation_id FROM statuses WHERE NOT reply) AS s WHERE conversations.id = s.conversation_id')
+    end
+  end
+
+  def down
+    true
+  end
+end
diff --git a/db/migrate/20200721221427_add_public_to_conversations.rb b/db/migrate/20200721221427_add_public_to_conversations.rb
new file mode 100644
index 000000000..392bd7418
--- /dev/null
+++ b/db/migrate/20200721221427_add_public_to_conversations.rb
@@ -0,0 +1,7 @@
+class AddPublicToConversations < ActiveRecord::Migration[5.2]
+  def change
+    safety_assured do
+      add_column :conversations, :public, :boolean, default: false, null: false
+    end
+  end
+end
diff --git a/db/migrate/20200721221659_backfill_conversation_visibility.rb b/db/migrate/20200721221659_backfill_conversation_visibility.rb
new file mode 100644
index 000000000..93394b825
--- /dev/null
+++ b/db/migrate/20200721221659_backfill_conversation_visibility.rb
@@ -0,0 +1,15 @@
+class BackfillConversationVisibility < ActiveRecord::Migration[5.2]
+  disable_ddl_transaction!
+
+  def up
+    Rails.logger.info('Backfilling thread visibility...')
+
+    safety_assured do
+      execute('UPDATE conversations SET public = true FROM (SELECT account_id, conversation_id FROM statuses WHERE NOT reply AND visibility IN (0, 1)) AS s WHERE conversations.id = s.conversation_id')
+    end
+  end
+
+  def down
+    true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 674ff8ab6..901c13c76 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_21_195456) do
+ActiveRecord::Schema.define(version: 2020_07_21_221659) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -286,6 +286,9 @@ ActiveRecord::Schema.define(version: 2020_07_21_195456) do
     t.string "uri"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.bigint "account_id"
+    t.boolean "public", default: false, null: false
+    t.index ["account_id"], name: "index_conversations_on_account_id"
     t.index ["uri"], name: "index_conversations_on_uri", unique: true
   end
 
@@ -964,6 +967,7 @@ ActiveRecord::Schema.define(version: 2020_07_21_195456) do
   add_foreign_key "bookmarks", "statuses", on_delete: :cascade
   add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
   add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
+  add_foreign_key "conversations", "accounts"
   add_foreign_key "custom_filters", "accounts", on_delete: :cascade
   add_foreign_key "devices", "accounts", on_delete: :cascade
   add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade