Run a site

Abuse reports

How people report something, how administrators deal with it, and how an installation sets the reasons people can choose from. It is all in abuse-report-service, under /abuse-reports.

Its routes are in the API reference: abuse-report-service.

Reports and cases

Anything can be reported: a person, a group, a post, a reply, a hashtag or a location.

A report is one person's complaint. A case is one reported thing and the decision about it. Everybody who reports the same thing while its case is open joins that case, so a post that twenty people report is one case for an administrator to handle.

A case is open until somebody decides it, and then actioned (something was done) or dismissed (nothing needed doing). If the same thing is reported again after its case was closed, a new case opens, and the earlier one shows as its history.

Reports are not nodes. They are kept in two tables of their own, abuse_cases and abuse_reports, so that nothing which lists, searches, federates or exports content can ever show one, and so that a case outlives the thing it was about.

Reporting something

POST /abuse-reports with { "target_id": 123, "reason_key": "spam", "details": "…" }.

Answer Meaning
201 Reported
200 You had already reported it. Your report now has the new reason and details
400 The reason is not on the list or does not apply to this kind of thing, the reason needs an explanation and none was given, the rule is not one of the rules it is under (rule_required, rule_not_applicable), or it is your own
403 You are not signed in, or the app you use may not write for you (account:write)
404 There is no such thing that you can see
429 More than 20 reports in the last hour
  • Signed in only.
  • You can only report what you can see. Something hidden from you answers exactly as something that does not exist, so reporting cannot be used to find out what is hidden.
  • Not yourself, and not what you wrote.
  • One report per person per case. Reporting again changes yours.
  • Details are optional, up to 1,000 characters, except where the reason requires them.

The reasons list

GET /abuse-reports/reasons?type=<node type> returns the reasons that can be given for that kind of thing, in the reader's language (?lang=fr-FR, or the browser's Accept-Language).

The service carries a default list, in English and French:

Spam · Harassment or bullying · Racism · Sexism · Other hate · Violence or threats · Self-harm or suicide · Sexual content shared without consent · Child safety · Impersonation (people and groups only) · Private information · Misleading or false information · Intellectual property or copyright · Trademark · Illegal content or activity · Something else (an explanation is required)

Using your own reasons

What a community forbids is its own business. To replace the list:

  1. Copy abuse-report-service/data/reasons/reasons.json and edit it.
  2. Make the file available to the service: a volume in docker-compose, a ConfigMap on Kubernetes.
  3. Set REASONS_PATH to where it is mounted.

The file is read again whenever it changes, so an edit takes effect without a restart.

{
  "reasons": [
    {
      "key": "hate_racism",
      "label": { "en-GB": "Racism", "fr-FR": "Racisme" },
      "description": {
        "en-GB": "Attacks or slurs based on race or ethnicity.",
        "fr-FR": "Attaques ou injures fondées sur la race ou l’origine ethnique."
      },
      "applies_to": ["node.actor.", "node.medium."],
      "requires_details": false,
      "atproto_reason": "com.atproto.moderation.defs#reasonRude"
    }
  ]
}
Field Meaning
key What is stored on each report. Up to 64 characters, and unique. Treat it as permanent: reports that used a key you remove show the key in place of a label
label The name, by language. en-GB is required
description A sentence shown under the name. Optional
applies_to The kinds of thing the reason can be given for, as type prefixes: node.actor. (people and groups), node.medium. (posts and replies), node.tag. (hashtags and places). Leave it out for everything
requires_details true makes the reporter explain
atproto_reason The AT Protocol reason this corresponds to. Not used yet; it is there for when reports are sent to other servers

A file that cannot be used, because it is missing, is not valid JSON, has no reasons, repeats a key or lacks an en-GB label, is not applied in part. The built-in list is used until it is fixed, and the service's log says what is wrong with it.

Breaking a rule

Besides a reason from the list, a report can say which rule was broken: one of the site's rules (site.json, configuration.md) or one of the rules of the group or event it was posted in, or of the group or event itself. The rules are the reasons here, so they are not in the reasons file.

POST /abuse-reports with { "target_id": 123, "reason_key": "rule", "rule": "Be kind", "rule_profile_id": 456 }.

Field Value
reason_key rule
rule The rule's title as the reporter read it, up to 100 characters
rule_profile_id 0 for the site's rules, or the group's or event's id
  • A group's or an event's rule is checked: it must be one of its rules now, and the thing reported must be the group or event, be posted in it, or be a reply in a thread posted in it. Otherwise rule_not_applicable.
  • The site's rules are taken as given. This service does not read site.json; the administrator reading the report has the rules in front of them.
  • Stored with the report, in abuse_reports.rule_title and rule_profile_id. The case answers with rule, rule_profile_id and the profile itself as rule_profile.
  • Filtered like any reason: GET /abuse-reports/cases?reason=rule.

A group's own reports

A group's or an event's administrators see the reports against its rules: GET /abuse-reports/profiles/:id/cases (?status=open, the default, and limit, start), in the web app the Reports tab of its settings.

Answer Meaning
200 The cases with a report against its rules, newest first, each with those reports: rule, details and created_at
403 You administer neither it nor the site
404 There is no such group or event
  • Nobody's name. Who reported is shown to the site's administrators only.
  • Never what is about them. A report on the group or event itself, or on something the viewer is answerable for, is left out: whoever is reported is not told.
  • They act where it is. Deleting a post on its page closes its case, as for any deleted post. Deciding a case (Dismiss, Close) stays with the site's administrators, whose case it is.

What administrators do

In the web app, under Administration › Reports. The endpoints:

Endpoint Does
GET /abuse-reports/cases?status=open&reason=spam Lists cases, the most recently reported first, 20 at a time (limit, offset). status is open, actioned or dismissed. reason is optional: with it, only the cases in which somebody gave that reason
GET /abuse-reports/cases/summary { "open": n }, for the number on the Administration menu
GET /abuse-reports/cases/:id The case: what was reported and by whom, each report's reason and details, and the earlier cases the same thing has had
POST /abuse-reports/cases/:id/resolve Closes it: { "status": "actioned" or "dismissed", "action": "…", "note": "…" }
POST /abuse-reports/cases/:id/reopen Opens a closed case again

The service records decisions. It does not carry them out. Deleting a post, disabling an account or archiving a group is done through the service that owns it, under that service's own rules: an administrator who may not archive a group cannot do it from here either. The app does the action first, then records the case as actioned with what was done: deleted, disabled, archived, blocked, removed_as_spam or other. A person or a group reported can be removed as spam from its case, which is then closed as removed_as_spam (Removing as spam).

Two administrators deciding the same case at once get one decision recorded; the second is told it was already decided (409). A closed case cannot be reopened while a newer case for the same thing is open (409, newer_case_open): use the newer one. Reopening a case that is open already is answered 409 with already_open.

Nobody is emailed about a report. Administrators see the number waiting on the Administration menu.

When the reported thing is deleted

There is nothing left to decide about something that no longer exists, so its open case is closed as actioned, deleted, with nobody named as having decided it.

  • A post or reply is gone the moment it is deleted.
  • A person or group is not: deleting one starts a 30-day wait during which it can be restored. Its case stays open until it is purged.

The service hears of this through an event. Events can be lost, so opening a case also checks whether the thing still exists.

When a person who filed reports is purged, their reports are removed with everything else they wrote. A case that other people also reported keeps going with one report fewer. An open case that only they had reported is removed, because no complaint is left to decide. A case already decided stays, as the record of the decision.

This too arrives as an event, and nothing checks again if it is lost: the reports then stay, naming an account that is only a reserved handle.

Who sees what

Sees
The reporter That their report was received. Nothing after that
The person reported Nothing. They are not told of the report, or who made it
Administrators and super administrators Every case, every report, and who made it

The network address a report came from is stored with it and is not shown in the app. It is blanked 90 days after the report was filed; the report itself stays.

Filing a report needs the account:write scope. Reading cases needs actors:read and deciding them actors:write, on top of being an administrator.

Where the code is

What Where
The service abuse-report-service/
Reporting something abuse-report-service/cmd/api/handlers/reports.go (Add)
The reasons list, and your own abuse-report-service/data/reasons/reasons.go (BuiltIn, Parse, Source), abuse-report-service/data/reasons/reasons.json, setting REASONS_PATH
What administrators do abuse-report-service/cmd/api/handlers/cases.go, abuse-report-service/cmd/api/permissions/permissions.go
Cases and reports abuse-report-service/data/repositories/cases.go, tables abuse_cases and abuse_reports in migration-job/schema/init.sql
When the reported thing is deleted abuse-report-service/cmd/api/handlers/events.go (Handle)
Blanking network addresses after 90 days abuse-report-service/cmd/api/main.go (forgetOldAddresses)
Limits: 20 reports an hour, 1,000 characters abuse-report-service/constants/constants.go
Tests of the rules abuse-report-service/cmd/api/handlers/handlers_test.go, abuse-report-service/data/repositories/cases_integration_test.go
The web app src/containers/admin/Reports/, src/api/abuseReports.js (anahita-web)

From anahitasocial/anahita-services, 2026-10-27.

Written by Claude, reviewed by Rastin Mehr · About this