+206
-111
atkafka/atkafka.go
+206
-111
atkafka/atkafka.go
···
30
bootstrapServers []string
31
outputTopic string
32
ospreyCompat bool
33
-
logger *slog.Logger
34
35
-
producer *Producer
36
37
plcClient *PlcClient
38
}
39
40
type ServerArgs struct {
41
-
RelayHost string
42
-
PlcHost string
43
BootstrapServers []string
44
OutputTopic string
45
-
OspreyCompat bool
46
-
Logger *slog.Logger
47
}
48
49
-
func NewServer(args *ServerArgs) *Server {
50
if args.Logger == nil {
51
args.Logger = slog.Default()
52
}
53
54
var plcClient *PlcClient
55
if args.PlcHost != "" {
56
plcClient = NewPlcClient(&PlcClientArgs{
···
58
})
59
}
60
61
-
return &Server{
62
relayHost: args.RelayHost,
63
plcClient: plcClient,
64
bootstrapServers: args.BootstrapServers,
···
66
ospreyCompat: args.OspreyCompat,
67
logger: args.Logger,
68
}
69
}
70
71
func (s *Server) Run(ctx context.Context) error {
···
147
return nil
148
}
149
150
-
type EventMetadata struct {
151
-
DidDocument *identity.DIDDocument `json:"didDocument,omitempty"`
152
-
PdsHost string `json:"pdsHost,omitempty"`
153
-
Handle string `json:"handle,omitempty"`
154
-
DidCreatedAt string `json:"didCreatedAt,omitempty"`
155
-
AccountAge int64 `json:"accountAge"`
156
-
}
157
-
158
-
func (s *Server) FetchEventMetadata(ctx context.Context, did string) (*EventMetadata, error) {
159
-
var didDocument *identity.DIDDocument
160
var pdsHost string
161
var handle string
162
var didCreatedAt string
···
167
if s.plcClient != nil {
168
wg.Go(func() {
169
logger := s.logger.With("component", "didDoc")
170
-
doc, err := s.plcClient.GetDIDDoc(ctx, did)
171
if err != nil {
172
logger.Error("error fetching did doc", "did", did, "err", err)
173
return
174
}
175
-
didDocument = doc
176
-
177
-
for _, svc := range doc.Service {
178
-
if svc.ID == "#atproto_pds" {
179
-
pdsHost = svc.ServiceEndpoint
180
-
break
181
-
}
182
-
}
183
-
184
-
for _, aka := range doc.AlsoKnownAs {
185
-
if strings.HasPrefix(aka, "at://") {
186
-
handle = strings.TrimPrefix(aka, "at://")
187
-
break
188
-
}
189
-
}
190
})
191
192
wg.Go(func() {
···
217
Handle: handle,
218
DidCreatedAt: didCreatedAt,
219
AccountAge: accountAge,
220
-
}, nil
221
}
222
223
func (s *Server) handleEvent(ctx context.Context, evt *events.XRPCStreamEvent) error {
···
230
var collection string
231
var actionName string
232
233
if evt.RepoCommit != nil {
234
// read the repo
235
rr, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(evt.RepoCommit.Blocks))
236
if err != nil {
···
238
return nil
239
}
240
241
for _, op := range evt.RepoCommit.Ops {
242
kind := repomgr.EventKind(op.Action)
243
collection = strings.Split(op.Path, "/")[0]
244
rkey := strings.Split(op.Path, "/")[1]
245
atUri := fmt.Sprintf("at://%s/%s/%s", evt.RepoCommit.Repo, collection, rkey)
246
247
kindStr := "create"
248
switch kind {
249
case repomgr.EvtKindUpdateRecord:
···
298
Operation: &atkOp,
299
}
300
301
-
eventMetadata, err := s.FetchEventMetadata(dispatchCtx, evt.RepoCommit.Repo)
302
-
if err != nil {
303
-
logger.Error("error fetching event metadata", "err", err)
304
-
} else {
305
kafkaEvt.Metadata = eventMetadata
306
}
307
308
-
var kafkaEvtBytes []byte
309
if s.ospreyCompat {
310
// create the wrapper event for osprey
311
ospreyKafkaEvent := OspreyAtKafkaEvent{
···
320
SendTime: time.Now().Format(time.RFC3339),
321
}
322
323
-
kafkaEvtBytes, err = json.Marshal(&ospreyKafkaEvent)
324
} else {
325
-
kafkaEvtBytes, err = json.Marshal(&kafkaEvt)
326
}
327
if err != nil {
328
return fmt.Errorf("failed to marshal kafka event: %w", err)
329
}
330
331
-
if err := s.produceAsync(ctx, evt.RepoCommit.Repo, kafkaEvtBytes); err != nil {
332
-
return err
333
-
}
334
}
335
} else {
336
defer func() {
···
389
}
390
391
if did != "" {
392
-
eventMetadata, err := s.FetchEventMetadata(dispatchCtx, did)
393
if err != nil {
394
logger.Error("error fetching event metadata", "err", err)
395
-
} else {
396
kafkaEvt.Metadata = eventMetadata
397
}
398
}
399
400
// create the kafka event bytes
401
-
var kafkaEvtBytes []byte
402
var err error
403
404
if s.ospreyCompat {
···
415
SendTime: time.Now().Format(time.RFC3339),
416
}
417
418
-
kafkaEvtBytes, err = json.Marshal(&ospreyKafkaEvent)
419
} else {
420
-
kafkaEvtBytes, err = json.Marshal(&kafkaEvt)
421
}
422
if err != nil {
423
return fmt.Errorf("failed to marshal kafka event: %w", err)
424
}
425
426
-
if err := s.produceAsync(ctx, did, kafkaEvtBytes); err != nil {
427
return err
428
}
429
}
···
441
producedEvents.WithLabelValues(status).Inc()
442
}
443
444
-
if !s.ospreyCompat {
445
-
if err := s.producer.ProduceAsync(ctx, key, msg, callback); err != nil {
446
-
return fmt.Errorf("failed to produce message: %w", err)
447
-
}
448
-
} else if s.ospreyCompat {
449
-
if err := s.producer.ProduceAsync(ctx, key, msg, callback); err != nil {
450
-
return fmt.Errorf("failed to produce message: %w", err)
451
-
}
452
}
453
454
return nil
455
}
456
-
457
-
type AtKafkaOp struct {
458
-
Action string `json:"action"`
459
-
Collection string `json:"collection"`
460
-
Rkey string `json:"rkey"`
461
-
Uri string `json:"uri"`
462
-
Cid string `json:"cid"`
463
-
Path string `json:"path"`
464
-
Record map[string]any `json:"record"`
465
-
}
466
-
467
-
type AtKafkaIdentity struct {
468
-
Seq int64 `json:"seq"`
469
-
Handle string `json:"handle"`
470
-
}
471
-
472
-
type AtKafkaInfo struct {
473
-
Name string `json:"name"`
474
-
Message *string `json:"message,omitempty"`
475
-
}
476
-
477
-
type AtKafkaAccount struct {
478
-
Active bool `json:"active"`
479
-
Seq int64 `json:"seq"`
480
-
Status *string `json:"status,omitempty"`
481
-
}
482
-
483
-
type AtKafkaEvent struct {
484
-
Did string `json:"did"`
485
-
Timestamp string `json:"timestamp"`
486
-
Metadata *EventMetadata `json:"eventMetadata"`
487
-
488
-
Operation *AtKafkaOp `json:"operation,omitempty"`
489
-
Account *AtKafkaAccount `json:"account,omitempty"`
490
-
Identity *AtKafkaIdentity `json:"identity,omitempty"`
491
-
Info *AtKafkaInfo `json:"info,omitempty"`
492
-
}
493
-
494
-
// Intentionally using snake case since that is what Osprey expects
495
-
type OspreyEventData struct {
496
-
ActionName string `json:"action_name"`
497
-
ActionId int64 `json:"action_id"`
498
-
Data AtKafkaEvent `json:"data"`
499
-
Timestamp string `json:"timestamp"`
500
-
SecretData map[string]string `json:"secret_data"`
501
-
Encoding string `json:"encoding"`
502
-
}
503
-
504
-
type OspreyAtKafkaEvent struct {
505
-
Data OspreyEventData `json:"data"`
506
-
SendTime string `json:"send_time"`
507
-
}
···
30
bootstrapServers []string
31
outputTopic string
32
ospreyCompat bool
33
34
+
watchedServices []string
35
+
ignoredServices []string
36
37
+
watchedCollections []string
38
+
ignoredCollections []string
39
+
40
+
producer *Producer
41
plcClient *PlcClient
42
+
logger *slog.Logger
43
}
44
45
type ServerArgs struct {
46
+
// network params
47
+
RelayHost string
48
+
PlcHost string
49
+
50
+
// for watched and ignoed services or collections, only one list may be supplied
51
+
// for both services and collections, wildcards are acceptable. for example:
52
+
// app.bsky.* will watch/ignore any collection that falls under the app.bsky namespace.
53
+
// *.bsky.network will watch/ignore any event that falls under the bsky.network list of PDSes
54
+
55
+
// list of services that are events will be emitted for
56
+
WatchedServices []string
57
+
// list of services that events are ignored for
58
+
IgnoredServices []string
59
+
60
+
// list of collections that events are emitted for
61
+
WatchedCollections []string
62
+
// list of collections that events are ignored for
63
+
IgnoredCollections []string
64
+
65
+
// kafka params
66
BootstrapServers []string
67
OutputTopic string
68
+
69
+
// osprey-specific params
70
+
OspreyCompat bool
71
+
72
+
// other
73
+
Logger *slog.Logger
74
}
75
76
+
func NewServer(args *ServerArgs) (*Server, error) {
77
if args.Logger == nil {
78
args.Logger = slog.Default()
79
}
80
81
+
if len(args.WatchedServices) > 0 && len(args.IgnoredServices) > 0 {
82
+
return nil, fmt.Errorf("you may only specify a list of watched services _or_ ignored services, not both")
83
+
}
84
+
85
+
if (len(args.WatchedServices) > 0 || len(args.IgnoredServices) > 0) && args.PlcHost == "" {
86
+
return nil, fmt.Errorf("unable to support watched/ignored services without specifying a PLC host")
87
+
}
88
+
89
+
if len(args.WatchedCollections) > 0 && len(args.IgnoredCollections) > 0 {
90
+
return nil, fmt.Errorf("you may only specify a list of watched collections _or_ ignored collections, not both")
91
+
}
92
+
93
var plcClient *PlcClient
94
if args.PlcHost != "" {
95
plcClient = NewPlcClient(&PlcClientArgs{
···
97
})
98
}
99
100
+
s := &Server{
101
relayHost: args.RelayHost,
102
plcClient: plcClient,
103
bootstrapServers: args.BootstrapServers,
···
105
ospreyCompat: args.OspreyCompat,
106
logger: args.Logger,
107
}
108
+
109
+
if len(args.WatchedServices) > 0 {
110
+
watchedServices := make([]string, 0, len(args.WatchedServices))
111
+
for _, service := range args.WatchedServices {
112
+
watchedServices = append(watchedServices, strings.TrimPrefix(strings.TrimPrefix(service, "*."), "."))
113
+
}
114
+
s.watchedServices = watchedServices
115
+
} else if len(args.IgnoredServices) > 0 {
116
+
ignoredServices := make([]string, 0, len(args.IgnoredServices))
117
+
for _, service := range args.IgnoredServices {
118
+
ignoredServices = append(ignoredServices, strings.TrimPrefix(strings.TrimPrefix(service, "*."), "."))
119
+
}
120
+
s.ignoredServices = ignoredServices
121
+
}
122
+
123
+
if len(args.WatchedCollections) > 0 {
124
+
watchedCollections := make([]string, 0, len(args.WatchedCollections))
125
+
for _, collection := range args.WatchedCollections {
126
+
watchedCollections = append(watchedCollections, strings.TrimSuffix(strings.TrimSuffix(collection, ".*"), "."))
127
+
}
128
+
s.watchedCollections = watchedCollections
129
+
} else if len(args.IgnoredCollections) > 0 {
130
+
ignoredCollections := make([]string, 0, len(args.IgnoredCollections))
131
+
for _, collection := range args.IgnoredCollections {
132
+
ignoredCollections = append(ignoredCollections, strings.TrimSuffix(strings.TrimSuffix(collection, ".*"), "."))
133
+
}
134
+
s.ignoredCollections = ignoredCollections
135
+
}
136
+
137
+
return s, nil
138
}
139
140
func (s *Server) Run(ctx context.Context) error {
···
216
return nil
217
}
218
219
+
func (s *Server) FetchEventMetadata(ctx context.Context, did string) (*EventMetadata, *identity.Identity, error) {
220
+
var ident *identity.Identity
221
+
var didDocument identity.DIDDocument
222
var pdsHost string
223
var handle string
224
var didCreatedAt string
···
229
if s.plcClient != nil {
230
wg.Go(func() {
231
logger := s.logger.With("component", "didDoc")
232
+
var err error
233
+
ident, err = s.plcClient.GetIdentity(ctx, did)
234
if err != nil {
235
logger.Error("error fetching did doc", "did", did, "err", err)
236
return
237
}
238
+
didDocument = ident.DIDDocument()
239
+
pdsHost = ident.PDSEndpoint()
240
+
handle = ident.Handle.String()
241
})
242
243
wg.Go(func() {
···
268
Handle: handle,
269
DidCreatedAt: didCreatedAt,
270
AccountAge: accountAge,
271
+
}, ident, nil
272
}
273
274
func (s *Server) handleEvent(ctx context.Context, evt *events.XRPCStreamEvent) error {
···
281
var collection string
282
var actionName string
283
284
+
var evtKey string
285
+
var evtsToProduce [][]byte
286
+
287
if evt.RepoCommit != nil {
288
+
// key events by DID
289
+
evtKey = evt.RepoCommit.Repo
290
+
291
// read the repo
292
rr, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(evt.RepoCommit.Blocks))
293
if err != nil {
···
295
return nil
296
}
297
298
+
eventMetadata, ident, err := s.FetchEventMetadata(dispatchCtx, evt.RepoCommit.Repo)
299
+
if err != nil {
300
+
logger.Error("error fetching event metadata", "err", err)
301
+
} else if ident != nil {
302
+
skip := false
303
+
pdsEndpoint := ident.PDSEndpoint()
304
+
u, err := url.Parse(pdsEndpoint)
305
+
if err != nil {
306
+
return fmt.Errorf("failed to parse pds host: %w", err)
307
+
}
308
+
pdsHost := u.Hostname()
309
+
310
+
if pdsHost != "" {
311
+
if len(s.watchedServices) > 0 {
312
+
skip = true
313
+
for _, watchedService := range s.watchedServices {
314
+
if watchedService == pdsHost || strings.HasSuffix(pdsHost, "."+watchedService) {
315
+
skip = false
316
+
break
317
+
}
318
+
}
319
+
} else if len(s.ignoredServices) > 0 {
320
+
for _, ignoredService := range s.ignoredServices {
321
+
if ignoredService == pdsHost || strings.HasSuffix(pdsHost, "."+ignoredService) {
322
+
skip = true
323
+
break
324
+
}
325
+
}
326
+
}
327
+
}
328
+
329
+
if skip {
330
+
logger.Debug("skipping event based on pds host", "pdsHost", pdsHost)
331
+
return nil
332
+
}
333
+
}
334
+
335
for _, op := range evt.RepoCommit.Ops {
336
kind := repomgr.EventKind(op.Action)
337
collection = strings.Split(op.Path, "/")[0]
338
rkey := strings.Split(op.Path, "/")[1]
339
atUri := fmt.Sprintf("at://%s/%s/%s", evt.RepoCommit.Repo, collection, rkey)
340
341
+
skip := false
342
+
if len(s.watchedCollections) > 0 {
343
+
skip = true
344
+
for _, watchedCollection := range s.watchedCollections {
345
+
if watchedCollection == collection || strings.HasPrefix(collection, watchedCollection+".") {
346
+
skip = false
347
+
break
348
+
}
349
+
}
350
+
} else if len(s.ignoredCollections) > 0 {
351
+
for _, ignoredCollection := range s.ignoredCollections {
352
+
if ignoredCollection == collection || strings.HasPrefix(collection, ignoredCollection+".") {
353
+
skip = true
354
+
break
355
+
}
356
+
}
357
+
}
358
+
359
+
if skip {
360
+
logger.Debug("skipping event based on collection", "collection", collection)
361
+
continue
362
+
}
363
+
364
kindStr := "create"
365
switch kind {
366
case repomgr.EvtKindUpdateRecord:
···
415
Operation: &atkOp,
416
}
417
418
+
if eventMetadata != nil {
419
kafkaEvt.Metadata = eventMetadata
420
}
421
422
+
var evtBytes []byte
423
if s.ospreyCompat {
424
// create the wrapper event for osprey
425
ospreyKafkaEvent := OspreyAtKafkaEvent{
···
434
SendTime: time.Now().Format(time.RFC3339),
435
}
436
437
+
evtBytes, err = json.Marshal(&ospreyKafkaEvent)
438
} else {
439
+
evtBytes, err = json.Marshal(&kafkaEvt)
440
}
441
if err != nil {
442
return fmt.Errorf("failed to marshal kafka event: %w", err)
443
}
444
445
+
evtsToProduce = append(evtsToProduce, evtBytes)
446
}
447
} else {
448
defer func() {
···
501
}
502
503
if did != "" {
504
+
// key events by DID
505
+
evtKey = did
506
+
eventMetadata, ident, err := s.FetchEventMetadata(dispatchCtx, did)
507
if err != nil {
508
logger.Error("error fetching event metadata", "err", err)
509
+
} else if ident != nil {
510
+
skip := false
511
+
pdsEndpoint := ident.PDSEndpoint()
512
+
u, err := url.Parse(pdsEndpoint)
513
+
if err != nil {
514
+
return fmt.Errorf("failed to parse pds host: %w", err)
515
+
}
516
+
pdsHost := u.Hostname()
517
+
518
+
if pdsHost != "" {
519
+
if len(s.watchedServices) > 0 {
520
+
skip = true
521
+
for _, watchedService := range s.watchedServices {
522
+
if watchedService == pdsHost || strings.HasSuffix(pdsHost, "."+watchedService) {
523
+
skip = false
524
+
break
525
+
}
526
+
}
527
+
} else if len(s.ignoredServices) > 0 {
528
+
for _, ignoredService := range s.ignoredServices {
529
+
if ignoredService == pdsHost || strings.HasSuffix(pdsHost, "."+ignoredService) {
530
+
skip = true
531
+
break
532
+
}
533
+
}
534
+
}
535
+
}
536
+
537
+
if skip {
538
+
logger.Debug("skipping event based on pds host", "pdsHost", pdsHost)
539
+
return nil
540
+
}
541
+
542
kafkaEvt.Metadata = eventMetadata
543
}
544
+
} else {
545
+
// key events without a DID by "unknown"
546
+
evtKey = "<unknown>"
547
}
548
549
// create the kafka event bytes
550
+
var evtBytes []byte
551
var err error
552
553
if s.ospreyCompat {
···
564
SendTime: time.Now().Format(time.RFC3339),
565
}
566
567
+
evtBytes, err = json.Marshal(&ospreyKafkaEvent)
568
} else {
569
+
evtBytes, err = json.Marshal(&kafkaEvt)
570
}
571
if err != nil {
572
return fmt.Errorf("failed to marshal kafka event: %w", err)
573
}
574
575
+
evtsToProduce = append(evtsToProduce, evtBytes)
576
+
}
577
+
578
+
for _, evtBytes := range evtsToProduce {
579
+
if err := s.produceAsync(ctx, evtKey, evtBytes); err != nil {
580
return err
581
}
582
}
···
594
producedEvents.WithLabelValues(status).Inc()
595
}
596
597
+
if err := s.producer.ProduceAsync(ctx, key, msg, callback); err != nil {
598
+
return fmt.Errorf("failed to produce message: %w", err)
599
}
600
601
return nil
602
}
+16
-44
atkafka/plc.go
+16
-44
atkafka/plc.go
···
6
"errors"
7
"fmt"
8
"io"
9
-
"net"
10
"net/http"
11
"time"
12
···
19
20
type PlcClient struct {
21
client *http.Client
22
-
dir *identity.BaseDirectory
23
-
plcHost string
24
-
docCache *lru.LRU[string, *identity.DIDDocument]
25
auditCache *lru.LRU[string, *DidAuditEntry]
26
}
27
28
type PlcClientArgs struct {
···
33
client := robusthttp.NewClient(robusthttp.WithMaxRetries(2))
34
client.Timeout = 3 * time.Second
35
36
-
baseDir := identity.BaseDirectory{
37
-
PLCURL: args.PlcHost,
38
-
PLCLimiter: rate.NewLimiter(rate.Limit(200), 100),
39
-
HTTPClient: *client,
40
-
Resolver: net.Resolver{
41
-
PreferGo: true,
42
-
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
43
-
dialer := net.Dialer{Timeout: time.Second * 5}
44
-
nameserver := address
45
-
return dialer.DialContext(ctx, network, nameserver)
46
-
},
47
},
48
-
TryAuthoritativeDNS: true,
49
-
// primary Bluesky PDS instance only supports HTTP resolution method
50
-
SkipDNSDomainSuffixes: []string{".bsky.social"},
51
}
52
-
53
-
docCache := lru.NewLRU(100_000, func(_ string, _ *identity.DIDDocument) {
54
-
cacheSize.WithLabelValues("did_doc").Dec()
55
-
}, 5*time.Minute)
56
57
auditCache := lru.NewLRU(100_000, func(_ string, _ *DidAuditEntry) {
58
cacheSize.WithLabelValues("audit_log").Dec()
···
60
61
return &PlcClient{
62
client: client,
63
-
dir: &baseDir,
64
plcHost: args.PlcHost,
65
-
docCache: docCache,
66
-
auditCache: auditCache,
67
}
68
}
69
···
92
93
type DidAuditLog []DidAuditEntry
94
95
-
func (c *PlcClient) GetDIDDoc(ctx context.Context, did string) (*identity.DIDDocument, error) {
96
status := "error"
97
-
cached := false
98
99
defer func() {
100
-
plcRequests.WithLabelValues("did_doc", status, fmt.Sprintf("%t", cached)).Inc()
101
}()
102
103
-
if val, ok := c.docCache.Get(did); ok {
104
-
status = "ok"
105
-
cached = true
106
-
return val, nil
107
-
}
108
-
109
-
didDoc, err := c.dir.ResolveDID(ctx, syntax.DID(did))
110
if err != nil {
111
return nil, fmt.Errorf("failed to lookup DID: %w", err)
112
}
113
114
-
if didDoc == nil {
115
-
return nil, fmt.Errorf("DID Document not found")
116
-
}
117
-
118
-
if c.docCache != nil {
119
-
c.docCache.Add(did, didDoc)
120
-
}
121
-
122
cacheSize.WithLabelValues("did_doc").Inc()
123
status = "ok"
124
125
-
return didDoc, nil
126
}
127
128
var ErrAuditLogNotFound = errors.New("audit log not found for DID")
···
6
"errors"
7
"fmt"
8
"io"
9
"net/http"
10
"time"
11
···
18
19
type PlcClient struct {
20
client *http.Client
21
+
dir *identity.CacheDirectory
22
auditCache *lru.LRU[string, *DidAuditEntry]
23
+
plcHost string
24
}
25
26
type PlcClientArgs struct {
···
31
client := robusthttp.NewClient(robusthttp.WithMaxRetries(2))
32
client.Timeout = 3 * time.Second
33
34
+
baseDirectory := identity.BaseDirectory{
35
+
PLCURL: "https://plc.directory",
36
+
HTTPClient: http.Client{
37
+
Timeout: time.Second * 5,
38
},
39
+
PLCLimiter: rate.NewLimiter(rate.Limit(200), 100),
40
+
TryAuthoritativeDNS: true,
41
+
SkipDNSDomainSuffixes: []string{".bsky.social", ".staging.bsky.dev"},
42
}
43
+
directory := identity.NewCacheDirectory(&baseDirectory, 100_000, time.Hour*48, time.Minute*15, time.Minute*15)
44
45
auditCache := lru.NewLRU(100_000, func(_ string, _ *DidAuditEntry) {
46
cacheSize.WithLabelValues("audit_log").Dec()
···
48
49
return &PlcClient{
50
client: client,
51
+
dir: &directory,
52
+
auditCache: auditCache,
53
plcHost: args.PlcHost,
54
}
55
}
56
···
79
80
type DidAuditLog []DidAuditEntry
81
82
+
func (c *PlcClient) GetIdentity(ctx context.Context, did string) (*identity.Identity, error) {
83
status := "error"
84
85
defer func() {
86
+
plcRequests.WithLabelValues("did_doc", status, "unknown").Inc()
87
}()
88
89
+
identity, err := c.dir.LookupDID(ctx, syntax.DID(did))
90
if err != nil {
91
return nil, fmt.Errorf("failed to lookup DID: %w", err)
92
}
93
94
cacheSize.WithLabelValues("did_doc").Inc()
95
status = "ok"
96
97
+
return identity, nil
98
}
99
100
var ErrAuditLogNotFound = errors.New("audit log not found for DID")
+63
atkafka/types.go
+63
atkafka/types.go
···
···
1
+
package atkafka
2
+
3
+
import "github.com/bluesky-social/indigo/atproto/identity"
4
+
5
+
type AtKafkaOp struct {
6
+
Action string `json:"action"`
7
+
Collection string `json:"collection"`
8
+
Rkey string `json:"rkey"`
9
+
Uri string `json:"uri"`
10
+
Cid string `json:"cid"`
11
+
Path string `json:"path"`
12
+
Record map[string]any `json:"record"`
13
+
}
14
+
15
+
type AtKafkaIdentity struct {
16
+
Seq int64 `json:"seq"`
17
+
Handle string `json:"handle"`
18
+
}
19
+
20
+
type AtKafkaInfo struct {
21
+
Name string `json:"name"`
22
+
Message *string `json:"message,omitempty"`
23
+
}
24
+
25
+
type AtKafkaAccount struct {
26
+
Active bool `json:"active"`
27
+
Seq int64 `json:"seq"`
28
+
Status *string `json:"status,omitempty"`
29
+
}
30
+
31
+
type AtKafkaEvent struct {
32
+
Did string `json:"did"`
33
+
Timestamp string `json:"timestamp"`
34
+
Metadata *EventMetadata `json:"eventMetadata"`
35
+
36
+
Operation *AtKafkaOp `json:"operation,omitempty"`
37
+
Account *AtKafkaAccount `json:"account,omitempty"`
38
+
Identity *AtKafkaIdentity `json:"identity,omitempty"`
39
+
Info *AtKafkaInfo `json:"info,omitempty"`
40
+
}
41
+
42
+
// Intentionally using snake case since that is what Osprey expects
43
+
type OspreyEventData struct {
44
+
ActionName string `json:"action_name"`
45
+
ActionId int64 `json:"action_id"`
46
+
Data AtKafkaEvent `json:"data"`
47
+
Timestamp string `json:"timestamp"`
48
+
SecretData map[string]string `json:"secret_data"`
49
+
Encoding string `json:"encoding"`
50
+
}
51
+
52
+
type OspreyAtKafkaEvent struct {
53
+
Data OspreyEventData `json:"data"`
54
+
SendTime string `json:"send_time"`
55
+
}
56
+
57
+
type EventMetadata struct {
58
+
DidDocument identity.DIDDocument `json:"didDocument,omitempty"`
59
+
PdsHost string `json:"pdsHost,omitempty"`
60
+
Handle string `json:"handle,omitempty"`
61
+
DidCreatedAt string `json:"didCreatedAt,omitempty"`
62
+
AccountAge int64 `json:"accountAge"`
63
+
}
+39
-7
cmd/atkafka/main.go
+39
-7
cmd/atkafka/main.go
···
20
telemetry.CLIFlagMetricsListenAddress,
21
&cli.StringFlag{
22
Name: "relay-host",
23
Value: "wss://bsky.network",
24
EnvVars: []string{"ATKAFKA_RELAY_HOST"},
25
},
26
&cli.StringSliceFlag{
27
Name: "bootstrap-servers",
28
EnvVars: []string{"ATKAFKA_BOOTSTRAP_SERVERS"},
29
Required: true,
30
},
31
&cli.StringFlag{
32
Name: "output-topic",
33
EnvVars: []string{"ATKAFKA_OUTPUT_TOPIC"},
34
Required: true,
35
},
36
&cli.BoolFlag{
37
Name: "osprey-compatible",
38
EnvVars: []string{"ATKAFKA_OSPREY_COMPATIBLE"},
39
Value: false,
40
},
41
&cli.StringFlag{
42
Name: "plc-host",
43
EnvVars: []string{"ATKAFKA_PLC_HOST"},
44
},
45
},
46
Action: func(cmd *cli.Context) error {
47
ctx := context.Background()
···
49
telemetry.StartMetrics(cmd)
50
logger := telemetry.StartLogger(cmd)
51
52
-
s := atkafka.NewServer(&atkafka.ServerArgs{
53
-
RelayHost: cmd.String("relay-host"),
54
-
BootstrapServers: cmd.StringSlice("bootstrap-servers"),
55
-
OutputTopic: cmd.String("output-topic"),
56
-
OspreyCompat: cmd.Bool("osprey-compatible"),
57
-
PlcHost: cmd.String("plc-host"),
58
-
Logger: logger,
59
})
60
61
if err := s.Run(ctx); err != nil {
62
return fmt.Errorf("error running server: %w", err)
···
20
telemetry.CLIFlagMetricsListenAddress,
21
&cli.StringFlag{
22
Name: "relay-host",
23
+
Usage: "Websocket host to subscribe to for events",
24
Value: "wss://bsky.network",
25
EnvVars: []string{"ATKAFKA_RELAY_HOST"},
26
},
27
&cli.StringSliceFlag{
28
Name: "bootstrap-servers",
29
+
Usage: "List of Kafka bootstrap servers",
30
EnvVars: []string{"ATKAFKA_BOOTSTRAP_SERVERS"},
31
Required: true,
32
},
33
&cli.StringFlag{
34
Name: "output-topic",
35
+
Usage: "The Kafka topic to produce events to",
36
EnvVars: []string{"ATKAFKA_OUTPUT_TOPIC"},
37
Required: true,
38
},
39
&cli.BoolFlag{
40
Name: "osprey-compatible",
41
+
Usage: "Whether or not events should be formulated in an Osprey-compatible format",
42
EnvVars: []string{"ATKAFKA_OSPREY_COMPATIBLE"},
43
Value: false,
44
},
45
&cli.StringFlag{
46
Name: "plc-host",
47
+
Usage: "The host of the PLC directory you want to use for event metadata",
48
EnvVars: []string{"ATKAFKA_PLC_HOST"},
49
},
50
+
&cli.StringSliceFlag{
51
+
Name: "watched-services",
52
+
Usage: "A list of ATProto services inside a user's DID document that you want to watch. Wildcards like *.bsky.network are allowed.",
53
+
EnvVars: []string{"ATKAFKA_WATCHED_SERVICES"},
54
+
},
55
+
&cli.StringSliceFlag{
56
+
Name: "ignored-services",
57
+
Usage: "A list of ATProto services inside a user's DID document that you want to ignore. Wildcards like *.bsky.network are allowed.",
58
+
EnvVars: []string{"ATKAFKA_IGNORED_SERVICES"},
59
+
},
60
+
&cli.StringSliceFlag{
61
+
Name: "watched-collections",
62
+
Usage: "A list of collections that you want to watch. Wildcards like *.bsky.app are allowed.",
63
+
EnvVars: []string{"ATKAFKA_WATCHED_COLLECTIONS"},
64
+
},
65
+
&cli.StringSliceFlag{
66
+
Name: "ignored-collections",
67
+
Usage: "A list of collections that you want to ignore. Wildcards like *.bsky.app are allowed.",
68
+
EnvVars: []string{"ATKAFKA_IGNORED_COLLECTIONS"},
69
+
},
70
},
71
Action: func(cmd *cli.Context) error {
72
ctx := context.Background()
···
74
telemetry.StartMetrics(cmd)
75
logger := telemetry.StartLogger(cmd)
76
77
+
s, err := atkafka.NewServer(&atkafka.ServerArgs{
78
+
RelayHost: cmd.String("relay-host"),
79
+
BootstrapServers: cmd.StringSlice("bootstrap-servers"),
80
+
OutputTopic: cmd.String("output-topic"),
81
+
OspreyCompat: cmd.Bool("osprey-compatible"),
82
+
PlcHost: cmd.String("plc-host"),
83
+
WatchedServices: cmd.StringSlice("watched-services"),
84
+
IgnoredServices: cmd.StringSlice("ignored-services"),
85
+
WatchedCollections: cmd.StringSlice("watched-collections"),
86
+
IgnoredCollections: cmd.StringSlice("ignored-collections"),
87
+
Logger: logger,
88
})
89
+
if err != nil {
90
+
return fmt.Errorf("failed to create new server: %w", err)
91
+
}
92
93
if err := s.Run(ctx); err != nil {
94
return fmt.Errorf("error running server: %w", err)
+4
docker-compose.yml
+4
docker-compose.yml