nandi/freeqsay-nimpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/freeqsay-nim.git
git clone ssh://git@rickub.com/nandi/freeqsay-nim.git

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

resolve.nim · 102 lines · 3.7 KBNim Blame HistoryRaw
freeqsay: Nim port of the Clojure implementation fa53aa2 nandi 6h ago1## Resolve Bluesky / ATProto handles to DIDs via the public AppView.
2## Avatar traits are still a pure function of the DID after resolution.
3
4import std/[httpclient, json, strutils]
5
6const defaultEndpoint* =
7 "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle"
8
9type
10 HttpResponse* = object
11 status*: int ## 0 means the request never reached the server
12 body*: JsonNode
13 error*: string
14
15 HttpGet* = proc (url: string): HttpResponse {.gcsafe.}
16
17 Identity* = object
18 did*: string
19 handle*: string
20
21 ResolveOpts* = object
22 endpoint*: string
23 httpGet*: HttpGet ## test hook; nil uses the real transport
24
25func normalizeHandle*(handle: string): string =
26 ## Strip @ and whitespace.
27 handle.strip().strip(trailing = false, chars = {'@'}).toLowerAscii
28
29func isDid*(s: string): bool =
30 ## True if the string is already a DID: `did:<method>:…`, method case-insensitive.
31 let t = s.strip()
32 if not t.toLowerAscii.startsWith("did:"):
33 return false
34 var i = 4
35 while i < t.len and t[i] in {'a'..'z', 'A'..'Z', '0'..'9'}:
36 inc i
37 i > 4 and i < t.len and t[i] == ':'
38
39func urlEncode(s: string): string =
40 for c in s:
41 if c in {'A'..'Z', 'a'..'z', '0'..'9', '_', '.', '~', '-'}:
42 result.add c
43 else:
44 result.add '%' & toHex(int(uint8(c)), 2)
45
46proc httpGetJson(url: string): HttpResponse {.gcsafe.} =
47 ## GET `url` and parse the body as JSON. `status: 0` means the request never
48 ## reached the server — the transport itself failed — and `error` says why.
49 {.cast(gcsafe).}:
50 try:
51 let client = newHttpClient(timeout = 15_000)
52 defer: client.close()
53 let resp = client.get(url)
54 # `status` is "200 OK". Body parsing gets its own try: an error page from
55 # the AppView must stay an HTTP failure, not look like a dead transport.
56 let status = parseInt(resp.status.split(' ')[0])
57 var body: JsonNode
58 try: body = parseJson(resp.body) except CatchableError: discard
59 HttpResponse(status: status, body: body)
60 except CatchableError as e:
61 HttpResponse(status: 0, error: e.msg)
62
63proc resolveHandle*(handle: string, opts = ResolveOpts()): string =
64 ## Resolve a handle to its DID. Raises if the handle cannot be resolved.
65 let clean = normalizeHandle(handle)
66 if clean.len == 0:
67 raise newException(ValueError, "empty handle")
68 if isDid(clean):
69 return clean.strip()
70
71 let
72 endpoint = if opts.endpoint.len > 0: opts.endpoint else: defaultEndpoint
73 url = endpoint & "?handle=" & urlEncode(clean)
74 get = if opts.httpGet != nil: opts.httpGet else: HttpGet(httpGetJson)
75 resp = get(url)
76
77 if resp.status == 0:
78 # No HTTP status at all: the request never left the machine. Saying "is
79 # that a real handle?" here blames the user for our own missing transport.
80 # Name it, and point at the offline path.
81 raise newException(IOError,
82 "couldn't reach the Bluesky API to resolve @" & clean &
83 (if resp.error.len > 0: "" & resp.error else: "") &
84 "\nPass the DID directly (freeqsay did:plc:… …) to skip resolution.")
85 if resp.status != 200:
86 raise newException(IOError,
87 "couldn't resolve @" & clean & " — is that a real Bluesky handle? (" &
88 $resp.status & ")")
89
90 let did = if resp.body != nil and resp.body.kind == JObject and
91 resp.body.hasKey("did"): resp.body["did"].getStr else: ""
92 if did.len == 0 or not isDid(did):
93 raise newException(IOError, "resolveHandle returned no DID for @" & clean)
94 did
95
96proc resolveIdentity*(handleOrDid: string, opts = ResolveOpts()): Identity =
97 ## Accept either a handle or a DID; resolve handles, pass DIDs through.
98 let raw = handleOrDid.strip()
99 if isDid(raw):
100 Identity(did: raw)
101 else:
102 Identity(did: resolveHandle(raw, opts), handle: normalizeHandle(raw))