bl0rt's irc bot
  • Scheme 85.4%
  • Dockerfile 14.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-21 12:45:57 -04:00
bl0t Log to std out 2026-09-21 09:08:13 -04:00
docker Modularize and prep for container deployment 2026-09-19 11:01:15 -04:00
templates Migrate secrets to sops 2026-09-21 12:45:57 -04:00
.gitignore Modularize and prep for container deployment 2026-09-19 11:01:15 -04:00
.sops.yaml Migrate secrets to sops 2026-09-21 12:45:57 -04:00
Chart.yaml Another k8s restructure 2026-09-20 14:56:39 -04:00
main.scm Log to std out 2026-09-21 09:08:13 -04:00
README.md Log to std out 2026-09-21 09:08:13 -04:00
values.yaml Another k8s restructure 2026-09-20 14:56:39 -04:00

bl0t

Personal IRC bot, written in Guile Scheme. Connects to IRC, and (as it grows) interacts with other services on the cluster -- starting with zoitestream (streaming scheduling/access), eventually the Molly Brown Gemini capsule once that's migrated off the legacy NixOS box.

Layout

bl0t/
├── main.scm            entry point / application logic
├── bl0t/
│   ├── irc.scm          (bl0t irc) - low-level IRC client bits.
│   │                     Candidate for extraction into its own
│   │                     standalone repo/library once stable.
│   ├── zoitestream.scm  (bl0t zoitestream) - HTTP client for the
│   │                     zoitestream streaming API
│   └── config.scm       (bl0t config) - env var loading helpers
├── docker/
│   └── Dockerfile        Guile runtime + system deps ONLY -- no
│                         application source is baked into the image.
├── Chart.yaml           This repo IS the Helm chart -- see "Deployment
├── values.yaml          model" below.
├── templates/
└── README.md

As more small reusable pieces get pulled out (the IRC library being the obvious first one), they get their own (bl0t whatever) module for now, and may eventually graduate to their own repo/library once the interface is stable enough to reuse elsewhere.

Local development

GUILE_LOAD_PATH=. guile main.scm

main.scm does not add its own directory to %load-path (that turned out to be unreliable -- see "A gotcha we hit" below). Set GUILE_LOAD_PATH to the repo root (or -L .) so (use-modules (bl0t irc)) etc. resolve, both locally and in the deployed container (where the Dockerfile sets GUILE_LOAD_PATH=/app/src for the same reason).

Logging

All IRC traffic (send-irc/handle-line in bl0t/irc.scm) logs to log-port, which defaults to stdout. This is deliberate: in the deployed pod, stdout is exactly what fluent-bit already ships to S3/Athena for every other service in this cluster -- bl0t needs zero extra logging infrastructure to show up there, it just needed to actually write to stdout instead of a private file.

Locally, running guile main.scm directly means that noisy IRC traffic shares your terminal with anything you type. For actual REPL-based iteration, run it as a REPL server instead so your interactive session and the log noise are two separate streams:

GUILE_LOAD_PATH=. guile --listen main.scm

Then connect a client separately -- nc localhost 37146, or geiser-connect-local in Emacs -- and evaluate things there. The --listen terminal keeps showing the raw IRC firehose; your client connection only shows the results of what you evaluate. Redirect/tee the --listen terminal's output to a file if you want it to persist past that terminal closing -- there's no code-level file logging to configure, it's just stdout either way.

Deployment model

This repo is the Helm chart that ArgoCD deploys -- Chart.yaml, values.yaml, and templates/ live right here, alongside the actual .scm source they package. There used to be a second copy of this chart in dumpnet-argo/charts/bl0t/, with the .scm files manually copied into it -- that duplication was a real design mistake (the two copies drifted out of sync more than once) and has been resolved by moving the chart here instead of leaving a copy behind.

This is deliberately not the usual "build a Docker image per code change" pattern used by the other services in this cluster (repertory-api, zoitestream). Guile is interpreted and this bot's source is small, so instead:

  • The Docker image (built from docker/Dockerfile) contains only the Guile runtime and system packages -- it rarely needs to change.
  • The actual source (main.scm, bl0t/*.scm) is packaged straight into a Kubernetes ConfigMap by templates/configmap.yaml (via Helm's .Files.Get), mounted into the container at /app/src.
  • A checksum of that ConfigMap's contents is baked into the Deployment's pod-template annotations, so any code change automatically triggers a pod restart on the next ArgoCD sync -- no manual docker build/push/kubectl delete pod cycle required.
  • The dumpnet-argo ArgoCD Application (manifests/services/bl0t.yaml there) points its primary Helm source directly at this repo, and only pulls the global values.yaml (registry host/user, domain, etc.) from dumpnet-argo as a second values-only source -- same multi-source pattern every other chart in that repo already uses.

Day-to-day update loop

  1. Edit .scm files here.
  2. git push.
  3. ArgoCD picks up the change (its normal reconciliation loop, or near-instantly if a Forgejo repo webhook is pointed at ArgoCD's webhook endpoint), the ConfigMap content and checksum annotation change together, and the pod restarts automatically running the new code. Nothing else to do, and nothing to touch in dumpnet-argo.
  4. Only rebuild/push the Docker image (git.keane.sh/ian/bl0t:latest) if you change system-level dependencies (a new apt package, a Guile version bump, etc.) -- ordinary application logic changes never need this. make build-bl0t in dumpnet-argo handles that build/push.

Adding a new source file

ConfigMap keys can't contain slashes, so any file under bl0t/ needs three touch points, not just dropping the file in:

  1. Add the file under bl0t/ as usual.
  2. Add a flattened data: entry in templates/configmap.yaml (e.g. bl0t/whatever.scm -> key bl0t_whatever.scm).
  3. Add a matching items: entry in templates/bl0t.yaml's volume definition, mapping that flattened key back to the real bl0t/whatever.scm path inside the mounted volume.

Forgetting step 2 or 3 means the file exists in the repo but either isn't in the ConfigMap, or is in the ConfigMap but not mounted where main.scm's (use-modules ...) expects to find it.

A gotcha we hit

An earlier version of the deployed main.scm had (add-to-load-path (dirname (current-filename))) at the top, to make (use-modules (bl0t irc)) resolve without relying on GUILE_LOAD_PATH. Don't do this: current-filename only resolves to a real path when Guile successfully compiles the file to bytecode first. If that compile step fails for any reason and Guile falls back to plain interpretation, current-filename evaluates to #f instead, and (dirname #f) crashes with a fairly opaque scm_to_utf8_stringn error. This is exactly what took the bot down in production once. Rely on GUILE_LOAD_PATH (already set by the Dockerfile, and by convention set by hand for local dev per above) instead.

Secrets

bl0t-secrets (IRC connection details, the zoitestream CREDS_TOKEN) comes from the usual single dumpnet Secrets Manager blob via ESO, same convention as every other service in dumpnet-argo -- see templates/external-secret.yaml. Nothing bot-specific about how secrets are handled; only the application source delivery mechanism is unusual in this chart.

Non-goals for this chart

  • No Service/Ingress: this is an outbound-only IRC client, nothing needs to route traffic to it.
  • No CI/build pipeline: deliberately avoided, see above.
  • Gemini/Molly Brown hosting: a separate future chart, not this one, even though bl0t will eventually write files that a Gemini server serves.

Why not a normal Docker-image-per-change pipeline?

See the gemlog writeup in ~/code/gemlog for the full reasoning -- short version: for a small, frequently-iterated, interpreted-language service like this, a ConfigMap-mounted-source + checksum-annotation pattern gets you real GitOps (push to git, cluster reconciles) without a build step, while staying comfortably within ConfigMap's 1MiB limit for the foreseeable future.