The path dependency on ../bbcode-rs only resolved on a machine where the two checkouts happened to sit side by side; anywhere else the build died with "failed to read /path/to/bbcode-rs/Cargo.toml". Now fetched from https://github.com/usips/bbcode-rs, which is publicly readable, and pinned to rev 8b0c374 so a force-push upstream cannot silently change what this compiles against. Not crates.io: the `bbcode` name there is an unrelated project (a BBCode-to-HTML compiler by ChrisMacNaughton, v1.0.2), so publishing this parser would need a different name. Verified by building and running the tests with ~/Source/bbcode-rs moved out of the way entirely. Co-Authored-By: Claude Fable 5 <[email protected]>
SockChat
A terminal chat client, in Rust. Connects over Tor to the hidden service by default.
This is a rewrite of the Go sockchat client. It talks the same protocol and keeps the same key bindings, with Tor built in rather than assumed.
Building
cargo build --release
The binary lands in target/release/sockchat.
Usage
sockchat --username "your name"
You'll be asked for your password once. The session is then saved to
~/.config/sockchat-rs/session.json (owner-only), so subsequent runs start
straight up. --logout forgets it.
The password is never written to disk. To avoid the prompt — for a script, or a client that starts unattended — put it in the environment instead:
SOCKCHAT_PASSWORD=... sockchat --username "your name"
Two-factor authentication
If the account has 2FA, you'll be asked for the six digits after the password.
That happens once per machine: the client tells the server to trust it, which
issues an xf_tfa_trust cookie that is saved with the session.
To skip even that first prompt, put the TOTP secret in the config file:
{ "totp_secret": "GEZD GNBV GY3T QOJQ ..." }
That's the base32 string behind the "can't scan the code?" / "enter this key manually" link when you set 2FA up — the same seed your authenticator app holds. The client then generates the codes itself and login is fully unattended. It is verified against the RFC 6238 test vectors, and it handles the formats sites display (lowercase, spaced, hyphenated).
Worth being deliberate about: storing the seed next to the saved session puts both factors on one machine, which is most of what 2FA is protecting against. For a personal machine that's usually the trade you want; for anything shared, leave it out and type the six digits.
Email and backup codes can't be derived from a seed, so those still prompt.
Cookies instead
The old flow still works if you'd rather not give the client your password:
sockchat --cookies="$COOKIES"
Where $COOKIES is the raw cookies value from the site's request header — use
the network tab in your browser's dev tools, find the chat.ws connection, and
copy the Cookie header. It looks like:
xf_tfa_trust=VALUE; xf_user=VALUE; xf_csrf=VALUE; xf_session=VALUE
You don't need all of these. The downside is that when the session expires you
have to fetch them again by hand; with --username the client just logs back in.
Tor
Tor is embedded — there is no tor daemon to install or run. The client
bootstraps its own Tor client (Arti)
on startup, which takes anywhere from a few seconds to a minute the first time
while it fetches a directory consensus. Subsequent runs reuse the cache in
~/.config/sockchat-rs/tor-cache.
If you'd rather use a Tor you already run:
sockchat --tor proxy # 127.0.0.1:9050
sockchat --proxy socks5h://127.0.0.1:9150 # Tor Browser
Use socks5h, not socks5, so hostnames resolve inside Tor.
Clearnet
sockchat --clearnet
This targets kiwifarms.st:9443 over TLS. Tor still carries the connection
unless you also pass --no-tor.
Keys
| Key | Action |
|---|---|
Enter |
Send |
Ctrl-R |
Switch room |
Tab |
Expand @<user id> to @username, |
Shift-Tab |
Toggle lurker mode (hides the input box) |
F5 |
Reconnect |
Up / Down |
Input history |
PageUp / PageDown |
Scroll |
Ctrl-Home / Ctrl-End |
Jump to oldest / newest |
Ctrl-A Ctrl-E Ctrl-U Ctrl-K Ctrl-W |
Line editing |
Esc |
Clear input |
Ctrl-C |
Quit |
Mentioning users by numeric ID works anywhere in a message and more than once:
@160024 stfu becomes @y a t s, stfu on Tab. Unrecognised IDs are left
alone.
Rooms
Ctrl-R opens a switcher: type a room number and press Enter, or pick one from
the list with the arrow keys. Typing /join 5 still works too.
The list is fetched from the site on startup, so it shows the real rooms — General, Lolcows, Beauty Parlor, and so on — in the order the site lists them. The current room's name also appears in the status bar.
You can override any name, or add one for a room the site doesn't advertise:
{ "rooms": { "8": "the gunt zone" } }
Rooms you visit that aren't in the list get added to it for the session, and if the list can't be fetched the switcher still accepts a typed number.
Configuration
~/.config/sockchat-rs/config.json, created on first run:
{
"username": "",
"password": "",
"totp_secret": "",
"cookies": "",
"host": "kiwifarmsaaf4t2h7gc3dfc5ojhmqruw2nit3uejrpiagrxeuxiyxcyd.onion",
"port": 443,
"tls": true,
"tor": "embedded",
"proxy": "socks5h://127.0.0.1:9050",
"read_only": false,
"room": 1,
"user_id": -1,
"user_agent": "Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0"
}
Set username here to avoid passing --username every time. CLI flags override
the file but are not written back to it, so they stay one-off.
password is supported but empty by default, and best left that way — the
prompt and SOCKCHAT_PASSWORD exist so it never has to touch the disk.
user_id is only used for mention notifications and is read from your xf_user
cookie automatically when left at -1.
If you used the Go client, its cookies, room, user ID and read-only setting are
imported from ~/.config/sockchat/config.json on first run. Host and transport
settings are deliberately not imported, and the Go client's config is never
written to.
Notes
- Proof of work. The site sits behind Tartarus/KiwiFlare. When a request is
answered with HTTP 203, the client solves the SHA-256 challenge on all cores
and submits it, then carries the resulting
ttrs_clearancecookie onto the websocket. This is automatic; the status line shows when it happens. Against the live service a difficulty-17 challenge solves in about 10ms. - Cohort relays are not used. Tartarus's cohort system is a browser Service
Worker failover path for when the origin is unreachable — it relays through
separate hosts using a credential minted during login. It is an alternative to
a cookie session, not an extra gate, and does not apply to a client that holds
a session cookie and reaches the origin directly. See
CLAUDE.mdfor the details and for what implementing it would require. - The hidden service speaks TLS. It serves a publicly trusted certificate
for the
.onionname on port 443, and 308-redirects port 80 to it, so the client useswss://there just as it does on clearnet. Redirects are followed during the websocket handshake, which tungstenite does not do by itself. - Logs. The TUI owns the terminal, so nothing is printed to stdout. Errors
go to
~/.config/sockchat-rs/error_logs/, and the file is deleted on exit if nothing was written to it. Anything you need to see appears in the chat feed as asockchat:line. - Notifications fire on mentions and whispers. They are suppressed while the terminal has focus — you're already looking at it — and for history the server replays after a reconnect. Focus is detected through terminal focus reporting; terminals that don't support it simply always notify.
- BBCode is parsed by bbcode-rs,
fetched from git and pinned to a revision. Bold, italics, underline,
strikethrough, colour and spoilers become terminal styling; spoilers are
dimmed rather than hidden. Layout tags that mean nothing in a terminal
(
[size],[heading],[center],[font], …) are dropped while keeping their text.[br]starts an indented continuation line. - Links from
[url],[img]and bare URLs render underlined in cyan, which is what terminals with URL autodetection need in order to make them clickable. True OSC-8 hyperlinks aren't possible here: ratatui renders into a cell buffer, and embedding escape sequences in cell content would corrupt its layout and diffing.
Testing
cargo test # unit tests, no network
cargo test --test probe -- --ignored --nocapture # live probes over Tor
The probes bootstrap real Tor and talk to the live service. They are read-only: they fetch pages, solve the gate, and open a socket. Nothing is posted.