LiquidChat websocket client library (chat.liquidbounce.net)
Project description
liquidchat
⚠️ Project status: alpha. API surface may change without warning until 1.0.
A modern, typed Python client for the LiquidChat websocket protocol used by
chat.liquidbounce.net. Ported and modernized from the original
olotldiscordbot/liquidchat/ package.
Installation
# From source (private repo for now)
git clone git@github.com:clawdbot-silly-waddle/liquidchat.git
cd liquidchat
uv sync # or: pip install -e '.[dev]'
Requires Python 3.14+.
For the interactive CLI (liquidchat chat, liquidchat token info,
…), install with the cli extra:
pip install 'liquidchat[cli]' # adds cyclopts + prompt_toolkit + rich
# or, in a uv project:
uv add 'liquidchat[cli]'
The console script liquidchat is registered automatically. See
## CLI below.
Two clients
Client— one-shot. Opens a fresh websocket, performs an operation (validate a token, send a chat message, ban / unban / batch-ban a user), and closes. Ideal for cron jobs, validation endpoints, and one-off moderation.PersistentClient— long-lived. Auto-reconnects, dispatches inbound events toHandlerscallbacks, and exposes the full action set (chat sends, ban / unban) on the live connection. Use this for bots and sustained moderation.
Both clients require a JWT for chat.liquidbounce.net (obtained via
axolotl-client.net). Moderation actions
require the JWT user to be listed in the server's moderators file.
Client
import asyncio
from liquidchat import Client
async def main() -> None:
client = Client(token="<jwt>")
# Validation
if not await client.validate():
return
# Chat
await client.send_message("hello, chat!")
# Moderation (requires moderator perms server-side)
ok = await client.ban_user("<uuid>")
results = await client.ban_users_batch(
["<uuid>", "..."], progress=lambda d, t, r: print(f"{d}/{t}")
)
asyncio.run(main())
validate() returns False on bad credentials or an unreachable server.
Use validate_strict() if you need network errors to propagate instead.
Chaining actions on one connection
Client.session() opens a single websocket and yields a Session you
can run multiple actions on, avoiding the cost of reconnecting and
re-logging-in between operations:
async with client.session() as s:
await s.send_message("about to clean up...")
await s.ban_user("<uuid>")
await s.unban_user("<other-uuid>")
await s.send_private_message("victim", "you've been warned")
Pass accept_private_messages=False to the session if you don't expect
private messages in response.
PersistentClient
import asyncio
from liquidchat import Handlers, PersistentClient
async def on_message(author, content):
print(f"<{author.name}> {content}")
async def main() -> None:
async with PersistentClient(
token="<jwt>",
handlers=Handlers(on_message=on_message),
) as client:
await client.send_chat("hi everyone")
# Moderation works on the same connection (if the JWT has perms)
await client.ban_user("<uuid>")
await asyncio.sleep(3600)
asyncio.run(main())
async with starts the client, waits until it's logged in, and tears
it down on exit. Use start() / stop() explicitly if you need
finer-grained control.
Handlers accepts on_message, on_private_message, on_user_count,
on_error, plus lifecycle hooks (on_connect, on_login_success,
on_disconnect, on_reconnect).
Reconnection is governed by ReconnectPolicy(base_delay, max_delay, max_attempts); pass a custom policy via the reconnect= constructor
argument.
Differences from the original
- Strict typing (basedpyright
strictmode on src/). - Pydantic v2 frozen models for the wire format.
- Tagged-union parsing via
parse_message()returningLiquidChatMessage. - No silent
ssl.CERT_NONE— verified TLS by default; opt-ininsecure_ssl=True. - Singletons removed; instantiate clients explicitly.
- Reconnection extracted into a
ReconnectPolicydataclass. - Two clients (one-shot + persistent) instead of the original five-class hierarchy.
Development
uv sync
uv run pytest
uv run ruff check .
uv run basedpyright
Username / UUID lookup
PersistentClient.get_username(uuid) and get_uuid(name) consult a
local cache populated from inbound chat traffic — no Mojang API
call is made. They return None until the user has been observed in
chat.
For lookups beyond the cache (or in CLI/one-shot scripts), use the
liquidchat.mojang helpers, which call Mojang's public profile API
via mcapi-auth:
from liquidchat.mojang import MojangClient, resolve_uuid, resolve_username
# One-shot (creates and tears down an httpx.AsyncClient):
uuid = await resolve_uuid("Notch") # "069a79f4-44e9-4726-a5be-fca90e38aaf5"
name = await resolve_username(uuid) # "Notch"
# Batched (reuse one client):
async with MojangClient() as mojang:
for name in names:
print(name, await mojang.resolve_uuid(name))
Returns None on a clean "not found" (HTTP 404 / 204). Other HTTP
failures raise MojangHTTPError; network errors propagate as
httpx.RequestError. For the full Mojang/Microsoft surface (auth chain,
textures, blocked-servers, piston-meta, skin/cape management, …) reach
into mcapi-auth directly — it's a runtime dependency.
Token validation
Two flavours, depending on what you need:
Server-side validation (Client.validate / Client.validate_strict)
opens a websocket and performs the real LoginJWT handshake. The
server checks signature, expiry, and claim structure — that's real
validation. validate returns False on either rejected creds or
server-unreachable; validate_strict distinguishes the two.
Offline validation (liquidchat.jwt) parses the JWT locally — no
network round-trip, but it cannot verify the signature (we don't have
axochat's signing key). Use this as a cheap preflight check, e.g. to
refresh proactively before opening the socket:
from liquidchat.jwt import inspect_token, is_token_expired, InvalidTokenError
try:
info = inspect_token(jwt)
print(info.name, info.uuid, info.expires_at)
except InvalidTokenError as e:
print("malformed token:", e)
if is_token_expired(jwt, leeway=30.0):
jwt = await refresh_token()
Offline checks: well-formedness (3 base64url segments), header alg
present and not none, payload decodes to a JSON object containing
exp (numeric) and user.{name, uuid} (non-empty strings), and the
configurable exp clock check.
CLI
The optional liquidchat console script (install with the cli extra)
gives you a chat REPL plus the same operations the library exposes,
straight from your shell. It uses Cyclopts for the command tree,
prompt_toolkit for the bottom-anchored chat prompt, and Rich
for pretty token output.
$ liquidchat --help
Usage: liquidchat COMMAND
Commands:
account Manage liquidchat credential profiles.
ban Ban a player by UUID or username (via the active profile's JWT).
chat Open an interactive LiquidChat session for a given profile.
login Sign in via Microsoft → Mojang → AxoChat and store creds per profile.
mojang Public Mojang profile lookups.
send Send a single chat message and exit.
token JWT inspection, validation, and rotation.
unban Unban a player by UUID or username (via the active profile's JWT).
Profiles & token resolution
Credentials are organised by profile — one directory per Minecraft
account under $LIQUIDCHAT_HOME (default ~/.config/liquidchat):
~/.config/liquidchat/
├── default # plain-text: name of the default profile
└── profiles/
├── hanimetv/
│ ├── jwt # liquidchat JWT (chmod 0600)
│ └── refresh_token.json # MSA refresh token (mcapi-auth)
└── alt-account/
└── ...
Every subcommand that uses the JWT (chat, send, ban, unban,
token …) accepts --account NAME and --token <jwt>. Resolution
order:
--token <jwt>(explicit).LIQUIDCHAT_TOKENenv var.profiles/<name>/jwtfor the selected profile, where<name>is chosen by:--account→LIQUIDCHAT_ACCOUNTenv →defaultpointer file.
Manage profiles with:
liquidchat account list # show profiles + default marker
liquidchat account use alt-account # change the default
liquidchat account remove old-account # delete a profile
Heads up: the official
chat.liquidbounce.netdeployment has been serving an expired TLS certificate since 2020. Every subcommand that opens the chat websocket defaults to insecure mode (no TLS verification) so the public server works out of the box. Pass--no-insecureagainst a deployment with a valid cert.
Logging in (no token? start here)
liquidchat login # profile name = your MC username
liquidchat login --account alt-account # explicit profile name
liquidchat login --client-id prism # default — v2 / Prism Launcher
liquidchat login --client-id java # legacy Java launcher (00000000402b5328)
liquidchat login --client-id bedrock-nintendo # Bedrock / Switch client_id
--client-id accepts either an alias (resolved via
mcapi_auth.KNOWN_CLIENT_IDS) or a literal client_id. v1 (compressed
Live-Connect) client_ids always use the OOB browser paste-back flow;
v2 (Azure-AD GUID) client_ids honour --flow {device-code, browser}.
Auto-flow matrix (default --flow device-code):
| client_id | type | actual flow chosen |
|---|---|---|
java, bedrock-*, xbox-* |
v1 | browser-v1 (OOB paste-back) |
prism, liquidlauncher/liquidbounce |
v2 | device-code |
edu, office365 |
v2 | device-code |
Available aliases:
- v2 / Azure-AD GUID (XboxLive.signin scope):
prism(default),edu,office365,liquidlauncher,liquidbounce(the last two share Azure app0add8caf-…). - v1 / Live-Connect compressed (MBI_SSL scope):
java,bedrock-win32(currently broken upstream — returnsinvalid_requesteven for the OOB redirect),bedrock-android,bedrock-ios,bedrock-nintendo,bedrock-playstation,xbox-app-ios,xbox-gamepass-ios.
Flow override flags
liquidchat login --flow browser --client-id liquidlauncher # v2 loopback
liquidchat login --flow browser --client-id prism # v2 loopback (root path)
liquidchat login --flow device-code --client-id edu # only flow that works for edu/office365
For v2 clients with a known loopback registration, the right
(bind_host, redirect_path) pair is picked up automatically from
mcapi_auth.KNOWN_CLIENT_REDIRECTS (currently: prism → http://127.0.0.1:*/, liquidlauncher → http://localhost:*/login).
Override manually if you need to:
--bind-host 127.0.0.1 | localhost (default: per-client or 127.0.0.1)
--bind-port <int> (default: 0 = OS-picked ephemeral)
--redirect-path /callback (default: per-client or /callback)
--force-flow (escape hatch — disable auto v1/v2
dispatch and route --flow literally,
e.g. for testing exotic combinations)
--flow browser with edu/office365 will print a warning
(no loopback URL is registered on their Azure apps) and then fail
at the authorize step — use --flow device-code instead. The same
warning fires under --force-flow --flow browser against any v1
client_id (since login_via_browser targets the v2 endpoint, which
rejects v1 IDs as AADSTS70001).
Runs the full Microsoft → Mojang → AxoChat auth chain end-to-end:
- Walks you through MSA device-code authentication via
mcapi-auth(the refresh token is staged to a temp file when--accountis omitted, then moved intoprofiles/<username>/refresh_token.jsononce the MSA flow reveals the username). - Opens the websocket and runs
RequestMojangInfo→sessionserver join→LoginMojang→RequestJWTback-to-back. - Writes the resulting JWT to
profiles/<name>/jwtand promotes the profile to default if no default exists yet (override with--set-default/--no-set-default).
Pass --no-remember to skip persisting the MSA refresh token (a
one-shot, fully ephemeral login).
After that, every other subcommand picks the token up automatically.
liquidchat token refresh rotates it on the same connection without
re-running the MSA flow (and writes the new JWT back to the profile
by default; pass --no-save to print it instead).
Interactive chat
liquidchat chat # uses the default profile
liquidchat chat --account alt-account # pick a specific profile
liquidchat chat --anonymous # read-only, no login required
Opens a PersistentClient, prints inbound chat (with timestamps +
colour) to the scrollback, and reads from a bottom-anchored prompt
that survives reconnects and pretty-printed lifecycle events. Slash
commands:
| Command | Effect |
|---|---|
/help |
Show the in-session command list. |
/quit, /exit, Ctrl-D |
Close the connection and exit. |
/ban <user|uuid> |
Ban — usernames are resolved via Mojang. |
/unban <user|uuid> |
Unban — same resolution. |
/pm <user> <text> |
Send a private message (server-side support varies). |
/count |
Request a user-count broadcast. |
/whois <user> |
Look up a username in the local UUID cache. |
/refresh-jwt |
Send RequestJWT and print the new token. |
Anything that doesn't start with / is sent as a public chat message.
One-shot subcommands
liquidchat send "deploy went out, watching graphs"
liquidchat token info # pretty table: name / uuid / exp / status
liquidchat token info --raw # raw header + payload JSON
liquidchat token validate # round-trip with the server
liquidchat token refresh # rotate and write back to profile
liquidchat token refresh --no-save > /tmp/new.jwt
liquidchat ban CheaterMcCheatface
liquidchat unban 069a79f444e94726a5befca90e38aaf5
liquidchat mojang uuid Notch # 069a79f4-44e9-4726-a5be-fca90e38aaf5
liquidchat mojang name 069a79f4-44e9-4726-a5be-fca90e38aaf5
More examples
See the examples/ directory for runnable snippets
grouped by theme: basic.py (one-shot
send/validate), moderation.py (batch
ban + automod), bot.py (chat bots, custom
reconnect, user lookup), and mojang.py
(Mojang API fallback). The examples/README.md
also documents the ban/unban return-value contract in detail.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file liquidchat-0.8.8.tar.gz.
File metadata
- Download URL: liquidchat-0.8.8.tar.gz
- Upload date:
- Size: 156.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9315d3858af7aed8c7ee97233f64c594d53f6560372d83318ed27c17cc75874
|
|
| MD5 |
1fb7d7db397bddcfad0aecc860b3703f
|
|
| BLAKE2b-256 |
77c56dfbf75b51ae045d6ea9f183b15cd3165d5b2524a4c30a09313b4d940231
|
File details
Details for the file liquidchat-0.8.8-py3-none-any.whl.
File metadata
- Download URL: liquidchat-0.8.8-py3-none-any.whl
- Upload date:
- Size: 48.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37bf7b52cb8d9100566766fabcb54302b97a6720632dbfcb54a35c59ca03e00b
|
|
| MD5 |
c2ac1ad793312bf7612d3399b83236e9
|
|
| BLAKE2b-256 |
aa14d35d60cd51b0b9ffa21756c55b2ffd218dbfd21a275983e9e1d8965fd2cd
|