Automated conservative cleanup for Forgejo SEO spam accounts
  • Python 99.4%
  • Dockerfile 0.6%
Find a file
2026-05-25 12:57:07 +04:00
forgejo_janitor Skip redundant profile sanitization 2026-05-25 12:57:07 +04:00
tests Skip redundant profile sanitization 2026-05-25 12:57:07 +04:00
.gitignore Add Forgejo spam janitor 2026-05-23 14:29:46 +04:00
config.example.toml Expand signup spam heuristics 2026-05-25 11:24:51 +04:00
docker-compose.example.yml Monitor and quarantine spam content 2026-05-23 15:00:01 +04:00
Dockerfile Add Forgejo spam janitor 2026-05-23 14:29:46 +04:00
pyproject.toml Add Forgejo spam janitor 2026-05-23 14:29:46 +04:00
README.md Expand signup spam heuristics 2026-05-25 11:24:51 +04:00

Forgejo Janitor

Automated, conservative cleanup for Forgejo SEO spam accounts.

The janitor scores recently registered users, or users with recent repository/issue/comment activity, using profile, repository, issue, comment, and SSH-key signals. It is dry-run by default and never deletes accounts. Executed actions are limited to:

  • sanitize_profile: clear profile full_name, website, location, and description.
  • restrict_user: set is_restricted = true.
  • prohibit_login: set prohibit_login = true for high-confidence spam only.
  • quarantine_repo: make a spam repository private, archive it, and clear repo metadata.
  • redact_issue: replace spam issue title/body and close/lock the issue.
  • redact_comment: replace spam comment body.

It intentionally does not print raw email addresses. Reports include only email domains.

Members of protected organizations are never scored or acted on. The default protected org is pleroma.

Quick Start

python -m venv .venv
. .venv/bin/activate
pip install -e '.[test]'
pytest

Run a dry scan:

DATABASE_URL='postgresql://forgejo:password@db:5432/forgejo' \
  forgejo-janitor scan --config config.example.toml --output summary

Run with optional bare repository scanning:

DATABASE_URL='postgresql://forgejo:password@db:5432/forgejo' \
  forgejo-janitor scan \
  --config config.example.toml \
  --repo-root /data/git/repositories \
  --output jsonl

Apply planned actions:

DATABASE_URL='postgresql://forgejo:password@db:5432/forgejo' \
  forgejo-janitor run \
  --config config.example.toml \
  --repo-root /data/git/repositories \
  --max-actions 100 \
  --execute

Monitor recent repo/issue/comment activity continuously:

DATABASE_URL='postgresql://forgejo:password@db:5432/forgejo' \
  forgejo-janitor monitor \
  --config config.example.toml \
  --repo-root /data/git/repositories \
  --activity-minutes 5 \
  --interval 60 \
  --max-actions 100 \
  --execute

Omit --execute to keep monitor mode in dry-run mode.

Email Notifications

Pass --notify-email to send an email when actions are planned or applied. Recipients default to active Forgejo admins from the database unless JANITOR_EMAIL_TO is set.

export JANITOR_SMTP_HOST=mail.example
export JANITOR_SMTP_PORT=587
export JANITOR_SMTP_USERNAME=janitor@example
export JANITOR_SMTP_PASSWORD=secret
export JANITOR_EMAIL_FROM=janitor@example
export JANITOR_EMAIL_TO=admin1@example,admin2@example

Set JANITOR_SMTP_STARTTLS=false to disable STARTTLS.

Omit --execute to keep run in dry-run mode.

Docker

Build locally:

docker build -t forgejo-janitor:latest .

Dry-run with Docker:

docker run --rm \
  --network forgejo_default \
  -e DATABASE_URL='postgresql://forgejo:password@db:5432/forgejo' \
  -v /opt/forgejo/volumes/forgejo/git/repositories:/data/git/repositories:ro \
  -v ./config.example.toml:/config/janitor.toml:ro \
  forgejo-janitor:latest scan --config /config/janitor.toml --repo-root /data/git/repositories

Use docker-compose.example.yml as a starting point for a monitor deployment.

Policy

Thresholds and heuristics live in config.example.toml. The default policy is tuned to remove SEO backlink value first and reserve login prohibition/content redaction for high-confidence issue/repository/comment spam.

Known-good automation should be allowlisted by username or email domain. The example allowlists pleroma-ci and pleroma.social.

Organization members that must never be touched should be listed under allowlist.protected_orgs. The example protects all members of the pleroma organization.

Development

pytest
python -m forgejo_janitor.cli scan --help