Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [recycler] -- 2026-06-02

### Changes
- **Least-privilege invite everywhere.** The leftover `.help` "Add to server"
button (`cogs/_help_view.py`) still built an Administrator (`permissions=8`)
invite; it now uses the single source of truth in `clanklib/permissions.py`
like `.invite`/`.about`/`.setup`, so every invite asks only for the
permissions the bot uses (`permissions=805391380`). The manifest now declares
this set under `discord` in `auren.json` so the Auren platform's Invite
button matches, and a new test pins the manifest value to
`required_bot_permissions()` so they can never drift. Deployment docs updated.

## [recycler] -- 2026-06-01

### Changes
Expand Down
4 changes: 4 additions & 0 deletions auren.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"repo": "hilleywyn/recycler"
},
"channels": ["discord"],
"discord": {
"permissions": "805391380",
"scopes": ["bot", "applications.commands"]
},
"runtime": {
"dockerfile": "Dockerfile",
"entrypoint": "main.py"
Expand Down
8 changes: 4 additions & 4 deletions cogs/_help_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@


def _invite_url(bot: Any) -> str:
# Single source of truth: clanklib.permissions builds an invite that asks
# for exactly the permissions the bot uses -- never Administrator.
from clanklib.permissions import invite_url
from clanklib.settings import setting
cid = getattr(bot.user, "id", None) or setting(bot, "DISCORD_CLIENT_ID", "")
return (
f"https://discord.com/oauth2/authorize?client_id={cid}"
"&permissions=8&scope=bot%20applications.commands"
)
return invite_url(cid)


def _build_panel(bot: Any, prefix: str, chosen_keys: list[str], author_id: int):
Expand Down
20 changes: 14 additions & 6 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,25 @@ install pulls that package from git.

### 1.2 Inviting the bot

Recycler creates and deletes roles, channels and webhooks (that is the whole
job), so it needs the **Administrator** permission. Build an invite URL:
Recycler asks for only the permissions it actually uses -- **never
Administrator**. The set is defined once in `clanklib/permissions.py` and drives
the invite link, the `.setup` audit, and the Auren platform's Invite button:

- View Channels, Send Messages, Embed Links, Read Message History (core)
- Manage Channels, Manage Roles (recreate channels/roles on backup/template restore)
- Manage Webhooks (replay archived messages for chatlog/sync)
- Ban Members (propagate bans between synced guilds)

Build an invite URL (the `permissions` value is the union of the above):

```
https://discord.com/oauth2/authorize?client_id=<CLIENT_ID>&permissions=8&scope=bot%20applications.commands
https://discord.com/oauth2/authorize?client_id=<CLIENT_ID>&permissions=805391380&scope=bot%20applications.commands
```

Once the bot is running you can also just type `.invite` (or `.about`) and it
prints this URL for you. Administrator (`permissions=8`) is required for
backup/template restores; without it, restores will partially fail with
permission errors.
prints this exact URL for you, or `.setup` to audit what is missing. Make sure
the bot's role sits **above** the roles/channels it manages, or restores will
partially fail with permission errors.

### 1.3 A PostgreSQL database

Expand Down
15 changes: 15 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,18 @@ def __init__(self, gp):

def test_pretty_perm_label():
assert perms.pretty_perm("manage_roles") == "Manage Roles"


def test_manifest_permissions_match_required():
"""auren.json's declared invite permissions (which the Auren platform's
Invite button reads) must equal the code's required set, so the two can
never drift apart."""
import json
from pathlib import Path

manifest = json.loads(
(Path(__file__).resolve().parent.parent / "auren.json").read_text()
)
declared = int(manifest["discord"]["permissions"])
assert declared == perms.required_bot_permissions().value
assert declared != 8 # never Administrator
Loading