forked from
tangled.org/core
Monorepo for Tangled
1package idresolver
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "net/http"
8 "slices"
9 "strings"
10 "sync"
11 "time"
12
13 "github.com/bluesky-social/indigo/atproto/identity"
14 "github.com/bluesky-social/indigo/atproto/identity/redisdir"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 "github.com/carlmjohnson/versioninfo"
17)
18
19type Resolver struct {
20 directory identity.Directory
21 base *identity.BaseDirectory
22}
23
24func BaseDirectory(plcUrl string) *identity.BaseDirectory {
25 base := identity.BaseDirectory{
26 PLCURL: plcUrl,
27 HTTPClient: http.Client{
28 Timeout: time.Second * 10,
29 Transport: &http.Transport{
30 // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad.
31 IdleConnTimeout: time.Millisecond * 1000,
32 MaxIdleConns: 100,
33 },
34 },
35 Resolver: net.Resolver{
36 Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
37 d := net.Dialer{Timeout: time.Second * 3}
38 return d.DialContext(ctx, network, address)
39 },
40 },
41 TryAuthoritativeDNS: true,
42 // primary Bluesky PDS instance only supports HTTP resolution method
43 SkipDNSDomainSuffixes: []string{".bsky.social"},
44 UserAgent: "indigo-identity/" + versioninfo.Short(),
45 }
46 return &base
47}
48
49func DefaultResolver(plcUrl string) *Resolver {
50 base := BaseDirectory(plcUrl)
51 cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5)
52 return &Resolver{
53 directory: cached,
54 base: base,
55 }
56}
57
58func RedisDirectory(base *identity.BaseDirectory, url string) (identity.Directory, error) {
59 hitTTL := time.Hour * 24
60 errTTL := time.Second * 30
61 invalidHandleTTL := time.Minute * 5
62 return redisdir.NewRedisDirectory(base, url, hitTTL, errTTL, invalidHandleTTL, 10000)
63}
64
65func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) {
66 base := BaseDirectory(plcUrl)
67 directory, err := RedisDirectory(base, redisUrl)
68 if err != nil {
69 return nil, err
70 }
71 return &Resolver{
72 directory: directory,
73 base: base,
74 }, nil
75}
76
77type handleResolver interface {
78 ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error)
79}
80
81func (r *Resolver) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
82 if hr, ok := r.directory.(handleResolver); ok {
83 return hr.ResolveHandle(ctx, handle)
84 }
85 return r.base.ResolveHandle(ctx, handle)
86}
87
88func (r *Resolver) ResolveAtIdentifier(ctx context.Context, input string) (*identity.Identity, error) {
89 if did, err := syntax.ParseDID(input); err == nil {
90 return r.directory.LookupDID(ctx, did)
91 }
92 handle, err := syntax.ParseHandle(input)
93 if err != nil {
94 return nil, fmt.Errorf("not a did or handle: %w", err)
95 }
96 handle = handle.Normalize()
97 did, err := r.base.ResolveHandle(ctx, handle)
98 if err != nil {
99 return nil, fmt.Errorf("resolve handle %q: %w", handle, err)
100 }
101 ident, err := r.directory.LookupDID(ctx, did)
102 if err != nil {
103 return nil, fmt.Errorf("lookup did for %q: %w", handle, err)
104 }
105 aka := "at://" + handle.String()
106 if !slices.ContainsFunc(ident.AlsoKnownAs, func(s string) bool { return strings.EqualFold(s, aka) }) {
107 return nil, fmt.Errorf("handle %q not declared in alsoKnownAs for %s", handle, did)
108 }
109 return ident, nil
110}
111
112func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
113 id, err := syntax.ParseAtIdentifier(arg)
114 if err != nil {
115 return nil, err
116 }
117
118 return r.directory.Lookup(ctx, id)
119}
120
121func (r *Resolver) ResolveIdents(ctx context.Context, idents []string) []*identity.Identity {
122 results := make([]*identity.Identity, len(idents))
123 var wg sync.WaitGroup
124
125 done := make(chan struct{})
126 defer close(done)
127
128 for idx, ident := range idents {
129 wg.Add(1)
130 go func(index int, id string) {
131 defer wg.Done()
132
133 select {
134 case <-ctx.Done():
135 results[index] = nil
136 case <-done:
137 results[index] = nil
138 default:
139 identity, _ := r.ResolveIdent(ctx, id)
140 results[index] = identity
141 }
142 }(idx, ident)
143 }
144
145 wg.Wait()
146 return results
147}
148
149func (r *Resolver) InvalidateIdent(ctx context.Context, arg string) error {
150 id, err := syntax.ParseAtIdentifier(arg)
151 if err != nil {
152 return err
153 }
154
155 return r.directory.Purge(ctx, id)
156}
157
158func (r *Resolver) Directory() identity.Directory {
159 return r.directory
160}