diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 72f358bb5b..8e341aa48e 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -23,6 +23,10 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: []) + if Mastodon::Feature.collections_enabled? + params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: []) + else + params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: []) + end end end diff --git a/app/models/collection.rb b/app/models/collection.rb index 3681c41d84..e11cb73188 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -26,6 +26,7 @@ class Collection < ApplicationRecord belongs_to :tag, optional: true has_many :collection_items, dependent: :delete_all + has_many :collection_reports, dependent: :delete_all validates :name, presence: true validates :description, presence: true diff --git a/app/models/collection_report.rb b/app/models/collection_report.rb new file mode 100644 index 0000000000..e0edb03d0b --- /dev/null +++ b/app/models/collection_report.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: collection_reports +# +# id :bigint(8) not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# collection_id :bigint(8) not null +# report_id :bigint(8) not null +# +class CollectionReport < ApplicationRecord + belongs_to :collection + belongs_to :report +end diff --git a/app/models/report.rb b/app/models/report.rb index 17c1503436..86fbda1d2b 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -40,6 +40,8 @@ class Report < ApplicationRecord belongs_to :assigned_account, optional: true end + has_many :collection_reports, dependent: :delete_all + has_many :collections, through: :collection_reports has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy has_many :notifications, as: :activity, dependent: :destroy diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb index f4e9af2494..e5fe309dcb 100644 --- a/app/serializers/rest/report_serializer.rb +++ b/app/serializers/rest/report_serializer.rb @@ -2,7 +2,8 @@ class REST::ReportSerializer < ActiveModel::Serializer attributes :id, :action_taken, :action_taken_at, :category, :comment, - :forwarded, :created_at, :status_ids, :rule_ids + :forwarded, :created_at, :status_ids, :rule_ids, + :collection_ids has_one :target_account, serializer: REST::AccountSerializer @@ -17,4 +18,8 @@ class REST::ReportSerializer < ActiveModel::Serializer def rule_ids object&.rule_ids&.map(&:to_s) end + + def collection_ids + object.collection_ids.map(&:to_s) + end end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index c95e216c79..433cd9cb8c 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -7,6 +7,7 @@ class ReportService < BaseService @source_account = source_account @target_account = target_account @status_ids = options.delete(:status_ids).presence || [] + @collection_ids = options.delete(:collection_ids).presence || [] @comment = options.delete(:comment).presence || '' @category = options[:rule_ids].present? ? 'violation' : (options.delete(:category).presence || 'other') @rule_ids = options.delete(:rule_ids).presence @@ -32,6 +33,7 @@ class ReportService < BaseService @report = @source_account.reports.create!( target_account: @target_account, status_ids: reported_status_ids, + collection_ids: reported_collection_ids, comment: @comment, uri: @options[:uri], forwarded: forward_to_origin?, @@ -91,6 +93,10 @@ class ReportService < BaseService scope.where(id: Array(@status_ids)).pluck(:id) end + def reported_collection_ids + @target_account.collections.find(Array(@collection_ids)).pluck(:id) + end + def payload Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account)) end diff --git a/db/migrate/20260212131934_create_collection_reports.rb b/db/migrate/20260212131934_create_collection_reports.rb new file mode 100644 index 0000000000..cb3c9fcaa0 --- /dev/null +++ b/db/migrate/20260212131934_create_collection_reports.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateCollectionReports < ActiveRecord::Migration[8.0] + def change + create_table :collection_reports do |t| + t.references :collection, null: false, foreign_key: { on_delete: :cascade } + t.references :report, null: false, foreign_key: { on_delete: :cascade } + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9311dbac5c..470b3e1efe 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[8.0].define(version: 2026_02_12_113020) do +ActiveRecord::Schema[8.0].define(version: 2026_02_12_131934) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -372,6 +372,15 @@ ActiveRecord::Schema[8.0].define(version: 2026_02_12_113020) do t.index ["object_uri"], name: "index_collection_items_on_object_uri", unique: true, where: "(activity_uri IS NOT NULL)" end + create_table "collection_reports", force: :cascade do |t| + t.bigint "collection_id", null: false + t.bigint "report_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["collection_id"], name: "index_collection_reports_on_collection_id" + t.index ["report_id"], name: "index_collection_reports_on_report_id" + end + create_table "collections", id: :bigint, default: -> { "timestamp_id('collections'::text)" }, force: :cascade do |t| t.bigint "account_id", null: false t.string "name", null: false @@ -1431,6 +1440,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_02_12_113020) do add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade add_foreign_key "collection_items", "accounts" add_foreign_key "collection_items", "collections", on_delete: :cascade + add_foreign_key "collection_reports", "collections", on_delete: :cascade + add_foreign_key "collection_reports", "reports", on_delete: :cascade add_foreign_key "collections", "accounts" add_foreign_key "collections", "tags" add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade diff --git a/spec/requests/api/v1/reports_spec.rb b/spec/requests/api/v1/reports_spec.rb index 38d9f542c2..247ff979c5 100644 --- a/spec/requests/api/v1/reports_spec.rb +++ b/spec/requests/api/v1/reports_spec.rb @@ -11,20 +11,22 @@ RSpec.describe 'Reports' do end let!(:admin) { Fabricate(:admin_user) } - let(:status) { Fabricate(:status) } - let(:target_account) { status.account } + let(:target_account) { Fabricate(:account) } let(:category) { 'other' } let(:forward) { nil } let(:rule_ids) { nil } + let(:status_ids) { nil } + let(:collection_ids) { nil } let(:params) do { - status_ids: [status.id], + status_ids:, + collection_ids:, account_id: target_account.id, comment: 'reasons', - category: category, - rule_ids: rule_ids, - forward: forward, + category:, + rule_ids:, + forward:, } end @@ -38,7 +40,6 @@ RSpec.describe 'Reports' do .to start_with('application/json') expect(response.parsed_body).to match( a_hash_including( - status_ids: [status.id.to_s], category: category, comment: 'reasons' ) @@ -57,18 +58,6 @@ RSpec.describe 'Reports' do ) end - context 'when a status does not belong to the reported account' do - let(:target_account) { Fabricate(:account) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - expect(response.content_type) - .to start_with('application/json') - end - end - context 'when a category is chosen' do let(:category) { 'spam' } @@ -91,5 +80,69 @@ RSpec.describe 'Reports' do expect(target_account.targeted_reports.first.rule_ids).to contain_exactly(rule.id) end end + + context 'with attached status' do + let(:status) { Fabricate(:status, account: target_account) } + let(:status_ids) { [status.id] } + + it 'creates a report including the status ids', :aggregate_failures, :inline_jobs do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body).to match( + a_hash_including( + status_ids: [status.id.to_s], + category: category, + comment: 'reasons' + ) + ) + end + + context 'when a status does not belong to the reported account' do + let(:status) { Fabricate(:status) } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + expect(response.content_type) + .to start_with('application/json') + end + end + end + + context 'with attached collection', feature: :collections do + let(:collection) { Fabricate(:collection, account: target_account) } + let(:collection_ids) { [collection.id] } + + it 'creates a report including the collection ids', :aggregate_failures, :inline_jobs do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body).to match( + a_hash_including( + collection_ids: [collection.id.to_s], + category: category, + comment: 'reasons' + ) + ) + end + + context 'when a collection does not belong to the reported account' do + let(:collection) { Fabricate(:collection) } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + expect(response.content_type) + .to start_with('application/json') + end + end + end end end