feat: initial implementation

Asyncio IRC bot with decorator-based plugin system.
Zero external dependencies, Python 3.11+.

- IRC protocol: message parsing, formatting, async TCP/TLS connection
- Plugin system: @command and @event decorators, file-based loading
- Bot orchestrator: connect, dispatch, reconnect, nick recovery
- CLI: argparse entry point with TOML config
- Built-in plugins: ping, help, version, echo
- 28 unit tests for parser and plugin system
This commit is contained in:
user
2026-02-15 00:37:31 +01:00
commit bf45abcbad
23 changed files with 1398 additions and 0 deletions

59
docs/CHEATSHEET.md Normal file
View File

@@ -0,0 +1,59 @@
# Cheatsheet
## Quick Commands
```bash
make install # Setup venv + install
make test # Run tests
make lint # Lint with ruff
make run # Start bot
make link # Symlink to ~/.local/bin
derp -c config.toml # Run with custom config
derp -v # Verbose/debug mode
```
## Bot Commands
```
!ping # Pong
!help # List commands
!help <cmd> # Command help
!version # Bot version
!echo <text> # Echo text back
```
## Plugin Template
```python
from derp.plugin import command, event
@command("name", help="Description")
async def cmd_name(bot, message):
text = message.text.split(None, 1)
await bot.reply(message, "response")
@event("JOIN")
async def on_join(bot, message):
await bot.send(message.target, f"Hi {message.nick}")
```
## Message Object
```
msg.nick # Sender nick
msg.target # Channel or nick
msg.text # Message body
msg.is_channel # True if channel
msg.prefix # nick!user@host
msg.command # PRIVMSG, JOIN, etc.
msg.params # All params list
```
## Config Locations
```
1. --config PATH # CLI flag
2. ./config/derp.toml # Project dir
3. ~/.config/derp/derp.toml # User config
4. Built-in defaults # Fallback
```

74
docs/DEBUG.md Normal file
View File

@@ -0,0 +1,74 @@
# Debugging
## Verbose Mode
```bash
derp --verbose
```
Shows all IRC traffic:
```
>>> NICK derp
>>> USER derp 0 * :derp IRC bot
<<< :server 001 derp :Welcome
>>> JOIN #test
```
## Log Levels
Set in `config/derp.toml`:
```toml
[logging]
level = "debug" # debug, info, warning, error
```
## Common Issues
### Connection refused
```
ERROR derp.irc connection lost: [Errno 111] Connection refused
```
- Check `host` and `port` in config
- Verify TLS setting matches port (6697 = TLS, 6667 = plain)
- Test connectivity: `nc -zv <host> <port>`
### Nickname in use
The bot appends `_` to the nick and retries automatically. Check logs for:
```
<<< :server 433 * derp :Nickname is already in use
>>> NICK derp_
```
### TLS certificate errors
If the server uses a self-signed certificate, you may need to adjust the SSL context. Currently uses system default CA bundle.
### Plugin load failures
```
ERROR derp.plugin failed to load plugin: plugins/broken.py
```
- Check plugin file for syntax errors: `python -c "import plugins.broken"`
- Ensure handlers are `async def`
- Check imports (`from derp.plugin import command, event`)
### No response to commands
- Verify `prefix` in config matches what you type
- Check that the plugin is loaded (look for "loaded plugin" in verbose output)
- Ensure the bot has joined the channel
## Testing IRC Connection
```bash
# Test raw IRC (without the bot)
openssl s_client -connect irc.libera.chat:6697
# Then type: NICK testbot / USER testbot 0 * :test
```

52
docs/INSTALL.md Normal file
View File

@@ -0,0 +1,52 @@
# Installation
## Prerequisites
- Python 3.11+
- git
## Setup
```bash
cd ~/git/derp
make install
```
This creates a `.venv`, installs derp in editable mode, and adds dev tools.
## Symlink
```bash
make link
```
Installs `derp` to `~/.local/bin/`. Verify:
```bash
which derp
derp --version
```
## Manual Install
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
## Configuration
Copy and edit the default config:
```bash
cp config/derp.toml ~/.config/derp/derp.toml
# Edit server, nick, channels
```
Config search order:
1. Path given via `--config`
2. `./config/derp.toml`
3. `~/.config/derp/derp.toml`
4. Built-in defaults

97
docs/USAGE.md Normal file
View File

@@ -0,0 +1,97 @@
# Usage Guide
## Running
```bash
# From project directory
derp
# With options
derp --config /path/to/derp.toml --verbose
```
## CLI Flags
| Flag | Description |
|------|-------------|
| `-c, --config PATH` | Config file path |
| `-v, --verbose` | Debug logging |
| `-V, --version` | Print version |
| `-h, --help` | Show help |
## Configuration
All settings in `config/derp.toml`:
```toml
[server]
host = "irc.libera.chat" # IRC server hostname
port = 6697 # Port (6697 = TLS, 6667 = plain)
tls = true # Enable TLS encryption
nick = "derp" # Bot nickname
user = "derp" # Username (ident)
realname = "derp IRC bot" # Real name field
password = "" # Server password (optional)
[bot]
prefix = "!" # Command prefix character
channels = ["#test"] # Channels to join on connect
plugins_dir = "plugins" # Plugin directory path
[logging]
level = "info" # Logging level: debug, info, warning, error
```
## Built-in Commands
| Command | Description |
|---------|-------------|
| `!ping` | Bot responds with "pong" |
| `!help` | List all available commands |
| `!help <cmd>` | Show help for a specific command |
| `!version` | Show bot version |
| `!echo <text>` | Echo back text (example plugin) |
## Writing Plugins
Create a `.py` file in the `plugins/` directory:
```python
from derp.plugin import command, event
@command("hello", help="Greet the user")
async def cmd_hello(bot, message):
"""Handler receives bot instance and parsed Message."""
await bot.reply(message, f"Hello, {message.nick}!")
@event("JOIN")
async def on_join(bot, message):
"""Event handlers fire on IRC events (JOIN, PART, QUIT, etc.)."""
if message.nick != bot.nick:
await bot.send(message.target, f"Welcome, {message.nick}")
```
### Plugin API
The `bot` object provides:
| Method | Description |
|--------|-------------|
| `bot.send(target, text)` | Send message to channel or nick |
| `bot.reply(msg, text)` | Reply to source (channel or PM) |
| `bot.action(target, text)` | Send `/me` action |
| `bot.join(channel)` | Join a channel |
| `bot.part(channel [, reason])` | Leave a channel |
| `bot.quit([reason])` | Disconnect from server |
The `message` object provides:
| Attribute | Description |
|-----------|-------------|
| `message.nick` | Sender's nickname |
| `message.prefix` | Full `nick!user@host` prefix |
| `message.command` | IRC command (PRIVMSG, JOIN, etc.) |
| `message.target` | First param (channel or nick) |
| `message.text` | Trailing text content |
| `message.is_channel` | Whether target is a channel |
| `message.params` | All message parameters |