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
56 if err != nil {
57 // Check if it's a "not found" error
58 errStr := err.Error()
59 if strings.Contains(errStr, "not found") ||
60 strings.Contains(errStr, "NoRecordsFound") ||
61 strings.Contains(errStr, "404") {
62 return nil, &ErrNotFound{
63 Identifier: identifier,
64 Reason: errStr,
65 }
66 }
67
68 return nil, &ErrResolutionFailed{
69 Identifier: identifier,
70 Reason: errStr,
71 }
72 }
73
74 // Extract PDS URL from identity
75 pdsURL := ident.PDSEndpoint()
76
77 return &Identity{
78 DID: ident.DID.String(),
79 Handle: ident.Handle.String(),
80 PDSURL: pdsURL,
81 ResolvedAt: time.Now().UTC(),
82 Method: MethodHTTPS, // Default - Indigo doesn't expose which method was used
83 }, nil
84}
85
86// ResolveHandle specifically resolves a handle to DID and PDS URL
87func (r *baseResolver) ResolveHandle(ctx context.Context, handle string) (did, pdsURL string, err error) {
88 ident, err := r.Resolve(ctx, handle)
89 if err != nil {
90 return "", "", err
91 }
92
93 return ident.DID, ident.PDSURL, nil
94}
95
96// ResolveDID retrieves a DID document and extracts the PDS endpoint
97func (r *baseResolver) ResolveDID(ctx context.Context, didStr string) (*DIDDocument, error) {
98 did, err := syntax.ParseDID(didStr)
99 if err != nil {
100 return nil, &ErrInvalidIdentifier{
101 Identifier: didStr,
102 Reason: fmt.Sprintf("invalid DID format: %v", err),
103 }
104 }
105
106 ident, err := r.directory.LookupDID(ctx, did)
107 if err != nil {
108 return nil, &ErrResolutionFailed{
109 Identifier: didStr,
110 Reason: err.Error(),
111 }
112 }
113
114 // Construct our DID document from Indigo's identity
115 doc := &DIDDocument{
116 DID: ident.DID.String(),
117 Service: []Service{},
118 }
119
120 // Extract PDS service endpoint
121 pdsURL := ident.PDSEndpoint()
122 if pdsURL != "" {
123 doc.Service = append(doc.Service, Service{
124 ID: "#atproto_pds",
125 Type: "AtprotoPersonalDataServer",
126 ServiceEndpoint: pdsURL,
127 })
128 }
129
130 return doc, nil
131}
132
133// Purge is a no-op for base resolver (no caching)
134func (r *baseResolver) Purge(ctx context.Context, identifier string) error {
135 // Base resolver doesn't cache, so nothing to purge
136 return nil
137}