A reference client for the JoystickTV bot API, in Python. It's the companion
to public-bot-example-swift
(the native macOS example) and covers the same API surface as a small CLI:
- Public-client OAuth — authorization code + PKCE over a loopback redirect (RFC 7636 / RFC 8252), no client secret.
- Token lifecycle — refresh (with rotation) and RFC 7009 revocation.
- REST — identity, stream status, chat messages, moderation, banned words.
- Realtime gateway — the ActionCable stream, decoding both the legacy v1
envelope and the v2 envelope (a flat
type+ objectdata).
pip install -e .Register a Public bot in your JoystickTV account and set its redirect URL to
http://127.0.0.1/callback (a loopback host — the example binds a random port at
runtime). Then:
export JOYSTICK_CLIENT_ID=<your bot's OAuth client id>
# Optional — defaults to https://joystick.tv:
export JOYSTICK_BASE_URL=https://joystick.tv
# Optional — gateway envelope version, "v2" (default) or "v1":
export JOYSTICK_EVENT_VERSION=v2joystick-example login # authorize via PKCE (opens a browser), save tokens
joystick-example refresh | logout | introspect
joystick-example whoami # bot identity + granted scopes
joystick-example stream show # live state / title / viewer count
joystick-example stream set --title "…" --tag foo --tag bar
joystick-example say "gg" # post a chat message as the bot
joystick-example whisper <username> "…" # whisper a user
joystick-example connect # stream realtime gateway events until Ctrl-C
joystick-example followers | subscribers # username lists
joystick-example moderators list | add <username> | remove <username>
joystick-example mod delete|mute|ban <message-id> | unban <username>
joystick-example words list | add <word> | remove <word>(Or run without installing: python -m joystick_example <command>.)
Each command maps to one endpoint / scope, so the source doubles as documentation:
| Command | Route | Scope |
|---|---|---|
whoami |
GET /api/v1/me/identity |
identity:read |
stream show / set |
GET / PUT /api/v1/me/stream |
stream:read / stream:manage |
say |
POST /api/v1/chat/messages |
chat:write |
whisper |
POST /api/v1/chat/whispers |
chat:whisper:write |
mod delete/mute/ban / unban |
…/chat/messages/:id… / …/chat/users/:u/ban |
chat:moderate |
followers / subscribers |
GET /api/v1/{followers,subscribers} |
followers:read / subscribers:read |
moderators list/add/remove |
GET/POST/DELETE /api/v1/moderators |
moderators:read / moderators:manage |
words |
…/api/v1/banned-words |
chat:moderate |
introspect |
POST /api/oauth/introspect |
— |
connect |
wss://<host>/cable (GatewayChannel) |
— |
connect opens the ActionCable socket, authenticating with the bot's bearer
JWT as the ?token= query param (the same access token the REST calls use —
no secret, which is the point of a public client). It subscribes to
GatewayChannel, and — unless you pass --event-version v1 — opts into the
v2 event envelope:
joystick_example/events.py decodes both v1 and v2 into a small GatewayEvent,
so a deploy that predates v2 (and ignores the subscribe param) still works — you
just receive v1 envelopes. Token refresh is proactive: a long-running connect
checks the JWT exp and rotates the token before it lapses.
pip install -e ".[dev]"
python -m pytestThe event tests run against real captured v1 and v2 frames, so they pin the decoder to the actual wire format.
{ "v": 2, "type": "tipped", "id": "…", "channel_id": "…", "occurred_at": "2026-07-07T14:52:31Z", "text": "BusTester tipped 50 tokens", "data": { "who": "BusTester", "how_much": 50, "tip_menu_item": "Hydrate" } }