Browse Source

Improve support for aspects/circles (#8950)

* Add silent column to mentions

* Save silent mentions in ActivityPub Create handler and optimize it

Move networking calls out of the database transaction

* Add "limited" visibility level masked as "private" in the API

Unlike DMs, limited statuses are pushed into home feeds. The access
control rules between direct and limited statuses is almost the same,
except for counter and conversation logic

* Ensure silent column is non-null, add spec

* Ensure filters don't check silent mentions for blocks/mutes

As those are "this person is also allowed to see" rather than "this
person is involved", therefore does not warrant filtering

* Clean up code

* Use Status#active_mentions to limit returned mentions

* Fix code style issues

* Use Status#active_mentions in Notification

And remove stream_entry eager-loading from Notification
master
Eugen Rochko 5 years ago
committed by GitHub
parent
commit
ddd30f331c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 142 additions and 35 deletions
  1. +1
    -1
      app/lib/activitypub/activity.rb
  2. +23
    -1
      app/lib/activitypub/activity/create.rb
  3. +3
    -3
      app/lib/activitypub/tag_manager.rb
  4. +4
    -4
      app/lib/feed_manager.rb
  5. +1
    -1
      app/lib/formatter.rb
  6. +1
    -1
      app/lib/ostatus/atom_serializer.rb
  7. +1
    -1
      app/models/account_conversation.rb
  8. +8
    -0
      app/models/mention.rb
  9. +1
    -1
      app/models/notification.rb
  10. +10
    -5
      app/models/status.rb
  11. +1
    -1
      app/models/stream_entry.rb
  12. +4
    -4
      app/policies/status_policy.rb
  13. +1
    -1
      app/serializers/activitypub/note_serializer.rb
  14. +12
    -1
      app/serializers/rest/status_serializer.rb
  15. +1
    -1
      app/services/batched_remove_status_service.rb
  16. +12
    -0
      app/services/fan_out_on_write_service.rb
  17. +1
    -1
      app/services/remove_status_service.rb
  18. +1
    -1
      app/views/stream_entries/_detailed_status.html.haml
  19. +1
    -1
      app/workers/activitypub/distribution_worker.rb
  20. +1
    -5
      app/workers/activitypub/reply_distribution_worker.rb
  21. +23
    -0
      db/migrate/20181010141500_add_silent_to_mentions.rb
  22. +2
    -1
      db/schema.rb
  23. +29
    -0
      spec/lib/activitypub/activity/create_spec.rb

+ 1
- 1
app/lib/activitypub/activity.rb View File

@@ -96,7 +96,7 @@ class ActivityPub::Activity
end end


def notify_about_mentions(status) def notify_about_mentions(status)
status.mentions.includes(:account).each do |mention|
status.active_mentions.includes(:account).each do |mention|
next unless mention.account.local? && audience_includes?(mention.account) next unless mention.account.local? && audience_includes?(mention.account)
NotifyService.new.call(mention.account, mention) NotifyService.new.call(mention.account, mention)
end end


+ 23
- 1
app/lib/activitypub/activity/create.rb View File

@@ -28,6 +28,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity


process_status_params process_status_params
process_tags process_tags
process_audience


ApplicationRecord.transaction do ApplicationRecord.transaction do
@status = Status.create!(@params) @status = Status.create!(@params)
@@ -66,6 +67,27 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
end end


def process_audience
(as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]

# Unlike with tags, there is no point in resolving accounts we don't already
# know here, because silent mentions would only be used for local access
# control anyway
account = account_from_uri(audience)

next if account.nil? || @mentions.any? { |mention| mention.account_id == account.id }

@mentions << Mention.new(account: account, silent: true)

# If there is at least one silent mention, then the status can be considered
# as a limited-audience status, and not strictly a direct message
next unless @params[:visibility] == :direct

@params[:visibility] = :limited
end
end

def attach_tags(status) def attach_tags(status)
@tags.each do |tag| @tags.each do |tag|
status.tags << tag status.tags << tag
@@ -113,7 +135,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity


return if account.nil? return if account.nil?


@mentions << Mention.new(account: account)
@mentions << Mention.new(account: account, silent: false)
end end


def process_emoji(tag) def process_emoji(tag)


+ 3
- 3
app/lib/activitypub/tag_manager.rb View File

@@ -58,8 +58,8 @@ class ActivityPub::TagManager
[COLLECTIONS[:public]] [COLLECTIONS[:public]]
when 'unlisted', 'private' when 'unlisted', 'private'
[account_followers_url(status.account)] [account_followers_url(status.account)]
when 'direct'
status.mentions.map { |mention| uri_for(mention.account) }
when 'direct', 'limited'
status.active_mentions.map { |mention| uri_for(mention.account) }
end end
end end


@@ -80,7 +80,7 @@ class ActivityPub::TagManager
cc << COLLECTIONS[:public] cc << COLLECTIONS[:public]
end end


cc.concat(status.mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility?
cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility? || status.limited_visibility?


cc cc
end end


+ 4
- 4
app/lib/feed_manager.rb View File

@@ -88,7 +88,7 @@ class FeedManager
end end


query.each do |status| query.each do |status|
next if status.direct_visibility? || filter?(:home, status, into_account)
next if status.direct_visibility? || status.limited_visibility? || filter?(:home, status, into_account)
add_to_feed(:home, into_account.id, status) add_to_feed(:home, into_account.id, status)
end end


@@ -156,12 +156,12 @@ class FeedManager
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
return true if phrase_filtered?(status, receiver_id, :home) return true if phrase_filtered?(status, receiver_id, :home)


check_for_blocks = status.mentions.pluck(:account_id)
check_for_blocks = status.active_mentions.pluck(:account_id)
check_for_blocks.concat([status.account_id]) check_for_blocks.concat([status.account_id])


if status.reblog? if status.reblog?
check_for_blocks.concat([status.reblog.account_id]) check_for_blocks.concat([status.reblog.account_id])
check_for_blocks.concat(status.reblog.mentions.pluck(:account_id))
check_for_blocks.concat(status.reblog.active_mentions.pluck(:account_id))
end end


return true if blocks_or_mutes?(receiver_id, check_for_blocks, :home) return true if blocks_or_mutes?(receiver_id, check_for_blocks, :home)
@@ -188,7 +188,7 @@ class FeedManager
# This filter is called from NotifyService, but already after the sender of # This filter is called from NotifyService, but already after the sender of
# the notification has been checked for mute/block. Therefore, it's not # the notification has been checked for mute/block. Therefore, it's not
# necessary to check the author of the toot for mute/block again # necessary to check the author of the toot for mute/block again
check_for_blocks = status.mentions.pluck(:account_id)
check_for_blocks = status.active_mentions.pluck(:account_id)
check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil? check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?


should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)


+ 1
- 1
app/lib/formatter.rb View File

@@ -27,7 +27,7 @@ class Formatter
return html.html_safe # rubocop:disable Rails/OutputSafety return html.html_safe # rubocop:disable Rails/OutputSafety
end end


linkable_accounts = status.mentions.map(&:account)
linkable_accounts = status.active_mentions.map(&:account)
linkable_accounts << status.account linkable_accounts << status.account


html = raw_content html = raw_content


+ 1
- 1
app/lib/ostatus/atom_serializer.rb View File

@@ -354,7 +354,7 @@ class OStatus::AtomSerializer
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language) append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language)


status.mentions.sort_by(&:id).each do |mentioned|
status.active_mentions.sort_by(&:id).each do |mentioned|
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account))
end end




+ 1
- 1
app/models/account_conversation.rb View File

@@ -85,7 +85,7 @@ class AccountConversation < ApplicationRecord
private private


def participants_from_status(recipient, status) def participants_from_status(recipient, status)
((status.mentions.pluck(:account_id) + [status.account_id]).uniq - [recipient.id]).sort
((status.active_mentions.pluck(:account_id) + [status.account_id]).uniq - [recipient.id]).sort
end end
end end




+ 8
- 0
app/models/mention.rb View File

@@ -8,6 +8,7 @@
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint(8) # account_id :bigint(8)
# silent :boolean default(FALSE), not null
# #


class Mention < ApplicationRecord class Mention < ApplicationRecord
@@ -18,10 +19,17 @@ class Mention < ApplicationRecord


validates :account, uniqueness: { scope: :status } validates :account, uniqueness: { scope: :status }


scope :active, -> { where(silent: false) }
scope :silent, -> { where(silent: true) }

delegate( delegate(
:username, :username,
:acct, :acct,
to: :account, to: :account,
prefix: true prefix: true
) )

def active?
!silent?
end
end end

+ 1
- 1
app/models/notification.rb View File

@@ -24,7 +24,7 @@ class Notification < ApplicationRecord
favourite: 'Favourite', favourite: 'Favourite',
}.freeze }.freeze


STATUS_INCLUDES = [:account, :application, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :application, :media_attachments, :tags, mentions: :account]].freeze
STATUS_INCLUDES = [:account, :application, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :media_attachments, :tags, active_mentions: :account]].freeze


belongs_to :account, optional: true belongs_to :account, optional: true
belongs_to :from_account, class_name: 'Account', optional: true belongs_to :from_account, class_name: 'Account', optional: true


+ 10
- 5
app/models/status.rb View File

@@ -37,7 +37,7 @@ class Status < ApplicationRecord


update_index('statuses#status', :proper) if Chewy.enabled? update_index('statuses#status', :proper) if Chewy.enabled?


enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility


belongs_to :application, class_name: 'Doorkeeper::Application', optional: true belongs_to :application, class_name: 'Doorkeeper::Application', optional: true


@@ -51,7 +51,8 @@ class Status < ApplicationRecord
has_many :favourites, inverse_of: :status, dependent: :destroy has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
has_many :mentions, dependent: :destroy
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify has_many :media_attachments, dependent: :nullify


has_and_belongs_to_many :tags has_and_belongs_to_many :tags
@@ -89,7 +90,7 @@ class Status < ApplicationRecord
:status_stat, :status_stat,
:tags, :tags,
:stream_entry, :stream_entry,
mentions: :account,
active_mentions: :account,
reblog: [ reblog: [
:account, :account,
:application, :application,
@@ -98,7 +99,7 @@ class Status < ApplicationRecord
:media_attachments, :media_attachments,
:conversation, :conversation,
:status_stat, :status_stat,
mentions: :account,
active_mentions: :account,
], ],
thread: :account thread: :account


@@ -171,7 +172,11 @@ class Status < ApplicationRecord
end end


def hidden? def hidden?
private_visibility? || direct_visibility?
private_visibility? || direct_visibility? || limited_visibility?
end

def distributable?
public_visibility? || unlisted_visibility?
end end


def with_media? def with_media?


+ 1
- 1
app/models/stream_entry.rb View File

@@ -48,7 +48,7 @@ class StreamEntry < ApplicationRecord
end end


def mentions def mentions
orphaned? ? [] : status.mentions.map(&:account)
orphaned? ? [] : status.active_mentions.map(&:account)
end end


private private


+ 4
- 4
app/policies/status_policy.rb View File

@@ -12,7 +12,7 @@ class StatusPolicy < ApplicationPolicy
end end


def show? def show?
if direct?
if requires_mention?
owned? || mention_exists? owned? || mention_exists?
elsif private? elsif private?
owned? || following_author? || mention_exists? owned? || following_author? || mention_exists?
@@ -22,7 +22,7 @@ class StatusPolicy < ApplicationPolicy
end end


def reblog? def reblog?
!direct? && (!private? || owned?) && show? && !blocking_author?
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
end end


def favourite? def favourite?
@@ -41,8 +41,8 @@ class StatusPolicy < ApplicationPolicy


private private


def direct?
record.direct_visibility?
def requires_mention?
record.direct_visibility? || record.limited_visibility?
end end


def owned? def owned?


+ 1
- 1
app/serializers/activitypub/note_serializer.rb View File

@@ -68,7 +68,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
end end


def virtual_tags def virtual_tags
object.mentions.to_a.sort_by(&:id) + object.tags + object.emojis
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
end end


def atom_uri def atom_uri


+ 12
- 1
app/serializers/rest/status_serializer.rb View File

@@ -36,6 +36,17 @@ class REST::StatusSerializer < ActiveModel::Serializer
!current_user.nil? !current_user.nil?
end end


def visibility
# This visibility is masked behind "private"
# to avoid API changes because there are no
# UX differences
if object.limited_visibility?
'private'
else
object.visibility
end
end

def uri def uri
OStatus::TagManager.instance.uri_for(object) OStatus::TagManager.instance.uri_for(object)
end end
@@ -88,7 +99,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
end end


def ordered_mentions def ordered_mentions
object.mentions.to_a.sort_by(&:id)
object.active_mentions.to_a.sort_by(&:id)
end end


class ApplicationSerializer < ActiveModel::Serializer class ApplicationSerializer < ActiveModel::Serializer


+ 1
- 1
app/services/batched_remove_status_service.rb View File

@@ -12,7 +12,7 @@ class BatchedRemoveStatusService < BaseService
def call(statuses) def call(statuses)
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a } statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }


@mentions = statuses.map { |s| [s.id, s.mentions.includes(:account).to_a] }.to_h
@mentions = statuses.map { |s| [s.id, s.active_mentions.includes(:account).to_a] }.to_h
@tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h @tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h


@stream_entry_batches = [] @stream_entry_batches = []


+ 12
- 0
app/services/fan_out_on_write_service.rb View File

@@ -10,6 +10,8 @@ class FanOutOnWriteService < BaseService


if status.direct_visibility? if status.direct_visibility?
deliver_to_own_conversation(status) deliver_to_own_conversation(status)
elsif status.limited_visibility?
deliver_to_mentioned_followers(status)
else else
deliver_to_self(status) if status.account.local? deliver_to_self(status) if status.account.local?
deliver_to_followers(status) deliver_to_followers(status)
@@ -53,6 +55,16 @@ class FanOutOnWriteService < BaseService
end end
end end


def deliver_to_mentioned_followers(status)
Rails.logger.debug "Delivering status #{status.id} to limited followers"

status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account
next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mention.account_id)
FeedManager.instance.push_to_home(mentioned_account, status)
end
end

def render_anonymous_payload(status) def render_anonymous_payload(status)
@payload = InlineRenderer.render(status, nil, :status) @payload = InlineRenderer.render(status, nil, :status)
@payload = Oj.dump(event: :update, payload: @payload) @payload = Oj.dump(event: :update, payload: @payload)


+ 1
- 1
app/services/remove_status_service.rb View File

@@ -8,7 +8,7 @@ class RemoveStatusService < BaseService
@status = status @status = status
@account = status.account @account = status.account
@tags = status.tags.pluck(:name).to_a @tags = status.tags.pluck(:name).to_a
@mentions = status.mentions.includes(:account).to_a
@mentions = status.active_mentions.includes(:account).to_a
@reblogs = status.reblogs.to_a @reblogs = status.reblogs.to_a
@stream_entry = status.stream_entry @stream_entry = status.stream_entry
@options = options @options = options


+ 1
- 1
app/views/stream_entries/_detailed_status.html.haml View File

@@ -51,7 +51,7 @@
- if status.direct_visibility? - if status.direct_visibility?
%span.detailed-status__link< %span.detailed-status__link<
= fa_icon('envelope') = fa_icon('envelope')
- elsif status.private_visibility?
- elsif status.private_visibility? || status.limited_visibility?
%span.detailed-status__link< %span.detailed-status__link<
= fa_icon('lock') = fa_icon('lock')
- else - else


+ 1
- 1
app/workers/activitypub/distribution_worker.rb View File

@@ -23,7 +23,7 @@ class ActivityPub::DistributionWorker
private private


def skip_distribution? def skip_distribution?
@status.direct_visibility?
@status.direct_visibility? || @status.limited_visibility?
end end


def relayable? def relayable?


+ 1
- 5
app/workers/activitypub/reply_distribution_worker.rb View File

@@ -9,7 +9,7 @@ class ActivityPub::ReplyDistributionWorker
@status = Status.find(status_id) @status = Status.find(status_id)
@account = @status.thread&.account @account = @status.thread&.account


return if @account.nil? || skip_distribution?
return unless @account.present? && @status.distributable?


ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[signed_payload, @status.account_id, inbox_url] [signed_payload, @status.account_id, inbox_url]
@@ -20,10 +20,6 @@ class ActivityPub::ReplyDistributionWorker


private private


def skip_distribution?
@status.private_visibility? || @status.direct_visibility?
end

def inboxes def inboxes
@inboxes ||= @account.followers.inboxes @inboxes ||= @account.followers.inboxes
end end


+ 23
- 0
db/migrate/20181010141500_add_silent_to_mentions.rb View File

@@ -0,0 +1,23 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')

class AddSilentToMentions < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers

disable_ddl_transaction!

def up
safety_assured do
add_column_with_default(
:mentions,
:silent,
:boolean,
allow_null: false,
default: false
)
end
end

def down
remove_column :mentions, :silent
end
end

+ 2
- 1
db/schema.rb View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.


ActiveRecord::Schema.define(version: 2018_10_07_025445) do
ActiveRecord::Schema.define(version: 2018_10_10_141500) do


# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -301,6 +301,7 @@ ActiveRecord::Schema.define(version: 2018_10_07_025445) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "account_id" t.bigint "account_id"
t.boolean "silent", default: false, null: false
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true
t.index ["status_id"], name: "index_mentions_on_status_id" t.index ["status_id"], name: "index_mentions_on_status_id"
end end


+ 29
- 0
spec/lib/activitypub/activity/create_spec.rb View File

@@ -105,6 +105,31 @@ RSpec.describe ActivityPub::Activity::Create do
end end
end end


context 'limited' do
let(:recipient) { Fabricate(:account) }

let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
}
end

it 'creates status' do
status = sender.statuses.first

expect(status).to_not be_nil
expect(status.visibility).to eq 'limited'
end

it 'creates silent mention' do
status = sender.statuses.first
expect(status.mentions.first).to be_silent
end
end

context 'direct' do context 'direct' do
let(:recipient) { Fabricate(:account) } let(:recipient) { Fabricate(:account) }


@@ -114,6 +139,10 @@ RSpec.describe ActivityPub::Activity::Create do
type: 'Note', type: 'Note',
content: 'Lorem ipsum', content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient), to: ActivityPub::TagManager.instance.uri_for(recipient),
tag: {
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(recipient),
},
} }
end end




Loading…
Cancel
Save