@atproto/bsky 0.0.25 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/buf.gen.yaml +12 -0
  3. package/dist/api/app/bsky/unspecced/getTaggedSuggestions.d.ts +3 -0
  4. package/dist/bsync.d.ts +8 -0
  5. package/dist/config.d.ts +20 -0
  6. package/dist/context.d.ts +6 -3
  7. package/dist/courier.d.ts +8 -0
  8. package/dist/db/database-schema.d.ts +2 -1
  9. package/dist/db/index.js +15 -1
  10. package/dist/db/index.js.map +3 -3
  11. package/dist/db/migrations/20240124T023719200Z-tagged-suggestions.d.ts +3 -0
  12. package/dist/db/migrations/index.d.ts +1 -0
  13. package/dist/db/tables/tagged-suggestion.d.ts +9 -0
  14. package/dist/index.js +47930 -16807
  15. package/dist/index.js.map +3 -3
  16. package/dist/indexer/config.d.ts +8 -0
  17. package/dist/indexer/context.d.ts +3 -0
  18. package/dist/ingester/config.d.ts +8 -0
  19. package/dist/ingester/context.d.ts +3 -0
  20. package/dist/ingester/mute-subscription.d.ts +22 -0
  21. package/dist/lexicon/index.d.ts +2 -0
  22. package/dist/lexicon/lexicons.d.ts +48 -0
  23. package/dist/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.d.ts +39 -0
  24. package/dist/notifications.d.ts +27 -16
  25. package/dist/proto/bsync_connect.d.ts +25 -0
  26. package/dist/proto/bsync_pb.d.ts +90 -0
  27. package/dist/proto/courier_connect.d.ts +25 -0
  28. package/dist/proto/courier_pb.d.ts +91 -0
  29. package/dist/services/actor/index.d.ts +2 -2
  30. package/dist/services/indexing/index.d.ts +2 -2
  31. package/dist/services/util/post.d.ts +6 -6
  32. package/dist/util/retry.d.ts +2 -0
  33. package/package.json +15 -7
  34. package/proto/courier.proto +56 -0
  35. package/src/api/app/bsky/graph/muteActor.ts +32 -5
  36. package/src/api/app/bsky/graph/muteActorList.ts +32 -5
  37. package/src/api/app/bsky/graph/unmuteActor.ts +32 -5
  38. package/src/api/app/bsky/graph/unmuteActorList.ts +32 -5
  39. package/src/api/app/bsky/notification/registerPush.ts +42 -8
  40. package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +21 -0
  41. package/src/api/index.ts +2 -0
  42. package/src/bsync.ts +41 -0
  43. package/src/config.ts +79 -0
  44. package/src/context.ts +12 -6
  45. package/src/courier.ts +41 -0
  46. package/src/db/database-schema.ts +2 -0
  47. package/src/db/migrations/20240124T023719200Z-tagged-suggestions.ts +15 -0
  48. package/src/db/migrations/index.ts +1 -0
  49. package/src/db/tables/tagged-suggestion.ts +11 -0
  50. package/src/index.ts +26 -3
  51. package/src/indexer/config.ts +36 -0
  52. package/src/indexer/context.ts +6 -0
  53. package/src/indexer/index.ts +27 -3
  54. package/src/ingester/config.ts +34 -0
  55. package/src/ingester/context.ts +6 -0
  56. package/src/ingester/index.ts +18 -0
  57. package/src/ingester/mute-subscription.ts +213 -0
  58. package/src/lexicon/index.ts +12 -0
  59. package/src/lexicon/lexicons.ts +50 -0
  60. package/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts +65 -0
  61. package/src/notifications.ts +165 -149
  62. package/src/proto/bsync_connect.ts +54 -0
  63. package/src/proto/bsync_pb.ts +459 -0
  64. package/src/proto/courier_connect.ts +50 -0
  65. package/src/proto/courier_pb.ts +473 -0
  66. package/src/services/actor/index.ts +17 -2
  67. package/src/services/indexing/processor.ts +1 -1
  68. package/src/util/retry.ts +12 -0
  69. package/tests/notification-server.test.ts +59 -19
  70. package/tests/subscription/mutes.test.ts +170 -0
  71. package/tests/views/suggestions.test.ts +22 -0
@@ -0,0 +1,473 @@
1
+ // @generated by protoc-gen-es v1.6.0 with parameter "target=ts,import_extension=.ts"
2
+ // @generated from file courier.proto (package courier, syntax proto3)
3
+ /* eslint-disable */
4
+ // @ts-nocheck
5
+
6
+ import type {
7
+ BinaryReadOptions,
8
+ FieldList,
9
+ JsonReadOptions,
10
+ JsonValue,
11
+ PartialMessage,
12
+ PlainMessage,
13
+ } from '@bufbuild/protobuf'
14
+ import { Message, proto3, Struct, Timestamp } from '@bufbuild/protobuf'
15
+
16
+ /**
17
+ * @generated from enum courier.AppPlatform
18
+ */
19
+ export enum AppPlatform {
20
+ /**
21
+ * @generated from enum value: APP_PLATFORM_UNSPECIFIED = 0;
22
+ */
23
+ UNSPECIFIED = 0,
24
+
25
+ /**
26
+ * @generated from enum value: APP_PLATFORM_IOS = 1;
27
+ */
28
+ IOS = 1,
29
+
30
+ /**
31
+ * @generated from enum value: APP_PLATFORM_ANDROID = 2;
32
+ */
33
+ ANDROID = 2,
34
+
35
+ /**
36
+ * @generated from enum value: APP_PLATFORM_WEB = 3;
37
+ */
38
+ WEB = 3,
39
+ }
40
+ // Retrieve enum metadata with: proto3.getEnumType(AppPlatform)
41
+ proto3.util.setEnumType(AppPlatform, 'courier.AppPlatform', [
42
+ { no: 0, name: 'APP_PLATFORM_UNSPECIFIED' },
43
+ { no: 1, name: 'APP_PLATFORM_IOS' },
44
+ { no: 2, name: 'APP_PLATFORM_ANDROID' },
45
+ { no: 3, name: 'APP_PLATFORM_WEB' },
46
+ ])
47
+
48
+ /**
49
+ * Ping
50
+ *
51
+ * @generated from message courier.PingRequest
52
+ */
53
+ export class PingRequest extends Message<PingRequest> {
54
+ constructor(data?: PartialMessage<PingRequest>) {
55
+ super()
56
+ proto3.util.initPartial(data, this)
57
+ }
58
+
59
+ static readonly runtime: typeof proto3 = proto3
60
+ static readonly typeName = 'courier.PingRequest'
61
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
62
+
63
+ static fromBinary(
64
+ bytes: Uint8Array,
65
+ options?: Partial<BinaryReadOptions>,
66
+ ): PingRequest {
67
+ return new PingRequest().fromBinary(bytes, options)
68
+ }
69
+
70
+ static fromJson(
71
+ jsonValue: JsonValue,
72
+ options?: Partial<JsonReadOptions>,
73
+ ): PingRequest {
74
+ return new PingRequest().fromJson(jsonValue, options)
75
+ }
76
+
77
+ static fromJsonString(
78
+ jsonString: string,
79
+ options?: Partial<JsonReadOptions>,
80
+ ): PingRequest {
81
+ return new PingRequest().fromJsonString(jsonString, options)
82
+ }
83
+
84
+ static equals(
85
+ a: PingRequest | PlainMessage<PingRequest> | undefined,
86
+ b: PingRequest | PlainMessage<PingRequest> | undefined,
87
+ ): boolean {
88
+ return proto3.util.equals(PingRequest, a, b)
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @generated from message courier.PingResponse
94
+ */
95
+ export class PingResponse extends Message<PingResponse> {
96
+ constructor(data?: PartialMessage<PingResponse>) {
97
+ super()
98
+ proto3.util.initPartial(data, this)
99
+ }
100
+
101
+ static readonly runtime: typeof proto3 = proto3
102
+ static readonly typeName = 'courier.PingResponse'
103
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
104
+
105
+ static fromBinary(
106
+ bytes: Uint8Array,
107
+ options?: Partial<BinaryReadOptions>,
108
+ ): PingResponse {
109
+ return new PingResponse().fromBinary(bytes, options)
110
+ }
111
+
112
+ static fromJson(
113
+ jsonValue: JsonValue,
114
+ options?: Partial<JsonReadOptions>,
115
+ ): PingResponse {
116
+ return new PingResponse().fromJson(jsonValue, options)
117
+ }
118
+
119
+ static fromJsonString(
120
+ jsonString: string,
121
+ options?: Partial<JsonReadOptions>,
122
+ ): PingResponse {
123
+ return new PingResponse().fromJsonString(jsonString, options)
124
+ }
125
+
126
+ static equals(
127
+ a: PingResponse | PlainMessage<PingResponse> | undefined,
128
+ b: PingResponse | PlainMessage<PingResponse> | undefined,
129
+ ): boolean {
130
+ return proto3.util.equals(PingResponse, a, b)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @generated from message courier.Notification
136
+ */
137
+ export class Notification extends Message<Notification> {
138
+ /**
139
+ * @generated from field: string id = 1;
140
+ */
141
+ id = ''
142
+
143
+ /**
144
+ * @generated from field: string recipient_did = 2;
145
+ */
146
+ recipientDid = ''
147
+
148
+ /**
149
+ * @generated from field: string title = 3;
150
+ */
151
+ title = ''
152
+
153
+ /**
154
+ * @generated from field: string message = 4;
155
+ */
156
+ message = ''
157
+
158
+ /**
159
+ * @generated from field: string collapse_key = 5;
160
+ */
161
+ collapseKey = ''
162
+
163
+ /**
164
+ * @generated from field: bool always_deliver = 6;
165
+ */
166
+ alwaysDeliver = false
167
+
168
+ /**
169
+ * @generated from field: google.protobuf.Timestamp timestamp = 7;
170
+ */
171
+ timestamp?: Timestamp
172
+
173
+ /**
174
+ * @generated from field: google.protobuf.Struct additional = 8;
175
+ */
176
+ additional?: Struct
177
+
178
+ constructor(data?: PartialMessage<Notification>) {
179
+ super()
180
+ proto3.util.initPartial(data, this)
181
+ }
182
+
183
+ static readonly runtime: typeof proto3 = proto3
184
+ static readonly typeName = 'courier.Notification'
185
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
186
+ { no: 1, name: 'id', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
187
+ {
188
+ no: 2,
189
+ name: 'recipient_did',
190
+ kind: 'scalar',
191
+ T: 9 /* ScalarType.STRING */,
192
+ },
193
+ { no: 3, name: 'title', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
194
+ { no: 4, name: 'message', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
195
+ {
196
+ no: 5,
197
+ name: 'collapse_key',
198
+ kind: 'scalar',
199
+ T: 9 /* ScalarType.STRING */,
200
+ },
201
+ {
202
+ no: 6,
203
+ name: 'always_deliver',
204
+ kind: 'scalar',
205
+ T: 8 /* ScalarType.BOOL */,
206
+ },
207
+ { no: 7, name: 'timestamp', kind: 'message', T: Timestamp },
208
+ { no: 8, name: 'additional', kind: 'message', T: Struct },
209
+ ])
210
+
211
+ static fromBinary(
212
+ bytes: Uint8Array,
213
+ options?: Partial<BinaryReadOptions>,
214
+ ): Notification {
215
+ return new Notification().fromBinary(bytes, options)
216
+ }
217
+
218
+ static fromJson(
219
+ jsonValue: JsonValue,
220
+ options?: Partial<JsonReadOptions>,
221
+ ): Notification {
222
+ return new Notification().fromJson(jsonValue, options)
223
+ }
224
+
225
+ static fromJsonString(
226
+ jsonString: string,
227
+ options?: Partial<JsonReadOptions>,
228
+ ): Notification {
229
+ return new Notification().fromJsonString(jsonString, options)
230
+ }
231
+
232
+ static equals(
233
+ a: Notification | PlainMessage<Notification> | undefined,
234
+ b: Notification | PlainMessage<Notification> | undefined,
235
+ ): boolean {
236
+ return proto3.util.equals(Notification, a, b)
237
+ }
238
+ }
239
+
240
+ /**
241
+ * @generated from message courier.PushNotificationsRequest
242
+ */
243
+ export class PushNotificationsRequest extends Message<PushNotificationsRequest> {
244
+ /**
245
+ * @generated from field: repeated courier.Notification notifications = 1;
246
+ */
247
+ notifications: Notification[] = []
248
+
249
+ constructor(data?: PartialMessage<PushNotificationsRequest>) {
250
+ super()
251
+ proto3.util.initPartial(data, this)
252
+ }
253
+
254
+ static readonly runtime: typeof proto3 = proto3
255
+ static readonly typeName = 'courier.PushNotificationsRequest'
256
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
257
+ {
258
+ no: 1,
259
+ name: 'notifications',
260
+ kind: 'message',
261
+ T: Notification,
262
+ repeated: true,
263
+ },
264
+ ])
265
+
266
+ static fromBinary(
267
+ bytes: Uint8Array,
268
+ options?: Partial<BinaryReadOptions>,
269
+ ): PushNotificationsRequest {
270
+ return new PushNotificationsRequest().fromBinary(bytes, options)
271
+ }
272
+
273
+ static fromJson(
274
+ jsonValue: JsonValue,
275
+ options?: Partial<JsonReadOptions>,
276
+ ): PushNotificationsRequest {
277
+ return new PushNotificationsRequest().fromJson(jsonValue, options)
278
+ }
279
+
280
+ static fromJsonString(
281
+ jsonString: string,
282
+ options?: Partial<JsonReadOptions>,
283
+ ): PushNotificationsRequest {
284
+ return new PushNotificationsRequest().fromJsonString(jsonString, options)
285
+ }
286
+
287
+ static equals(
288
+ a:
289
+ | PushNotificationsRequest
290
+ | PlainMessage<PushNotificationsRequest>
291
+ | undefined,
292
+ b:
293
+ | PushNotificationsRequest
294
+ | PlainMessage<PushNotificationsRequest>
295
+ | undefined,
296
+ ): boolean {
297
+ return proto3.util.equals(PushNotificationsRequest, a, b)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * @generated from message courier.PushNotificationsResponse
303
+ */
304
+ export class PushNotificationsResponse extends Message<PushNotificationsResponse> {
305
+ constructor(data?: PartialMessage<PushNotificationsResponse>) {
306
+ super()
307
+ proto3.util.initPartial(data, this)
308
+ }
309
+
310
+ static readonly runtime: typeof proto3 = proto3
311
+ static readonly typeName = 'courier.PushNotificationsResponse'
312
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
313
+
314
+ static fromBinary(
315
+ bytes: Uint8Array,
316
+ options?: Partial<BinaryReadOptions>,
317
+ ): PushNotificationsResponse {
318
+ return new PushNotificationsResponse().fromBinary(bytes, options)
319
+ }
320
+
321
+ static fromJson(
322
+ jsonValue: JsonValue,
323
+ options?: Partial<JsonReadOptions>,
324
+ ): PushNotificationsResponse {
325
+ return new PushNotificationsResponse().fromJson(jsonValue, options)
326
+ }
327
+
328
+ static fromJsonString(
329
+ jsonString: string,
330
+ options?: Partial<JsonReadOptions>,
331
+ ): PushNotificationsResponse {
332
+ return new PushNotificationsResponse().fromJsonString(jsonString, options)
333
+ }
334
+
335
+ static equals(
336
+ a:
337
+ | PushNotificationsResponse
338
+ | PlainMessage<PushNotificationsResponse>
339
+ | undefined,
340
+ b:
341
+ | PushNotificationsResponse
342
+ | PlainMessage<PushNotificationsResponse>
343
+ | undefined,
344
+ ): boolean {
345
+ return proto3.util.equals(PushNotificationsResponse, a, b)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * @generated from message courier.RegisterDeviceTokenRequest
351
+ */
352
+ export class RegisterDeviceTokenRequest extends Message<RegisterDeviceTokenRequest> {
353
+ /**
354
+ * @generated from field: string did = 1;
355
+ */
356
+ did = ''
357
+
358
+ /**
359
+ * @generated from field: string token = 2;
360
+ */
361
+ token = ''
362
+
363
+ /**
364
+ * @generated from field: string app_id = 3;
365
+ */
366
+ appId = ''
367
+
368
+ /**
369
+ * @generated from field: courier.AppPlatform platform = 4;
370
+ */
371
+ platform = AppPlatform.UNSPECIFIED
372
+
373
+ constructor(data?: PartialMessage<RegisterDeviceTokenRequest>) {
374
+ super()
375
+ proto3.util.initPartial(data, this)
376
+ }
377
+
378
+ static readonly runtime: typeof proto3 = proto3
379
+ static readonly typeName = 'courier.RegisterDeviceTokenRequest'
380
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
381
+ { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
382
+ { no: 2, name: 'token', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
383
+ { no: 3, name: 'app_id', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
384
+ {
385
+ no: 4,
386
+ name: 'platform',
387
+ kind: 'enum',
388
+ T: proto3.getEnumType(AppPlatform),
389
+ },
390
+ ])
391
+
392
+ static fromBinary(
393
+ bytes: Uint8Array,
394
+ options?: Partial<BinaryReadOptions>,
395
+ ): RegisterDeviceTokenRequest {
396
+ return new RegisterDeviceTokenRequest().fromBinary(bytes, options)
397
+ }
398
+
399
+ static fromJson(
400
+ jsonValue: JsonValue,
401
+ options?: Partial<JsonReadOptions>,
402
+ ): RegisterDeviceTokenRequest {
403
+ return new RegisterDeviceTokenRequest().fromJson(jsonValue, options)
404
+ }
405
+
406
+ static fromJsonString(
407
+ jsonString: string,
408
+ options?: Partial<JsonReadOptions>,
409
+ ): RegisterDeviceTokenRequest {
410
+ return new RegisterDeviceTokenRequest().fromJsonString(jsonString, options)
411
+ }
412
+
413
+ static equals(
414
+ a:
415
+ | RegisterDeviceTokenRequest
416
+ | PlainMessage<RegisterDeviceTokenRequest>
417
+ | undefined,
418
+ b:
419
+ | RegisterDeviceTokenRequest
420
+ | PlainMessage<RegisterDeviceTokenRequest>
421
+ | undefined,
422
+ ): boolean {
423
+ return proto3.util.equals(RegisterDeviceTokenRequest, a, b)
424
+ }
425
+ }
426
+
427
+ /**
428
+ * @generated from message courier.RegisterDeviceTokenResponse
429
+ */
430
+ export class RegisterDeviceTokenResponse extends Message<RegisterDeviceTokenResponse> {
431
+ constructor(data?: PartialMessage<RegisterDeviceTokenResponse>) {
432
+ super()
433
+ proto3.util.initPartial(data, this)
434
+ }
435
+
436
+ static readonly runtime: typeof proto3 = proto3
437
+ static readonly typeName = 'courier.RegisterDeviceTokenResponse'
438
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
439
+
440
+ static fromBinary(
441
+ bytes: Uint8Array,
442
+ options?: Partial<BinaryReadOptions>,
443
+ ): RegisterDeviceTokenResponse {
444
+ return new RegisterDeviceTokenResponse().fromBinary(bytes, options)
445
+ }
446
+
447
+ static fromJson(
448
+ jsonValue: JsonValue,
449
+ options?: Partial<JsonReadOptions>,
450
+ ): RegisterDeviceTokenResponse {
451
+ return new RegisterDeviceTokenResponse().fromJson(jsonValue, options)
452
+ }
453
+
454
+ static fromJsonString(
455
+ jsonString: string,
456
+ options?: Partial<JsonReadOptions>,
457
+ ): RegisterDeviceTokenResponse {
458
+ return new RegisterDeviceTokenResponse().fromJsonString(jsonString, options)
459
+ }
460
+
461
+ static equals(
462
+ a:
463
+ | RegisterDeviceTokenResponse
464
+ | PlainMessage<RegisterDeviceTokenResponse>
465
+ | undefined,
466
+ b:
467
+ | RegisterDeviceTokenResponse
468
+ | PlainMessage<RegisterDeviceTokenResponse>
469
+ | undefined,
470
+ ): boolean {
471
+ return proto3.util.equals(RegisterDeviceTokenResponse, a, b)
472
+ }
473
+ }
@@ -12,6 +12,7 @@ import { GraphService } from '../graph'
12
12
  import { LabelService } from '../label'
13
13
  import { AtUri } from '@atproto/syntax'
14
14
  import { ids } from '../../lexicon/lexicons'
15
+ import { Platform } from '../../notifications'
15
16
 
16
17
  export * from './types'
17
18
 
@@ -21,8 +22,8 @@ export class ActorService {
21
22
  constructor(
22
23
  public db: Database,
23
24
  public imgUriBuilder: ImageUriBuilder,
24
- private graph: FromDb<GraphService>,
25
- private label: FromDb<LabelService>,
25
+ graph: FromDb<GraphService>,
26
+ label: FromDb<LabelService>,
26
27
  ) {
27
28
  this.views = new ActorViews(this.db, this.imgUriBuilder, graph, label)
28
29
  }
@@ -214,6 +215,20 @@ export class ActorService {
214
215
  }
215
216
  }
216
217
  }
218
+
219
+ async registerPushDeviceToken(
220
+ did: string,
221
+ token: string,
222
+ platform: Platform,
223
+ appId: string,
224
+ ) {
225
+ await this.db
226
+ .asPrimary()
227
+ .db.insertInto('notification_push_token')
228
+ .values({ did, token, platform, appId })
229
+ .onConflict((oc) => oc.doNothing())
230
+ .execute()
231
+ }
217
232
  }
218
233
 
219
234
  type ActorResult = Actor
@@ -257,7 +257,7 @@ export class RecordProcessor<T, S> {
257
257
  const notifServer = this.notifServer
258
258
  sendOnCommit.push(async () => {
259
259
  try {
260
- const preparedNotifs = await notifServer.prepareNotifsToSend(chunk)
260
+ const preparedNotifs = await notifServer.prepareNotifications(chunk)
261
261
  await notifServer.processNotifications(preparedNotifs)
262
262
  } catch (error) {
263
263
  dbLogger.error({ error }, 'error sending push notifications')
package/src/util/retry.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AxiosError } from 'axios'
2
2
  import { XRPCError, ResponseType } from '@atproto/xrpc'
3
3
  import { RetryOptions, retry } from '@atproto/common'
4
+ import { Code, ConnectError } from '@connectrpc/connect'
4
5
 
5
6
  export async function retryHttp<T>(
6
7
  fn: () => Promise<T>,
@@ -24,3 +25,14 @@ export function retryableHttp(err: unknown) {
24
25
  const retryableHttpStatusCodes = new Set([
25
26
  408, 425, 429, 500, 502, 503, 504, 522, 524,
26
27
  ])
28
+
29
+ export async function retryConnect<T>(
30
+ fn: () => Promise<T>,
31
+ opts: RetryOptions = {},
32
+ ): Promise<T> {
33
+ return retry(fn, { retryable: retryableConnect, ...opts })
34
+ }
35
+
36
+ export function retryableConnect(err: unknown) {
37
+ return err instanceof ConnectError && err.code === Code.Unavailable
38
+ }
@@ -1,14 +1,18 @@
1
1
  import AtpAgent, { AtUri } from '@atproto/api'
2
2
  import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
3
- import { NotificationServer } from '../src/notifications'
3
+ import {
4
+ CourierNotificationServer,
5
+ GorushNotificationServer,
6
+ } from '../src/notifications'
4
7
  import { Database } from '../src'
8
+ import { createCourierClient } from '../src/courier'
5
9
 
6
10
  describe('notification server', () => {
7
11
  let network: TestNetwork
8
12
  let agent: AtpAgent
9
13
  let pdsAgent: AtpAgent
10
14
  let sc: SeedClient
11
- let notifServer: NotificationServer
15
+ let notifServer: GorushNotificationServer
12
16
 
13
17
  // account dids, for convenience
14
18
  let alice: string
@@ -24,14 +28,17 @@ describe('notification server', () => {
24
28
  await network.processAll()
25
29
  await network.bsky.processAll()
26
30
  alice = sc.dids.alice
27
- notifServer = network.bsky.ctx.notifServer
31
+ notifServer = new GorushNotificationServer(
32
+ network.bsky.ctx.db.getPrimary(),
33
+ 'http://mock',
34
+ )
28
35
  })
29
36
 
30
37
  afterAll(async () => {
31
38
  await network.close()
32
39
  })
33
40
 
34
- describe('registerPushNotification', () => {
41
+ describe('registerPush', () => {
35
42
  it('registers push notification token and device.', async () => {
36
43
  const res = await agent.api.app.bsky.notification.registerPush(
37
44
  {
@@ -95,19 +102,14 @@ describe('notification server', () => {
95
102
  })
96
103
 
97
104
  describe('NotificationServer', () => {
98
- it('gets user tokens from db', async () => {
99
- const tokens = await notifServer.getTokensByDid([alice])
100
- expect(tokens[alice][0].token).toEqual('123')
101
- })
102
-
103
105
  it('gets notification display attributes: title and body', async () => {
104
106
  const db = network.bsky.ctx.db.getPrimary()
105
107
  const notif = await getLikeNotification(db, alice)
106
108
  if (!notif) throw new Error('no notification found')
107
- const attrs = await notifServer.getNotificationDisplayAttributes([notif])
108
- if (!attrs.length)
109
+ const views = await notifServer.getNotificationViews([notif])
110
+ if (!views.length)
109
111
  throw new Error('no notification display attributes found')
110
- expect(attrs[0].title).toEqual('bobby liked your post')
112
+ expect(views[0].title).toEqual('bobby liked your post')
111
113
  })
112
114
 
113
115
  it('filters notifications that violate blocks', async () => {
@@ -126,11 +128,11 @@ describe('notification server', () => {
126
128
  did: notif.author,
127
129
  author: notif.did,
128
130
  }
129
- const attrs = await notifServer.getNotificationDisplayAttributes([
131
+ const views = await notifServer.getNotificationViews([
130
132
  notif,
131
133
  flippedNotif,
132
134
  ])
133
- expect(attrs.length).toBe(0)
135
+ expect(views.length).toBe(0)
134
136
  const uri = new AtUri(blockRef.uri)
135
137
  await pdsAgent.api.app.bsky.graph.block.delete(
136
138
  { repo: alice, rkey: uri.rkey },
@@ -147,8 +149,8 @@ describe('notification server', () => {
147
149
  { actor: notif.author },
148
150
  { headers: sc.getHeaders(alice), encoding: 'application/json' },
149
151
  )
150
- const attrs = await notifServer.getNotificationDisplayAttributes([notif])
151
- expect(attrs.length).toBe(0)
152
+ const views = await notifServer.getNotificationViews([notif])
153
+ expect(views.length).toBe(0)
152
154
  await pdsAgent.api.app.bsky.graph.unmuteActor(
153
155
  { actor: notif.author },
154
156
  { headers: sc.getHeaders(alice), encoding: 'application/json' },
@@ -182,13 +184,20 @@ describe('notification server', () => {
182
184
  { list: listRef.uri },
183
185
  { headers: sc.getHeaders(alice), encoding: 'application/json' },
184
186
  )
185
- const attrs = await notifServer.getNotificationDisplayAttributes([notif])
186
- expect(attrs.length).toBe(0)
187
+ const views = await notifServer.getNotificationViews([notif])
188
+ expect(views.length).toBe(0)
187
189
  await pdsAgent.api.app.bsky.graph.unmuteActorList(
188
190
  { list: listRef.uri },
189
191
  { headers: sc.getHeaders(alice), encoding: 'application/json' },
190
192
  )
191
193
  })
194
+ })
195
+
196
+ describe('GorushNotificationServer', () => {
197
+ it('gets user tokens from db', async () => {
198
+ const tokens = await notifServer.getTokensByDid([alice])
199
+ expect(tokens[alice][0].token).toEqual('123')
200
+ })
192
201
 
193
202
  it('prepares notification to be sent', async () => {
194
203
  const db = network.bsky.ctx.db.getPrimary()
@@ -198,7 +207,7 @@ describe('notification server', () => {
198
207
  notif,
199
208
  notif /* second one will get dropped by rate limit */,
200
209
  ]
201
- const prepared = await notifServer.prepareNotifsToSend(notifAsArray)
210
+ const prepared = await notifServer.prepareNotifications(notifAsArray)
202
211
  expect(prepared).toEqual([
203
212
  {
204
213
  collapse_id: 'like',
@@ -218,6 +227,37 @@ describe('notification server', () => {
218
227
  })
219
228
  })
220
229
 
230
+ describe('CourierNotificationServer', () => {
231
+ it('prepares notification to be sent', async () => {
232
+ const db = network.bsky.ctx.db.getPrimary()
233
+ const notif = await getLikeNotification(db, alice)
234
+ if (!notif) throw new Error('no notification found')
235
+ const courierNotifServer = new CourierNotificationServer(
236
+ db,
237
+ createCourierClient({ baseUrl: 'http://mock', httpVersion: '2' }),
238
+ )
239
+ const prepared = await courierNotifServer.prepareNotifications([notif])
240
+ expect(prepared[0]?.id).toBeTruthy()
241
+ expect(prepared.map((p) => p.toJson())).toEqual([
242
+ {
243
+ id: prepared[0].id, // already ensured it exists
244
+ recipientDid: notif.did,
245
+ title: 'bobby liked your post',
246
+ message: 'again',
247
+ collapseKey: 'like',
248
+ timestamp: notif.sortAt,
249
+ // this is missing, appears to be a quirk of toJson()
250
+ // alwaysDeliver: false,
251
+ additional: {
252
+ reason: notif.reason,
253
+ uri: notif.recordUri,
254
+ subject: notif.reasonSubject,
255
+ },
256
+ },
257
+ ])
258
+ })
259
+ })
260
+
221
261
  async function getLikeNotification(db: Database, did: string) {
222
262
  return await db.db
223
263
  .selectFrom('notification')