docs: add IRC bot plugins design and implementation plan
Made-with: Cursor
This commit is contained in:
157
docs/plans/2026-03-12-irc-plugins-design.md
Normal file
157
docs/plans/2026-03-12-irc-plugins-design.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# NtR IRC Bot Plugins — Design Document
|
||||
|
||||
> **Date**: 2026-03-12
|
||||
> **Status**: Approved
|
||||
|
||||
## Purpose
|
||||
|
||||
Two functionally-identical IRC bot plugins (Sopel and Limnoria) that query the NtR SoundCloud Fetcher API to serve playlist and track information in IRC channels.
|
||||
|
||||
## Approach
|
||||
|
||||
Fully independent plugins with no shared code. Each plugin is a self-contained unit you drop into the bot's plugin directory. Both follow identical internal structure and function naming so a diff between them shows only framework-specific glue.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
plugins/
|
||||
├── sopel/
|
||||
│ └── ntr_playlist.py # Single-file Sopel plugin
|
||||
└── limnoria/
|
||||
└── NtrPlaylist/ # Limnoria package plugin
|
||||
├── __init__.py # Plugin metadata
|
||||
├── config.py # Registry values
|
||||
├── plugin.py # Command handlers
|
||||
└── test.py # Test stub
|
||||
```
|
||||
|
||||
Sopel: copy `ntr_playlist.py` into `~/.sopel/plugins/`.
|
||||
Limnoria: copy `NtrPlaylist/` into `limnoria-bot/plugins/`.
|
||||
|
||||
## Commands
|
||||
|
||||
| Trigger | API Call | Auth | Output |
|
||||
|---------|----------|------|--------|
|
||||
| `!1`, `!2`, ... `!N` | `GET /playlist/{n}` | None | Single track |
|
||||
| `!song <episode> <position>` | `GET /shows/by-episode/{episode}` → filter by position | None | Single track |
|
||||
| `!refresh` | `POST /admin/refresh` (bearer token) | Admin nicks | Refresh result |
|
||||
| `!status` | `GET /health` | None | Full health info |
|
||||
| `!playlist` | `GET /playlist` | None | Comma-separated track list |
|
||||
| `!playlist <episode>` | `GET /shows/by-episode/{episode}` | None | Comma-separated track list |
|
||||
|
||||
### Number commands (`!1`, `!2`, etc.)
|
||||
|
||||
Neither framework natively registers bare-digit commands. Both plugins use a regex/rule pattern to match messages starting with `!` followed by one or more digits and nothing else.
|
||||
|
||||
- Sopel: `@plugin.rule(r'^!(\d+)$')`
|
||||
- Limnoria: `doPrivmsg` override checking against `r'^!(\d+)$'`
|
||||
|
||||
### Output Formats
|
||||
|
||||
**Single track** (`!N`, `!song`):
|
||||
```
|
||||
Song #3: Night Drive by SomeArtist - https://soundcloud.com/someartist/night-drive
|
||||
```
|
||||
|
||||
**Playlist** (`!playlist`):
|
||||
```
|
||||
Episode 530 (9 tracks): Night Drive by SomeArtist, Running Through My Mind by Purrple Panther, ...
|
||||
```
|
||||
|
||||
**Status** (`!status`):
|
||||
```
|
||||
Status: OK | Poller: alive | Last fetch: 2026-03-12T02:00:00+00:00 | Tracks this week: 9
|
||||
```
|
||||
|
||||
**Refresh** (`!refresh`):
|
||||
```
|
||||
Refreshed — 9 tracks
|
||||
```
|
||||
|
||||
**Errors** (pass through API detail):
|
||||
```
|
||||
No track at position 15
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Both plugins expose the same three settings:
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `api_base_url` | string | `http://127.0.0.1:8000` | NtR API base URL (no trailing slash) |
|
||||
| `admin_token` | string | *(empty)* | Bearer token for `POST /admin/refresh` |
|
||||
| `admin_nicks` | list of strings | *(empty)* | IRC nicknames allowed to run `!refresh` |
|
||||
|
||||
### Sopel (INI)
|
||||
|
||||
```ini
|
||||
[ntr_playlist]
|
||||
api_base_url = http://127.0.0.1:8000
|
||||
admin_token = secret-token-here
|
||||
admin_nicks =
|
||||
NicktheRat
|
||||
SomeOtherAdmin
|
||||
```
|
||||
|
||||
Defined via `StaticSection` with `ValidatedAttribute` and `ListAttribute`.
|
||||
|
||||
### Limnoria (registry)
|
||||
|
||||
```
|
||||
config plugins.NtrPlaylist.apiBaseUrl http://127.0.0.1:8000
|
||||
config plugins.NtrPlaylist.adminToken secret-token-here
|
||||
config plugins.NtrPlaylist.adminNicks NicktheRat SomeOtherAdmin
|
||||
```
|
||||
|
||||
`adminToken` marked `private=True`. `adminNicks` uses `SpaceSeparatedListOfStrings`.
|
||||
|
||||
### Admin Check
|
||||
|
||||
Compare the triggering user's IRC nickname against `admin_nicks`, case-insensitive. Framework-level admin/owner systems are not used. This keeps admin management identical and portable between plugins.
|
||||
|
||||
## HTTP Client
|
||||
|
||||
Both plugins use `urllib.request` from stdlib — no external dependencies beyond the bot framework.
|
||||
|
||||
### Internal Helpers
|
||||
|
||||
**`_api_get(base_url, path)`** → `dict`
|
||||
- `GET {base_url}{path}` with `Accept: application/json`
|
||||
- 10-second timeout
|
||||
- On success: parse and return JSON
|
||||
- On HTTP error: parse JSON error body, raise `ApiError(status_code, detail)`
|
||||
- On connection failure: raise `ApiError(0, "Cannot reach API")`
|
||||
|
||||
**`_api_post(base_url, path, token, body=None)`** → `dict`
|
||||
- Same as above but `POST` with `Authorization: Bearer {token}` and `Content-Type: application/json`
|
||||
- Used only by `!refresh`
|
||||
|
||||
### Error Handling
|
||||
|
||||
```
|
||||
try:
|
||||
data = _api_get(base_url, f"/playlist/{position}")
|
||||
reply with formatted track
|
||||
except ApiError as e:
|
||||
reply with e.detail
|
||||
```
|
||||
|
||||
No retries at the plugin level. The API is local; IRC users expect immediate responses. If the API is down, the user sees "Cannot reach API".
|
||||
|
||||
No async concerns — both frameworks run command handlers synchronously. A blocking 10-second timeout is acceptable.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Independent plugins, no shared code | Each plugin is one self-contained directory. No deployment headaches with shared imports. |
|
||||
| Regex for `!N` commands | Frameworks don't support bare-digit command names. Regex catch-all is the cleanest solution. |
|
||||
| Plugin-config admin nicks only | Portable between frameworks. No coupling to Sopel's `core.admins` or Limnoria's capability system. |
|
||||
| No caching | API is local, responses are fast. Simplifies code and guarantees fresh data. |
|
||||
| stdlib `urllib.request` | Zero external dependencies. Both frameworks already have Python 3.11+. |
|
||||
| `!song` fetches full show, filters client-side | API has no single-track-by-episode endpoint. The response is small enough that client-side filtering is fine. |
|
||||
|
||||
## Dependencies
|
||||
|
||||
None beyond the bot framework itself and Python 3.11+ stdlib.
|
||||
Reference in New Issue
Block a user