feat(reports): delete reports and configure retention (#3338)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 01:41:47 +02:00
1 parent 93d1f8af46
commit 82abd03a37
106 files changed
+1457 -170

No files matched your search

@@ -140,7 +140,7 @@ The acting credential must already have every value in `acls`, unless it has `*`
| --- | --- | --- |
| name<sup>1</sup> | string | The name given to the key (1-100 characters) |
| expires_in_days?<sup>2</sup> | integer | The number of days until the key expires (1-365) |
| acls<sup>3</sup> | array[string] | The [ACLs](/admin-api/#acl-registry) stored on the key, each a registry value (at most 105) |
| acls<sup>3</sup> | array[string] | The [ACLs](/admin-api/#acl-registry) stored on the key, each a registry value (at most 106) |
<sup>1</sup> A value that is empty after trimming is rejected, so whitespace alone is not a name
@@ -218,7 +218,7 @@ An update never rotates the credential, and no field on this route changes the e
| Field | Type | Description |
| --- | --- | --- |
| name?<sup>1</sup> | string | The replacement name for the key (1-100 characters) |
| acls?<sup>2</sup> | array[string] | The complete replacement set of [ACLs](/admin-api/#acl-registry), each a registry value (at most 105) |
| acls?<sup>2</sup> | array[string] | The complete replacement set of [ACLs](/admin-api/#acl-registry), each a registry value (at most 106) |
<sup>1</sup> A value that is empty after trimming is rejected
@@ -61,7 +61,7 @@ A body with none of those fields resolves to no ACL at all and applies no change
Fluxer checks each ACL an Admin grants against the ACLs that Admin holds. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs also refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it looks up the target account before it checks the granted ACLs, so an unknown ID fails first with 404 `UNKNOWN_USER`.
[Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 105 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`.
[Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 106 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`.
:::caution[`*` satisfies every present and future ACL]
A key whose owning account holds `*` skips the owner check. An Admin holding `*` can grant any ACL.
@@ -348,6 +348,7 @@ An entry with any other action has `access` set to `write`.
| delete_all_user_messages_dry_run | The same operation was run with `dry_run` set, so no message was deleted |
| delete_guild | A guild was deleted |
| delete_message | One message was deleted |
| delete_report | A report was deleted with its stored evidence |
| delete_voice_region | A voice region was deleted |
| delete_voice_server | A registered voice server was deleted |
| delete_webauthn_credential | A WebAuthn credential was removed from an account |
@@ -481,7 +482,7 @@ Returns every Admin permission string the instance recognises, in [ACL registry]
| Field | Type | Description |
| --- | --- | --- |
| acls<sup>1</sup> | array[string] | The permission strings the Admin API recognises (at most 105 entries) |
| acls<sup>1</sup> | array[string] | The permission strings the Admin API recognises (at most 106 entries) |
<sup>1</sup> The response is the registry itself and does not vary with the caller's own ACL set
@@ -576,6 +577,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
| message:delete | Deletes one message |
| message:lookup | Reads, browses, and searches [messages](/admin-api/messages/) through the Admin API |
| message:shred | Queues an explicit message shred job and reads its state |
| report:delete | Deletes a [report](/admin-api/reports/#delete-report) with its stored evidence |
| report:resolve | Updates and resolves [reports](/admin-api/reports/), and places legal holds on them |
| report:view | Reads and searches reports |
| report:view:reporter_pii<sup>6</sup> | Unredacts the contact fields a reporter supplied |
@@ -1,12 +1,12 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Admin reports
description: Report listing and search, captured evidence, report resolution, and report retention.
description: Report listing and search, captured evidence, report resolution, report deletion, and report retention.
---
import RouteHeader from '@/components/RouteHeader.astro';
A report is one complaint filed against a message, an account, or a guild, together with the evidence captured when it was filed. These routes list and search reports, return that evidence, resolve a report with a comment shown to the reporter, and hold a report past its retention period.
A report is one complaint filed against a message, an account, or a guild, together with the evidence captured when it was filed. These routes list and search reports, return that evidence, resolve a report with a comment shown to the reporter, hold a report past its retention period, and delete a report before it.
Report submission belongs to the public [Reports](/http-api/reports/) resource, which defines the [report categories](/http-api/reports/#report-categories) and the [report reasons](/http-api/reports/#report-reasons) a report is filed under.
@@ -22,7 +22,7 @@ Fluxer reads the tag, username, display name, and discriminator of both accounts
## Admin report object
A report is written once, when the reporter files it. [Update report](#update-report) moves it from PENDING to RESOLVED with an optional comment. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) for one of the abuse reasons listed there also resolves the pending reports against the deleted account, with no public comment, and sends each reporter the generic notice [Update report](#update-report) describes. A report in the `child_safety`, `underage_user`, or `self_harm` category is never resolved this way and stays PENDING. A report is deleted at the end of its [retention period](#report-retention).
A report is written once, when the reporter files it. [Update report](#update-report) moves it from PENDING to RESOLVED with an optional comment. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) for one of the abuse reasons listed there also resolves the pending reports against the deleted account, with no public comment, and sends each reporter the generic notice [Update report](#update-report) describes. A report in the `child_safety`, `underage_user`, or `self_harm` category is never resolved this way and stays PENDING. A report is deleted at the end of its [retention period](#report-retention), or earlier through [Delete report](#delete-report).
[List reports](#list-reports) returns this object without message context. [Get report](#get-report) returns the [Admin report detail object](#admin-report-detail-object), which extends it.
@@ -519,12 +519,14 @@ A report resolution is the reduced view [Update report](#update-report) returns.
## Report retention
Fluxer deletes a report 365 days after it was filed, whether it is pending or resolved, together with its stored evidence: the attachment copies, the profile snapshot images, and its search index entry. A report with a [legal hold](#set-report-legal-hold) that ends later is kept until the next run after the hold ends. An evidence copy that a retained report also uses is kept for that report.
Fluxer deletes a report when its retention period ends, whether it is pending or resolved, together with its stored evidence: the attachment copies, the profile snapshot images, and its search index entry. The reporter's duplicate-report reservation for it goes too, so the reporter can report the same message, account or guild again.
The deletion runs once a day in `worker`. Each run logs one `Processed report retention` line with the counts it found and deleted. A report that fails to delete keeps its record and is retried on the next run.
The retention period is 365 days from when the report was filed, or the number of days in `FLUXER_REPORT_RETENTION_DAYS`. With `FLUXER_RESOLVED_REPORT_RETENTION_DAYS` set, a resolved report is deleted that many days after `resolved_at` when that comes first. A report with a [legal hold](#set-report-legal-hold) that ends later is kept until the next run after the hold ends. An evidence copy that a retained report also uses is kept for that report.
:::caution[Retention starts in dry-run mode]
Until `FLUXER_REPORT_RETENTION_DRY_RUN` is set to `false` on `worker`, each run only counts what it would delete and deletes nothing. The [operator configuration](/operator/configuration/#moderation-and-abuse) describes the setting.
The deletion runs once a day in `worker`. Each run logs one `Processed report retention` line with the counts it found and deleted and the two periods it used. A report that fails to delete keeps its record and is retried on the next run. [Delete report](#delete-report) deletes one report straight away by the same rules.
:::caution[Deleted evidence cannot be restored]
Retention deletes for real unless `FLUXER_REPORT_RETENTION_DRY_RUN` is `true` on `worker`. In a dry run each run only counts what it would delete and deletes nothing. The [operator configuration](/operator/configuration/#moderation-and-abuse) describes the three settings.
:::
## List reports
@@ -714,7 +716,7 @@ A reporter who does not already have the system channel open receives [Channel C
Places or clears a legal hold on one report and returns a [report legal hold](#report-legal-hold-object) object. Requires `report:resolve`.
A held report is kept past the end of its [retention period](#report-retention) until the hold ends. A hold can be placed on a pending or a resolved report, and a new hold replaces the previous one.
A held report is kept past the end of its [retention period](#report-retention) until the hold ends, and [Delete report](#delete-report) refuses it. A hold can be placed on a pending or a resolved report, and a new hold replaces the previous one.
### Path parameters
@@ -748,3 +750,37 @@ Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) wit
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
## Delete report
<RouteHeader method="DELETE" path="/v1/admin/reports/{report_id}" auditReason />
Deletes one report straight away. Requires `report:delete`, which `report:resolve` does not include.
This is the deletion [report retention](#report-retention) runs, without waiting for the retention period. It removes the report record with its captured message context and profile snapshot, its stored evidence copies, its search index entry, and the reporter's duplicate-report reservation for it. An evidence copy that another report also uses is kept for that report. A pending or a resolved report can be deleted, and the reporter is not told.
:::caution[Deletion is permanent]
Deleted evidence cannot be restored. A report with a legal hold that has not ended is refused. Clear the hold first when the report should go.
:::
### Path parameters
| Field | Type | Description |
| --- | --- | --- |
| report_id | snowflake | The ID of the report |
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 204 | empty | The report was deleted |
| 404 | [error response](/admin-api/#error-response) | Report does not exist and the request returns `UNKNOWN_REPORT` |
| 409 | [error response](/admin-api/#error-response) | The report has a legal hold that ends later and the request returns `REPORT_UNDER_LEGAL_HOLD` |
### Side effects
Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `delete_report`, target type `report`, and the report ID as target ID. The metadata keys are `report_id`, `report_type`, `status`, `objects_deleted` for the evidence copies removed, and `shared_objects_kept` for the copies kept because another report uses them. A refused request records nothing.
### Rate limit
200 requests per minute for each authenticated user, on the `admin:lookup` bucket.
@@ -59,7 +59,7 @@ When the caller lacks `user:view:email`, `user:view:dob`, or `user:view:ip`, Flu
| deletion_audit_log_reason | ?string | The private reason stored with the pending deletion, and null without `audit_log:view` |
| deletion_scheduled_by | ?snowflake | The ID of the account that scheduled the pending deletion, or null when it was not recorded |
| deletion_scheduled_at | ?ISO8601 timestamp | The time the pending deletion was scheduled, or null when it was not recorded |
| acls<sup>7</sup> | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 105 entries |
| acls<sup>7</sup> | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 106 entries |
| traits<sup>8</sup> | array[string] | The free-form operator labels set on the account, with at most 100 entries |
| has_totp<sup>9</sup> | boolean | Whether a TOTP authenticator is registered |
| authenticator_types<sup>9</sup> <sup>12</sup> | array[integer] | Registered [authenticator types](/http-api/users/#authenticator-types), with at most 10 entries |
@@ -1148,7 +1148,7 @@ A path naming the acting account fails with 403 `ACCESS_DENIED` before the accou
| Field | Type | Description |
| --- | --- | --- |
| acls<sup>1</sup> | array[string] | Replacement [Admin ACLs](/admin-api/#acl-registry), with at most 105 values |
| acls<sup>1</sup> | array[string] | Replacement [Admin ACLs](/admin-api/#acl-registry), with at most 106 values |
<sup>1</sup> Required. A value outside the [ACL registry](/admin-api/#acl-registry) fails body validation, and a repeated value is collapsed
@@ -882,6 +882,10 @@ Email verification is required for this action
The report form changed while you were filling it in
### `REPORT_UNDER_LEGAL_HOLD`
This report is under a legal hold and can't be deleted
### `RESOURCE_LOCKED`
This resource is being modified
@@ -47,7 +47,7 @@ Every accepted submission writes one report and returns it as a [report object](
Two kinds of repeat are refused with 409 `CONFLICT`:
- The same reporter reporting the same message again within 365 days of the earlier report.
- The same reporter reporting the same message again within 365 days of the earlier report, unless that report was deleted.
- The same reporter reporting the same user again within 24 hours of the earlier report.
Nothing deduplicates a Digital Services Act notice.
@@ -624,7 +624,7 @@ A reporter without `READ_MESSAGE_HISTORY` can report a message sent at or after
| 409 | [error response](/http-api/#error-response) | The answers came from an outdated flow and do not fit the current one, returning `REPORT_FLOW_OUTDATED` |
| 409<sup>1</sup> | [error response](/http-api/#error-response) | The reporter already reported this message, returning `CONFLICT` |
<sup>1</sup> The reporter, the channel, and the message form a submission key that expires 365 days after the report. A second report of the same message by the same reporter within that time is refused whichever answers it sends. A submission that fails or is rate limited releases the key
<sup>1</sup> The reporter, the channel, and the message form a submission key that expires 365 days after the report. A second report of the same message by the same reporter within that time is refused whichever answers it sends. A submission that fails or is rate limited releases the key, and so does [deleting the report](/admin-api/reports/#delete-report)
### Side effects
@@ -692,7 +692,7 @@ Fluxer checks the answers right after the claimed account and verified email che
| 409 | [error response](/http-api/#error-response) | The answers came from an outdated flow and do not fit the current one, returning `REPORT_FLOW_OUTDATED` |
| 409<sup>1</sup> | [error response](/http-api/#error-response) | The reporter already reported this user within 24 hours, returning `CONFLICT` |
<sup>1</sup> The reporter and the reported user form a submission key that expires 24 hours after the report. A submission that fails or is rate limited releases the key
<sup>1</sup> The reporter and the reported user form a submission key that expires 24 hours after the report. A submission that fails or is rate limited releases the key, and so does [deleting the report](/admin-api/reports/#delete-report)
### Side effects
@@ -1024,9 +1024,17 @@ Defaults to the inverse of `FLUXER_SELF_HOSTED`. Off by default on a self-hosted
With feeds off, Fluxer checks none of this data. The first worker start with feeds off removes the URL feed file and every `malware_bazaar` file-SHA ban that no Admin added. File-SHA bans added through the Admin API stay. Turning feeds back on downloads the URLs within six hours and the hashes within twelve. Compose forwards it from `.env`.
#### `FLUXER_REPORT_RETENTION_DAYS`
Default `365`. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes each report this many days after it was filed, pending or resolved, with its evidence copies and search entry, unless a legal hold is in place. Integer 1 to 36500, checked at startup. Compose forwards it from `.env`.
#### `FLUXER_RESOLVED_REPORT_RETENTION_DAYS`
Default unset, which leaves resolved reports to `FLUXER_REPORT_RETENTION_DAYS`. Read by `worker`. When set, a resolved report is deleted this many days after it was resolved, if that comes before the end of its retention period. Pending reports are not affected, and a legal hold still keeps a report. Integer 1 to 36500, checked at startup. Compose forwards it from `.env`.
#### `FLUXER_REPORT_RETENTION_DRY_RUN`
Default unset, which keeps the dry run. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes reports 365 days after they were filed, with their evidence copies and search entries, unless a legal hold is in place. Until this is `false` or `0`, the job only logs what it would delete. Any other value, or no value, keeps the dry run. Check the counts in the `Processed report retention` log line before you turn deletion on, because deleted evidence cannot be restored. Compose forwards it from `.env`.
Default `false`, so report retention deletes for real. Read by `worker`. With `true`, each run only counts what it would delete and deletes nothing. `true` or `false`, checked at startup. Use it to check the counts in the `Processed report retention` log line before the first real run, because deleted evidence cannot be restored. Compose forwards it from `.env`.
#### `FLUXER_BREACHED_PASSWORD_CHECK_ENABLED`