nandi/frqpublic Fork 0
13e70a45431157ad7c7167223100e62a5bd3473d
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.

README.md · 199 lines · 9.9 KBmarkdown Blame HistoryRaw
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago1# frq
2
3A **[freeq](https://github.com/codegod100/freeq)** client written in
4**[jolt](https://github.com/jolt-lang/jolt)**, as
5[glimmer](https://github.com/jolt-lang/glimmer) components painted by
6**[Vidya](https://tangled.org/nandi.uk/vidya)**/egui.
7
8It is a proof of concept port of [sleek](../sleek), which is the same client in
9Rust against egui directly. The screens are sleek's — connect, chats, chat,
10discover, settings, under a tab bar — but each is hiccup over glimmer's widget
11tags rather than immediate-mode drawing code, and state lives in ratoms instead
12of an `AppState` struct.
13
14```
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago15src/frq/atproto.jolt handle → DID → PDS → session, and the SASL payloads
16src/frq/oauth.jolt the broker flow: login URL, loopback capture, /session
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago17src/frq/store.jolt the saved sign-in, mode 600 in the config directory
Show who is talking, with their Bluesky picture f5548bd nandi 20d ago18src/frq/avatars.jolt profile pictures, by DID or handle
Show the pictures people paste 56cdac3 nandi 20d ago19src/frq/media.jolt image links: spot them, fetch them once, cache on disk
Send a picture by pasting it 8a491ba nandi 20d ago20src/frq/upload.jolt a pasted picture to freeq's media endpoint, as multipart
Calls f31ad3d nandi 19d ago21src/frq/av.jolt calls: the signaling, and a handle on the media plane
Say when each thing was said dbf3b86 nandi 20d ago22src/frq/clock.jolt the reader's own zone, twelve-hour times, day headings
Pick any emoji, in colour 18c5ccd nandi 20d ago23src/frq/emoji.jolt the picker's catalog: every drawable emoji and its name
Sign in with a Bluesky identity 5192124 nandi 20d ago24src/frq/irc.jolt IRC over TLS or TCP: parser, reader thread, SASL, PRIVMSG
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago25src/frq/state.jolt the ratoms every screen reads, and `apply-msg!`
26src/frq/app.jolt the screens
27```
28
Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago29## Tracing
30
31`FRQ_TRACE=1` prints every IRC line sent and received to stderr, which on
32Android is logcat.
33
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago34## Running
35
Take both native libraries from one place a66d1c1 nandi 19d ago36Both native libraries, then the app:
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago37
38```bash
39just lib
40just run
41```
42
Take both native libraries from one place a66d1c1 nandi 19d ago43`just lib` builds [jolt-native](https://gitlab.com/nandithebull/jolt-native),
44which is where every shared object this client loads comes from: `libvidya`,
45the retained-tree ABI glimmer paints through, and `libjoltmoq`, the AV media
46plane. They come out of one directory, and `just run` puts that one directory
47on the loader path.
Calls f31ad3d nandi 19d ago48
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago49`just run` is `jolt -M:frq` with `LD_LIBRARY_PATH` pointed at the built
50library. It connects to `irc.freeq.at:6697` over TLS and joins `#test`. Untick
51TLS on the connect screen (or point it at `127.0.0.1`) for a local server's
52plain listener:
53
54```bash
55cargo run --release --bin freeq-server # in the freeq checkout
56```
57
Sign in with a Bluesky identity 5192124 nandi 20d ago58## Signing in
59
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago60Three modes on the connect screen.
Sign in with a Bluesky identity 5192124 nandi 20d ago61
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago62**Bluesky** (OAuth, the default way in) follows sleek's flow: frq binds a
63loopback port, puts it in `return_to`, and opens
64`auth.freeq.at/auth/login?handle=…`. The broker runs the OAuth dance with the
65PDS and redirects back to that port with the handoff in the URL *fragment*, so
66it never reaches a server as a query string. The page frq serves there has one
67job: POST the fragment back to itself. What comes back is a single-use SASL
68`web-token` and a durable `broker_token`; later connections mint a fresh token
69from the durable one at `/session` and skip the browser.
Sign in with a Bluesky identity 5192124 nandi 20d ago70
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago71The durable token is saved to `$XDG_CONFIG_HOME/frq/session.edn` (mode 600) so
Connect on launch when an account is remembered 9237cf4 nandi 20d ago72a restart resumes without one, along with the handle and nick it belongs to —
73and it connects on its own at launch when one is there.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago74The web-token beside it is single-use and deliberately not saved. A token the
75broker no longer honours is dropped — from disk and memory — and the browser
76flow runs once more, rather than failing the same way on every Connect.
77
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago78**App password** signs in without a browser, straight to the user's own PDS:
79`resolveHandle` → DID → PDS from the DID document → `createSession`. The
80password goes to that PDS and nowhere else, is never written to disk, and is
81dropped once the session exists.
Sign in with a Bluesky identity 5192124 nandi 20d ago82
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago83Either way freeq sees only a token. The SASL mechanism is
84`ATPROTO-CHALLENGE` in both cases — `method: "web-token"`, which the server
85resolves through its own token store, or `method: "pds-session"` with the
86server's nonce echoed back so the token cannot be replayed elsewhere.
87
88A refused sign-in is reported and the connection carries on as a guest.
Sign in with a Bluesky identity 5192124 nandi 20d ago89
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago90## Android
91
92An APK with two shared libraries and no Java: `libvidya.so` (vidya's Rust/egui
93C ABI, which owns the event loop as the NativeActivity's own library) and
94`libjoltapp.so` (frq compiled to a Chez boot image). Both native halves come
95from the vidya checkout; only the boot image is frq's.
96
97```bash
98./android/build-apk.sh run # build, install, launch on a connected device
99./android/build-apk.sh log # logcat, filtered
100```
101
102Needs what vidya's Android build needs — SDK, NDK r29, and a cross-built Chez
103in `~/.cache/vidya-chez-android`.
104
105TLS does not work there: jolt reaches OpenSSL through the dynamic loader, and
106Android has no public `libssl` to load. The connect screen falls back to the
107plain `:6667` listener on its own, which is why the plain transport is the raw
108`socket`/`connect`/`send`/`recv` calls rather than jolt's `java.net.Socket`
109surface — that surface does not work on Android either, while the syscalls do.
110
111## What the PoC covers
112
113* TLS (`:6697`, via jolt.mvn-http's OpenSSL bindings) or plain TCP (`:6667`)
114* Guest connect (`NICK`/`USER`), `001` welcome, `PING`/`PONG` keepalive
115* Auto-joins `#test` on `irc.freeq.at`
116* Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
Ask for the backlog freeq restores channels without 94e59a2 nandi 20d ago117* Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
Say when each thing was said dbf3b86 nandi 20d ago118* Twelve-hour timestamps from the server's own clock, with a heading wherever
119 the day changes
Answer a message, not just read that one was answered 4876db8 nandi 20d ago120* A chip above a reply quoting what it answers, and a click that goes there;
121 ↩ beside a sender to answer them, with `+draft/reply` on the way out
Pick any emoji, in colour 18c5ccd nandi 20d ago122* Emoji reactions: colour pills under a message, ☺ beside the sender to open a
123 picker over every emoji Vidya can draw (popular first, then Unicode's own
124 groups, searchable by name), and a second click on a pill to take yours off
125 — sent as `TAGMSG`, and restored from the server's own tally when the
126 backlog comes back
Show the pictures people paste 56cdac3 nandi 20d ago127* Inline previews for PNG links, fetched once and cached under
Click a picture to see it full size 563ada3 nandi 20d ago128 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
Send a picture by pasting it 8a491ba nandi 20d ago129* Ctrl+V in the draft attaches the picture on the clipboard: it is previewed
130 under the box and uploaded to freeq's media endpoint while you write the line
131 it goes with, and only on the way out does it become the link — which is the
132 whole of what sending an image over IRC means. The draft itself is never
133 written into. Text pastes as text, as it always did: the picture path is the
134 keystroke the field had no text to answer with
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago135* Join/part notices, DMs bucketed under the sender's nick
136* Discover list, search over buffers, disconnect
Remember which rooms this client has been in 1d062fd nandi 20d ago137* The rooms you have opened, remembered across runs and listed in the order
138 you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
Order the chat list by what you were last in 75e557e nandi 20d ago139* Conversations listed most recently opened first
Show who is talking, with their Bluesky picture f5548bd nandi 20d ago140* Bluesky avatars beside the sender, resolved from the DID freeq tags each
141 message with
Calls f31ad3d nandi 19d ago142* Calls: a Call button opens one in a channel, a banner offers Join where
143 somebody already has, and in one there is mute, deafen, video and leave. Mute
144 and deafen are separate — a deafened microphone still carries your voice.
145 Whoever turns a camera on appears as a tile; the self-view is labelled You
146 and sits last, where it cannot push a face you are talking to off the row
147
148## Calls
149
150Signaling is IRC and lives here: `+freeq.at/av-start`, `av-join` and `av-leave`
151go out as TAGMSGs and the server broadcasts `+freeq.at/av-state` back, which is
152what actually moves this client's state — a press is optimistic, and the server
153settles it. Losing a race to open a call (`start-collision`) is answered by
154joining the call that won rather than by reporting an error, since the person
155asked to be in a call in that room and there is one.
156
157Media is not IRC and is not here. Audio and video ride MoQ — Media over QUIC —
158through freeq's SFU, and that is `libjoltmoq`: Opus, H.264, capture and
159transport, lifted out of sleek rather than written a second time in jolt.
160`src/frq/av.jolt` is the whole of what frq says to it, and two of its rules
161shape this side:
162
163* **Nothing calls back.** Status and video are polled, drained by a timer that
164 glimmer runs on the loop thread — the only thread allowed to touch a node.
165* **A frame is borrowed.** The decoder's own buffer is handed to Vidya as a
166 pointer and painted by an `:image` with a `:feed`. The pixels never become a
167 jolt value and are never copied on this side, which is the only way thirty
168 frames a second is affordable here.
169
170The SFU is dialled once the server has minted a token, not when we ask to join:
171a remote SFU refuses a connection without one, and the MoQ client then retries
172in a loop that looks exactly like a hang.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago173
174## Limits
175
176* **TLS and plain TCP only** — no WebSocket, no iroh. On Android, plain only.
Sign in with Bluesky OAuth, through freeq's broker 2d4a377 nandi 20d ago177* **No `did:key` signing, no credential gates, no E2EE.** Sign-in of either
178 kind needs TLS, so it is desktop-only — the Android build connects as a
179 guest.
Remember an OAuth sign-in across restarts 4aa71e7 nandi 20d ago180* **Only the broker token is persisted**, and only for OAuth. An app-password
181 sign-in is not remembered.
Show the pictures people paste 56cdac3 nandi 20d ago182* **Previews are PNG only** — the tree backend's decoder reads no other
183 format, and a fetch needs TLS, so the phone shows links. The link is left in
184 place either way.
185* **Nothing evicts the media cache.**
Calls f31ad3d nandi 19d ago186* **Calls are desktop-only.** `libjoltmoq` is not built for Android here, and
187 the camera and microphone paths that are would still need the runtime
188 permissions the APK does not ask for.
189* **One call at a time**, which is the media plane's rule and the microphone's.
190* **No call is offered in a DM** — freeq's AV signaling is a channel's.
Send a picture by pasting it 8a491ba nandi 20d ago191* **Pasting a picture needs a sign-in and a desktop.** The upload is filed
192 under the DID of a live session, so a guest cannot make one; and it is read
193 off the clipboard through the ABI's `vidya_clipboard_image_png`, which
194 arboard backs on desktop and nothing backs on Android. It also shares
195 nothing to your PDS and posts nothing to Bluesky — those fields are opt-in
196 and this client does not send them.
Calls f31ad3d nandi 19d ago197* **No scrollback trimming or threads.**
Stop mistaking a quiet connection for a closed one a225fb1 nandi 20d ago198* A sent line waits up to 200ms for the reader thread to flush it.
A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago199* Message lists are keyed vboxes; glimmer-vidya has no `:listbox` yet.