Skip to main content

LiquidChat websocket client library (chat.liquidbounce.net)

Project description

liquidchat

CI Python License: MIT Typed

⚠️ 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 to Handlers callbacks, 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 strict mode on src/).
  • Pydantic v2 frozen models for the wire format.
  • Tagged-union parsing via parse_message() returning LiquidChatMessage.
  • No silent ssl.CERT_NONE — verified TLS by default; opt-in insecure_ssl=True.
  • Singletons removed; instantiate clients explicitly.
  • Reconnection extracted into a ReconnectPolicy dataclass.
  • 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:

  1. --token <jwt> (explicit).
  2. LIQUIDCHAT_TOKEN env var.
  3. profiles/<name>/jwt for the selected profile, where <name> is chosen by: --accountLIQUIDCHAT_ACCOUNT env → default pointer 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.net deployment 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-insecure against 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 app 0add8caf-…).
  • v1 / Live-Connect compressed (MBI_SSL scope): java, bedrock-win32 (currently broken upstream — returns invalid_request even 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:

  1. Walks you through MSA device-code authentication via mcapi-auth (the refresh token is staged to a temp file when --account is omitted, then moved into profiles/<username>/refresh_token.json once the MSA flow reveals the username).
  2. Opens the websocket and runs RequestMojangInfosessionserver joinLoginMojangRequestJWT back-to-back.
  3. Writes the resulting JWT to profiles/<name>/jwt and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

liquidchat-0.8.8.tar.gz (156.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

liquidchat-0.8.8-py3-none-any.whl (48.6 kB view details)

Uploaded Python 3

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

Hashes for liquidchat-0.8.8.tar.gz
Algorithm Hash digest
SHA256 b9315d3858af7aed8c7ee97233f64c594d53f6560372d83318ed27c17cc75874
MD5 1fb7d7db397bddcfad0aecc860b3703f
BLAKE2b-256 77c56dfbf75b51ae045d6ea9f183b15cd3165d5b2524a4c30a09313b4d940231

See more details on using hashes here.

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

Hashes for liquidchat-0.8.8-py3-none-any.whl
Algorithm Hash digest
SHA256 37bf7b52cb8d9100566766fabcb54302b97a6720632dbfcb54a35c59ca03e00b
MD5 c2ac1ad793312bf7612d3399b83236e9
BLAKE2b-256 aa14d35d60cd51b0b9ffa21756c55b2ffd218dbfd21a275983e9e1d8965fd2cd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page