Metadata-Version: 2.3
Name: edwh-restic-discord
Version: 0.1.1
Summary: Discord webhook notifications for edwh-restic-plugin
Keywords: restic,backup,discord,notifications,edwh
Author: Robin van der Noord, Remco Boerma
Author-email: Robin van der Noord <robin.vdn@educationwarehouse.nl>, Remco Boerma <remco.b@educationwarehouse.nl>
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Dist: edwh-restic-plugin>=1.0.0
Requires-Dist: requests
Requires-Dist: termcolor
Requires-Dist: pytest ; extra == 'dev'
Requires-Dist: coverage[toml]>=6.5 ; extra == 'dev'
Requires-Dist: ruff ; extra == 'dev'
Requires-Dist: ty ; extra == 'dev'
Requires-Dist: types-requests ; extra == 'dev'
Requires-Python: >=3.12
Project-URL: Documentation, https://github.com/educationwarehouse/edwh-restic-discord#readme
Project-URL: Issues, https://github.com/educationwarehouse/edwh-restic-discord/issues
Project-URL: Source, https://github.com/educationwarehouse/edwh-restic-discord
Provides-Extra: dev
Description-Content-Type: text/markdown

# edwh-restic-discord

[Discord](https://discord.com) webhook notifications for
[edwh-restic-plugin](https://github.com/educationwarehouse/edwh-restic-plugin): a failed backup
reaches your Discord server instead of dying in a cron log.

Core emits an event per restic operation and ships no notifiers itself. This package is one: it
registers a channel called `discord`, and does nothing at all until `[restic.notify] channels` names
it.

## Installation

```console
(uv) pip install edwh-restic-discord
```

Installed beside `edwh-restic-plugin`, it is discovered through its entry point; no import or module
list is needed.

## Quick start

`.env` (gitignored, secrets only):

```dotenv
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/token_value
```

`.toml` (or `default.toml`, the committed template):

```toml
[restic.notify]
channels = ["discord"]
```

Then check it works without waiting for a real failure:

```console
edwh restic.notify-test
edwh restic.notify-test --all-events
```

**A webhook url is what activates the channel**: named in `channels` but without one, it is skipped
with a note and your backups run on.

## Routing: one webhook, or one per anything

The `.env` url above is the default for every event. The general form is a table whose keys say
*which events* go where:

```toml
[restic.notify.discord]

[restic.notify.discord.urls]
"*" = "https://discord.com/api/webhooks/1/all"          # everything
"backup" = "https://discord.com/api/webhooks/2/backups" # one operation
"warning" = "https://discord.com/api/webhooks/3/noisy"  # one level
"failed" = "https://discord.com/api/webhooks/4/oncall"  # one phase
"check.failed" = "https://discord.com/api/webhooks/5/damage"
```

Five kinds of key, freely mixed:

| Key                              | Means                        | Matches                              |
|----------------------------------|------------------------------|--------------------------------------|
| `"*"`                            | everything                   | any event                            |
| `"backup"`, `"backup.*"`         | one operation                | `backup.started`, `backup.failed`, … |
| `"failed"`, `"*.failed"`         | one phase, across operations | `backup.failed`, `check.failed`, …   |
| `"error"`, `"warning"`, `"info"` | one level                    | whatever core gives that level       |
| `"check.failed"`                 | one exact event              | only that one                         |

Operations are `backup`, `restore`, `check`, `forget`, `wipe`; phases are `started`, `succeeded`,
`failed`, `slow`.

The most specific key wins, and exactly one webhook set is chosen per event. A list value is how you
deliberately fan out:

```toml
[restic.notify.discord.urls]
"failed" = [
    "https://discord.com/api/webhooks/4/oncall",
    "https://discord.com/api/webhooks/6/audit",
]
```

`webhook` and `webhooks` are accepted aliases for `url` and `urls`. Values must be complete
`https://` webhook urls; there is no Discord equivalent of a bare-topic plus server form.

## Mentions by friendly name

Discord pings by numeric id, so `[restic.notify.discord.users]` and
`[restic.notify.discord.roles]` map names you choose to the ids from Discord:

```toml
[restic.notify.discord]
webhook = "https://discord.com/api/webhooks/1/all"

[restic.notify.discord.roles]
oncall = "123456789012345678"

[restic.notify.discord.users]
robin = "234567890123456789"

[restic.notify.discord.mentions]
"failed" = ["oncall"]
"backup.failed" = ["oncall", "robin"]
```

The mention table uses the same keys and precedence as webhooks. Values can be mapped names, or raw
Discord mentions such as `<@id>`, `<@&id>`, `@everyone` and `@here`. A matched route adds one short
`content` line to the message and allows those mention types to ping.

## What a notification looks like

Each event becomes a Discord embed:

- Title: `⏳ backup.failed - acme-prod@db-01` (phase picks the emoji)
- Description: a one-line headline, plus restic logs on failures
- Fields: repository, host, project, target, failed scripts, and other event details
- Color: info is blue, warning is orange, error is red

## All options

Under `[restic.notify.discord]`:

| Key                              | Default | Meaning                                              |
|----------------------------------|---------|------------------------------------------------------|
| `url`, `webhook`                 | —       | shorthand for the `"*"` route                        |
| `urls`, `webhooks`               | —       | the route table (or a bare value, same as `url`)     |
| `logs`                           | `true`  | include restic's stdout/stderr on failures           |
| `timeout`                        | `4.0`   | seconds per request                                  |
| `max_chars`                      | `3900`  | character budget for the embed description           |
| `username`                       | —       | webhook display-name override                        |
| `avatar_url`                     | —       | webhook avatar override                              |
| `users`                          | —       | map of friendly name to Discord user id              |
| `roles`                          | —       | map of friendly name to Discord role id              |
| `mention`, `mentions`            | —       | mention route table                                  |

Environment:

| Key                     | Meaning                                          |
|-------------------------|--------------------------------------------------|
| `DISCORD_WEBHOOK_URL`   | default webhook when TOML has no url route       |

`events` and `min_level` are read by **core**, not by this plugin, and still apply:

```toml
[restic.notify.discord]
webhook = "https://discord.com/api/webhooks/1/all"
min_level = "warning"      # core drops info events before routing ever sees them
```

You rarely need `events`: this plugin narrows core's subscription to what its own table can deliver,
so `restic.notify-test` reports an event with no matching route as filtered rather than as a silent
success. An event that reaches us with no route sends nothing and says so once per event name.

### A worked setup

```toml
[restic.notify]
channels = ["discord"]
project = "acme-prod-db01"

[restic.notify.discord]
logs = true
username = "restic"

[restic.notify.discord.urls]
"*" = "https://discord.com/api/webhooks/1/timeline"
"failed" = "https://discord.com/api/webhooks/2/oncall"
"check.failed" = "https://discord.com/api/webhooks/3/urgent"
"backup.slow" = "https://discord.com/api/webhooks/1/timeline"

[restic.notify.discord.roles]
oncall = "123456789012345678"

[restic.notify.discord.mentions]
"check.failed" = ["oncall"]
```

## Security note

A notifier runs in-process and is trusted like any other dependency. Events carry an allowlist of
fields and never the repository URI, but `logs` on a failure event is restic's full stdout/stderr,
which can include repository paths and hostnames. A Discord webhook url is also a secret: anyone who
has it can post to that channel. Keep it in `.env`, point failure routes only at channels you trust,
and set `logs = false` when the full terminal output should stay off Discord.

## Development

```console
uv pip install -e .[dev]
edwh test.run
edwh plugin.release # to publish a new version with vommit
```

## License

`edwh-restic-discord` is distributed under the terms of the
[MIT](https://spdx.org/licenses/MIT.html) license.
