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>
1b14d22 parent: 94d9710 modified
flutter/web/frq_host.js +11 -1 | @@ -99,7 +99,17 @@ | ||
| 99 | 99 | } |
| 100 | 100 | if (frq.needProof()) { |
| 101 | 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 | 113 | function (e) { |
| 104 | 114 | frq.signInFailed(String(e && e.message ? e.message : e)); |
| 105 | 115 | 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, | ||
| 64 | 64 | # the next letter reads somebody else's nick. |
| 65 | 65 | 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 | 78 | func prefixRank(prefix: string): int = |
| 68 | 79 | ## Where a prefix sorts. No prefix is last, which is why this is not simply |
| 69 | 80 | ## 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 += 1 | 65 | 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 simply | 79 | ## 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*() = | ||
| 864 | 864 | note(room, Message(frm: "*", text: who & " left", at: at, |
| 865 | 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 | 891 | of "353": |
| 868 | 892 | # NAMES, into the PENDING list. It arrives over as many lines as it |
| 869 | 893 | # 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 it | 892 | # 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 the | 893 | # 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": | ||
| 242 | 242 | dispatch(%*{"id": "window.size", "value": "banana"}) |
| 243 | 243 | dispatch(%*{"id": "window.size", "value": "0x0"}) |
| 244 | 244 | 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 == 1280 | 244 | 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" | ||