Add moderation consequences for collections (#37974)

This commit is contained in:
David Roetzel 2026-02-25 15:32:07 +01:00 committed by GitHub
parent ea34d35b32
commit f9326efef6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 169 additions and 18 deletions

View File

@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
log.human_identifier.present? ? link_to(log.human_identifier, admin_instance_path(log.human_identifier)) : I18n.t('admin.action_logs.unavailable_instance')
when 'Status'
when 'Status', 'Collection'
link_to log.human_identifier, log.permalink
when 'AccountWarning'
link_to log.human_identifier, disputes_strike_path(log.target_id)

View File

@ -18,6 +18,10 @@ class Admin::ModerationAction < Admin::BaseAction
@statuses ||= Status.with_discarded.where(id: status_ids).reorder(nil)
end
def collections
report.collections
end
def process_action!
case type
when 'delete'
@ -29,19 +33,16 @@ class Admin::ModerationAction < Admin::BaseAction
def handle_delete!
statuses.each { |status| authorize([:admin, status], :destroy?) }
collections.each { |collection| authorize([:admin, collection], :destroy?) }
ApplicationRecord.transaction do
statuses.each do |status|
status.discard_with_reblogs
log_action(:destroy, status)
end
report.resolve!(current_account)
log_action(:resolve, report)
delete_statuses!
delete_collections!
resolve_report!
process_strike!(:delete_statuses)
statuses.each { |status| Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) } unless target_account.local?
create_tombstones! unless target_account.local?
end
process_notification!
@ -50,10 +51,39 @@ class Admin::ModerationAction < Admin::BaseAction
end
def handle_mark_as_sensitive!
representative_account = Account.representative
collections.each { |collection| authorize([:admin, collection], :update?) }
# Can't use a transaction here because UpdateStatusService queues
# Sidekiq jobs
mark_statuses_as_sensitive!
mark_collections_as_sensitive!
resolve_report!
process_strike!(:mark_statuses_as_sensitive)
process_notification!
end
def delete_statuses!
statuses.each do |status|
status.discard_with_reblogs
log_action(:destroy, status)
end
end
def delete_collections!
collections.each do |collection|
collection.destroy!
log_action(:destroy, collection)
end
end
def create_tombstones!
(statuses + collections).each { |record| Tombstone.find_or_create_by(uri: record.uri, account: target_account, by_moderator: true) }
end
def mark_statuses_as_sensitive!
representative_account = Account.representative
statuses.includes(:media_attachments, preview_cards_status: :preview_card).find_each do |status|
next if status.discarded? || !(status.with_media? || status.with_preview_card?)
@ -66,14 +96,20 @@ class Admin::ModerationAction < Admin::BaseAction
end
log_action(:update, status)
report.resolve!(current_account)
log_action(:resolve, report)
end
end
process_strike!(:mark_statuses_as_sensitive)
def mark_collections_as_sensitive!
collections.each do |collection|
UpdateCollectionService.new.call(collection, sensitive: true)
process_notification!
log_action(:update, collection)
end
end
def resolve_report!
report.resolve!(current_account)
log_action(:resolve, report)
end
def target_account

View File

@ -69,6 +69,14 @@ class Collection < ApplicationRecord
:featured_collection
end
def to_log_human_identifier
account.acct
end
def to_log_permalink
ActivityPub::TagManager.instance.uri_for(self)
end
private
def tag_is_usable

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class Admin::CollectionPolicy < ApplicationPolicy
def index?
role.can?(:manage_reports, :manage_users)
end
def show?
role.can?(:manage_reports, :manage_users)
end
def destroy?
role.can?(:manage_reports)
end
def update?
role.can?(:manage_reports)
end
end

View File

@ -5,7 +5,7 @@
= link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(report), method: :post, class: 'button'
.report-actions__item__description
= t('admin.reports.actions.resolve_description_html')
- if statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? }
- if report.collections.any? || statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? }
.report-actions__item
.report-actions__item__button
= form.button t('admin.reports.mark_as_sensitive'),
@ -18,8 +18,8 @@
= form.button t('admin.reports.delete_and_resolve'),
name: :delete,
class: 'button button--destructive',
disabled: statuses.empty?,
title: statuses.empty? ? t('admin.reports.actions_no_posts') : ''
disabled: (report.collections + statuses).empty?,
title: (report.collections + statuses).empty? ? t('admin.reports.actions_no_posts') : ''
.report-actions__item__description
= t('admin.reports.actions.delete_description_html')
.report-actions__item

View File

@ -267,6 +267,7 @@ en:
demote_user_html: "%{name} demoted user %{target}"
destroy_announcement_html: "%{name} deleted announcement %{target}"
destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}"
destroy_collection_html: "%{name} removed collection by %{target}"
destroy_custom_emoji_html: "%{name} deleted emoji %{target}"
destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}"
destroy_domain_block_html: "%{name} unblocked domain %{target}"
@ -306,6 +307,7 @@ en:
unsilence_account_html: "%{name} undid limit of %{target}'s account"
unsuspend_account_html: "%{name} unsuspended %{target}'s account"
update_announcement_html: "%{name} updated announcement %{target}"
update_collection_html: "%{name} updated collection by %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}"
update_domain_block_html: "%{name} updated domain block for %{target}"
update_ip_block_html: "%{name} changed rule for IP %{target}"

View File

@ -32,6 +32,36 @@ RSpec.describe Admin::ModerationAction do
end
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }
before do
report.collections = collections
end
it 'deletes the collections and creates an action log' do
expect { subject.save! }.to change(Collection, :count).by(-2)
.and change(Admin::ActionLog, :count).by(3)
end
end
context 'with a remote collection', feature: :collections do
let(:status_ids) { [] }
let(:collection) { Fabricate(:remote_collection) }
let(:target_account) { collection.account }
before do
report.collections << collection
end
it 'creates a tombstone' do
expect { subject.save! }.to change(Tombstone, :count).by(1)
expect(Tombstone.last.uri).to eq collection.uri
end
end
end
context 'when `type` is `mark_as_sensitive`' do
@ -52,6 +82,24 @@ RSpec.describe Admin::ModerationAction do
end
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }
before do
report.collections = collections
end
it 'marks the collections as sensitive' do
subject.save!
collections.each do |collection|
expect(collection.reload).to be_sensitive
end
expect(report.reload).to be_action_taken
end
end
end
end
end

View File

@ -138,4 +138,18 @@ RSpec.describe Collection do
expect(subject.object_type).to eq :featured_collection
end
end
describe '#to_log_human_identifier' do
subject { Fabricate(:collection) }
it 'returns the account name' do
expect(subject.to_log_human_identifier).to eq subject.account.acct
end
end
describe '#to_log_permalink' do
it 'includes the URI of the collection' do
expect(subject.to_log_permalink).to eq ActivityPub::TagManager.instance.uri_for(subject)
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::CollectionPolicy do
let(:policy) { described_class }
let(:admin) { Fabricate(:admin_user).account }
let(:john) { Fabricate(:account) }
let(:collection) { Fabricate(:collection) }
permissions :index?, :show?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Collection)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Collection)
end
end
end
end