{"id":50912479,"url":"https://github.com/iammrcupp/annoybots","last_synced_at":"2026-07-10T04:00:29.423Z","repository":{"id":364714226,"uuid":"1267096871","full_name":"IamMrCupp/annoybots","owner":"IamMrCupp","description":"BMotion-style IRC/Twitch/Discord nuisance bots, rebuilt as one Go binary — many networks at once, a shared Redis 'botnet' bus, a Markov brain, a cross-platform partyline, eggdrop-style channel keeping, and a chat admin console. The modern eggdrop+BMotion, GitOps-deployed to Kubernetes.","archived":false,"fork":false,"pushed_at":"2026-07-03T00:54:50.000Z","size":707,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-03T01:08:22.886Z","etag":null,"topics":["bmotion","botnet","chatbot","discord","distroless","eggdrop","gitops","golang","irc","irc-bot","kubernetes","markov","redis","twitch"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/IamMrCupp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-12T08:06:54.000Z","updated_at":"2026-07-03T00:54:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/IamMrCupp/annoybots","commit_stats":null,"previous_names":["iammrcupp/annoybots"],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/IamMrCupp/annoybots","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IamMrCupp%2Fannoybots","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IamMrCupp%2Fannoybots/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IamMrCupp%2Fannoybots/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IamMrCupp%2Fannoybots/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IamMrCupp","download_url":"https://codeload.github.com/IamMrCupp/annoybots/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IamMrCupp%2Fannoybots/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35319810,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bmotion","botnet","chatbot","discord","distroless","eggdrop","gitops","golang","irc","irc-bot","kubernetes","markov","redis","twitch"],"created_at":"2026-06-16T11:02:39.073Z","updated_at":"2026-07-10T04:00:29.368Z","avatar_url":"https://github.com/IamMrCupp.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# annoybots\n\nA framework for chat \"nuisance\" bots — the modern, containerized descendant of\nthe classic [BMotion](http://bmotion.sourceforge.net/) IRC bots, rebuilt in Go\nfor Kubernetes.\n\nOne small Go binary holds many simultaneous chat connections at once (IRC\nnetworks, Twitch, and Discord) and routes every channel through a shared\n\"annoyance engine.\" **Each bot is the same binary with a different personality\nfile**, so you can run as many bots as you like — each with its own name,\ntriggers, and voice. The repo ships two example personalities, **Echo** and\n**Mimic**, to copy from.\n\n## What it does\n\n- **Keyword/regex triggers** → randomized, templated responses (`{nick}`, `{me}`, `{chan}`, capture groups).\n- **Ambient interjections** — random unprompted lines, with per-channel cooldowns.\n- **Quote packs** — drop-in `.txt` files surfaced randomly and via `!quote [pack]`; `!packs` lists what's available (same as the `/quote`, `/packs` slash commands on Discord).\n- **A learning \"brain\"** — an order-N Markov chain that learns from channel chatter and babbles it back, mangled. It persists to disk so learning survives restarts (just like the BMotion babble everyone remembers, minus the abandoned TCL stack).\n- **Multi-network in one process** — IRC + Twitch share the same wire protocol; Twitch just needs CAP negotiation, an `oauth:` token, and tighter rate limits, all handled automatically. Discord rides alongside on the same engine.\n- **Many bots, one binary** — personality is config, not code, so a new bot is a new YAML file (and a pod), never a fork.\n- **Games \u0026 toys** — karma (`name++` / `name--` / `!karma` / `!top`), `!roll`, `!8ball`, and a full **[IdleRPG](docs/idlerpg.md)** (idle to level, items, battles, random events, alignment/classes, and timed party **quests**) with a read-only **web dashboard**.\n- **Cross-network accounts** — `!register` / `!link` so one person is one identity (and one IdleRPG hero) whether they're on IRC or Discord. See [docs/accounts.md](docs/accounts.md).\n- **Leave a message** — `!message \u003cnick\u003e \u003ctext\u003e`, delivered when they're next around.\n- **Channel keeping** — eggdrop-style: an opped bot keeps its sibling bots opped.\n- **Chat admin console** — DM the bot to puppet it, edit quotes, manage channels and admins; identity-authenticated, tiered by access flag.\n- **Lua plugins** — eggdrop-style scripting: drop a `.lua` file to add `!commands` with no rebuild. See [docs/plugins.md](docs/plugins.md).\n\nFor the full command list see **[docs/commands.md](docs/commands.md)**.\n\n## Layout\n\n```\ncmd/annoybot         entrypoint\ncmd/dashboard        read-only IdleRPG web dashboard (separate binary, same image)\ninternal/engine      annoyance engine (triggers, interjections, quotes, commands)\ninternal/markov      persistent Markov \"brain\"\ninternal/bot         transport router (fans replies back to the right platform)\ninternal/botnet      inter-bot bus (Redis pub/sub) + skit coordinator\ninternal/event       transport-agnostic event dispatcher (joins/parts/quits/…)\ninternal/state       shared state store (Redis or in-memory) for karma/accounts/IdleRPG\ninternal/irc         IRC/Twitch transport (ergochat/irc-go) + per-network rate limiting\ninternal/discord     Discord transport (bwmarrin/discordgo) + slash commands\ninternal/admin       chat admin console (identity auth, access flags, partyline)\ninternal/games       karma, !roll, !8ball\ninternal/idlerpg     the IdleRPG game\ninternal/rpgweb      HTML rendering for the dashboard\ninternal/account     cross-network identity linking\ninternal/tell        \"!message \u003cnick\u003e\" deferred delivery\ninternal/plugin      eggdrop-style Lua scripting (command binds)\ninternal/ratelimit   token-bucket limiter (Twitch-aware)\ninternal/cooldown    per-channel cooldown tracking\ninternal/config      YAML config loading, validation, quote-pack loading\ninternal/health      /healthz + /readyz for Kubernetes probes\nconfigs/             echo.yaml, mimic.yaml — example personalities (+ networks)\ndata/quotes/         starter quote packs\ndata/skits.yaml      shared multi-bot skit scripts\ndata/plugins/        example Lua plugins\ndeploy/compose/      Docker Compose stack (no Kubernetes needed)\ndeploy/k8s/          kustomize base + per-bot overlays + the dashboard\ndocs/                command, IdleRPG, and accounts reference\n```\n\n## Quick start (Docker Compose)\n\nNo Kubernetes required. The [`deploy/compose`](deploy/compose) stack runs a bot,\nthe Redis it uses for state, and the IdleRPG dashboard:\n\n```sh\ncd deploy/compose\ncp .env.example .env          # fill in your tokens\n$EDITOR bot.yaml              # set your networks, channels, admins\ndocker compose up -d\n```\n\nThe dashboard comes up at \u003chttp://localhost:8080\u003e. See\n[`deploy/compose/README.md`](deploy/compose/README.md) for the details.\n\n## Run locally (from source)\n\n```sh\n# Export the secrets your config references first, e.g.:\nexport ECHO_LIBERA_SASL=...           # NickServ/SASL password\nexport ECHO_TWITCH_TOKEN=oauth:...    # Twitch chat token\n\nmake run-echo                          # or run-mimic\n# any config:  go run ./cmd/annoybot -config configs/yourbot.yaml\n```\n\nQuote-pack files resolve relative to `ANNOYBOT_QUOTES_DIR` (the Makefile points\nit at `data/quotes`).\n\n## Configuration\n\nEverything behavioral lives in `configs/\u003cbot\u003e.yaml`: networks, triggers,\ninterjection/quote rates and cooldowns, and Markov settings. **Secrets are never\nstored in the config** — each `*_env` field names an environment variable\n(populated from a Kubernetes Secret) that holds the actual token or password.\nStart from `configs/echo.yaml` or `configs/mimic.yaml`; both exercise every\nfeature and all three platforms.\n\n**Feature toggles.** The optional command subsystems — `games` (karma/dice/8-ball),\n`tell` (`!message`), and `accounts` — each default **on**; set `enabled: false` on\nany to carve it out. Combined with an empty/disabled personality, that's how you\nrun a single-purpose bot: [`configs/idlerpg.yaml`](configs/idlerpg.yaml) is a\nready-made **IdleRPG-only** bot that runs the game and stays silent on everything\nelse. See [docs/idlerpg.md](docs/idlerpg.md#a-dedicated-idlerpg-bot).\n\n**State persistence.** Karma, accounts, and IdleRPG live in a shared state store.\nThat store is **Redis when `botnet.enabled: true`** — so state persists across\nrestarts and is shared across all your bots — and **in-memory otherwise** (handy\nfor a quick local run, but it resets when the process stops). The Docker Compose\nstack and the k8s `deploy/k8s/redis` both stand Redis up for you.\n\n### Add a bot\n\nA bot is just the binary pointed at a different config, so adding one is purely\nadditive — no code changes:\n\n1. **Copy an example config.** `cp configs/echo.yaml configs/jeeves.yaml`, then\n   set `bot: jeeves` and `personality.name: \"Jeeves\"` and edit the networks,\n   triggers, banter, and quote packs to taste.\n2. **Name its secrets.** Each `*_env` field names an env var. For Kubernetes,\n   collect them into a `jeeves-bot-secrets` Secret (see [Deploy](#deploy)).\n3. **Introduce it to the others.** Add `\"Jeeves\"` to every other bot's\n   `personality.siblings`, and optionally give it lines in `data/skits.yaml`\n   (skit steps are matched by the `bot:` value, e.g. `bot: jeeves`).\n4. **Deploy it.** Copy `deploy/k8s/overlays/echo/` to `…/jeeves/`: point the\n   `bot-config` generator at `configs/jeeves.yaml` and the secretRef patch at\n   `jeeves-bot-secrets`. Add a Flux `Kustomization` (copy a block in\n   `deploy/k8s/flux/kustomizations.yaml`).\n\nThe engine, botnet bus, and admin console all work the same for any number of bots.\n\n### Twitch\n\nSet `kind: twitch` on a network and point `password_env` at an env var holding a\nchat oauth token. Server, TLS, CAPs, and conservative rate limits default\nautomatically. Note Twitch does not reliably broadcast joins/parts or mode\nchanges, so user/op tracking there is intentionally not relied upon.\n\n### Discord\n\nSet `kind: discord` on a network and point `password_env` at an env var holding\nthe **bot token** (no nick/server needed). Then:\n\n1. In the [Discord developer portal](https://discord.com/developers/applications),\n   create an application + bot, and **enable the privileged MESSAGE CONTENT\n   intent** under Bot → Privileged Gateway Intents. Without it the bot connects\n   but every message body arrives empty.\n2. Invite the bot to your server with an OAuth2 URL using the `bot` and\n   `applications.commands` scopes.\n3. List your server's ID under `guilds:` for instant `/quote` and `/annoy` slash\n   commands (global registration works too but can take up to an hour to appear).\n\n`channels` is an optional allowlist of channel IDs; empty means the bot responds\neverywhere it can see. Discord's own HTTP rate limits are handled by the client\nlibrary, so the token-bucket limiter is IRC/Twitch-only. The same triggers,\nquote packs, and Markov brain run on Discord unchanged; IRC `/me` actions render\nas italics.\n\n## Bot-to-bot interaction (the \"botnet\")\n\nLike the old eggdrop botnet BMotion used for coordinated trolling, the bots can\ntalk to each other — but safely.\n\n**Banter (cross-talk).** Each bot lists the others as `siblings`. A sibling's\nmessages can *only* produce capped banter — never normal triggers — so bots can\nnever trigger each other into an infinite flood. Banter is bounded twice: a\nper-channel cooldown *and* a hard \"max replies per rolling window\" cap.\n\n**Skits.** Multi-bot scripted bits live in a shared `data/skits.yaml` loaded by\nevery sibling bot. They coordinate over a Redis pub/sub bus, so a skit works even\nif the bots are on different platforms. The \"lead\" bot (owner of the first line)\ninitiates — via `!skit \u003cname\u003e` in a shared channel, or randomly via each skit's\n`chance` — then the bots perform their lines in lockstep, bounded by the step\ncount and a per-channel cooldown. Add your own by editing `data/skits.yaml`\n(each step's `bot:` must match a bot's `bot:` config value).\n\nEnable it with the `botnet:` block in each bot's config (all bots must share the\nsame `channel`). Deploy the shared bus once:\n\n```sh\nkubectl apply -k deploy/k8s/redis\n```\n\nThe bus carries only ephemeral coordination messages, so the Redis runs without\npersistence.\n\n**Federation.** Bots on *other* hosts (a VPS, a friend's box) can join the same\nbotnet by pointing at the same Redis over a private WireGuard/Headscale mesh — no\ncode changes, just `botnet.redis_addr` + a password. See\n[docs/federation.md](docs/federation.md) and the [`deploy/remote`](deploy/remote) kit.\n\n## Admin console (chat)\n\nDM a bot to run admin commands. Admins are matched by **verified identity** — an\nIRC services/NickServ account, a Discord user ID, or a Twitch login — never by\nspoofable nick, and commands are only honored in DMs. Configure admins in the\n`admin:` block of each bot's config.\n\n**First-run claim (no password to manage).** If the console is enabled but no\nadmins are configured, the bot prints a one-time **claim code** to its log on\nstartup. The first person to DM `!claim \u003ccode\u003e` (from a verified identity)\nbecomes the owner — their identity is recorded and the code is spent. Nothing to\ninvent, paste into a Secret, or keep around: it bootstraps straight into identity\nauth. (The code lives only in memory, so a restart prints a fresh one until it's\nclaimed; set `admin.state_path` so the claimed owner persists.)\n\nCommands (send `!help` for the list; full reference in [docs/commands.md](docs/commands.md)):\n\n- `!networks` — which networks the bot is currently connected to\n- `!join \u003cnet\u003e \u003c#chan\u003e` / `!part \u003cnet\u003e \u003c#chan\u003e` — channel ops\n- `!invite \u003cnet\u003e \u003c#chan\u003e \u003cnick\u003e` — IRC INVITE (bot needs ops on `+i` channels)\n- `!say \u003cnet\u003e \u003ctarget\u003e \u003ctext\u003e` / `!act \u003cnet\u003e \u003ctarget\u003e \u003ctext\u003e` — puppet the bot (the target can be a nick or service, e.g. `!say \u003cnet\u003e NickServ \"IDENTIFY …\"`)\n- `!identify \u003cnet\u003e [password]` — (re)authenticate to NickServ; omit the password to use the bot's configured secret (nothing sensitive typed in chat)\n- `!addquote \u003cpack\u003e \u003ctext\u003e` / `!delquote \u003cpack\u003e \u003ctext\u003e` — runtime quote editing\n- `!addadmin \u003cnet|*\u003e \u003caccount\u003e` / `!deladmin \u003cnet|*\u003e \u003caccount\u003e` / `!admins`\n- `!reload` — re-read quote-pack files and the skits file from disk, no restart\n  (network connections and personality triggers still require a restart)\n\nQuote and admin changes persist to the data volume and **sync to the sibling bots\nover the botnet bus**, so you only have to DM one of them. Channel control and\npuppeting stay local to the bot you DM. (`!delquote` only removes runtime-added\nlines; file-pack lines are immutable — edit the `.txt` to change those.)\n\n**Password fallback.** Identity auth is primary, but if a network has no services\n(or someone just isn't logged in), set `password_env` and an admin can\n`!login \u003cpassword\u003e` in a DM to get a time-limited session (`!logout` to end it).\nHeads up: without services this session is keyed by **nick**, which is spoofable,\nso it's weaker than account-based auth — it's a convenience for when services are\nunavailable, with a constant-time password check, a failed-attempt throttle, and\na configurable `session_ttl`. Leave `password_env` unset to disable it.\n\n## Deploy\n\nThis is built for **GitOps with Flux**: Conventional Commits → `release-please`\ncuts a semver release → CI builds the image → Flux's semver `ImagePolicy` rolls\nit out. Secrets are **hand-applied by default** (SOPS-encrypted secrets-in-Git\nare optional); quote/skit content is served from ConfigMaps so edits go live via\n`!reload`. See [`deploy/k8s/flux/README.md`](deploy/k8s/flux/README.md) for the\nfull wiring (Kustomizations, image automation, and the optional SOPS/age setup).\n\nEach bot deploys as a single-replica StatefulSet with its own PVC for the Markov\nbrain; Redis (the botnet bus *and* the shared state store for karma/accounts/\nIdleRPG) deploys once from `deploy/k8s/redis`.\n\n### IdleRPG web dashboard\n\nIf you run [IdleRPG](docs/idlerpg.md), deploy the read-only dashboard:\n\n```sh\nkubectl apply -k deploy/k8s/dashboard -n annoybots\nkubectl -n annoybots port-forward svc/rpg-dashboard 8080:80   # then open http://localhost:8080\n```\n\nIt's the same image with the `/dashboard` entrypoint, reads the shared Redis only\n(no secrets), and shows the rankings and any active quest. Point an Ingress at the\n`rpg-dashboard` Service to make it public.\n\n### Manual apply (no Flux)\n\n```sh\nkubectl create namespace annoybots\n\n# Hand-apply each bot's Secret — keys are listed in\n# deploy/k8s/overlays/\u003cbot\u003e/secret.example.yaml. Omit a key to disable a network:\nkubectl -n annoybots create secret generic echo-bot-secrets \\\n  --from-literal=ECHO_TWITCH_TOKEN='...' --from-literal=ECHO_DISCORD_TOKEN='...'\nkubectl -n annoybots create secret generic mimic-bot-secrets \\\n  --from-literal=MIMIC_TWITCH_TOKEN='...' --from-literal=MIMIC_DISCORD_TOKEN='...'\n\nkubectl apply -k deploy/k8s/redis\nkubectl kustomize --load-restrictor LoadRestrictionsNone deploy/k8s/overlays/echo  | kubectl apply -n annoybots -f -\nkubectl kustomize --load-restrictor LoadRestrictionsNone deploy/k8s/overlays/mimic | kubectl apply -n annoybots -f -\n```\n\n## Adding quotes\n\nDrop a `whatever.txt` in `data/quotes/` (one quote per line, `#` comments\nallowed), reference it in a bot's `personality.quotes.packs`, and list it in the\noverlay's `bot-quotes` `configMapGenerator`. With Flux, commit it and `!reload`;\notherwise rebuild the image (packs are also baked in at `/quotes` as a fallback).\n\nSome bundled packs are pop-culture one-liners (Futurama, South Park, …) in the\nold BMotion tradition — credited, with their rights-holder note, in\n[`data/quotes/CREDITS.md`](data/quotes/CREDITS.md). They're examples; swap in your\nown if you'd rather not redistribute them.\n\n## Develop\n\n```sh\nmake test     # go test ./...\nmake lint     # golangci-lint\nmake docker   # build the image\n```\n\n## Acknowledgments\n\nannoybots stands on the shoulders of the IRC bots that annoyed channels before it.\nNone of their code is used here — this is a clean-room rewrite in Go — but the\nideas, and a lot of the fun, are theirs:\n\n- **[eggdrop](https://www.eggheads.org/)** — the original scriptable IRC bot; the\n  flag/access model, partyline, and channel-keeping all trace back to it.\n- **[BMotion](http://bmotion.sourceforge.net/)** — the eggdrop TCL framework whose\n  ambient \"the bot just talks\" behavior, banter, and quote packs this is a love\n  letter to.\n- **[IdleRPG](http://idlerpg.net/)** (and the [falsovsky PHP fork](https://github.com/falsovsky/idlerpg))\n  — the idle game reimagined in [`internal/idlerpg`](internal/idlerpg).\n\nBuilt on [ergochat/irc-go](https://github.com/ergochat/irc-go),\n[bwmarrin/discordgo](https://github.com/bwmarrin/discordgo), and\n[redis/go-redis](https://github.com/redis/go-redis), among others — all permissively\nlicensed (MIT/BSD/Apache).\n\n## License\n\n[MIT](LICENSE) — do whatever you want, no warranty. The bundled pop-culture quote\npacks are the exception: they belong to their respective rights holders and are\n**not** covered by this license (see [`data/quotes/CREDITS.md`](data/quotes/CREDITS.md)).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiammrcupp%2Fannoybots","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiammrcupp%2Fannoybots","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiammrcupp%2Fannoybots/lists"}