Metadata-Version: 2.3
Name: zhongli
Version: 2026.6.4
Summary: Bot to boost/reblog posts with specified tags
Author: marvin8
Author-email: marvin8 <marvin8@tuta.io>
License: AGPL-3.0-or-later
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: aiosqlite~=0.22.1
Requires-Dist: cyclopts~=4.16.1
Requires-Dist: httpx[http2]~=0.28.1
Requires-Dist: imagehash~=4.3.2
Requires-Dist: loguru~=0.7.3
Requires-Dist: longwei~=1.5.1
Requires-Dist: msgspec~=0.21.1
Requires-Dist: pybreaker~=1.4.1
Requires-Dist: stamina~=26.1.0
Requires-Dist: tomli-w~=1.2.0
Requires-Dist: pillow~=12.2.0
Requires-Python: >=3.12, <3.15
Description-Content-Type: text/markdown

# Zhongli

Automatically boost/reblog Fediverse posts. Formerly **Fedibooster**.

[![Repo](https://img.shields.io/badge/repo-Codeberg.org-blue)](https://codeberg.org/MarvinsMastodonTools/zhongli) [![CI](https://ci.codeberg.org/api/badges/13923/status.svg)](https://ci.codeberg.org/repos/13923) [![Downloads](https://pepy.tech/badge/zhongli)](https://pepy.tech/project/zhongli) [![AGPL](https://www.gnu.org/graphics/agplv3-with-text-162x68.png)](https://codeberg.org/MarvinsMastodonTools/zhongli/src/branch/main/LICENSE.md)

## Overview

Zhongli consumes pre-filtered posts from [FenLiu](https://codeberg.org/marvinsmastodontools/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

```bash
pip install zhongli
```

Or from source:
```bash
git clone https://codeberg.org/marvinsmastodontools/zhongli.git
cd zhongli
uv sync
uv run zhongli
```

## Container

Images are published to `codeberg.org/marvinsmastodontools/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:

```toml
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

```bash
podman run -d \
  -v ./config.toml:/app/config/config.toml:ro \
  -v zhongli-data:/app/data \
  codeberg.org/marvinsmastodontools/zhongli
```

### Run as a one-shot / cron job

```bash
podman run --rm \
  -v ./config.toml:/app/config/config.toml:ro \
  -v zhongli-data:/app/data \
  codeberg.org/marvinsmastodontools/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`:
```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:

```bash
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 \
  codeberg.org/marvinsmastodontools/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](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) is the recommended way to run zhongli as a persistent rootless systemd service (requires Podman ≥ 4.4).

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

```ini
[Unit]
Description=Zhongli Fediverse reblog bot

[Container]
Image=codeberg.org/marvinsmastodontools/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

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

[Install]
WantedBy=default.target
```

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

```ini
[Volume]
```

Then enable and start:

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

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

## Configuration

Zhongli requires FenLiu and a Fediverse account:

```toml
[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

```bash
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

```bash
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](http://www.gnu.org/licenses/agpl-3.0.html)

## Support

- [Buy me coffee](https://www.buymeacoffee.com/marvin8)
- Monero: `88xtj3hqQEpXrb5KLCigRF1azxDh8r9XvYZPuXwaGaX5fWtgub1gQsn8sZCmEGhReZMww6RRaq5HZ48HjrNqmeccUHcwABg`
