Authorized low-frequency Twitch chat helper for a single configured channel.
osr-bot connects to one Twitch chat channel, listens for a configured restart message, waits a short delay, and sends a configured response.
Current default behavior:
- trigger:
game restarting - delay:
45seconds - response:
!play - cooldown after response:
60seconds
The app is intentionally designed as a single long-running process with in-memory state. It does not use a database.
The bot is not intended for spam, bot viewers, mass automation, or multi-channel use.
The app:
- only joins the configured Twitch channel;
- only processes messages from that configured channel;
- lowercases chat messages before trigger matching;
- only reacts when the message contains the configured trigger text;
- keeps a
pending_playflag in memory; - ignores duplicate triggers while a response is pending;
- applies a short cooldown after sending the response;
- runs as a single Kubernetes replica.
Expected duplicate handling:
12:00:00 -> game restarting
12:00:05 -> game restarting
12:00:10 -> game restarting
12:00:45 -> !play
Only one response should be sent.
All deployment-time configuration is expected to come from GitHub Actions secrets and Helm --set-string values. Do not commit real tokens, kubeconfigs, PATs, or channel-specific sensitive data to the repository.
Required Twitch configuration:
TWITCH_USERNAME: Twitch username used by the app.TWITCH_CHANNEL: target channel. The app accepts bothchanneland#channelformats.
Preferred OAuth mode:
TWITCH_CLIENT_ID: Twitch app Client ID.TWITCH_CLIENT_SECRET: Twitch app Client Secret.TWITCH_REFRESH_TOKEN: Twitch user refresh token for the account that writes to chat.
Fallback OAuth mode:
TWITCH_OAUTH_TOKEN: Twitch access token for the account. The app accepts bothoauth:<token>and raw token formats.
If TWITCH_REFRESH_TOKEN is present, the app refreshes a new access token at startup and ignores TWITCH_OAUTH_TOKEN for authentication. If Twitch returns a rotated refresh token, the app logs a warning without printing the token value.
Behavior configuration:
TRIGGER_TEXT: defaults togame restarting.RESPONSE_TEXT: defaults to!play.RESPONSE_DELAY_SECONDS: defaults to45.MIN_COOLDOWN_SECONDS: defaults to60.RUST_LOG: defaults toinfo.
The Helm chart lives in:
infra/chart/osr-bot
The single environment values file lives in:
infra/envs/values.yaml
The Deployment hardcodes:
replicas: 1Do not increase replicas unless the app is changed to use shared distributed locking. Multiple replicas could send duplicate chat messages.
The pod runs as an unprivileged user and logs to stdout.
The default image repository is GitHub Container Registry:
ghcr.io/e-lemongrab/osr-bot
The package can remain private. For private GHCR pulls, the deploy workflow creates or updates a Kubernetes docker-registry pull secret from GitHub Actions secrets and passes it to Helm as imagePullSecrets[0].name.
The container image must not contain secrets. Runtime secrets are injected only at deploy time.
Deployment is handled by:
.github/workflows/deploy.yaml
The workflow is manual via workflow_dispatch and accepts an image_tag input.
Pipeline flow:
- Cross-compile the Rust binary for ARM64 using
aarch64-unknown-linux-gnu. - Stage the binary under
dist/osr-bot/osr-bot. - Build a runtime-only Docker image.
- Push the image to GHCR.
- Write kubeconfig from
KUBE_CONFIG_B64. - Create/update the GHCR image pull secret in Kubernetes.
- Deploy with Helm.
The Dockerfile is runtime-only. It does not run cargo build inside Docker.
General deploy secrets:
IMAGE_REPOSITORY: for exampleghcr.io/e-lemongrab/osr-bot.NAMESPACE: for exampleosr-bot.KUBE_CONFIG_B64: base64-encoded kubeconfig for the deploy user.
GHCR pull secrets:
GHCR_USERNAMEGHCR_PAT: PAT with package read access.GHCR_EMAILIMAGE_PULL_SECRET_NAME: for exampleghcr-pull-secret.
Twitch secrets:
TWITCH_USERNAMETWITCH_CHANNELTWITCH_CLIENT_IDTWITCH_CLIENT_SECRETTWITCH_REFRESH_TOKENTWITCH_OAUTH_TOKEN: fallback access token.TRIGGER_TEXTRESPONSE_TEXTRESPONSE_DELAY_SECONDSMIN_COOLDOWN_SECONDS
Before making this repository public, verify:
- no real OAuth tokens are committed;
- no refresh tokens are committed;
- no GitHub PATs are committed;
- no kubeconfig is committed;
- no
.envfiles are committed; - no generated Kubernetes Secret manifests with real data are committed;
- no workflow logs contain manually printed secrets;
- no screenshots or markdown files contain private operational data.
The repository should be safe to make public if all sensitive values remain only in GitHub Actions secrets and Kubernetes secrets.
The container build expects the ARM64 binary to exist at:
dist/osr-bot/osr-bot
The CI workflow creates this file before Docker packaging.
Example local ARM64 build shape:
rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu --manifest-path src/osr-bot/rust/Cargo.toml
mkdir -p dist/osr-bot
cp target/aarch64-unknown-linux-gnu/release/osr-bot dist/osr-bot/osr-bot
docker build -f src/osr-bot/docker/Dockerfile -t ghcr.io/e-lemongrab/osr-bot:local .