nandi/frqpublic Fork 0
1b14d22
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.

The name the server settles on is the name we use

Signed in with Bluesky and still called `frq-guest`. Two causes, and
both are something dropped rather than something broken.

There was no `NICK` handler. freeq hands a guest a name of its choosing
and settles a signed-in connection on the account's — the registration
goes out before SASL finishes, so being renamed afterwards is the normal
path rather than an edge — and nothing followed it. The client went on
calling itself what it had asked to be called, which also means every
"is this mine?" test on a line said no, because those compare nicks: no
edit chip on your own messages, and your own lines treated as somebody
else's. The Clojure had this and the port lost it, along with
`members.renameUser`, which is back too: the prefix travels with the
rename, because an op who renames is still an op.

And on the web, `prepare` asks the PDS who the token belongs to — which
can be the first time anyone learns the handle, since a sign-in whose
`whoami` failed at the time leaves a session with a token and no name.
The host handed the core the proof out of that answer and threw the rest
away, so a nameless session stayed nameless and connected as whatever
was in the nick box. It hands over the session now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-20T11:17:21-07:00 Browse files
1b14d22 parent: 94d9710
modified flutter/web/frq_host.js +11 -1
@@ -99,7 +99,17 @@
9999 }
100100 if (frq.needProof()) {
101101 frqOauth.prepare().then(
102- function (s) { frq.proofReady(s.dpopProof); },
102+ function (s) {
103+ // The whole session, not just the proof. `prepare` asks the PDS
104+ // who the token belongs to, and that answer can be the first time
105+ // anyone here learns the handle — a sign-in whose `whoami` failed
106+ // at the time has a session with a token and no name, and a
107+ // nameless session connects as whatever is in the nick box, which
108+ // is `frq-guest`. Handing the proof over on its own threw the
109+ // answer away every time.
110+ frq.restoreSession(JSON.stringify(s));
111+ frq.proofReady(s.dpopProof);
112+ },
103113 function (e) {
104114 frq.signInFailed(String(e && e.message ? e.message : e));
105115 frq.proofReady("");
@@ -99,7 +99,17 @@
99 }99 }
100 if (frq.needProof()) {100 if (frq.needProof()) {
101 frqOauth.prepare().then(101 frqOauth.prepare().then(
102- function (s) { frq.proofReady(s.dpopProof); },102+ function (s) {
103+ // The whole session, not just the proof. `prepare` asks the PDS
104+ // who the token belongs to, and that answer can be the first time
105+ // anyone here learns the handle — a sign-in whose `whoami` failed
106+ // at the time has a session with a token and no name, and a
107+ // nameless session connects as whatever is in the nick box, which
108+ // is `frq-guest`. Handing the proof over on its own threw the
109+ // answer away every time.
110+ frq.restoreSession(JSON.stringify(s));
111+ frq.proofReady(s.dpopProof);
112+ },
103 function (e) {113 function (e) {
104 frq.signInFailed(String(e && e.message ? e.message : e));114 frq.signInFailed(String(e && e.message ? e.message : e));
105 frq.proofReady("");115 frq.proofReady("");
modified nim/src/frq/members.nim +11 -0
@@ -64,6 +64,17 @@ proc withMode*(users: var Table[string, string], modes: string,
6464 # the next letter reads somebody else's nick.
6565 i += 1
6666
67+proc renameUser*(users: var Table[string, string], old, fresh: string): bool =
68+ ## Somebody changed their nick, in this room. True where they were in it.
69+ ##
70+ ## The prefix travels with them: an op who renames is still an op, and
71+ ## dropping the `@` would take their mode off the list until the next NAMES.
72+ if old.len == 0 or fresh.len == 0 or not users.hasKey(old): return false
73+ let prefix = users[old]
74+ users.del(old)
75+ users[fresh] = prefix
76+ true
77+
6778 func prefixRank(prefix: string): int =
6879 ## Where a prefix sorts. No prefix is last, which is why this is not simply
6980 ## the index.
@@ -64,6 +64,17 @@ proc withMode*(users: var Table[string, string], modes: string,
64 # the next letter reads somebody else's nick.64 # the next letter reads somebody else's nick.
65 i += 165 i += 1
66 66
67+proc renameUser*(users: var Table[string, string], old, fresh: string): bool =
68+ ## Somebody changed their nick, in this room. True where they were in it.
69+ ##
70+ ## The prefix travels with them: an op who renames is still an op, and
71+ ## dropping the `@` would take their mode off the list until the next NAMES.
72+ if old.len == 0 or fresh.len == 0 or not users.hasKey(old): return false
73+ let prefix = users[old]
74+ users.del(old)
75+ users[fresh] = prefix
76+ true
77+
67 func prefixRank(prefix: string): int =78 func prefixRank(prefix: string): int =
68 ## Where a prefix sorts. No prefix is last, which is why this is not simply79 ## Where a prefix sorts. No prefix is last, which is why this is not simply
69 ## the index.80 ## the index.
modified nim/src/frq/reducer.nim +24 -0
@@ -864,6 +864,30 @@ proc drain*() =
864864 note(room, Message(frm: "*", text: who & " left", at: at,
865865 system: true))
866866
867+ of "NICK":
868+ # Somebody is called something else now — including us.
869+ #
870+ # Our own rename is the server settling what we are called, and it is
871+ # the usual way the nick on screen becomes the real one: freeq hands a
872+ # guest a name of its choosing, and settles a signed-in connection on
873+ # the account's. Without this the client goes on calling itself what it
874+ # asked to be called, which is how a reader signs in with Bluesky and
875+ # finds they are still `frq-guest` — and why every "is this mine?" test
876+ # on a line then says no.
877+ let who = nickOf(p.prefix)
878+ let fresh = if p.params.len >= 1: p.params[^1] else: ""
879+ if who.len > 0 and fresh.len > 0:
880+ for name in toSeq(app.rooms.keys):
881+ var r = app.rooms[name]
882+ if r.users.renameUser(who, fresh):
883+ app.rooms[name] = r
884+ if who == app.formNick:
885+ app.formNick = fresh
886+ if app.status.startsWith("Connected") or
887+ app.status.startsWith("Signed in"):
888+ app.status = "Connected as " & fresh
889+ trace("auth", "the server calls us " & fresh)
890+
867891 of "353":
868892 # NAMES, into the PENDING list. It arrives over as many lines as it
869893 # takes and ends with 366; replacing `users` on each would empty the
@@ -864,6 +864,30 @@ proc drain*() =
864 note(room, Message(frm: "*", text: who & " left", at: at,864 note(room, Message(frm: "*", text: who & " left", at: at,
865 system: true))865 system: true))
866 866
867+ of "NICK":
868+ # Somebody is called something else now — including us.
869+ #
870+ # Our own rename is the server settling what we are called, and it is
871+ # the usual way the nick on screen becomes the real one: freeq hands a
872+ # guest a name of its choosing, and settles a signed-in connection on
873+ # the account's. Without this the client goes on calling itself what it
874+ # asked to be called, which is how a reader signs in with Bluesky and
875+ # finds they are still `frq-guest` — and why every "is this mine?" test
876+ # on a line then says no.
877+ let who = nickOf(p.prefix)
878+ let fresh = if p.params.len >= 1: p.params[^1] else: ""
879+ if who.len > 0 and fresh.len > 0:
880+ for name in toSeq(app.rooms.keys):
881+ var r = app.rooms[name]
882+ if r.users.renameUser(who, fresh):
883+ app.rooms[name] = r
884+ if who == app.formNick:
885+ app.formNick = fresh
886+ if app.status.startsWith("Connected") or
887+ app.status.startsWith("Signed in"):
888+ app.status = "Connected as " & fresh
889+ trace("auth", "the server calls us " & fresh)
890+
867 of "353":891 of "353":
868 # NAMES, into the PENDING list. It arrives over as many lines as it892 # NAMES, into the PENDING list. It arrives over as many lines as it
869 # takes and ends with 366; replacing `users` on each would empty the893 # takes and ends with 366; replacing `users` on each would empty the
modified nim/tests/tsession.nim +34 -0
@@ -242,3 +242,37 @@ suite "how big the window is":
242242 dispatch(%*{"id": "window.size", "value": "banana"})
243243 dispatch(%*{"id": "window.size", "value": "0x0"})
244244 check app.windowWidth == 1280
245+
246+suite "being renamed":
247+ setup: reset()
248+
249+ test "the server settling our name is the name we use":
250+ # freeq hands a guest a name of its choosing and settles a signed-in
251+ # connection on the account's. Nothing followed that, so a reader could
252+ # sign in with Bluesky and go on being `frq-guest` — and every "is this
253+ # mine?" test on a line said no, because it compares nicks.
254+ joined("#freeq")
255+ say(":alice!a@h NICK alice.bsky.social")
256+ check app.formNick == "alice.bsky.social"
257+
258+ test "and somebody else's rename follows them round the room":
259+ joined("#freeq")
260+ say(":irc.freeq.at 353 alice = #freeq :alice @bob carol",
261+ ":irc.freeq.at 366 alice #freeq :End of /NAMES list",
262+ ":bob!b@h NICK robert")
263+ check app.rooms["#freeq"].users.hasKey("robert")
264+ check not app.rooms["#freeq"].users.hasKey("bob")
265+
266+ test "with the mode they had":
267+ # An op who renames is still an op; dropping the prefix would take their
268+ # mode off the list until the next NAMES.
269+ joined("#freeq")
270+ say(":irc.freeq.at 353 alice = #freeq :alice @bob",
271+ ":irc.freeq.at 366 alice #freeq :End of /NAMES list",
272+ ":bob!b@h NICK robert")
273+ check app.rooms["#freeq"].users["robert"] == "@"
274+
275+ test "and somebody we have never seen changes nothing":
276+ joined("#freeq")
277+ say(":stranger!s@h NICK someoneelse")
278+ check app.formNick == "alice"
@@ -242,3 +242,37 @@ suite "how big the window is":
242 dispatch(%*{"id": "window.size", "value": "banana"})242 dispatch(%*{"id": "window.size", "value": "banana"})
243 dispatch(%*{"id": "window.size", "value": "0x0"})243 dispatch(%*{"id": "window.size", "value": "0x0"})
244 check app.windowWidth == 1280244 check app.windowWidth == 1280
245+
246+suite "being renamed":
247+ setup: reset()
248+
249+ test "the server settling our name is the name we use":
250+ # freeq hands a guest a name of its choosing and settles a signed-in
251+ # connection on the account's. Nothing followed that, so a reader could
252+ # sign in with Bluesky and go on being `frq-guest` — and every "is this
253+ # mine?" test on a line said no, because it compares nicks.
254+ joined("#freeq")
255+ say(":alice!a@h NICK alice.bsky.social")
256+ check app.formNick == "alice.bsky.social"
257+
258+ test "and somebody else's rename follows them round the room":
259+ joined("#freeq")
260+ say(":irc.freeq.at 353 alice = #freeq :alice @bob carol",
261+ ":irc.freeq.at 366 alice #freeq :End of /NAMES list",
262+ ":bob!b@h NICK robert")
263+ check app.rooms["#freeq"].users.hasKey("robert")
264+ check not app.rooms["#freeq"].users.hasKey("bob")
265+
266+ test "with the mode they had":
267+ # An op who renames is still an op; dropping the prefix would take their
268+ # mode off the list until the next NAMES.
269+ joined("#freeq")
270+ say(":irc.freeq.at 353 alice = #freeq :alice @bob",
271+ ":irc.freeq.at 366 alice #freeq :End of /NAMES list",
272+ ":bob!b@h NICK robert")
273+ check app.rooms["#freeq"].users["robert"] == "@"
274+
275+ test "and somebody we have never seen changes nothing":
276+ joined("#freeq")
277+ say(":stranger!s@h NICK someoneelse")
278+ check app.formNick == "alice"