A community based topic aggregation platform built on atproto
1package identity
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9
10 indigoIdentity "github.com/bluesky-social/indigo/atproto/identity"
11 "github.com/bluesky-social/indigo/atproto/syntax"
12)
13
14// baseResolver implements Resolver using Indigo's identity resolution
15type baseResolver struct {
16 directory indigoIdentity.Directory
17}
18
19// newBaseResolver creates a new base resolver using Indigo
20func newBaseResolver(plcURL string, httpClient *http.Client) Resolver {
21 // Create Indigo's BaseDirectory which handles DNS and HTTPS resolution
22 dir := &indigoIdentity.BaseDirectory{
23 PLCURL: plcURL,
24 HTTPClient: *httpClient,
25 // Indigo will use default DNS resolver if not specified
26 }
27
28 return &baseResolver{
29 directory: dir,
30 }
31}
32
33// Resolve resolves a handle or DID to complete identity information
34func (r *baseResolver) Resolve(ctx context.Context, identifier string) (*Identity, error) {
35 identifier = strings.TrimSpace(identifier)
36
37 if identifier == "" {
38 return nil, &ErrInvalidIdentifier{
39 Identifier: identifier,
40 Reason: "identifier cannot be empty",
41 }
42 }
43
44 // Parse the identifier (could be handle or DID)
45 atID, err := syntax.ParseAtIdentifier(identifier)
46 if err != nil {
47 return nil, &ErrInvalidIdentifier{
48 Identifier: identifier,
49 Reason: fmt.Sprintf("invalid identifier format: %v", err),
50 }
51 }
52
53 // Resolve using Indigo's directory
54 ident, err := r.directory.Lookup(ctx, *atID)
55 if err != nil {
56 // Check if it's a "not found" error
57 errStr := err.Error()
58 if strings.Contains(errStr, "not found") ||
59 strings.Contains(errStr, "NoRecordsFound") ||
60 strings.Contains(errStr, "404") {
61 return nil, &ErrNotFound{
62 Identifier: identifier,
63 Reason: errStr,
64 }
65 }
66
67 return nil, &ErrResolutionFailed{
68 Identifier: identifier,
69 Reason: errStr,
70 }
71 }
72
73 // Extract PDS URL from identity
74 pdsURL := ident.PDSEndpoint()
75
76 return &Identity{
77 DID: ident.DID.String(),
78 Handle: ident.Handle.String(),
79 PDSURL: pdsURL,
80 ResolvedAt: time.Now().UTC(),
81 Method: MethodHTTPS, // Default - Indigo doesn't expose which method was used
82 }, nil
83}
84
85// ResolveHandle specifically resolves a handle to DID and PDS URL
86func (r *baseResolver) ResolveHandle(ctx context.Context, handle string) (did, pdsURL string, err error) {
87 ident, err := r.Resolve(ctx, handle)
88 if err != nil {
89 return "", "", err
90 }
91
92 return ident.DID, ident.PDSURL, nil
93}
94
95// ResolveDID retrieves a DID document and extracts the PDS endpoint
96func (r *baseResolver) ResolveDID(ctx context.Context, didStr string) (*DIDDocument, error) {
97 did, err := syntax.ParseDID(didStr)
98 if err != nil {
99 return nil, &ErrInvalidIdentifier{
100 Identifier: didStr,
101 Reason: fmt.Sprintf("invalid DID format: %v", err),
102 }
103 }
104
105 ident, err := r.directory.LookupDID(ctx, did)
106 if err != nil {
107 return nil, &ErrResolutionFailed{
108 Identifier: didStr,
109 Reason: err.Error(),
110 }
111 }
112
113 // Construct our DID document from Indigo's identity
114 doc := &DIDDocument{
115 DID: ident.DID.String(),
116 Service: []Service{},
117 }
118
119 // Extract PDS service endpoint
120 pdsURL := ident.PDSEndpoint()
121 if pdsURL != "" {
122 doc.Service = append(doc.Service, Service{
123 ID: "#atproto_pds",
124 Type: "AtprotoPersonalDataServer",
125 ServiceEndpoint: pdsURL,
126 })
127 }
128
129 return doc, nil
130}
131
132// Purge is a no-op for base resolver (no caching)
133func (r *baseResolver) Purge(ctx context.Context, identifier string) error {
134 // Base resolver doesn't cache, so nothing to purge
135 return nil
136}