Metadata-Version: 2.3
Name: zhongli
Version: 2026.5.21
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.11.2
Requires-Dist: httpx[http2]~=0.28.1
Requires-Dist: imagehash~=4.3.2
Requires-Dist: loguru~=0.7.3
Requires-Dist: longwei~=1.4.3
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)

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