aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /docs
init: mostly vibed
Diffstat (limited to 'docs')
-rw-r--r--docs/alfred.md132
-rw-r--r--docs/contessa.md59
-rw-r--r--docs/daffy.md55
-rw-r--r--docs/daft.md39
-rw-r--r--docs/herald.md63
-rw-r--r--docs/jeeves.md122
-rw-r--r--docs/osterman.md134
-rw-r--r--docs/salvador.md30
-rw-r--r--docs/shireen.md88
9 files changed, 722 insertions, 0 deletions
diff --git a/docs/alfred.md b/docs/alfred.md
new file mode 100644
index 0000000..c2e6a7d
--- /dev/null
+++ b/docs/alfred.md
@@ -0,0 +1,132 @@
+# alfred
+
+Server management bot. All commands are owner-only and work in channel or PM.
+
+## Dependencies
+
+```
+pip install requests
+```
+
+## Help system
+
+```
+!help list topics
+!help <topic> list commands for a topic
+!help <topic> <cmd> description and usage for a command
+```
+
+All help responses are sent as IRC NOTICE to the requester, not said publicly in channel.
+
+Running any command with no arguments shows the same output as `!help <topic>`.
+
+## Commands
+
+### Bot management
+
+```
+!bot list show all managed bots and their running state
+!bot all start|stop|restart control all bots at once
+!bot <name> start|stop|restart control a bot
+!bot <name> status check if a bot is running
+```
+
+Bots are discovered automatically from `~/.config/sopel/*.cfg`.
+
+### Server stats
+
+```
+!srv uptime uptime and load averages
+!srv disk disk usage for / [raw df output - needs formatting]
+!srv mem RAM and swap usage [raw free output - needs formatting]
+!srv load load averages and process count
+!srv status [service] rc-service status, all or one
+!srv net [iface] RX TX bytes for an interface
+!srv top [n] top n processes by CPU (default 5)
+!srv topmem [n] top n processes by memory (default 5)
+!srv io disk read write stats
+!srv conns TCP connection summary [raw ss -s output - needs formatting]
+!srv who logged-in users [raw who output - needs formatting]
+!srv logs <service> [n] last n log lines for a service (default 10)
+```
+
+`!server` is an alias for `!srv`.
+
+### Soju bouncer
+
+```
+!irc user list list all bouncer users [raw sojuctl output - needs formatting]
+!irc user add <nick> <pass> create a new user [raw sojuctl output - needs formatting]
+!irc user remove <nick> delete a user [raw sojuctl output - needs formatting]
+!irc user passwd <nick> <pass> change a user's password [raw sojuctl output - needs formatting]
+!irc user enable <nick> re-enable a disabled user [raw sojuctl output - needs formatting]
+!irc user disable <nick> disable a user [raw sojuctl output - needs formatting]
+!irc net list <user> list networks for a user [raw sojuctl output - needs formatting]
+!irc net status <user> show network connection status [raw sojuctl output - needs formatting]
+!irc net add <user> <name> [addr] add a network (preset or custom) [raw sojuctl output - needs formatting]
+!irc net remove <user> <name> remove a network [raw sojuctl output - needs formatting]
+!irc net quote <user> <net> <cmd> send a raw IRC command to a network [raw sojuctl output - needs formatting]
+!irc net presets show available network presets
+!irc bouncer bouncer-wide stats [raw sojuctl output - needs formatting]
+!irc notice <message> broadcast a notice to all users [raw sojuctl output - needs formatting]
+```
+
+### File hosting (0x0)
+
+```
+!files stats file count and total size
+!files list [n] last n uploads (default 5)
+!files shorten <url> [wkfo|gumx] shorten a URL
+!files mirror <url> [wkfo|gumx] mirror a remote URL
+!files prune delete expired files from disk [raw flask output - needs formatting]
+!files vscan run ClamAV scan on stored files [raw flask output - needs formatting]
+!files remove <filename> permanently remove a file
+```
+
+### Coffee tracker
+
+```
+!coffee stats today count, week bar graph, streak, all-time average
+!coffee +<n> add n cups to today
+!coffee -<n> remove n cups from today
+!coffee <n> set today's count to n
+!coffee +<n> yyyy-mm-dd add n cups to a past date
+!coffee <n> yyyy-mm-dd set a past date to n cups
+!coffee yes confirm a pending action
+!coffee no cancel a pending action
+```
+
+### WireGuard VPN
+
+```
+!vpn pubkey print server public key
+!vpn list list peers: name, IP, pubkey (first 24 chars)
+!vpn add <name> <pubkey> add peer, returns assigned IP and client config URL
+!vpn remove <name> remove peer live and from wg0.conf
+```
+
+`!vpn add` assigns the next free `10.0.0.x`, calls `wg set wg0 peer` live (no restart needed), rewrites `wg0.conf`, uploads a ready-to-use client `.conf` to wk.fo, and replies with the IP and URL.
+
+### Mailing list
+
+```
+!list members show all subscribers
+!list subscribe <email> subscribe an address
+!list unsubscribe <email> unsubscribe an address
+!list send <message> send a message to the list
+```
+
+Manages the mlmmj list at `list@gumx.cc`. Web archive at `mail.gumx.cc/list/archive/`.
+
+## Config keys
+
+| Key | Description |
+|-----|-------------|
+| `[coffee] data_path` | Path to coffee data JSON |
+| `[coffee] timezone` | Timezone for date calculations (default UTC) |
+| `[soju] config_path` | Path to soju config file |
+| `[files] url` | 0x0 API URL |
+| `[files] default_domain` | Default short domain (wkfo or gumx) |
+| `[files] db_path` | Path to 0x0 SQLite database |
+| `[files] data_path` | Path to 0x0 file storage directory |
+| `[files] app_path` | Path to 0x0 Flask app directory |
diff --git a/docs/contessa.md b/docs/contessa.md
new file mode 100644
index 0000000..b1d2e27
--- /dev/null
+++ b/docs/contessa.md
@@ -0,0 +1,59 @@
+# contessa
+
+Recipe suggestion bot. Suggests random recipes by meal type, lists available
+dishes per category, and gives full step-by-step recipes on request.
+
+## Dependencies
+
+No external libraries required.
+
+## Commands
+
+All commands require channel context (`@plugin.require_chanmsg`).
+
+| Command | Description |
+|---------|-------------|
+| `!breakfast [list]` | Random breakfast suggestion, or list all breakfast dishes |
+| `!lunch [list]` | Random lunch suggestion, or list all lunch dishes |
+| `!dinner [list]` | Random dinner suggestion, or list all dinner dishes |
+| `!snack [list]` | Random snack suggestion, or list all snacks |
+| `!recipe <name>` | Full recipe: description, ingredients, steps (3 lines) |
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
+
+## Recipe data
+
+Recipes are loaded from a JSON file at startup. Format:
+
+```json
+{
+ "breakfast": [
+ {
+ "name": "Dish Name",
+ "description": "One-line description.",
+ "ingredients": ["item 1", "item 2"],
+ "steps": ["Step one.", "Step two.", "Step three."]
+ }
+ ],
+ "lunch": [...],
+ "dinner": [...],
+ "snack": [...]
+}
+```
+
+The included `recipes.json` has 24 recipes per category (96 total), covering
+both Egyptian/Middle Eastern and Western/European dishes.
+
+`!recipe` matches by exact name first, then by substring across all categories.
+
+## Config keys
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `[contessa] recipes_path` | `/var/contessa/recipes.json` | Path to recipes JSON file |
diff --git a/docs/daffy.md b/docs/daffy.md
new file mode 100644
index 0000000..d9a8311
--- /dev/null
+++ b/docs/daffy.md
@@ -0,0 +1,55 @@
+# daffy
+
+Duck hunting game bot. Ducks appear at random intervals; players race to shoot
+them. Scores are stored in SQLite. The hunt auto-stops after a configurable
+number of rounds with no participants.
+
+## Dependencies
+
+No external libraries required.
+
+## Commands
+
+### Game (anyone in channel)
+| Command | Description |
+|---------|-------------|
+| `!bang` | Fire at the current duck |
+| `!reload` | Refill ammo (3-second cooldown) |
+| `!score` | Top 5 players for this channel |
+| `!score clear` | Clear scores for this channel (owner/op) |
+
+Owner can also use `!score clear all` or `!score clear #channel` from PM.
+
+### Hunt control (owner / op)
+| Command | Description |
+|---------|-------------|
+| `!hunt` | Show hunt state (on/off/active, idle counter) |
+| `!hunt start` | Start the hunt |
+| `!hunt stop` | Stop the hunt |
+| `!hunt idle <N>` | Set empty-round auto-stop threshold (0 = never); takes effect immediately if threshold is already met |
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
+
+## How it works
+
+- A duck spawns every `spawn_min` to `spawn_max` seconds (random, default 1-5 minutes).
+- First player to `!bang` scores a point. Each player has one shell; use `!reload` to reload.
+- `!bang` and `!reload` are ignored while the hunt is off.
+- If no one shoots within `flee_time` seconds (default 45), the duck flees and the round is marked empty.
+- After `idle_rounds` consecutive empty rounds the hunt auto-stops.
+
+## Config keys
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `[daffy] db_path` | `~/daffy_scores.db` | Path to SQLite scores database |
+| `[daffy] idle_rounds` | 3 | Empty rounds before auto-stop (0 = never) |
+| `[daffy] spawn_min` | 60 | Minimum seconds between duck spawns |
+| `[daffy] spawn_max` | 300 | Maximum seconds between duck spawns |
+| `[daffy] flee_time` | 45 | Seconds before an unshot duck flees |
diff --git a/docs/daft.md b/docs/daft.md
new file mode 100644
index 0000000..f3595ee
--- /dev/null
+++ b/docs/daft.md
@@ -0,0 +1,39 @@
+# daft
+
+YouTube playlist manager. Anyone can queue tracks; ops control playback.
+Tracks are played via an external player (managed outside the bot).
+
+## Dependencies
+
+No external libraries required.
+
+## Commands
+
+### Queue (anyone in channel)
+| Command | Description |
+|---------|-------------|
+| `!dj <youtube-url>` | Add a YouTube video to the queue |
+| `!np` | Show the currently playing track |
+| `!queue` | Show upcoming tracks |
+
+Accepted URL formats: `youtube.com/watch?v=...`, `youtu.be/...`.
+Shorts, embeds, and live streams are rejected.
+Queue cap: 50 tracks.
+
+### Playback (owner / op)
+| Command | Description |
+|---------|-------------|
+| `!play` | Start or resume playback |
+| `!stop` | Pause playback |
+| `!skip` | Skip to the next track |
+| `!remove` | Remove the current track from the queue |
+| `!loop on\|off` | Toggle circular queue looping |
+| `!clear` | Clear queue and stop playback |
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
diff --git a/docs/herald.md b/docs/herald.md
new file mode 100644
index 0000000..d651d4f
--- /dev/null
+++ b/docs/herald.md
@@ -0,0 +1,63 @@
+# herald
+
+Owner-follow bot with a configurable keyword responder.
+
+Herald tracks the owner's IRC presence and mirrors their channel movements.
+It can also respond to keywords in channel messages with random replies.
+
+## Dependencies
+
+No external libraries required.
+
+## Plugins
+
+| File | Role |
+|------|------|
+| `commands.py` | Follow logic and admin commands |
+| `respond.py` | Keyword-based auto-responder |
+
+## Commands (owner only)
+
+| Command | Description |
+|---------|-------------|
+| `!pause` | Pause keyword responses |
+| `!resume` | Resume keyword responses |
+| `!follow on` | Start tracking owner's channels (checks every 30s via WHOIS) |
+| `!follow off` | Stop tracking |
+| `!unfollow` | Immediately leave all follow-joined channels |
+| `!join #channel` | Join a channel manually |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
+
+## Automatic behaviour
+
+- When the owner PARTs a channel, herald leaves too (unless it's a preconfigured channel).
+- When the owner QUITs, herald leaves all non-preconfigured channels.
+- With `!follow on`, herald also joins any channel the owner is in that herald isn't.
+
+Preconfigured channels are the ones listed under `channels` in the config.
+
+## Keyword responder (`respond.py`)
+
+Configure in `[respond]`:
+
+```ini
+[respond]
+keywords =
+ hello
+ hi there
+responses =
+ Hey!
+ What's up?
+```
+
+When any `keyword` appears (case-insensitive) in a channel message, herald
+replies with a randomly chosen response.
+
+## Config keys
+
+| Key | Description |
+|-----|-------------|
+| `[respond] keywords` | Newline-separated list of trigger words |
+| `[respond] responses` | Newline-separated list of possible replies |
diff --git a/docs/jeeves.md b/docs/jeeves.md
new file mode 100644
index 0000000..77bd5e7
--- /dev/null
+++ b/docs/jeeves.md
@@ -0,0 +1,122 @@
+# jeeves
+
+Combined-features IRC bot. Brings together DJ queue management, duck hunting,
+recipes, news and weather, quote bank, image rendering, and keyword responses
+in a single bot with a shared runtime-configurable settings store.
+
+## Dependencies
+
+- Sopel IRC framework
+- Pillow (`pip install Pillow`) - only needed for `!draw`; all other features
+ work without it
+
+## Commands
+
+### DJ (anyone / op for controls)
+| Command | Description |
+|---------|-------------|
+| `!dj <url>` | Queue a YouTube URL (shorts, embeds, live not accepted) |
+| `!np` | Show currently playing track |
+| `!queue` | List up to 4 upcoming tracks |
+| `!loop` | Show loop state |
+| `!play` | Start or resume playback (op) |
+| `!stop` | Pause playback (op) |
+| `!skip` | Skip to next track (op) |
+| `!remove` | Remove current track from queue (op) |
+| `!loop <on\|off>` | Toggle queue looping (op) |
+| `!clear` | Clear queue and stop playback (op) |
+
+### Duck hunting (anyone in channel / op for control)
+| Command | Description |
+|---------|-------------|
+| `!bang` | Fire at the current duck |
+| `!reload` | Refill ammo (3-second cooldown) |
+| `!score` | Top 5 players for this channel |
+| `!score clear` | Clear scores for this channel (op/owner) |
+| `!hunt` | Show hunt state (includes current spawn/flee times) |
+| `!hunt start` | Start the hunt (op/owner) |
+| `!hunt stop` | Stop the hunt (op/owner) |
+
+### Recipes (anyone in channel)
+| Command | Description |
+|---------|-------------|
+| `!breakfast [list]` | Random breakfast suggestion, or list all |
+| `!lunch [list]` | Random lunch suggestion, or list all |
+| `!dinner [list]` | Random dinner suggestion, or list all |
+| `!snack [list]` | Random snack suggestion, or list all |
+| `!recipe <name>` | Full recipe: name, ingredients, steps |
+
+### News and weather (anyone)
+| Command | Description |
+|---------|-------------|
+| `!weather <city>` | Current weather conditions |
+| `!forecast <city>` | 3-day weather forecast |
+| `!headlines [global\|mena\|egypt] [count]` | Latest RSS headlines |
+| `!news` | Show auto-broadcast state |
+| `!news on\|off` | Enable/disable auto-broadcast (op/owner) |
+
+### Quotes (anyone / op for management)
+| Command | Description |
+|---------|-------------|
+| `!quote` | Post a random quote |
+| `!quote add <text>` | Add a quote (op/owner) |
+| `!quote del <N>` | Delete quote by index (op/owner) |
+| `!quote list` | Show total quote count |
+| `!quotes` | Show auto-post state |
+| `!quotes on\|off` | Enable/disable auto-posting (op/owner) |
+
+### Art (anyone in channel)
+| Command | Description |
+|---------|-------------|
+| `!draw [<lines>] <url>` | Render image as mIRC colour art (lines 4-12) |
+
+### Config (owner / op)
+| Command | Description |
+|---------|-------------|
+| `!set <key> <value>` | Set a config value (in channel: per-channel; in PM: global, owner only) |
+| `!config [#channel]` | Show all config values for a channel |
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List joined channels |
+| `!pause` | Pause keyword responses |
+| `!resume` | Resume keyword responses |
+
+### Help
+| Command | Description |
+|---------|-------------|
+| `!help` | Topics overview |
+| `!help <topic>` | Commands in that topic |
+| `!help <cmd>` | Command description |
+
+## Runtime config keys
+
+All keys are set via `!set <key> <value>`. Per-channel keys can be set in a
+channel (affecting that channel only) or in PM (setting the global default).
+Global-only keys must be set in PM by the owner.
+
+| Key | Default | Scope | Description |
+|-----|---------|-------|-------------|
+| `dj_track_duration` | 120 | global | Seconds per track before auto-advance |
+| `dj_queue_cap` | 50 | global | Max tracks in queue |
+| `art_height` | 6 | global | Default lines for `!draw` (4-12) |
+| `duck_spawn_min` | 60 | per-channel | Min seconds between duck spawns |
+| `duck_spawn_max` | 300 | per-channel | Max seconds between duck spawns |
+| `duck_flee_time` | 45 | per-channel | Seconds before duck flees |
+| `duck_idle_rounds` | 3 | per-channel | Empty rounds before auto-stop (0=off) |
+| `news_interval` | 90 | per-channel | Minutes between news broadcasts |
+| `news_count` | 1 | per-channel | Headlines per broadcast cycle |
+| `quotes_interval` | 60 | per-channel | Minutes between quote auto-posts |
+
+## Config file keys
+
+| Key | Description |
+|-----|-------------|
+| `[jeeves] db_path` | Path to main JSON database |
+| `[jeeves] duck_db_path` | Path to duck scores SQLite database |
+| `[jeeves] recipes_path` | Path to recipes JSON file |
+| `[respond] keywords` | Newline-separated trigger words for keyword responses |
+| `[respond] responses` | Newline-separated response lines (random choice) |
diff --git a/docs/osterman.md b/docs/osterman.md
new file mode 100644
index 0000000..2e81629
--- /dev/null
+++ b/docs/osterman.md
@@ -0,0 +1,134 @@
+# osterman
+
+Channel moderation bot. Manages bans, kicks, mutes, auto-modes, content filters,
+and idle tracking.
+
+## Dependencies
+
+No external libraries required.
+
+## Plugins
+
+| File | Role |
+|------|------|
+| `protect.py` | Setup, passive protection: flood, caps, repeat, badword, clone, join flood |
+| `commands.py` | All user-facing moderation and config commands |
+| `tracking.py` | SQLite event log, greeting, `!seen`, `!info`, `!log` |
+| `idle.py` | Idle detection tick, `!afk`, `!idle` |
+| `help.py` | Three-level help system |
+
+## Message routing
+
+| Output type | Delivery |
+|-------------|----------|
+| `!seen`, `!idle <nick>` results | Channel message |
+| `!info`, `!log` results | PM to nick |
+| `!ban list`, `!config`, `!filter list`, `!badword list` | PM to nick |
+| All other command feedback (errors, confirmations) | IRC NOTICE to nick |
+| Greetings, idle warns, bad word warn | IRC NOTICE to nick |
+
+## Help system
+
+```
+!help list topics
+!help <topic> list commands for a topic
+!help <topic> <cmd> description and usage for a command
+!help <cmd> description and usage (topic not required)
+```
+
+Topics: `mod`, `config`, `track`, `idle`, `admin`
+
+## Commands
+
+### Moderation (owner / ACL / op)
+
+| Command | Short | Description |
+|---------|-------|-------------|
+| `!kick <nick> [reason]` | `!k` | Kick |
+| `!ban <nick\|mask> [reason]` | `!b` | Ban + kick |
+| `!ban list` | `!b list` | View channel ban list (PM) |
+| `!bankick <nick\|mask> [reason]` | `!bk` | Ban + kick (explicit) |
+| `!unban <nick\|mask>` | `!ub` | Remove ban |
+| `!tempban <nick\|mask> <min> [reason]` | `!tb` | Timed ban (auto-lifts) |
+| `!mute <nick\|mask>` | `!mu` | Quiet (+q) |
+| `!unmute <nick\|mask>` | `!umu` | Unquiet (-q) |
+| `!voice <nick>` | `!v` | Give voice (+v) |
+| `!devoice <nick>` | `!dv` | Remove voice |
+| `!op <nick>` | `!o` | Give op (+o) |
+| `!deop <nick>` | `!do` | Remove op |
+| `!halfop <nick>` | `!hop` | Give halfop (+h) |
+| `!dehalfop <nick>` | `!dhop` | Remove halfop |
+| `!lock [m\|i]` | `!lk` | Lock channel (+mi, +m, or +i) |
+| `!unlock [m\|i]` | `!ulk` | Unlock channel |
+
+### Auto-modes (owner / ACL / op, per channel)
+
+| Command | Short | Description |
+|---------|-------|-------------|
+| `!autoop <add\|del\|list> [nick\|mask]` | `!ao` | Auto +o on join |
+| `!autovoice <add\|del\|list> [nick\|mask]` | `!av` | Auto +v on join |
+| `!autohalfop <add\|del\|list> [nick\|mask]` | `!ahop` | Auto +h on join |
+
+### Config / lists (owner / ACL)
+
+| Command | Short | Description |
+|---------|-------|-------------|
+| `!blacklist <add\|del\|list> [mask]` | `!bl` | Global ban list (all channels) |
+| `!whitelist <add\|del\|list> [mask]` | `!wl` | Global bypass list |
+| `!except <add\|del\|list> [mask]` | `!ex` | Per-channel bypass list |
+| `!acl <add\|del\|list> [nick]` | (none) | Trusted nicks (can use mod commands) |
+| `!filter <add\|del\|list> [pattern]` | `!f` | Per-channel regex content filter |
+| `!badword <add\|del\|list> [word]` | `!bw` | Per-channel word block list |
+| `!set <key> <value>` | `!s` | Set a config value (in channel: per-channel; in PM: global) |
+| `!config [#channel]` | `!cfg` | Show channel config (PM) |
+| `!idle <on\|off>` | `!id` | Toggle idle detection |
+
+### Tracking
+
+| Command | Auth | Description |
+|---------|------|-------------|
+| `!seen <nick>` | anyone | Last time nick was seen in this channel. Responds in channel. Owner/ACL may call from PM for cross-channel lookup. |
+| `!info <nick>` | anyone | Nick summary scoped to this channel (PM). Owner/ACL may call from PM for global lookup. |
+| `!idle <nick>` | anyone | How long nick has been idle in this channel. Responds in channel. |
+| `!log <nick> [n]` | owner / ACL | Full event log for nick (PM) |
+| `!afk` | anyone | Reset your idle timer |
+
+### Admin (owner)
+
+| Command | Short | Description |
+|---------|-------|-------------|
+| `!join <#channel>` | `!j` | Join a channel |
+| `!part [#channel]` | `!p` | Leave a channel |
+| `!chans` | `!c` | List channels |
+
+## Configurable settings (`!set <key> <value>`)
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `flood_threshold` | 5 | Messages before flood action |
+| `flood_window_sec` | 10 | Flood detection window (seconds) |
+| `caps_filter` | 1 | Enable caps filter (0/1) |
+| `caps_percent` | 70 | Caps percentage threshold |
+| `caps_min_len` | 10 | Minimum message length to check |
+| `repeat_filter` | 1 | Enable repeat filter (0/1) |
+| `repeat_threshold` | 3 | Repeated messages before action |
+| `repeat_window_sec` | 30 | Repeat detection window (seconds) |
+| `clone_limit` | 2 | Max connections from same host |
+| `join_flood_filter` | 1 | Enable join flood filter (0/1) |
+| `join_flood_count` | 5 | Joins before action |
+| `join_flood_window` | 10 | Join flood window (seconds) |
+| `nick_flood_filter` | 1 | Enable nick flood filter (0/1) |
+| `nick_flood_count` | 3 | Nick changes before action |
+| `nick_flood_window` | 60 | Nick flood window (seconds) |
+| `badword_action` | kick | Action on badword: `kick` or `warn` |
+| `tempban_max_min` | 60 | Max duration for `!tempban` (minutes) |
+| `idle_warn_min` | 45 | Minutes idle before warning |
+| `idle_kick_min` | 60 | Minutes idle before kick |
+| `log_limit` | 6 | Default number of events shown by `!log` |
+| `greet` | 1 | Send join greeting (0/1) |
+
+## Config keys
+
+| Key | Description |
+|-----|-------------|
+| `[osterman] db_path` | Path to JSON database file |
diff --git a/docs/salvador.md b/docs/salvador.md
new file mode 100644
index 0000000..c1206b0
--- /dev/null
+++ b/docs/salvador.md
@@ -0,0 +1,30 @@
+# salvador
+
+Image-to-IRC art bot. Converts an image URL into mIRC colour art using
+half-block characters and Floyd-Steinberg dithering against the 99-colour
+mIRC palette.
+
+## Dependencies
+
+```
+pip install Pillow
+```
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `!draw <url>` | Render image at default height (6 lines) |
+| `!draw <lines> <url>` | Render at custom height (4-12 lines) |
+
+The image is fetched, resized to fit within 60 chars wide, dithered to the
+mIRC palette, and output line by line with a 0.4s flood delay between lines.
+Images up to 5 MB are accepted; the Content-Type must be `image/*`.
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
diff --git a/docs/shireen.md b/docs/shireen.md
new file mode 100644
index 0000000..71cbaf7
--- /dev/null
+++ b/docs/shireen.md
@@ -0,0 +1,88 @@
+# shireen
+
+News and weather bot. Fetches current weather and forecasts from wttr.in,
+and streams headlines from configurable RSS feeds. Posts fresh headlines
+automatically at a per-channel interval and deduplicates across restarts.
+
+## Dependencies
+
+No external libraries required (uses `urllib` and `xml.etree.ElementTree`).
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `!weather <city>` | Current weather conditions |
+| `!forecast <city>` | Brief forecast (today + next 2 days) |
+| `!headlines [global\|mena\|egypt] [n]` | Fresh headlines, optionally filtered by label or count |
+| `!news` | Show auto-broadcast state for this channel |
+| `!news on` | Enable auto-broadcast (owner/op) |
+| `!news off` | Disable auto-broadcast (owner/op) |
+| `!news interval <min>` | Set broadcast interval in minutes (owner/op) |
+| `!news count <n>` | Set headlines per broadcast cycle (owner/op) |
+
+### Admin (owner)
+| Command | Description |
+|---------|-------------|
+| `!join #channel` | Join a channel |
+| `!part [#channel]` | Leave a channel |
+| `!chans` | List channels |
+| `!help` | Show usage (PM) |
+
+## Auto-broadcast
+
+Every 90 minutes (default, per channel) shireen posts 1 headline (default) it
+hasn't posted before. GUIDs are persisted to the data JSON so the same article
+is never repeated across restarts. The cap is 1000 seen GUIDs (oldest pruned).
+Interval and count are configurable per channel with `!news interval` and `!news count`.
+
+## URL cleaning
+
+All query strings and fragments are stripped from feed links before posting.
+News article URLs are fully identified by their path; query parameters from
+feeds are always tracking noise.
+
+## Default RSS feeds
+
+| ID | Name | Label |
+|----|------|-------|
+| `bbc_world` | BBC World | global |
+| `reuters_world` | Reuters World | global |
+| `ap_world` | AP News | global |
+| `guardian_world` | The Guardian | global |
+| `arab_news` | Arab News | mena |
+| `aljazeera` | Al Jazeera | mena |
+| `al_monitor` | Al-Monitor | mena |
+| `egypt_indep` | Egypt Independent | egypt |
+| `daily_news_egypt` | Daily News Egypt | egypt |
+
+Feeds can be modified directly in the data JSON (`feeds` array) while the bot
+is stopped. Each feed has: `id`, `name`, `url`, `label`, `enabled`.
+
+## Data file format
+
+Created automatically at `data_path` on first run:
+
+```json
+{
+ "channels": {
+ "#example": {
+ "news_enabled": true,
+ "broadcast_interval": 90,
+ "broadcast_count": 1,
+ "last_broadcast": 0
+ }
+ },
+ "feeds": [...],
+ "seen": ["guid1", "guid2"]
+}
+```
+
+## Config keys
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `[shireen] data_path` | `/var/shireen/data.json` | Path to state/config JSON (auto-created) |
+| `[shireen] broadcast_interval` | `90` | Default minutes between auto-broadcast cycles |
+| `[shireen] broadcast_count` | `1` | Default headlines posted per broadcast cycle |
+| `[shireen] headlines_count` | `1` | Default headlines returned by `!headlines` |