nandi/frqpublic Fork 0
4aa71e7f6e1d802baf3c1f6098e5ed1c2d998a7b
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Remember an OAuth sign-in across restarts 4aa71e7 · on 4aa71e7f6e1d802baf3c1f6098e5ed1c2d998a7b · nandi · 20d ago
README.md · 111 lines · 4.8 KBmarkdown
Blame HistoryOpen raw

frq

A freeq client written in
jolt, as
glimmer components painted by
Vidya/egui.

It is a proof of concept port of sleek, which is the same client in
Rust against egui directly. The screens are sleek's — connect, chats, chat,
discover, settings, under a tab bar — but each is hiccup over glimmer's widget
tags rather than immediate-mode drawing code, and state lives in ratoms instead
of an AppState struct.

src/frq/atproto.jolt handle → DID → PDS → session, and the SASL payloads
src/frq/oauth.jolt   the broker flow: login URL, loopback capture, /session
src/frq/store.jolt   the saved sign-in, mode 600 in the config directory
src/frq/irc.jolt     IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
src/frq/state.jolt   the ratoms every screen reads, and `apply-msg!`
src/frq/app.jolt     the screens

Running

libvidya from Vidya's Rust/egui backend, then the app:

just lib
just run

just run is jolt -M:frq with LD_LIBRARY_PATH pointed at the built
library. It connects to irc.freeq.at:6697 over TLS and joins #test. Untick
TLS on the connect screen (or point it at 127.0.0.1) for a local server's
plain listener:

cargo run --release --bin freeq-server        # in the freeq checkout

Signing in

Three modes on the connect screen.

Bluesky (OAuth, the default way in) follows sleek's flow: frq binds a
loopback port, puts it in return_to, and opens
auth.freeq.at/auth/login?handle=…. The broker runs the OAuth dance with the
PDS and redirects back to that port with the handoff in the URL fragment, so
it never reaches a server as a query string. The page frq serves there has one
job: POST the fragment back to itself. What comes back is a single-use SASL
web-token and a durable broker_token; later connections mint a fresh token
from the durable one at /session and skip the browser.

The durable token is saved to $XDG_CONFIG_HOME/frq/session.edn (mode 600) so
a restart resumes without one, along with the handle and nick it belongs to.
The web-token beside it is single-use and deliberately not saved. A token the
broker no longer honours is dropped — from disk and memory — and the browser
flow runs once more, rather than failing the same way on every Connect.

App password signs in without a browser, straight to the user's own PDS:
resolveHandle → DID → PDS from the DID document → createSession. The
password goes to that PDS and nowhere else, is never written to disk, and is
dropped once the session exists.

Either way freeq sees only a token. The SASL mechanism is
ATPROTO-CHALLENGE in both cases — method: "web-token", which the server
resolves through its own token store, or method: "pds-session" with the
server's nonce echoed back so the token cannot be replayed elsewhere.

A refused sign-in is reported and the connection carries on as a guest.

Android

An APK with two shared libraries and no Java: libvidya.so (vidya's Rust/egui
C ABI, which owns the event loop as the NativeActivity's own library) and
libjoltapp.so (frq compiled to a Chez boot image). Both native halves come
from the vidya checkout; only the boot image is frq's.

./android/build-apk.sh run      # build, install, launch on a connected device
./android/build-apk.sh log      # logcat, filtered

Needs what vidya's Android build needs — SDK, NDK r29, and a cross-built Chez
in ~/.cache/vidya-chez-android.

TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and
Android has no public libssl to load. The connect screen falls back to the
plain :6667 listener on its own, which is why the plain transport is the raw
socket/connect/send/recv calls rather than jolt's java.net.Socket
surface — that surface does not work on Android either, while the syscalls do.

What the PoC covers

  • TLS (:6697, via jolt.mvn-http's OpenSSL bindings) or plain TCP (:6667)
  • Guest connect (NICK/USER), 001 welcome, PING/PONG keepalive
  • Auto-joins #test on irc.freeq.at
  • Join channels, channel buffers with unread counts, send and receive PRIVMSG
  • Join/part notices, DMs bucketed under the sender's nick
  • Discover list, search over buffers, disconnect

Limits

  • TLS and plain TCP only — no WebSocket, no iroh. On Android, plain only.
  • No did:key signing, no credential gates, no E2EE. Sign-in of either
    kind needs TLS, so it is desktop-only — the Android build connects as a
    guest.
  • Only the broker token is persisted, and only for OAuth. An app-password
    sign-in is not remembered.
  • No scrollback trimming, avatars, reactions, threads, or calls.
  • Message lists are keyed vboxes; glimmer-vidya has no :listbox yet.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# frq

A **[freeq](https://github.com/codegod100/freeq)** client written in
**[jolt](https://github.com/jolt-lang/jolt)**, as
[glimmer](https://github.com/jolt-lang/glimmer) components painted by
**[Vidya](https://tangled.org/nandi.uk/vidya)**/egui.

It is a proof of concept port of [sleek](../sleek), which is the same client in
Rust against egui directly. The screens are sleek's — connect, chats, chat,
discover, settings, under a tab bar — but each is hiccup over glimmer's widget
tags rather than immediate-mode drawing code, and state lives in ratoms instead
of an `AppState` struct.

```
src/frq/atproto.jolt handle → DID → PDS → session, and the SASL payloads
src/frq/oauth.jolt   the broker flow: login URL, loopback capture, /session
src/frq/store.jolt   the saved sign-in, mode 600 in the config directory
src/frq/irc.jolt     IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
src/frq/state.jolt   the ratoms every screen reads, and `apply-msg!`
src/frq/app.jolt     the screens
```

## Running

`libvidya` from Vidya's Rust/egui backend, then the app:

```bash
just lib
just run
```

`just run` is `jolt -M:frq` with `LD_LIBRARY_PATH` pointed at the built
library. It connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick
TLS on the connect screen (or point it at `127.0.0.1`) for a local server's
plain listener:

```bash
cargo run --release --bin freeq-server        # in the freeq checkout
```

## Signing in

Three modes on the connect screen.

**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
loopback port, puts it in `return_to`, and opens
`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
PDS and redirects back to that port with the handoff in the URL *fragment*, so
it never reaches a server as a query string. The page frq serves there has one
job: POST the fragment back to itself. What comes back is a single-use SASL
`web-token` and a durable `broker_token`; later connections mint a fresh token
from the durable one at `/session` and skip the browser.

The durable token is saved to `$XDG_CONFIG_HOME/frq/session.edn` (mode 600) so
a restart resumes without one, along with the handle and nick it belongs to.
The web-token beside it is single-use and deliberately not saved. A token the
broker no longer honours is dropped — from disk and memory — and the browser
flow runs once more, rather than failing the same way on every Connect.

**App password** signs in without a browser, straight to the user's own PDS:
`resolveHandle` → DID → PDS from the DID document → `createSession`. The
password goes to that PDS and nowhere else, is never written to disk, and is
dropped once the session exists.

Either way freeq sees only a token. The SASL mechanism is
`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
resolves through its own token store, or `method: "pds-session"` with the
server's nonce echoed back so the token cannot be replayed elsewhere.

A refused sign-in is reported and the connection carries on as a guest.

## Android

An APK with two shared libraries and no Java: `libvidya.so` (vidya's Rust/egui
C ABI, which owns the event loop as the NativeActivity's own library) and
`libjoltapp.so` (frq compiled to a Chez boot image). Both native halves come
from the vidya checkout; only the boot image is frq's.

```bash
./android/build-apk.sh run      # build, install, launch on a connected device
./android/build-apk.sh log      # logcat, filtered
```

Needs what vidya's Android build needs — SDK, NDK r29, and a cross-built Chez
in `~/.cache/vidya-chez-android`.

TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and
Android has no public `libssl` to load. The connect screen falls back to the
plain `:6667` listener on its own, which is why the plain transport is the raw
`socket`/`connect`/`send`/`recv` calls rather than jolt's `java.net.Socket`
surface — that surface does not work on Android either, while the syscalls do.

## What the PoC covers

* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
* Auto-joins `#test` on `irc.freeq.at`
* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
* Join/part notices, DMs bucketed under the sender's nick
* Discover list, search over buffers, disconnect

## Limits

* **TLS and plain TCP only** — no WebSocket, no iroh. On Android, plain only.
* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
  kind needs TLS, so it is desktop-only — the Android build connects as a
  guest.
* **Only the broker token is persisted**, and only for OAuth. An app-password
  sign-in is not remembered.
* **No scrollback trimming, avatars, reactions, threads, or calls.**
* Message lists are keyed vboxes; glimmer-vidya has no `:listbox` yet.