Find out who somebody is
Opening a profile did nothing for most people in a freeq room, and the reason was that this client had no way to learn a DID. It expected an `account` tag. freeq does not send one: the identity is in the hostmask, as `freeq/plc/ngokl2gn` — the first eight characters of a DID, which is enough to tell two people apart and not enough to look either of them up. So `actorFor` fell back to the nick, and a nick only works when it is itself a handle. `nandi.uk` resolved; `livecodelife` and `zapnap`, who are just as authenticated, did not. The server will say, in two places. `WHO <channel>` puts the whole DID in the realname field of each 352 — one round trip for a whole room, against a WHOIS per face on screen — and `WHOIS` answers 330 for one nick, which is the last resort for somebody who has left. Both land in `dids`, and the WHO goes out at 366 beside the history request, the moment a room is known to have arrived. Guests are not recorded: freeq puts the literal "IRC User" in that field for an unauthenticated connection, and storing it would be a nick with a DID of "IRC User". `did:key:` agents get an honest panel rather than a failure. There is no Bluesky profile behind one — `getProfile` can only 400 — and freeq has several of them in its rooms, so "Could not look did:key:z6Mkp… up" was this client reporting a request it should never have made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4b72ad1 parent: ec74784 modified
nim/src/frq/cells.nim +11 -0 | @@ -92,6 +92,17 @@ type | ||
| 92 | 92 | apiBearer*: string |
| 93 | 93 | loginUrl*: string ## shown while the browser is open |
| 94 | 94 | |
| 95 | + dids*: Table[string, string] | |
| 96 | + ## Nick → DID, as the server reports it. | |
| 97 | + ## | |
| 98 | + ## freeq sends no `account` tag: the identity is in the hostmask, and | |
| 99 | + ## the hostmask carries eight characters of a DID — too few to resolve. | |
| 100 | + ## The whole of it comes back from `WHO` (352, in the realname field) | |
| 101 | + ## and from `WHOIS` (330), and this is where it is kept. Without it a | |
| 102 | + ## nick that is not itself a handle — `livecodelife`, `zapnap` — has no | |
| 103 | + ## identity a profile can be looked up by, which is why opening one did | |
| 104 | + ## nothing. | |
| 105 | + | |
| 95 | 106 | # Rooms. |
| 96 | 107 | rooms*: OrderedTable[string, Room] |
| 97 | 108 | current*: string |
| @@ -92,6 +92,17 @@ type | |||
| 92 | apiBearer*: string | 92 | apiBearer*: string |
| 93 | loginUrl*: string ## shown while the browser is open | 93 | loginUrl*: string ## shown while the browser is open |
| 94 | 94 | ||
| 95 | + dids*: Table[string, string] | ||
| 96 | + ## Nick → DID, as the server reports it. | ||
| 97 | + ## | ||
| 98 | + ## freeq sends no `account` tag: the identity is in the hostmask, and | ||
| 99 | + ## the hostmask carries eight characters of a DID — too few to resolve. | ||
| 100 | + ## The whole of it comes back from `WHO` (352, in the realname field) | ||
| 101 | + ## and from `WHOIS` (330), and this is where it is kept. Without it a | ||
| 102 | + ## nick that is not itself a handle — `livecodelife`, `zapnap` — has no | ||
| 103 | + ## identity a profile can be looked up by, which is why opening one did | ||
| 104 | + ## nothing. | ||
| 105 | + | ||
| 95 | # Rooms. | 106 | # Rooms. |
| 96 | rooms*: OrderedTable[string, Room] | 107 | rooms*: OrderedTable[string, Room] |
| 97 | current*: string | 108 | current*: string |
modified
nim/src/frq/profile.nim +9 -0 | @@ -67,6 +67,15 @@ func isHandle*(nick: string): bool = | ||
| 67 | 67 | # The last label is the TLD: at least two characters, and all letters. |
| 68 | 68 | labels > 0 and labelLen >= 2 and lastAllAlpha |
| 69 | 69 | |
| 70 | +func isAgent*(actor: string): bool = | |
| 71 | + ## Whether this identity is a `did:key:` — an agent that signs with a key | |
| 72 | + ## of its own rather than an account in somebody's PDS. | |
| 73 | + ## | |
| 74 | + ## There is no Bluesky profile behind one, so asking for it is a request | |
| 75 | + ## that can only 400. freeq has several in its rooms; they are not broken | |
| 76 | + ## accounts and should not be reported as one. | |
| 77 | + actor.startsWith("did:key:") | |
| 78 | + | |
| 70 | 79 | proc actorFor*(did, nick: string): string = |
| 71 | 80 | ## The identity to look a profile up by, or "" when there is none. |
| 72 | 81 | ## |
| @@ -67,6 +67,15 @@ func isHandle*(nick: string): bool = | |||
| 67 | # The last label is the TLD: at least two characters, and all letters. | 67 | # The last label is the TLD: at least two characters, and all letters. |
| 68 | labels > 0 and labelLen >= 2 and lastAllAlpha | 68 | labels > 0 and labelLen >= 2 and lastAllAlpha |
| 69 | 69 | ||
| 70 | +func isAgent*(actor: string): bool = | ||
| 71 | + ## Whether this identity is a `did:key:` — an agent that signs with a key | ||
| 72 | + ## of its own rather than an account in somebody's PDS. | ||
| 73 | + ## | ||
| 74 | + ## There is no Bluesky profile behind one, so asking for it is a request | ||
| 75 | + ## that can only 400. freeq has several in its rooms; they are not broken | ||
| 76 | + ## accounts and should not be reported as one. | ||
| 77 | + actor.startsWith("did:key:") | ||
| 78 | + | ||
| 70 | proc actorFor*(did, nick: string): string = | 79 | proc actorFor*(did, nick: string): string = |
| 71 | ## The identity to look a profile up by, or "" when there is none. | 80 | ## The identity to look a profile up by, or "" when there is none. |
| 72 | ## | 81 | ## |
modified
nim/src/frq/reducer.nim +36 -2 | @@ -509,13 +509,22 @@ proc dispatch*(event: JsonNode) = | ||
| 509 | 509 | of "profile.open": |
| 510 | 510 | # `nick:actor`, and the actor may be empty — a guest has no identity to |
| 511 | 511 | # fetch, and the panel says so rather than spinning. |
| 512 | - let (nick, who) = split2(arg) | |
| 512 | + let (nick, argWho) = split2(arg) | |
| 513 | 513 | if nick.len > 0: |
| 514 | + # The screen passes what the message itself knows. Where that is | |
| 515 | + # nothing — no `account` tag, and a nick that is not handle-shaped — | |
| 516 | + # the map filled in by WHO is the answer, and a WHOIS is the last | |
| 517 | + # resort for somebody who has since left the room. | |
| 518 | + var who = argWho | |
| 519 | + if who.len == 0: who = app.dids.getOrDefault(nick, "") | |
| 520 | + if who.len == 0: send("WHOIS " & nick) | |
| 514 | 521 | app.profileViewing = ProfileView(has: true, nick: nick, actor: who) |
| 515 | 522 | # Blocking, and this is the one place that can afford it: the render |
| 516 | 523 | # path must not, the socket threads have their own work, and the reader |
| 517 | 524 | # pressed a face and is already waiting. |
| 518 | - if who.len > 0: fetch(who) | |
| 525 | + # A `did:key:` agent has no Bluesky profile to fetch, and the panel | |
| 526 | + # says so rather than showing a failure it caused itself. | |
| 527 | + if who.len > 0 and not isAgent(who): fetch(who) | |
| 519 | 528 | |
| 520 | 529 | of "profile.close": app.profileViewing = ProfileView() |
| 521 | 530 | |
| @@ -741,6 +750,31 @@ proc drain*() = | ||
| 741 | 750 | # for an empty one is a test that never passes. |
| 742 | 751 | if not r.messages.anyIt(not it.system): |
| 743 | 752 | send("CHATHISTORY LATEST " & room & " * " & $historyLimit) |
| 753 | + # And who these people actually are. One WHO answers for the whole | |
| 754 | + # room; the alternative is a WHOIS per nick, which is a round trip | |
| 755 | + # per face on screen. | |
| 756 | + send("WHO " & room) | |
| 757 | + | |
| 758 | + of "352": | |
| 759 | + # WHO: `<me> <chan> <user> <host> <server> <nick> <flags> :<hops> <real>` | |
| 760 | + # | |
| 761 | + # freeq puts the full DID in the realname field — `did:plc:…` for an | |
| 762 | + # account, `did:key:…` for an agent, and the literal "IRC User" for a | |
| 763 | + # guest, who has no identity at all. The hostmask beside it carries | |
| 764 | + # `freeq/plc/ngokl2gn`: the first eight characters, which is enough to | |
| 765 | + # tell two people apart and not enough to look either of them up. | |
| 766 | + if p.params.len >= 8: | |
| 767 | + let who = p.params[5] | |
| 768 | + let real = p.params[^1] | |
| 769 | + let sp = real.find(' ') # the hop count comes first | |
| 770 | + let did = if sp >= 0: real[sp + 1 .. ^1].strip() else: "" | |
| 771 | + if did.startsWith("did:"): app.dids[who] = did | |
| 772 | + | |
| 773 | + of "330": | |
| 774 | + # WHOIS's `<nick> <account> :is authenticated as`. The same DID by a | |
| 775 | + # different road — one nick rather than a room of them. | |
| 776 | + if p.params.len >= 3 and p.params[2].startsWith("did:"): | |
| 777 | + app.dids[p.params[1]] = p.params[2] | |
| 744 | 778 | |
| 745 | 779 | of "MODE": |
| 746 | 780 | # A channel MODE, for the letters that change how someone is listed. |
| @@ -509,13 +509,22 @@ proc dispatch*(event: JsonNode) = | |||
| 509 | of "profile.open": | 509 | of "profile.open": |
| 510 | # `nick:actor`, and the actor may be empty — a guest has no identity to | 510 | # `nick:actor`, and the actor may be empty — a guest has no identity to |
| 511 | # fetch, and the panel says so rather than spinning. | 511 | # fetch, and the panel says so rather than spinning. |
| 512 | - let (nick, who) = split2(arg) | 512 | + let (nick, argWho) = split2(arg) |
| 513 | if nick.len > 0: | 513 | if nick.len > 0: |
| 514 | + # The screen passes what the message itself knows. Where that is | ||
| 515 | + # nothing — no `account` tag, and a nick that is not handle-shaped — | ||
| 516 | + # the map filled in by WHO is the answer, and a WHOIS is the last | ||
| 517 | + # resort for somebody who has since left the room. | ||
| 518 | + var who = argWho | ||
| 519 | + if who.len == 0: who = app.dids.getOrDefault(nick, "") | ||
| 520 | + if who.len == 0: send("WHOIS " & nick) | ||
| 514 | app.profileViewing = ProfileView(has: true, nick: nick, actor: who) | 521 | app.profileViewing = ProfileView(has: true, nick: nick, actor: who) |
| 515 | # Blocking, and this is the one place that can afford it: the render | 522 | # Blocking, and this is the one place that can afford it: the render |
| 516 | # path must not, the socket threads have their own work, and the reader | 523 | # path must not, the socket threads have their own work, and the reader |
| 517 | # pressed a face and is already waiting. | 524 | # pressed a face and is already waiting. |
| 518 | - if who.len > 0: fetch(who) | 525 | + # A `did:key:` agent has no Bluesky profile to fetch, and the panel |
| 526 | + # says so rather than showing a failure it caused itself. | ||
| 527 | + if who.len > 0 and not isAgent(who): fetch(who) | ||
| 519 | 528 | ||
| 520 | of "profile.close": app.profileViewing = ProfileView() | 529 | of "profile.close": app.profileViewing = ProfileView() |
| 521 | 530 | ||
| @@ -741,6 +750,31 @@ proc drain*() = | |||
| 741 | # for an empty one is a test that never passes. | 750 | # for an empty one is a test that never passes. |
| 742 | if not r.messages.anyIt(not it.system): | 751 | if not r.messages.anyIt(not it.system): |
| 743 | send("CHATHISTORY LATEST " & room & " * " & $historyLimit) | 752 | send("CHATHISTORY LATEST " & room & " * " & $historyLimit) |
| 753 | + # And who these people actually are. One WHO answers for the whole | ||
| 754 | + # room; the alternative is a WHOIS per nick, which is a round trip | ||
| 755 | + # per face on screen. | ||
| 756 | + send("WHO " & room) | ||
| 757 | + | ||
| 758 | + of "352": | ||
| 759 | + # WHO: `<me> <chan> <user> <host> <server> <nick> <flags> :<hops> <real>` | ||
| 760 | + # | ||
| 761 | + # freeq puts the full DID in the realname field — `did:plc:…` for an | ||
| 762 | + # account, `did:key:…` for an agent, and the literal "IRC User" for a | ||
| 763 | + # guest, who has no identity at all. The hostmask beside it carries | ||
| 764 | + # `freeq/plc/ngokl2gn`: the first eight characters, which is enough to | ||
| 765 | + # tell two people apart and not enough to look either of them up. | ||
| 766 | + if p.params.len >= 8: | ||
| 767 | + let who = p.params[5] | ||
| 768 | + let real = p.params[^1] | ||
| 769 | + let sp = real.find(' ') # the hop count comes first | ||
| 770 | + let did = if sp >= 0: real[sp + 1 .. ^1].strip() else: "" | ||
| 771 | + if did.startsWith("did:"): app.dids[who] = did | ||
| 772 | + | ||
| 773 | + of "330": | ||
| 774 | + # WHOIS's `<nick> <account> :is authenticated as`. The same DID by a | ||
| 775 | + # different road — one nick rather than a room of them. | ||
| 776 | + if p.params.len >= 3 and p.params[2].startsWith("did:"): | ||
| 777 | + app.dids[p.params[1]] = p.params[2] | ||
| 744 | 778 | ||
| 745 | of "MODE": | 779 | of "MODE": |
| 746 | # A channel MODE, for the letters that change how someone is listed. | 780 | # A channel MODE, for the letters that change how someone is listed. |
modified
nim/src/frq/screens/chat.nim +12 -1 | @@ -141,7 +141,11 @@ proc messageBody(s: State, room: Room, m: Message, highlit: bool): Node = | ||
| 141 | 141 | # A face is a way in to who someone is, so it takes the press that opens |
| 142 | 142 | # them — and so does the name beside it, since a name is the thing a |
| 143 | 143 | # reader is actually looking at. |
| 144 | - let senderActor = actorFor(m.account, m.frm) | |
| 144 | + # The `account` tag where the server sends one, and what WHO reported | |
| 145 | + # for this nick where it does not — freeq is the second case. | |
| 146 | + let senderActor = actorFor( | |
| 147 | + if m.account.len > 0: m.account else: s.dids.getOrDefault(m.frm, ""), | |
| 148 | + m.frm) | |
| 145 | 149 | let open = "profile.open:" & m.frm & ":" & senderActor |
| 146 | 150 | var row = hbox(%*{"spacing": 6}, |
| 147 | 151 | avatar(m.avatar, m.frm, size = faceSize, onClick = open), |
| @@ -296,6 +300,13 @@ proc profilePane(s: State): Node = | ||
| 296 | 300 | "A guest — no Bluesky identity to look up.") |
| 297 | 301 | return |
| 298 | 302 | |
| 303 | + if isAgent(actor): | |
| 304 | + result.children.add dimLabel( | |
| 305 | + "An agent — it signs with a key of its own rather than a Bluesky " & | |
| 306 | + "account, so there is no profile to show.") | |
| 307 | + result.children.add dimLabel(actor) | |
| 308 | + return | |
| 309 | + | |
| 299 | 310 | let (p, known) = entry(actor) |
| 300 | 311 | if not known or p.status == psLoading: |
| 301 | 312 | result.children.add spinner() |
| @@ -141,7 +141,11 @@ proc messageBody(s: State, room: Room, m: Message, highlit: bool): Node = | |||
| 141 | # A face is a way in to who someone is, so it takes the press that opens | 141 | # A face is a way in to who someone is, so it takes the press that opens |
| 142 | # them — and so does the name beside it, since a name is the thing a | 142 | # them — and so does the name beside it, since a name is the thing a |
| 143 | # reader is actually looking at. | 143 | # reader is actually looking at. |
| 144 | - let senderActor = actorFor(m.account, m.frm) | 144 | + # The `account` tag where the server sends one, and what WHO reported |
| 145 | + # for this nick where it does not — freeq is the second case. | ||
| 146 | + let senderActor = actorFor( | ||
| 147 | + if m.account.len > 0: m.account else: s.dids.getOrDefault(m.frm, ""), | ||
| 148 | + m.frm) | ||
| 145 | let open = "profile.open:" & m.frm & ":" & senderActor | 149 | let open = "profile.open:" & m.frm & ":" & senderActor |
| 146 | var row = hbox(%*{"spacing": 6}, | 150 | var row = hbox(%*{"spacing": 6}, |
| 147 | avatar(m.avatar, m.frm, size = faceSize, onClick = open), | 151 | avatar(m.avatar, m.frm, size = faceSize, onClick = open), |
| @@ -296,6 +300,13 @@ proc profilePane(s: State): Node = | |||
| 296 | "A guest — no Bluesky identity to look up.") | 300 | "A guest — no Bluesky identity to look up.") |
| 297 | return | 301 | return |
| 298 | 302 | ||
| 303 | + if isAgent(actor): | ||
| 304 | + result.children.add dimLabel( | ||
| 305 | + "An agent — it signs with a key of its own rather than a Bluesky " & | ||
| 306 | + "account, so there is no profile to show.") | ||
| 307 | + result.children.add dimLabel(actor) | ||
| 308 | + return | ||
| 309 | + | ||
| 299 | let (p, known) = entry(actor) | 310 | let (p, known) = entry(actor) |
| 300 | if not known or p.status == psLoading: | 311 | if not known or p.status == psLoading: |
| 301 | result.children.add spinner() | 312 | result.children.add spinner() |
modified
nim/tests/tsession.nim +50 -1 | @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## `conn.feed` puts a line in as though the server had sent it; `tryOutbound` |
| 10 | 10 | ## reads what went out. No socket at either end. |
| 11 | 11 | |
| 12 | -import std/[sequtils, strutils, tables, unittest] | |
| 12 | +import std/[json, sequtils, strutils, tables, unittest] | |
| 13 | 13 | import frq/[cells, model, reducer, rooms] |
| 14 | 14 | import frq/conn as tr |
| 15 | 15 | |
| @@ -104,3 +104,52 @@ suite "the rest of the conversation": | ||
| 104 | 104 | say(":bob!b@h PRIVMSG alice :a direct word") |
| 105 | 105 | check app.rooms.hasKey("bob") |
| 106 | 106 | check app.rooms["bob"].messages[^1].text == "a direct word" |
| 107 | + | |
| 108 | +suite "who is who": | |
| 109 | + setup: reset() | |
| 110 | + | |
| 111 | + test "WHO reports the whole DID where the hostmask has eight characters": | |
| 112 | + # `freeq/plc/ngokl2gn` is enough to tell two people apart and not enough | |
| 113 | + # to look either of them up. The realname field carries all of it. | |
| 114 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " & | |
| 115 | + "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p") | |
| 116 | + check app.dids["nandi.uk"] == "did:plc:ngokl2gnmpbvuvrfckja3g7p" | |
| 117 | + | |
| 118 | + test "an agent signs with a key, and that is an identity too": | |
| 119 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/key/z6Mkp5we irc.freeq.at " & | |
| 120 | + "cartographer H :0 did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4shCpSB") | |
| 121 | + check app.dids["cartographer"].startsWith("did:key:") | |
| 122 | + | |
| 123 | + test "a guest has none, and is not recorded as having one": | |
| 124 | + # freeq puts the literal "IRC User" there for an unauthenticated | |
| 125 | + # connection, which is not a DID and must not be stored as one. | |
| 126 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/guest irc.freeq.at " & | |
| 127 | + "adam12 H :0 IRC User") | |
| 128 | + check not app.dids.hasKey("adam12") | |
| 129 | + | |
| 130 | + test "WHOIS answers for one nick the same way": | |
| 131 | + say(":irc.freeq.at 330 alice zapnap did:plc:k2n3e2vsabcdefghijklmnop " & | |
| 132 | + ":is authenticated as") | |
| 133 | + check app.dids["zapnap"] == "did:plc:k2n3e2vsabcdefghijklmnop" | |
| 134 | + | |
| 135 | + test "and the room is asked who is in it once it has arrived": | |
| 136 | + joined("#freeq") | |
| 137 | + say(":server 366 alice #freeq :End of /NAMES list") | |
| 138 | + check "WHO #freeq" in sent() | |
| 139 | + | |
| 140 | +suite "opening a profile": | |
| 141 | + setup: reset() | |
| 142 | + | |
| 143 | + test "uses what WHO reported for a nick that is not a handle": | |
| 144 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/k2n3e2vs irc.freeq.at " & | |
| 145 | + "zapnap H :0 did:plc:k2n3e2vsabcdefghijklmnop") | |
| 146 | + dispatch(%*{"id": "profile.open:zapnap:"}) | |
| 147 | + check app.profileViewing.actor == "did:plc:k2n3e2vsabcdefghijklmnop" | |
| 148 | + | |
| 149 | + test "asks the server about somebody it has never seen": | |
| 150 | + dispatch(%*{"id": "profile.open:stranger:"}) | |
| 151 | + check "WHOIS stranger" in sent() | |
| 152 | + | |
| 153 | + test "and what the message itself knew still wins": | |
| 154 | + dispatch(%*{"id": "profile.open:bob:did:plc:fromtheaccounttag"}) | |
| 155 | + check app.profileViewing.actor == "did:plc:fromtheaccounttag" | |
| @@ -9,7 +9,7 @@ | |||
| 9 | ## `conn.feed` puts a line in as though the server had sent it; `tryOutbound` | 9 | ## `conn.feed` puts a line in as though the server had sent it; `tryOutbound` |
| 10 | ## reads what went out. No socket at either end. | 10 | ## reads what went out. No socket at either end. |
| 11 | 11 | ||
| 12 | -import std/[sequtils, strutils, tables, unittest] | 12 | +import std/[json, sequtils, strutils, tables, unittest] |
| 13 | import frq/[cells, model, reducer, rooms] | 13 | import frq/[cells, model, reducer, rooms] |
| 14 | import frq/conn as tr | 14 | import frq/conn as tr |
| 15 | 15 | ||
| @@ -104,3 +104,52 @@ suite "the rest of the conversation": | |||
| 104 | say(":bob!b@h PRIVMSG alice :a direct word") | 104 | say(":bob!b@h PRIVMSG alice :a direct word") |
| 105 | check app.rooms.hasKey("bob") | 105 | check app.rooms.hasKey("bob") |
| 106 | check app.rooms["bob"].messages[^1].text == "a direct word" | 106 | check app.rooms["bob"].messages[^1].text == "a direct word" |
| 107 | + | ||
| 108 | +suite "who is who": | ||
| 109 | + setup: reset() | ||
| 110 | + | ||
| 111 | + test "WHO reports the whole DID where the hostmask has eight characters": | ||
| 112 | + # `freeq/plc/ngokl2gn` is enough to tell two people apart and not enough | ||
| 113 | + # to look either of them up. The realname field carries all of it. | ||
| 114 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " & | ||
| 115 | + "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p") | ||
| 116 | + check app.dids["nandi.uk"] == "did:plc:ngokl2gnmpbvuvrfckja3g7p" | ||
| 117 | + | ||
| 118 | + test "an agent signs with a key, and that is an identity too": | ||
| 119 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/key/z6Mkp5we irc.freeq.at " & | ||
| 120 | + "cartographer H :0 did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4shCpSB") | ||
| 121 | + check app.dids["cartographer"].startsWith("did:key:") | ||
| 122 | + | ||
| 123 | + test "a guest has none, and is not recorded as having one": | ||
| 124 | + # freeq puts the literal "IRC User" there for an unauthenticated | ||
| 125 | + # connection, which is not a DID and must not be stored as one. | ||
| 126 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/guest irc.freeq.at " & | ||
| 127 | + "adam12 H :0 IRC User") | ||
| 128 | + check not app.dids.hasKey("adam12") | ||
| 129 | + | ||
| 130 | + test "WHOIS answers for one nick the same way": | ||
| 131 | + say(":irc.freeq.at 330 alice zapnap did:plc:k2n3e2vsabcdefghijklmnop " & | ||
| 132 | + ":is authenticated as") | ||
| 133 | + check app.dids["zapnap"] == "did:plc:k2n3e2vsabcdefghijklmnop" | ||
| 134 | + | ||
| 135 | + test "and the room is asked who is in it once it has arrived": | ||
| 136 | + joined("#freeq") | ||
| 137 | + say(":server 366 alice #freeq :End of /NAMES list") | ||
| 138 | + check "WHO #freeq" in sent() | ||
| 139 | + | ||
| 140 | +suite "opening a profile": | ||
| 141 | + setup: reset() | ||
| 142 | + | ||
| 143 | + test "uses what WHO reported for a nick that is not a handle": | ||
| 144 | + say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/k2n3e2vs irc.freeq.at " & | ||
| 145 | + "zapnap H :0 did:plc:k2n3e2vsabcdefghijklmnop") | ||
| 146 | + dispatch(%*{"id": "profile.open:zapnap:"}) | ||
| 147 | + check app.profileViewing.actor == "did:plc:k2n3e2vsabcdefghijklmnop" | ||
| 148 | + | ||
| 149 | + test "asks the server about somebody it has never seen": | ||
| 150 | + dispatch(%*{"id": "profile.open:stranger:"}) | ||
| 151 | + check "WHOIS stranger" in sent() | ||
| 152 | + | ||
| 153 | + test "and what the message itself knew still wins": | ||
| 154 | + dispatch(%*{"id": "profile.open:bob:did:plc:fromtheaccounttag"}) | ||
| 155 | + check app.profileViewing.actor == "did:plc:fromtheaccounttag" | ||
modified
nim/tests/tstore.nim +1 -1 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | ## and writes the config of whoever runs it, which would both lie about the |
| 6 | 6 | ## result and cost them their room list. |
| 7 | 7 | |
| 8 | -import std/[json, os, sets, tables, times, unittest] | |
| 8 | +import std/[json, os, tables, times, unittest] | |
| 9 | 9 | |
| 10 | 10 | let sandbox = getTempDir() / "frq-tstore-" & $epochTime() |
| 11 | 11 | putEnv("XDG_CONFIG_HOME", sandbox) |
| @@ -5,7 +5,7 @@ | |||
| 5 | ## and writes the config of whoever runs it, which would both lie about the | 5 | ## and writes the config of whoever runs it, which would both lie about the |
| 6 | ## result and cost them their room list. | 6 | ## result and cost them their room list. |
| 7 | 7 | ||
| 8 | -import std/[json, os, sets, tables, times, unittest] | 8 | +import std/[json, os, tables, times, unittest] |
| 9 | 9 | ||
| 10 | let sandbox = getTempDir() / "frq-tstore-" & $epochTime() | 10 | let sandbox = getTempDir() / "frq-tstore-" & $epochTime() |
| 11 | putEnv("XDG_CONFIG_HOME", sandbox) | 11 | putEnv("XDG_CONFIG_HOME", sandbox) |