@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,213 @@
1
+ import assert from 'node:assert'
2
+ import { PrimaryDatabase } from '../db'
3
+ import { Redis } from '../redis'
4
+ import { BsyncClient, Code, isBsyncError } from '../bsync'
5
+ import { MuteOperation, MuteOperation_Type } from '../proto/bsync_pb'
6
+ import logger from './logger'
7
+ import { wait } from '@atproto/common'
8
+ import {
9
+ AtUri,
10
+ InvalidDidError,
11
+ ensureValidAtUri,
12
+ ensureValidDid,
13
+ } from '@atproto/syntax'
14
+ import { ids } from '../lexicon/lexicons'
15
+
16
+ const CURSOR_KEY = 'ingester:mute:cursor'
17
+
18
+ export class MuteSubscription {
19
+ ac = new AbortController()
20
+ running: Promise<void> | undefined
21
+ cursor: string | null = null
22
+
23
+ constructor(
24
+ public db: PrimaryDatabase,
25
+ public redis: Redis,
26
+ public bsyncClient: BsyncClient,
27
+ ) {}
28
+
29
+ async start() {
30
+ if (this.running) return
31
+ this.ac = new AbortController()
32
+ this.running = this.run()
33
+ .catch((err) => {
34
+ // allow this to cause an unhandled rejection, let deployment handle the crash.
35
+ logger.error({ err }, 'mute subscription crashed')
36
+ throw err
37
+ })
38
+ .finally(() => (this.running = undefined))
39
+ }
40
+
41
+ private async run() {
42
+ this.cursor = await this.getCursor()
43
+ while (!this.ac.signal.aborted) {
44
+ try {
45
+ // get page of mute ops, long-polling
46
+ const page = await this.bsyncClient.scanMuteOperations(
47
+ {
48
+ limit: 100,
49
+ cursor: this.cursor ?? undefined,
50
+ },
51
+ { signal: this.ac.signal },
52
+ )
53
+ if (!page.cursor) {
54
+ throw new BadResponseError('cursor is missing')
55
+ }
56
+ // process
57
+ const now = new Date()
58
+ for (const op of page.operations) {
59
+ if (this.ac.signal.aborted) return
60
+ if (op.type === MuteOperation_Type.ADD) {
61
+ await this.handleAddOp(op, now)
62
+ } else if (op.type === MuteOperation_Type.REMOVE) {
63
+ await this.handleRemoveOp(op)
64
+ } else if (op.type === MuteOperation_Type.CLEAR) {
65
+ await this.handleClearOp(op)
66
+ } else {
67
+ logger.warn(
68
+ { id: op.id, type: op.type },
69
+ 'unknown mute subscription op type',
70
+ )
71
+ }
72
+ }
73
+ // update cursor
74
+ await this.setCursor(page.cursor)
75
+ this.cursor = page.cursor
76
+ } catch (err) {
77
+ if (isBsyncError(err, Code.Canceled)) {
78
+ return // canceled, probably from destroy()
79
+ }
80
+ if (err instanceof BadResponseError) {
81
+ logger.warn({ err }, 'bad response from bsync')
82
+ } else {
83
+ logger.error({ err }, 'unexpected error processing mute subscription')
84
+ }
85
+ await wait(1000) // wait a second before trying again
86
+ }
87
+ }
88
+ }
89
+
90
+ async handleAddOp(op: MuteOperation, createdAt: Date) {
91
+ assert(op.type === MuteOperation_Type.ADD)
92
+ if (!isValidDid(op.actorDid)) {
93
+ logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
94
+ return
95
+ }
96
+ if (isValidDid(op.subject)) {
97
+ await this.db.db
98
+ .insertInto('mute')
99
+ .values({
100
+ subjectDid: op.subject,
101
+ mutedByDid: op.actorDid,
102
+ createdAt: createdAt.toISOString(),
103
+ })
104
+ .onConflict((oc) => oc.doNothing())
105
+ .execute()
106
+ } else {
107
+ const listUri = isValidAtUri(op.subject)
108
+ ? new AtUri(op.subject)
109
+ : undefined
110
+ if (listUri?.collection !== ids.AppBskyGraphList) {
111
+ logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
112
+ return
113
+ }
114
+ await this.db.db
115
+ .insertInto('list_mute')
116
+ .values({
117
+ listUri: op.subject,
118
+ mutedByDid: op.actorDid,
119
+ createdAt: createdAt.toISOString(),
120
+ })
121
+ .onConflict((oc) => oc.doNothing())
122
+ .execute()
123
+ }
124
+ }
125
+
126
+ async handleRemoveOp(op: MuteOperation) {
127
+ assert(op.type === MuteOperation_Type.REMOVE)
128
+ if (!isValidDid(op.actorDid)) {
129
+ logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
130
+ return
131
+ }
132
+ if (isValidDid(op.subject)) {
133
+ await this.db.db
134
+ .deleteFrom('mute')
135
+ .where('subjectDid', '=', op.subject)
136
+ .where('mutedByDid', '=', op.actorDid)
137
+ .execute()
138
+ } else {
139
+ const listUri = isValidAtUri(op.subject)
140
+ ? new AtUri(op.subject)
141
+ : undefined
142
+ if (listUri?.collection !== ids.AppBskyGraphList) {
143
+ logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
144
+ return
145
+ }
146
+ await this.db.db
147
+ .deleteFrom('list_mute')
148
+ .where('listUri', '=', op.subject)
149
+ .where('mutedByDid', '=', op.actorDid)
150
+ .execute()
151
+ }
152
+ }
153
+
154
+ async handleClearOp(op: MuteOperation) {
155
+ assert(op.type === MuteOperation_Type.CLEAR)
156
+ if (!isValidDid(op.actorDid)) {
157
+ logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
158
+ return
159
+ }
160
+ if (op.subject) {
161
+ logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
162
+ return
163
+ }
164
+ await this.db.db
165
+ .deleteFrom('mute')
166
+ .where('mutedByDid', '=', op.actorDid)
167
+ .execute()
168
+ await this.db.db
169
+ .deleteFrom('list_mute')
170
+ .where('mutedByDid', '=', op.actorDid)
171
+ .execute()
172
+ }
173
+
174
+ async getCursor(): Promise<string | null> {
175
+ return await this.redis.get(CURSOR_KEY)
176
+ }
177
+
178
+ async setCursor(cursor: string): Promise<void> {
179
+ await this.redis.set(CURSOR_KEY, cursor)
180
+ }
181
+
182
+ async destroy() {
183
+ this.ac.abort()
184
+ await this.running
185
+ }
186
+
187
+ get destroyed() {
188
+ return this.ac.signal.aborted
189
+ }
190
+ }
191
+
192
+ class BadResponseError extends Error {}
193
+
194
+ const isValidDid = (did: string) => {
195
+ try {
196
+ ensureValidDid(did)
197
+ return true
198
+ } catch (err) {
199
+ if (err instanceof InvalidDidError) {
200
+ return false
201
+ }
202
+ throw err
203
+ }
204
+ }
205
+
206
+ const isValidAtUri = (uri: string) => {
207
+ try {
208
+ ensureValidAtUri(uri)
209
+ return true
210
+ } catch {
211
+ return false
212
+ }
213
+ }
@@ -124,6 +124,7 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica
124
124
  import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush'
125
125
  import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'
126
126
  import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators'
127
+ import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions'
127
128
  import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton'
128
129
  import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton'
129
130
  import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton'
@@ -1613,6 +1614,17 @@ export class AppBskyUnspeccedNS {
1613
1614
  return this._server.xrpc.method(nsid, cfg)
1614
1615
  }
1615
1616
 
1617
+ getTaggedSuggestions<AV extends AuthVerifier>(
1618
+ cfg: ConfigOf<
1619
+ AV,
1620
+ AppBskyUnspeccedGetTaggedSuggestions.Handler<ExtractAuth<AV>>,
1621
+ AppBskyUnspeccedGetTaggedSuggestions.HandlerReqCtx<ExtractAuth<AV>>
1622
+ >,
1623
+ ) {
1624
+ const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore
1625
+ return this._server.xrpc.method(nsid, cfg)
1626
+ }
1627
+
1616
1628
  getTimelineSkeleton<AV extends AuthVerifier>(
1617
1629
  cfg: ConfigOf<
1618
1630
  AV,
@@ -7908,6 +7908,54 @@ export const schemaDict = {
7908
7908
  },
7909
7909
  },
7910
7910
  },
7911
+ AppBskyUnspeccedGetTaggedSuggestions: {
7912
+ lexicon: 1,
7913
+ id: 'app.bsky.unspecced.getTaggedSuggestions',
7914
+ defs: {
7915
+ main: {
7916
+ type: 'query',
7917
+ description:
7918
+ 'Get a list of suggestions (feeds and users) tagged with categories',
7919
+ parameters: {
7920
+ type: 'params',
7921
+ properties: {},
7922
+ },
7923
+ output: {
7924
+ encoding: 'application/json',
7925
+ schema: {
7926
+ type: 'object',
7927
+ required: ['suggestions'],
7928
+ properties: {
7929
+ suggestions: {
7930
+ type: 'array',
7931
+ items: {
7932
+ type: 'ref',
7933
+ ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',
7934
+ },
7935
+ },
7936
+ },
7937
+ },
7938
+ },
7939
+ },
7940
+ suggestion: {
7941
+ type: 'object',
7942
+ required: ['tag', 'subjectType', 'subject'],
7943
+ properties: {
7944
+ tag: {
7945
+ type: 'string',
7946
+ },
7947
+ subjectType: {
7948
+ type: 'string',
7949
+ knownValues: ['actor', 'feed'],
7950
+ },
7951
+ subject: {
7952
+ type: 'string',
7953
+ format: 'uri',
7954
+ },
7955
+ },
7956
+ },
7957
+ },
7958
+ },
7911
7959
  AppBskyUnspeccedGetTimelineSkeleton: {
7912
7960
  lexicon: 1,
7913
7961
  id: 'app.bsky.unspecced.getTimelineSkeleton',
@@ -8242,6 +8290,8 @@ export const ids = {
8242
8290
  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',
8243
8291
  AppBskyUnspeccedGetPopularFeedGenerators:
8244
8292
  'app.bsky.unspecced.getPopularFeedGenerators',
8293
+ AppBskyUnspeccedGetTaggedSuggestions:
8294
+ 'app.bsky.unspecced.getTaggedSuggestions',
8245
8295
  AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton',
8246
8296
  AppBskyUnspeccedSearchActorsSkeleton:
8247
8297
  'app.bsky.unspecced.searchActorsSkeleton',
@@ -0,0 +1,65 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth } from '@atproto/xrpc-server'
10
+
11
+ export interface QueryParams {}
12
+
13
+ export type InputSchema = undefined
14
+
15
+ export interface OutputSchema {
16
+ suggestions: Suggestion[]
17
+ [k: string]: unknown
18
+ }
19
+
20
+ export type HandlerInput = undefined
21
+
22
+ export interface HandlerSuccess {
23
+ encoding: 'application/json'
24
+ body: OutputSchema
25
+ headers?: { [key: string]: string }
26
+ }
27
+
28
+ export interface HandlerError {
29
+ status: number
30
+ message?: string
31
+ }
32
+
33
+ export type HandlerOutput = HandlerError | HandlerSuccess
34
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
35
+ auth: HA
36
+ params: QueryParams
37
+ input: HandlerInput
38
+ req: express.Request
39
+ res: express.Response
40
+ }
41
+ export type Handler<HA extends HandlerAuth = never> = (
42
+ ctx: HandlerReqCtx<HA>,
43
+ ) => Promise<HandlerOutput> | HandlerOutput
44
+
45
+ export interface Suggestion {
46
+ tag: string
47
+ subjectType: 'actor' | 'feed' | (string & {})
48
+ subject: string
49
+ [k: string]: unknown
50
+ }
51
+
52
+ export function isSuggestion(v: unknown): v is Suggestion {
53
+ return (
54
+ isObj(v) &&
55
+ hasProp(v, '$type') &&
56
+ v.$type === 'app.bsky.unspecced.getTaggedSuggestions#suggestion'
57
+ )
58
+ }
59
+
60
+ export function validateSuggestion(v: unknown): ValidationResult {
61
+ return lexicons.validate(
62
+ 'app.bsky.unspecced.getTaggedSuggestions#suggestion',
63
+ v,
64
+ )
65
+ }