mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-07 12:16:14 +00:00
Add support for ingesting quote policies (#34479)
This commit is contained in:
parent
1a1f3f037d
commit
9ed6a14d45
|
@ -83,7 +83,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_status_params
|
def process_status_params
|
||||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object)
|
@status_parser = ActivityPub::Parser::StatusParser.new(
|
||||||
|
@json,
|
||||||
|
followers_collection: @account.followers_url,
|
||||||
|
actor_uri: ActivityPub::TagManager.instance.uri_for(@account),
|
||||||
|
object: @object
|
||||||
|
)
|
||||||
|
|
||||||
attachment_ids = process_attachments.take(Status::MEDIA_ATTACHMENTS_LIMIT).map(&:id)
|
attachment_ids = process_attachments.take(Status::MEDIA_ATTACHMENTS_LIMIT).map(&:id)
|
||||||
|
|
||||||
|
@ -105,6 +110,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
media_attachment_ids: attachment_ids,
|
media_attachment_ids: attachment_ids,
|
||||||
ordered_media_attachment_ids: attachment_ids,
|
ordered_media_attachment_ids: attachment_ids,
|
||||||
poll: process_poll,
|
poll: process_poll,
|
||||||
|
quote_approval_policy: @status_parser.quote_policy,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ class ActivityPub::Parser::StatusParser
|
||||||
# @param [Hash] json
|
# @param [Hash] json
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option options [String] :followers_collection
|
# @option options [String] :followers_collection
|
||||||
|
# @option options [String] :actor_uri
|
||||||
# @option options [Hash] :object
|
# @option options [Hash] :object
|
||||||
def initialize(json, **options)
|
def initialize(json, **options)
|
||||||
@json = json
|
@json = json
|
||||||
|
@ -101,6 +102,18 @@ class ActivityPub::Parser::StatusParser
|
||||||
@object.dig(:shares, :totalItems)
|
@object.dig(:shares, :totalItems)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_policy
|
||||||
|
flags = 0
|
||||||
|
policy = @object.dig('interactionPolicy', 'canQuote')
|
||||||
|
return flags if policy.blank?
|
||||||
|
|
||||||
|
flags |= quote_subpolicy(policy['automaticApproval'])
|
||||||
|
flags <<= 16
|
||||||
|
flags |= quote_subpolicy(policy['manualApproval'])
|
||||||
|
|
||||||
|
flags
|
||||||
|
end
|
||||||
|
|
||||||
def quote_uri
|
def quote_uri
|
||||||
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
|
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
|
||||||
value_or_id(as_array(@object[key]).first)
|
value_or_id(as_array(@object[key]).first)
|
||||||
|
@ -113,6 +126,29 @@ class ActivityPub::Parser::StatusParser
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def quote_subpolicy(subpolicy)
|
||||||
|
flags = 0
|
||||||
|
|
||||||
|
allowed_actors = as_array(subpolicy)
|
||||||
|
allowed_actors.uniq!
|
||||||
|
|
||||||
|
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public')
|
||||||
|
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection])
|
||||||
|
# TODO: we don't actually store that collection URI
|
||||||
|
# flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed]
|
||||||
|
|
||||||
|
# Remove the special-meaning actor URI
|
||||||
|
allowed_actors.delete(@options[:actor_uri])
|
||||||
|
|
||||||
|
# Tagged users are always allowed, so remove them
|
||||||
|
allowed_actors -= as_array(@object['tag']).filter_map { |tag| tag['href'] if equals_or_includes?(tag['type'], 'Mention') }
|
||||||
|
|
||||||
|
# Any unrecognized actor is marked as unknown
|
||||||
|
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] unless allowed_actors.empty?
|
||||||
|
|
||||||
|
flags
|
||||||
|
end
|
||||||
|
|
||||||
def raw_language_code
|
def raw_language_code
|
||||||
if content_language_map?
|
if content_language_map?
|
||||||
@object['contentMap'].keys.first
|
@object['contentMap'].keys.first
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
# trendable :boolean
|
# trendable :boolean
|
||||||
# ordered_media_attachment_ids :bigint(8) is an Array
|
# ordered_media_attachment_ids :bigint(8) is an Array
|
||||||
# fetched_replies_at :datetime
|
# fetched_replies_at :datetime
|
||||||
|
# quote_approval_policy :integer default(0), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Status < ApplicationRecord
|
class Status < ApplicationRecord
|
||||||
|
@ -44,6 +45,13 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
MEDIA_ATTACHMENTS_LIMIT = 4
|
MEDIA_ATTACHMENTS_LIMIT = 4
|
||||||
|
|
||||||
|
QUOTE_APPROVAL_POLICY_FLAGS = {
|
||||||
|
unknown: (1 << 0),
|
||||||
|
public: (1 << 1),
|
||||||
|
followers: (1 << 2),
|
||||||
|
followed: (1 << 3),
|
||||||
|
}.freeze
|
||||||
|
|
||||||
rate_limit by: :account, family: :statuses
|
rate_limit by: :account, family: :statuses
|
||||||
|
|
||||||
self.discard_column = :deleted_at
|
self.discard_column = :deleted_at
|
||||||
|
|
|
@ -10,7 +10,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
|
|
||||||
@activity_json = activity_json
|
@activity_json = activity_json
|
||||||
@json = object_json
|
@json = object_json
|
||||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json)
|
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: status.account.followers_url, actor_uri: ActivityPub::TagManager.instance.uri_for(status.account))
|
||||||
@uri = @status_parser.uri
|
@uri = @status_parser.uri
|
||||||
@status = status
|
@status = status
|
||||||
@account = status.account
|
@account = status.account
|
||||||
|
@ -41,6 +41,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
Status.transaction do
|
Status.transaction do
|
||||||
record_previous_edit!
|
record_previous_edit!
|
||||||
update_media_attachments!
|
update_media_attachments!
|
||||||
|
update_interaction_policies!
|
||||||
update_poll!
|
update_poll!
|
||||||
update_immediate_attributes!
|
update_immediate_attributes!
|
||||||
update_metadata!
|
update_metadata!
|
||||||
|
@ -62,12 +63,17 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
|
|
||||||
def handle_implicit_update!
|
def handle_implicit_update!
|
||||||
with_redis_lock("create:#{@uri}") do
|
with_redis_lock("create:#{@uri}") do
|
||||||
|
update_interaction_policies!
|
||||||
update_poll!(allow_significant_changes: false)
|
update_poll!(allow_significant_changes: false)
|
||||||
queue_poll_notifications!
|
queue_poll_notifications!
|
||||||
update_counts!
|
update_counts!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_interaction_policies!
|
||||||
|
@status.quote_approval_policy = @status_parser.quote_policy
|
||||||
|
end
|
||||||
|
|
||||||
def update_media_attachments!
|
def update_media_attachments!
|
||||||
previous_media_attachments = @status.media_attachments.to_a
|
previous_media_attachments = @status.media_attachments.to_a
|
||||||
previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
|
previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddQuoteApprovalPolicyToStatuses < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :quote_approval_policy, :integer, null: false, default: 0
|
||||||
|
end
|
||||||
|
end
|
|
@ -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[8.0].define(version: 2025_04_25_134654) do
|
ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) 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 "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -1086,6 +1086,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_25_134654) do
|
||||||
t.boolean "trendable"
|
t.boolean "trendable"
|
||||||
t.bigint "ordered_media_attachment_ids", array: true
|
t.bigint "ordered_media_attachment_ids", array: true
|
||||||
t.datetime "fetched_replies_at"
|
t.datetime "fetched_replies_at"
|
||||||
|
t.integer "quote_approval_policy", default: 0, null: false
|
||||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||||
t.index ["account_id"], name: "index_statuses_on_account_id"
|
t.index ["account_id"], name: "index_statuses_on_account_id"
|
||||||
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
|
||||||
|
|
|
@ -7,10 +7,11 @@ RSpec.describe ActivityPub::Parser::StatusParser do
|
||||||
|
|
||||||
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
|
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
|
||||||
let(:follower) { Fabricate(:account, username: 'bob') }
|
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||||
|
let(:context) { 'https://www.w3.org/ns/activitystreams' }
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': context,
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
@ -47,4 +48,116 @@ RSpec.describe ActivityPub::Parser::StatusParser do
|
||||||
language: :en
|
language: :en
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#quote_policy' do
|
||||||
|
subject do
|
||||||
|
described_class
|
||||||
|
.new(
|
||||||
|
json,
|
||||||
|
actor_uri: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
followers_collection: sender.followers_url
|
||||||
|
).quote_policy
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:context) do
|
||||||
|
[
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
canQuote: {
|
||||||
|
'@id': 'gts:canQuote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
automaticApproval: {
|
||||||
|
'@id': 'gts:automaticApproval',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
manualApproval: {
|
||||||
|
'@id': 'gts:manualApproval',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when nobody is allowed to quote' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||||
|
type: 'Note',
|
||||||
|
to: [
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
],
|
||||||
|
interactionPolicy: {
|
||||||
|
canQuote: {
|
||||||
|
automaticApproval: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: 'bleh',
|
||||||
|
published: 1.hour.ago.utc.iso8601,
|
||||||
|
updated: 1.hour.ago.utc.iso8601,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a policy not allowing anyone to quote' do
|
||||||
|
expect(subject).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when everybody is allowed to quote' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||||
|
type: 'Note',
|
||||||
|
to: [
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
],
|
||||||
|
interactionPolicy: {
|
||||||
|
canQuote: {
|
||||||
|
automaticApproval: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: 'bleh',
|
||||||
|
published: 1.hour.ago.utc.iso8601,
|
||||||
|
updated: 1.hour.ago.utc.iso8601,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a policy not allowing anyone to quote' do
|
||||||
|
expect(subject).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when everybody is allowed to quote but only followers are automatically approved' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||||
|
type: 'Note',
|
||||||
|
to: [
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
],
|
||||||
|
interactionPolicy: {
|
||||||
|
canQuote: {
|
||||||
|
automaticApproval: sender.followers_url,
|
||||||
|
manualApproval: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: 'bleh',
|
||||||
|
published: 1.hour.ago.utc.iso8601,
|
||||||
|
updated: 1.hour.ago.utc.iso8601,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a policy allowing everyone including followers' do
|
||||||
|
expect(subject).to eq Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] | (Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user