Skip to main content

Zhongli

Automatically boost/reblog Fediverse posts. Formerly Fedibooster.

Repo CI Downloads AGPL

Overview

Zhongli consumes pre-filtered posts from FenLiu's queue and automatically reblogs them to your Fediverse account. FenLiu handles the heavy lifting (filtering, spam detection, curation); Zhongli handles the reblogging and duplicate prevention.

FenLiu configuration is required. There is no legacy mode.

Status

Phase Status Notes
1: Infrastructure ✅ Complete FenLiu client, models, tests
2: FenLiu-Only Mode ✅ Complete ReblogService, queue polling, attachments
3: Validation Optimization ✅ Complete MinimalValidator, pre-filtered posts
4: Documentation ✅ Complete FenLiu-only, updated config
5: UX Polish ⏳ Optional Metrics, dry-run, progress indicators

Install

pip install zhongli

Or from source:

git clone https://forge.marvin8.zone/marvin8/dujiangyan.git
cd zhongli
uv sync
uv run zhongli

Container

Images are published to registry.marvin8.zone/marvin8/zhongli on every release.

Volumes

Path Purpose
/app/config/config.toml Configuration file (required, bind-mount)
/app/data/ SQLite cache database (persist across runs)

Config for container use

Add cache_db_path so the database lands on the persistent volume:

run_continuously = true
delay_between_posts = 300
cache_db_path = "/app/data/cache.db"

[fediverse]
domain_name = "mastodon.social"
api_token = "your-token"

[fenliu]
base_url = "https://fenliu.example.com"
api_key = "your-api-key"

Run as a daemon

podman run -d \
  -v ./config.toml:/app/config/config.toml:ro \
  -v zhongli-data:/app/data \
  registry.marvin8.zone/marvin8/zhongli

Run as a one-shot / cron job

podman run --rm \
  -v ./config.toml:/app/config/config.toml:ro \
  -v zhongli-data:/app/data \
  registry.marvin8.zone/marvin8/zhongli \
  /app/config/config.toml --max-posts 10

The container starts as root, fixes config permissions to 0600, then drops to an unprivileged zhongli user (UID 1000) before running.

Health check

The image includes a HEALTHCHECK that passes as long as /app/data/zhongli.log was written within the last 15 minutes. To activate it, mount a logging config that writes to that path:

logging-config.toml:

[[handlers]]
sink = "sys.stdout"
format = "{message}"
level = "INFO"

[[handlers]]
sink = "/app/data/zhongli.log"
rotation = "1 day"
retention = 3
level = "DEBUG"
serialize = true

Then pass it to zhongli:

podman run -d \
  -v ./config.toml:/app/config/config.toml:ro \
  -v ./logging-config.toml:/app/config/logging-config.toml:ro \
  -v zhongli-data:/app/data \
  registry.marvin8.zone/marvin8/zhongli \
  /app/config/config.toml --logging-config /app/config/logging-config.toml

If your delay_between_posts exceeds 10 minutes, override the healthcheck interval to avoid false negatives: add --health-interval=<2× delay> to your podman run invocation, or set HealthInterval in your Quadlet unit.

Quadlet (systemd)

Quadlet is the recommended way to run zhongli as a persistent rootless systemd service (requires Podman ≥ 4.4).

Create ~/.config/containers/systemd/zhongli.container:

[Unit]
Description=Zhongli Fediverse reblog bot

[Container]
Image=registry.marvin8.zone/marvin8/zhongli:latest
AutoUpdate=registry
Volume=%h/.config/zhongli/config.toml:/app/config/config.toml:ro
Volume=%h/.config/zhongli/logging-config.toml:/app/config/logging-config.toml:ro
Volume=zhongli-data.volume:/app/data
Exec=/app/config/config.toml --logging-config /app/config/logging-config.toml
HealthOnFailure=restart

[Service]
Restart=on-failure
RestartSec=30s

[Install]
WantedBy=default.target

Create ~/.config/containers/systemd/zhongli-data.volume:

[Volume]

Then enable and start:

systemctl --user daemon-reload
systemctl --user start zhongli
systemctl --user status zhongli

Logs via: journalctl --user -u zhongli -f

Author Opt-Out Signals

Zhongli respects the following Fediverse opt-out conventions. When detected, the post is permanently rejected (not retried):

Signal Where checked Returned error type
#noBot in author bio account.note (HTML-stripped) nobot
#noIndex in author bio account.note (HTML-stripped) noindex
#noBot as a post tag status.tags nobot

These checks run on every post before the duplicate-attachment scan.

Configuration

Zhongli requires FenLiu and a Fediverse account:

[fediverse]
domain_name = "mastodon.social"
api_token = "your-token"

[fenliu]
base_url = "https://fenliu.example.com"
api_key = "your-api-key"

# Optional
run_continuously = false
delay_between_posts = 300
reblog_sensitive = false
max_reblog = 5
# allow_insecure_http = false  # set true only if FenLiu is on a local network without HTTPS

Configuration explained:

  • [fediverse] - Your Mastodon/Pixelfed/Misskey account
  • [fenliu] - Connection to FenLiu queue service
  • run_continuously - Keep polling or exit after max_reblog posts
  • delay_between_posts - Seconds between reblogs (0 for no delay)
  • reblog_sensitive - Whether to boost sensitive/NSFW posts
  • max_reblog - Stop after boosting N posts (0 for unlimited)
  • fenliu.skip_missing_alt_text - Skip posts where any attachment lacks alt text (default: false)
  • fenliu.allow_insecure_http - Allow an HTTP base_url for private local-network deployments where HTTPS is not feasible. Never enable this in production. http://localhost and http://127.0.0.1 are always exempt and do not require this flag (default: false)

Security: config.toml is created with 0600 permissions (owner read/write only). If you copy or restore the file from another location, re-apply: chmod 600 config.toml. Zhongli will refuse to start if it detects the file is readable by group or world.

cache.db (attachment deduplication database) is also created with 0600 permissions. If the file already exists with looser permissions, zhongli logs a warning with a chmod 600 hint.

fenliu.base_url must use https:// to protect the API key in transit. The only exceptions are http://localhost and http://127.0.0.1 (always permitted for local development), and any URL when allow_insecure_http = true is set explicitly for private-network deployments.

Debug log: When using --logging-config logging-config.toml, debug output is written to ~/.cache/zhongli/zhongli.log. This directory is created automatically and is accessible only by the owner. Never configure /tmp/ as a log destination — it is world-readable.

Usage

zhongli config.toml                 # Run with config file
zhongli config.toml --max-posts 10  # Override max posts
zhongli --help                      # Show all options

How It Works

  1. Poll FenLiu Queue - Get next curated post
  2. Validate - Quick checks (has content, not a reply, respects sensitive setting)
  3. Find on Fediverse - Search for post by URL
  4. Check for Duplicates - Compare attachments (URL, hash, perceptual)
  5. Reblog - Boost to your account with circuit breaker protection
  6. Report Feedback - ACK (success), NACK (transient error), ERROR (permanent)
  7. Repeat - Until configured limit or continuous mode ends

Features

  • FenLiu Integration - Consume pre-curated, pre-filtered posts
  • Duplicate Prevention - Three-layer attachment detection (URL, SHA-256, dHash)
  • Reliable Reblogging - Circuit breaker pattern, retry logic, error classification
  • Smart Feedback - ACK/NACK/ERROR reporting to FenLiu
  • Production Ready - 132 tests, full type checking, comprehensive error handling

Development

uv sync              # Install deps
prek run --all-files # Pre-commit checks
uv run tryke test    # Run tests (132 tests; from repo root: uv run tryke test --root packages/zhongli)
nox                  # Full CI simulation

Recent Work

  • ✅ Phase 3: Validation optimized for FenLiu pre-filtered posts
  • ✅ Phase 2: Full FenLiu-only mode operational
  • ✅ Phase 1: FenLiu infrastructure complete with 132 tests
  • ✅ Attachment deduplication system (URL, content hash, perceptual hash)
  • ✅ Circuit breaker pattern integrated
  • ✅ Complete feedback mechanism (ACK/NACK/ERROR)

Next Steps

Phase 5 - UX polish and observability (metrics, dry-run mode, progress indicators)

Privacy / Federation behaviour

When Zhongli looks up a post by URL, it first queries its home instance's local index without triggering any outbound federation. If the post is already known (e.g. it appeared in a followed account's timeline), no federation request is made.

If the post is not in the local index, Zhongli falls back to a federated lookup (resolve=True). This causes the home instance to fetch the post directly from the remote instance. Remote instance administrators can observe this request, which reveals that your instance is interested in that post. This is an inherent consequence of ActivityPub federation and cannot be avoided when a post is not yet locally cached.

Operator note: If you are operating Zhongli on a small or single-user instance, remote administrators may be able to infer your curation patterns from federation traffic.

License

GNU AGPL v3.0

Support

  • Buy me coffee
  • Monero: 88xtj3hqQEpXrb5KLCigRF1azxDh8r9XvYZPuXwaGaX5fWtgub1gQsn8sZCmEGhReZMww6RRaq5HZ48HjrNqmeccUHcwABg

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zhongli-2026.8.15.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zhongli-2026.8.15-py3-none-any.whl (30.2 kB view details)

Uploaded Python 3

File details

Details for the file zhongli-2026.8.15.tar.gz.

File metadata

  • Download URL: zhongli-2026.8.15.tar.gz
  • Upload date:
  • Size: 26.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for zhongli-2026.8.15.tar.gz
Algorithm Hash digest
SHA256 5b5b787b1c13bb5ae2bfda315bbc00d42e78ca9a20bada882af261cc8659b7be
MD5 e9e81db1175c0a5ff62d735749f91635
BLAKE2b-256 94553a91b752ca59e209c30922d88c357ac78039a8c9ad11e05ec6a11397f0bf

See more details on using hashes here.

File details

Details for the file zhongli-2026.8.15-py3-none-any.whl.

File metadata

  • Download URL: zhongli-2026.8.15-py3-none-any.whl
  • Upload date:
  • Size: 30.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for zhongli-2026.8.15-py3-none-any.whl
Algorithm Hash digest
SHA256 78c9e2a77d6643b83c848b46d614c2b45d7aa366fdfd5da72041220a82d2ad2b
MD5 a339666e5e3b3235117ebf4ca7f3708c
BLAKE2b-256 1206fc548b260476a1577782015cbd76bbc81ee701b7320c9af10964d52b34f1

See more details on using hashes here.

Release history Release notifications | RSS feed

2026.8.30

2 files

This release

2026.8.15 This release

2 files

2026.8.4

2 files

2026.7.20

2 files

2026.6.30

2 files

2026.6.23

2 files

2026.6.4

2 files

2026.5.166

2 files

2026.5.26

2 files

2026.5.23

2 files

2026.5.21

2 files

2026.5.18

2 files

2026.5.1

2 files

2026.4.22

2 files

2026.4.5

2 files

2026.4.3

2 files

2026.3.25.1.1

2 files

2026.3.19

2 files

2026.3.15

2 files

2026.3.4

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page