Metadata-Version: 2.4
Name: muc-banbot
Version: 3.0.0
Summary: XMPP multi-room ban management and moderation bot
Author-email: ~creme <creme@envs.net>
License-Expression: GPL-3.0-only
Project-URL: Homepage, https://github.com/envs-net/muc_banbot
Project-URL: Repository, https://github.com/envs-net/muc_banbot
Project-URL: Issues, https://github.com/envs-net/muc_banbot/issues
Keywords: xmpp,muc,moderation,banbot,rtbl
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: envs-xmpp<2.0,>=1.0.0
Requires-Dist: slixmpp<2,>=1.8
Requires-Dist: aiosqlite<1,>=0.20
Requires-Dist: psutil<8,>=5.9
Provides-Extra: omemo
Requires-Dist: slixmpp-omemo<3,>=2; extra == "omemo"
Provides-Extra: dev
Requires-Dist: hypothesis>=6; extra == "dev"
Requires-Dist: mutmut>=3; extra == "dev"
Requires-Dist: mypy<2,>=1.8; extra == "dev"
Requires-Dist: pip-audit<3,>=2.7; extra == "dev"
Requires-Dist: ruff<1,>=0.9; extra == "dev"
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=1; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: pytest-mock>=3; extra == "dev"
Dynamic: license-file

# BanBot - XMPP Multi-Room Ban Management Bot - [![calver](https://img.shields.io/github/v/release/envs-net/muc_banbot)](https://github.com/envs-net/muc_banbot/releases/latest) / [![Build Status](https://drone.envs.net/api/badges/envs/muc_banbot/status.svg)](https://drone.envs.net/envs/muc_banbot)

BanBot is an XMPP bot for centralized ban management across multiple MUC rooms (Multi-User Chat).

It provides admin-room based moderation, protects configured MUCs from unwanted users, and supports temporary bans, domain bans, ban synchronization, audit logging, ignorelists, health checks, protections, RTBL/PubSub integration, and optional OMEMO support for encrypted commands and replies.

---

## Features

* 🛡️ Central admin room for all administrative commands
* 🧩 Dynamic addition/removal of protected rooms
* 🔒 Optional OMEMO support: encrypted commands receive encrypted replies
* ❌ Ban, temporary ban, unban, banlist, bansearch, why, and redaction commands
* 🌐 Domain-based bans (`*.domain.tld`) to ban all users from a domain
* ⏱️ Automatic temporary ban expiration with human-readable durations
* 📊 Smart duplicate ban handling with automatic conversion between permanent and temporary bans
* 🐞 Nick-only ban support with best-effort JID upgrade when the user rejoins
* ⚠️ Admin/owner protection for direct, nick-based, and domain-based bans
* 🚫 Global ignorelist/whitelist for exact JIDs and domain-based ban protection
* 📦 Startup and manual synchronization of room bans/admins
* 🏥 Health checks, reconnect awareness, admin-right monitoring, and dynamic `!status`
* 🛡️ RTBL subscriptions via PubSub for SHA-256 JID hashes and plaintext domain bans
* 🔄 Periodic RTBL refresh with quiet/no-change behavior and snapshot reconciliation
* ♻️ RTBL snapshot reconciliation with stale local ban cleanup
* 📡 Optional own RTBL publish feed for local bans
* 🧾 SQLite audit log and structured JSON event logs
* 💾 CSV import/export with managed safety backups
* 🗄️ Managed ZIP backup archives with manifest, restore command, verification, and automatic startup backups
* ✅ Startup/runtime config validation with safe `!reloadconfig`
* 🚦 Rate limiting for public protected-room commands
* 🧯 Protections for flood spam, first-message media, mention limits, wordlists, join waves, trusted reporters, and policy-change notifications
* 📜 Optional public room policy text via `!rules` / `!policy`
* ⬆️ Optional GitHub release checks
* 🖼️ Avatar/vCard support via XEP-0054, XEP-0084, and XEP-0153
* 🧪 Extensive pytest suite, coverage, property tests, mutation-testing support, and Drone CI

---

## Installation / Quickstart

Requires **Python 3.12+**. The project is developed and tested with Python 3.13.

### Recommended hardened systemd deployment

Keep the application checkout separate from mutable configuration and data:

```text
/srv/adminbot/muc_banbot/       repository + virtualenv
/etc/muc_banbot/config.py       runtime configuration
/var/lib/muc_banbot/            database, backups, exports and OMEMO state
```

Bootstrap the service account and checkout, pin a tagged release, then use the
preservation-first deploy helper:

```bash
sudo useradd -m -s /bin/bash adminbot -d /srv/adminbot
sudo -u adminbot git clone https://git.envs.net/envs/muc_banbot.git /srv/adminbot/muc_banbot
cd /srv/adminbot/muc_banbot

git fetch --tags
LATEST_TAG="$(git tag --sort=-v:refname | head -n1)"
git checkout "$LATEST_TAG"

./scripts/deploy.sh install --dry-run
sudo ./scripts/deploy.sh install
```

The deploy frontend uses the shared `envs-xmpp` operations layer. On a fresh checkout it bootstraps the exact deployment-tooling version into `$XDG_CACHE_HOME/envs-xmpp/deploy/` (or `~/.cache/envs-xmpp/deploy/`) before continuing; no manual pre-install is required. `ENVS_XMPP_DEPLOY_SOURCE` can point to a local checkout or wheel for pre-release testing.

On a fresh install the helper creates `/etc/muc_banbot/config.py` once, with
absolute mutable paths below `/var/lib/muc_banbot`, and then stops so credentials
can be edited safely. Hardened deployments must keep `DB_FILE`, `DB_BACKUP_DIR`,
`EXPORT_DIR`, and `OMEMO_STORAGE_FILE` below the configured data directory; old
relative source-tree paths are rejected by `deploy.sh check`. The expected
permission baseline is `/etc/muc_banbot` `0750`, `config.py` `0600`, and
`/var/lib/muc_banbot` `0700`, owned by the service user/group. Rerun
`sudo ./scripts/deploy.sh install` after editing the config. Existing config,
database/data and systemd unit files are never overwritten automatically. A bare
`./scripts/deploy.sh` only prints help.

Useful read-only checks:

```bash
./scripts/deploy.sh status
sudo ./scripts/deploy.sh check
```

The recommended [`contrib/muc_banbot.service`](contrib/muc_banbot.service) uses
`Type=notify`, `WatchdogSec=60`, `ProtectSystem=strict`, disables Python
bytecode writes in `/etc`, and grants write access only to `/etc/muc_banbot`
and `/var/lib/muc_banbot`. See
[docs/deployment.md](docs/deployment.md) for the complete deployment and
migration notes.

### PyPI installation

BanBot is also published on PyPI as `muc-banbot`. For local testing, development
environments, or non-systemd installs it can be installed directly into a
virtualenv:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install muc-banbot

# Optional OMEMO support:
# pip install "muc-banbot[omemo]"

python -m pip show muc-banbot
```

To run the wheel directly, create an operator `config.py` in the working
directory (or point `MUC_BANBOT_CONFIG` at one). The installed sample can be
copied without a source checkout:

```bash
python - <<'PY'
from pathlib import Path
import config_sample

Path("config.py").write_bytes(Path(config_sample.__file__).read_bytes())
PY
chmod 600 config.py
$EDITOR config.py
muc_banbot
```

The wheel includes the default avatar as a packaged read-only asset, so the
default `AVATAR_PATH = "avatar.png"` also works outside a source checkout. An
existing working-directory `avatar.png` still takes precedence, and configured
subpaths or absolute paths keep their historical operator-controlled semantics.
The PyPI package does not create `/etc/muc_banbot`, `/var/lib/muc_banbot`, or a
systemd unit; the tagged Git checkout plus `./scripts/deploy.sh` remains the
recommended production deployment path.

### Legacy/source-tree installation (still supported)

The historical installation layout remains supported for operators who prefer
manual control or are not ready to migrate existing systems:

```bash
sudo useradd -m -s /bin/bash adminbot -d /srv/adminbot
sudo su - adminbot

cd /srv/adminbot
git clone https://git.envs.net/envs/muc_banbot.git
cd muc_banbot

git fetch --tags
LATEST_TAG="$(git tag --sort=-v:refname | head -n1)"
git checkout "$LATEST_TAG"

python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install -e .

# Optional OMEMO support:
# pip install -e ".[omemo]"

install -m 0600 config_sample.py config.py
$EDITOR config.py

muc_banbot
```

In this mode relative paths such as `DB_FILE = "banbot.db"` and
`DB_BACKUP_DIR = "data/backups"` continue to resolve below the checkout. The
legacy `python muc_banbot.py` launcher also remains supported. For systemd, use
[`contrib/muc_banbot-legacy.service`](contrib/muc_banbot-legacy.service) or keep
your existing unit. `MUC_BANBOT_CONFIG` remains available for custom layouts.

`main` is the development branch. Production deployments should use stable
`vX.Y.Z` release tags.

## Updating to a New Release

The deploy helper is the recommended update path:

```bash
cd /srv/adminbot/muc_banbot
./scripts/deploy.sh update --dry-run
sudo ./scripts/deploy.sh update
# Explicit release when desired:
# sudo ./scripts/deploy.sh update --to v2.6.4
```

It refuses tracked local Git modifications, never deploys `main` automatically,
queries remote release tags without bulk-overwriting local tags, fetches only
the selected stable tag, asks separately before stopping/starting the service,
and creates a consistent SQLite pre-update backup while the service is stopped.
For legacy source-tree layouts it additionally protects operator files inside
the checkout across the tag switch.

The previous fully manual update workflow remains supported:

```bash
cd /srv/adminbot/muc_banbot
git fetch --tags
LATEST_TAG="$(git tag --sort=-v:refname | head -n1)"
git checkout "$LATEST_TAG"
source venv/bin/activate
pip install -e .
# Optional: pip install -e ".[omemo]"

sudo systemctl restart muc_banbot
```

If release notes mention new configuration options, compare the active config
with `config_sample.py` and add settings you want to customize.

---

## Minimal Configuration

Copy `config_sample.py` to `config.py` and set at least:

```python
JID = "adminbot@example.org"
PASSWORD = "secret"
RESOURCE = "service"
ADMIN_ROOM = "admin@conference.example.org"
NICK = "BanBot"
DB_FILE = "banbot.db"
```

Most runtime settings can be reloaded with `!reloadconfig`. Startup-only settings such as the bot account/room identity, database path, RTBL enable/publish setup, and OMEMO setup require a restart.

See [docs/configuration.md](docs/configuration.md) for the full configuration reference.

---

## Important Commands

Examples assume the default command prefix `!`.

| Command | Description |
| --- | --- |
| `!help [all\|page\|last]` / `!help <command>` | Show available commands or focused help for every command topic, including subtopics such as `room invite` and `rtbl publish` |
| `!status` | Show bot health, uptime, rooms, bans, RTBL, DB state, and protection status |
| `!tasks [all\|failed]` | Show supervised background workers, restart/backoff state, restart counts, and runtime/systemd watchdog health |
| `!config [all\|page\|last]` / `!config show [all\|page\|last]` | Show active configuration grouped in `config_sample.py` section order; secrets are hidden |
| `!config search/find <query>` | Search config option names and displayed values |
| `!config diff [all\|page\|last]` | Show current values that differ from `config_sample.py` defaults |
| `!config set <KEY> <value>` | Change a runtime-writable configuration value |
| `!config unset <KEY>` | Reset a runtime-writable configuration value to `config_sample.py` default |
| `!reload` / `!reloadconfig` | Validate and reload runtime configuration |
| `!restart confirm` | Stop the bot so a supervisor can restart it |
| `!checkupdate` / `!updatecheck` | Check whether a newer release is available |
| `!whoami` | Show your affiliation, role, and permissions |
| `!audit [all/page/last/query]` | Show audit log entries |
| `!backup` | Create a managed full ZIP backup archive |
| `!backup list [all/page/last]` | List managed full backup archives |
| `!backup show <file/latest>` | Inspect one managed backup archive |
| `!backup verify <file/latest>` | Verify a managed backup archive |
| `!backup delete/remove/del/rm <file/latest>` | Delete a managed full backup archive |
| `!restore <file/latest> confirm` | Restore a managed full backup archive |
| `!room add <room>` | Add a protected room |
| `!room remove/delete/del/rm <room>` | Remove a protected room |
| `!room list [all/page]` | List protected rooms with join state and bot affiliation |
| `!room rejoin <room/all>` | Retry joining one or all protected rooms |
| `!room invite list [all/page/last]` | List pending room invites |
| `!room invite accept/decline/remove/delete/del/rm <id>` | Accept or decline a pending room invite |
| `!policy` / `!rules show/set/clear/delete/remove/enable/disable` | Manage public room policy text |
| `!ban <jid/nick/domain> [comment]` | Add a permanent ban or update an existing ban reason |
| `!tempban <jid/nick> <10m/2h/1d> [comment]` | Add or update a temporary ban; omitted comments preserve the old reason |
| `!unban <jid/nick/domain>` | Remove a ban |
| `!redact <jid> [reason]` / `!redact id ...` / `!redact cleanup` | Redact indexed messages or clean old redaction index entries |
| `!protections list [all/page/last]` | List protection enabled/disabled and observe state |
| `!protection enable/disable <name>` | Toggle a protection |
| `!protections <name> config/set` | Show or edit one protection config |
| `!report <nick/jid> [reason]` | Trusted reporter command when enabled |
| `!banlist` / `!blacklist [all/page/last]` | Show active bans |
| `!banlist rtbl` / `!blacklist rtbl [all/page/last]` | Show raw RTBL hash/domain entries |
| `!bansearch <query> [all/page/last]` | Search bans by target, issuer, comment, or RTBL reason |
| `!why <nick/jid>` | Explain why a user is banned |
| `!ignore [list/all/page/last]` | Show the global ignorelist |
| `!ignore add/remove/delete/del/rm <jid/domain>` | Manage protected exact JIDs and domains |
| `!whitelist [list/all/page/last/add/remove/delete/del/rm]` | Alias for `!ignore ...` |
| `!sync` | Rejoin rooms, verify admin rights, and enforce active bans |
| `!syncadmins` | Update admin list from the admin room |
| `!syncbans` | Sync bans from rooms into the database and enforce them |
| `!omemo status` | Inspect OMEMO readiness and storage state |
| `!omemo devices` | List visible admin-room recipients and local storage hints |
| `!omemo reset [confirm]` | Rotate local OMEMO storage after confirmation |
| `!rtbl list [all\|page\|last]`, `!rtbl add/delete/remove/refresh` | Manage RTBL subscriptions |
| `!rtbl publish status/sync` | Manage the bot's own RTBL publish feed |
| `!export [list/show/delete/remove/del/rm]` | Manage CSV ban exports |
| `!import <file> [dryrun]` | Import bans from CSV with validation and optional dry-run |

For paginated commands, the standalone `all` argument disables paging and prints the complete result set. Examples: `!audit all`, `!banlist all`, `!banlist rtbl all`, `!bansearch all spam`, `!ignore list all`, `!whitelist all`, and `!room list all`.

Full command reference: [docs/commands.md](docs/commands.md).


---

## Room Invite Service

When `ROOM_INVITES_ENABLED=True`, BanBot can receive MUC invites for potential protected rooms. Invites are announced in the admin room and must be accepted or declined with `!room invite` commands. Pending invites older than `ROOM_INVITE_MAX_AGE_DAYS` are expired automatically; set it to `0` to keep them indefinitely. BanBot does not auto-join invited rooms.

## Message Redaction

Optional redaction support indexes room-assigned stanza IDs for messages BanBot sees in protected rooms. Message bodies are not stored. Admins can redact all known messages from a bare JID with `!redact <jid> [reason]` or target a specific stanza ID with `!redact id <room_jid> <stanza_id> [reason]`.
If a server applies a moderation request without returning a usable IQ result or live confirmation, BanBot verifies the resulting moderation tombstone through the room's MAM archive before counting the request as successful. Servers without verifiable MAM results remain reported as `Unconfirmed` rather than failed.

See [docs/commands.md](docs/commands.md) and [docs/configuration.md](docs/configuration.md#redaction-settings).

## Protections

BanBot includes an optional protection system for common MUC spam and abuse patterns. Protections can be listed, enabled, disabled, and tuned at runtime from the admin room:

```text
!protections list all
!protection enable flood
!protections flood config
!protections flood set max_messages 10
```

Currently available protections cover flood spam, repeated/similar messages, first-message media spam, excessive mentions, monitored words from new joiners, join waves, trusted reporter workflows, and policy-change notifications.

Use conservative settings first and enable individual protections per need. For active rooms, `notify` or short `tempban` settings are useful while tuning thresholds; stronger actions such as permanent bans should only be enabled after the behavior is verified for your community.

See [docs/protections.md](docs/protections.md) for all protection names, aliases, actions, configuration keys, and operational notes.

## OMEMO

BanBot supports optional OMEMO replies. OMEMO dependencies are not required for normal plaintext operation. If `OMEMO_ENABLED=True` but the optional Python/system libraries are missing, BanBot starts with OMEMO disabled and logs a clear warning.

The behavior is dynamic:

```text
plaintext command  -> plaintext reply
OMEMO command      -> OMEMO reply
```

Encrypted MUC replies are sent to current occupants with visible real JIDs as far as possible. Occupants with unusable OMEMO devices are skipped; plaintext fallback is controlled by configuration. Admins can inspect local OMEMO state with `!omemo status`, show current visible admin-room OMEMO recipients with `!omemo devices`, and rotate the local OMEMO store with `!omemo reset confirm` when the bot identity changed or devices got stale. `!omemo devices` also shows conservative local storage hints, but those hints are diagnostic only and may be stale.

See [docs/omemo.md](docs/omemo.md).

---

## RTBL / PubSub

BanBot can subscribe to RTBL PubSub nodes containing SHA-256 bare-JID hashes and plaintext domain bans. Successful refreshes reconcile local RTBL cache state with the current node snapshot. Removed RTBL items are cleaned up locally, and stale `issuer=rtbl` bans are automatically unbanned.

```text
!rtbl add xmppbl.org muc_bans_sha256
!rtbl add xmppbl.org spam_source_domains
!rtbl refresh
!banlist rtbl all
```

BanBot can also publish local non-RTBL bans to its own RTBL feed.

See [docs/rtbl.md](docs/rtbl.md) and [docs/rtbl_pubsub-setup.md](docs/rtbl_pubsub-setup.md).

---

## Tests and CI

Install dev dependencies and run tests:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pytest
```

Run with coverage:

```bash
pytest --cov=banbot --cov-report=term-missing
```

The test suite also includes Hypothesis-based property tests for pure helper logic such as duration parsing, human-readable time formatting, JID/domain normalization, RTBL utilities, paging helpers, and ban-target normalization.

Drone CI runs the offline pytest suite with coverage on pushes and tags to `main`. Live XMPP/Prosody and OMEMO integration tests are opt-in and skipped by default.

See [docs/testing.md](docs/testing.md) for the full testing workflow and [tests/README.md](tests/README.md) for the test-suite layout.

---

## Documentation

The full documentation is split into focused guides. Start with the [documentation index](docs/README.md), or jump directly to a topic:

* [Architecture](docs/architecture.md)
* [Configuration](docs/configuration.md)
* [Commands](docs/commands.md)
* [Backups and Restore](docs/backups.md)
* [Rooms and Invites](docs/rooms.md)
* [Import / Export](docs/import-export.md)
* [OMEMO](docs/omemo.md)
* [RTBL / PubSub](docs/rtbl.md)
* [Prosody PubSub Setup](docs/rtbl_pubsub-setup.md)
* [Public Policy / Rules](docs/policy.md)
* [Protections](docs/protections.md)
* [Admin Protection](docs/admin-protection.md)
* [Database](docs/database.md)
* [Testing and CI](docs/testing.md)
* [Release Checklist](docs/release-checklist.md)
* [Troubleshooting](docs/troubleshooting.md)

---

## Security Notes

* The bot account must have admin/owner rights in every protected room.
* The admin room is the single source of truth for command permissions.
* Admins/owners are protected from manual, nick-based, domain-based, and RTBL-applied bans.
* Domain bans such as `*.domain.tld` reject overly generic targets such as `*.com`.
* CSV imports create a managed full backup before writing data, but dry-runs do not create backups or change the database.
* The ignorelist protects exact JIDs from all bans and domains from domain-based/RTBL domain matches.
* If RTBL publishing is enabled, ensure the configured PubSub nodes are not writable by arbitrary users.


## Moderation inspection and editing

- `!baninfo <target>` shows the complete current ban metadata.
- `!history <target> [all|page|last]` shows the audit-backed moderation history.
- `!banedit <target> reason <text>` changes a reason.
- `!banedit <target> duration <duration>` resets a tempban from now.
- `!banedit <target> extend|reduce <duration>` adjusts a tempban.
- `!banedit <target> permanent` converts a tempban to a permanent ban.
- `!banedit <target> temp <duration>` converts or resets a ban to a tempban.
- `!banedit <nick> jid <user@domain.tld>` converts a nick-only ban into a JID ban.

Protection observe mode can be enabled with `!protections <name> observe on`. Matching events are announced and audited, but no kick, ban, redaction, warning, or lockdown is applied. Disable it with `observe off` or `enforce`. `!protections list` marks action-capable protections with `[observe]` and the notification-only `PolicyChangeNotification` with `[notify-only]`; attempting to enable observe mode for a notification-only protection is rejected.
