@atproto/bsky 0.0.25 → 0.0.27

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 (81) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/buf.gen.yaml +12 -0
  3. package/dist/api/app/bsky/graph/getRelationships.d.ts +3 -0
  4. package/dist/api/app/bsky/unspecced/getTaggedSuggestions.d.ts +3 -0
  5. package/dist/bsync.d.ts +8 -0
  6. package/dist/config.d.ts +20 -0
  7. package/dist/context.d.ts +6 -3
  8. package/dist/courier.d.ts +8 -0
  9. package/dist/db/database-schema.d.ts +2 -1
  10. package/dist/db/index.js +15 -1
  11. package/dist/db/index.js.map +3 -3
  12. package/dist/db/migrations/20240124T023719200Z-tagged-suggestions.d.ts +3 -0
  13. package/dist/db/migrations/index.d.ts +1 -0
  14. package/dist/db/tables/tagged-suggestion.d.ts +9 -0
  15. package/dist/index.js +48246 -16828
  16. package/dist/index.js.map +3 -3
  17. package/dist/indexer/config.d.ts +8 -0
  18. package/dist/indexer/context.d.ts +3 -0
  19. package/dist/ingester/config.d.ts +8 -0
  20. package/dist/ingester/context.d.ts +3 -0
  21. package/dist/ingester/mute-subscription.d.ts +22 -0
  22. package/dist/lexicon/index.d.ts +4 -0
  23. package/dist/lexicon/lexicons.d.ts +153 -0
  24. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +7 -1
  25. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +15 -0
  26. package/dist/lexicon/types/app/bsky/graph/getRelationships.d.ts +38 -0
  27. package/dist/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.d.ts +39 -0
  28. package/dist/notifications.d.ts +27 -16
  29. package/dist/proto/bsync_connect.d.ts +25 -0
  30. package/dist/proto/bsync_pb.d.ts +90 -0
  31. package/dist/proto/courier_connect.d.ts +25 -0
  32. package/dist/proto/courier_pb.d.ts +91 -0
  33. package/dist/services/actor/index.d.ts +2 -2
  34. package/dist/services/indexing/index.d.ts +2 -2
  35. package/dist/services/util/post.d.ts +6 -6
  36. package/dist/util/retry.d.ts +2 -0
  37. package/package.json +15 -7
  38. package/proto/courier.proto +56 -0
  39. package/src/api/app/bsky/graph/getRelationships.ts +71 -0
  40. package/src/api/app/bsky/graph/muteActor.ts +32 -5
  41. package/src/api/app/bsky/graph/muteActorList.ts +32 -5
  42. package/src/api/app/bsky/graph/unmuteActor.ts +32 -5
  43. package/src/api/app/bsky/graph/unmuteActorList.ts +32 -5
  44. package/src/api/app/bsky/notification/registerPush.ts +42 -8
  45. package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +21 -0
  46. package/src/api/index.ts +4 -0
  47. package/src/bsync.ts +41 -0
  48. package/src/config.ts +79 -0
  49. package/src/context.ts +12 -6
  50. package/src/courier.ts +41 -0
  51. package/src/db/database-schema.ts +2 -0
  52. package/src/db/migrations/20240124T023719200Z-tagged-suggestions.ts +15 -0
  53. package/src/db/migrations/index.ts +1 -0
  54. package/src/db/tables/tagged-suggestion.ts +11 -0
  55. package/src/index.ts +26 -3
  56. package/src/indexer/config.ts +36 -0
  57. package/src/indexer/context.ts +6 -0
  58. package/src/indexer/index.ts +27 -3
  59. package/src/ingester/config.ts +34 -0
  60. package/src/ingester/context.ts +6 -0
  61. package/src/ingester/index.ts +18 -0
  62. package/src/ingester/mute-subscription.ts +213 -0
  63. package/src/lexicon/index.ts +24 -0
  64. package/src/lexicon/lexicons.ts +167 -0
  65. package/src/lexicon/types/app/bsky/actor/defs.ts +19 -0
  66. package/src/lexicon/types/app/bsky/graph/defs.ts +41 -0
  67. package/src/lexicon/types/app/bsky/graph/getRelationships.ts +53 -0
  68. package/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts +65 -0
  69. package/src/notifications.ts +165 -149
  70. package/src/proto/bsync_connect.ts +54 -0
  71. package/src/proto/bsync_pb.ts +459 -0
  72. package/src/proto/courier_connect.ts +50 -0
  73. package/src/proto/courier_pb.ts +473 -0
  74. package/src/services/actor/index.ts +17 -2
  75. package/src/services/indexing/processor.ts +1 -1
  76. package/src/util/retry.ts +12 -0
  77. package/tests/notification-server.test.ts +59 -19
  78. package/tests/subscription/mutes.test.ts +170 -0
  79. package/tests/views/__snapshots__/follows.test.ts.snap +28 -0
  80. package/tests/views/follows.test.ts +10 -0
  81. package/tests/views/suggestions.test.ts +22 -0
@@ -10,6 +10,10 @@ export interface IngesterConfigValues {
10
10
  redisPassword?: string
11
11
  repoProvider: string
12
12
  labelProvider?: string
13
+ bsyncUrl?: string
14
+ bsyncApiKey?: string
15
+ bsyncHttpVersion?: '1.1' | '2'
16
+ bsyncIgnoreBadTls?: boolean
13
17
  ingesterPartitionCount: number
14
18
  ingesterNamespace?: string
15
19
  ingesterSubLockId?: number
@@ -42,6 +46,16 @@ export class IngesterConfig {
42
46
  overrides?.redisPassword || process.env.REDIS_PASSWORD || undefined
43
47
  const repoProvider = overrides?.repoProvider || process.env.REPO_PROVIDER // E.g. ws://abc.com:4000
44
48
  const labelProvider = overrides?.labelProvider || process.env.LABEL_PROVIDER
49
+ const bsyncUrl =
50
+ overrides?.bsyncUrl || process.env.BSKY_BSYNC_URL || undefined
51
+ const bsyncApiKey =
52
+ overrides?.bsyncApiKey || process.env.BSKY_BSYNC_API_KEY || undefined
53
+ const bsyncHttpVersion =
54
+ overrides?.bsyncHttpVersion || process.env.BSKY_BSYNC_HTTP_VERSION || '2'
55
+ const bsyncIgnoreBadTls =
56
+ overrides?.bsyncIgnoreBadTls ||
57
+ process.env.BSKY_BSYNC_IGNORE_BAD_TLS === 'true'
58
+ assert(bsyncHttpVersion === '1.1' || bsyncHttpVersion === '2')
45
59
  const ingesterPartitionCount =
46
60
  overrides?.ingesterPartitionCount ||
47
61
  maybeParseInt(process.env.INGESTER_PARTITION_COUNT)
@@ -72,6 +86,10 @@ export class IngesterConfig {
72
86
  redisPassword,
73
87
  repoProvider,
74
88
  labelProvider,
89
+ bsyncUrl,
90
+ bsyncApiKey,
91
+ bsyncHttpVersion,
92
+ bsyncIgnoreBadTls,
75
93
  ingesterPartitionCount,
76
94
  ingesterSubLockId,
77
95
  ingesterNamespace,
@@ -117,6 +135,22 @@ export class IngesterConfig {
117
135
  return this.cfg.labelProvider
118
136
  }
119
137
 
138
+ get bsyncUrl() {
139
+ return this.cfg.bsyncUrl
140
+ }
141
+
142
+ get bsyncApiKey() {
143
+ return this.cfg.bsyncApiKey
144
+ }
145
+
146
+ get bsyncHttpVersion() {
147
+ return this.cfg.bsyncHttpVersion
148
+ }
149
+
150
+ get bsyncIgnoreBadTls() {
151
+ return this.cfg.bsyncIgnoreBadTls
152
+ }
153
+
120
154
  get ingesterPartitionCount() {
121
155
  return this.cfg.ingesterPartitionCount
122
156
  }
@@ -2,6 +2,7 @@ import { PrimaryDatabase } from '../db'
2
2
  import { Redis } from '../redis'
3
3
  import { IngesterConfig } from './config'
4
4
  import { LabelSubscription } from './label-subscription'
5
+ import { MuteSubscription } from './mute-subscription'
5
6
 
6
7
  export class IngesterContext {
7
8
  constructor(
@@ -10,6 +11,7 @@ export class IngesterContext {
10
11
  redis: Redis
11
12
  cfg: IngesterConfig
12
13
  labelSubscription?: LabelSubscription
14
+ muteSubscription?: MuteSubscription
13
15
  },
14
16
  ) {}
15
17
 
@@ -28,6 +30,10 @@ export class IngesterContext {
28
30
  get labelSubscription(): LabelSubscription | undefined {
29
31
  return this.opts.labelSubscription
30
32
  }
33
+
34
+ get muteSubscription(): MuteSubscription | undefined {
35
+ return this.opts.muteSubscription
36
+ }
31
37
  }
32
38
 
33
39
  export default IngesterContext
@@ -5,7 +5,9 @@ import { Redis } from '../redis'
5
5
  import { IngesterConfig } from './config'
6
6
  import { IngesterContext } from './context'
7
7
  import { IngesterSubscription } from './subscription'
8
+ import { authWithApiKey, createBsyncClient } from '../bsync'
8
9
  import { LabelSubscription } from './label-subscription'
10
+ import { MuteSubscription } from './mute-subscription'
9
11
 
10
12
  export { IngesterConfig } from './config'
11
13
  export type { IngesterConfigValues } from './config'
@@ -27,14 +29,28 @@ export class BskyIngester {
27
29
  cfg: IngesterConfig
28
30
  }): BskyIngester {
29
31
  const { db, redis, cfg } = opts
32
+ const bsyncClient = cfg.bsyncUrl
33
+ ? createBsyncClient({
34
+ baseUrl: cfg.bsyncUrl,
35
+ httpVersion: cfg.bsyncHttpVersion ?? '2',
36
+ nodeOptions: { rejectUnauthorized: !cfg.bsyncIgnoreBadTls },
37
+ interceptors: cfg.bsyncApiKey
38
+ ? [authWithApiKey(cfg.bsyncApiKey)]
39
+ : [],
40
+ })
41
+ : undefined
30
42
  const labelSubscription = cfg.labelProvider
31
43
  ? new LabelSubscription(db, cfg.labelProvider)
32
44
  : undefined
45
+ const muteSubscription = bsyncClient
46
+ ? new MuteSubscription(db, redis, bsyncClient)
47
+ : undefined
33
48
  const ctx = new IngesterContext({
34
49
  db,
35
50
  redis,
36
51
  cfg,
37
52
  labelSubscription,
53
+ muteSubscription,
38
54
  })
39
55
  const sub = new IngesterSubscription(ctx, {
40
56
  service: cfg.repoProvider,
@@ -73,11 +89,13 @@ export class BskyIngester {
73
89
  )
74
90
  }, 500)
75
91
  await this.ctx.labelSubscription?.start()
92
+ await this.ctx.muteSubscription?.start()
76
93
  this.sub.run()
77
94
  return this
78
95
  }
79
96
 
80
97
  async destroy(opts?: { skipDb: boolean }): Promise<void> {
98
+ await this.ctx.muteSubscription?.destroy()
81
99
  await this.ctx.labelSubscription?.destroy()
82
100
  await this.sub.destroy()
83
101
  clearInterval(this.subStatsInterval)
@@ -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
+ }
@@ -114,6 +114,7 @@ import * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks
114
114
  import * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes'
115
115
  import * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists'
116
116
  import * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes'
117
+ import * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships'
117
118
  import * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor'
118
119
  import * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor'
119
120
  import * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList'
@@ -124,6 +125,7 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica
124
125
  import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush'
125
126
  import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'
126
127
  import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators'
128
+ import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions'
127
129
  import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton'
128
130
  import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton'
129
131
  import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton'
@@ -1479,6 +1481,17 @@ export class AppBskyGraphNS {
1479
1481
  return this._server.xrpc.method(nsid, cfg)
1480
1482
  }
1481
1483
 
1484
+ getRelationships<AV extends AuthVerifier>(
1485
+ cfg: ConfigOf<
1486
+ AV,
1487
+ AppBskyGraphGetRelationships.Handler<ExtractAuth<AV>>,
1488
+ AppBskyGraphGetRelationships.HandlerReqCtx<ExtractAuth<AV>>
1489
+ >,
1490
+ ) {
1491
+ const nsid = 'app.bsky.graph.getRelationships' // @ts-ignore
1492
+ return this._server.xrpc.method(nsid, cfg)
1493
+ }
1494
+
1482
1495
  getSuggestedFollowsByActor<AV extends AuthVerifier>(
1483
1496
  cfg: ConfigOf<
1484
1497
  AV,
@@ -1613,6 +1626,17 @@ export class AppBskyUnspeccedNS {
1613
1626
  return this._server.xrpc.method(nsid, cfg)
1614
1627
  }
1615
1628
 
1629
+ getTaggedSuggestions<AV extends AuthVerifier>(
1630
+ cfg: ConfigOf<
1631
+ AV,
1632
+ AppBskyUnspeccedGetTaggedSuggestions.Handler<ExtractAuth<AV>>,
1633
+ AppBskyUnspeccedGetTaggedSuggestions.HandlerReqCtx<ExtractAuth<AV>>
1634
+ >,
1635
+ ) {
1636
+ const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore
1637
+ return this._server.xrpc.method(nsid, cfg)
1638
+ }
1639
+
1616
1640
  getTimelineSkeleton<AV extends AuthVerifier>(
1617
1641
  cfg: ConfigOf<
1618
1642
  AV,
@@ -4615,6 +4615,7 @@ export const schemaDict = {
4615
4615
  'lex:app.bsky.actor.defs#personalDetailsPref',
4616
4616
  'lex:app.bsky.actor.defs#feedViewPref',
4617
4617
  'lex:app.bsky.actor.defs#threadViewPref',
4618
+ 'lex:app.bsky.actor.defs#interestsPref',
4618
4619
  ],
4619
4620
  },
4620
4621
  },
@@ -4718,6 +4719,23 @@ export const schemaDict = {
4718
4719
  },
4719
4720
  },
4720
4721
  },
4722
+ interestsPref: {
4723
+ type: 'object',
4724
+ required: ['tags'],
4725
+ properties: {
4726
+ tags: {
4727
+ type: 'array',
4728
+ maxLength: 100,
4729
+ items: {
4730
+ type: 'string',
4731
+ maxLength: 640,
4732
+ maxGraphemes: 64,
4733
+ },
4734
+ description:
4735
+ "A list of tags which describe the account owner's interests gathered during onboarding.",
4736
+ },
4737
+ },
4738
+ },
4721
4739
  },
4722
4740
  },
4723
4741
  AppBskyActorGetPreferences: {
@@ -6925,6 +6943,45 @@ export const schemaDict = {
6925
6943
  },
6926
6944
  },
6927
6945
  },
6946
+ notFoundActor: {
6947
+ type: 'object',
6948
+ description: 'indicates that a handle or DID could not be resolved',
6949
+ required: ['actor', 'notFound'],
6950
+ properties: {
6951
+ actor: {
6952
+ type: 'string',
6953
+ format: 'at-identifier',
6954
+ },
6955
+ notFound: {
6956
+ type: 'boolean',
6957
+ const: true,
6958
+ },
6959
+ },
6960
+ },
6961
+ relationship: {
6962
+ type: 'object',
6963
+ description:
6964
+ 'lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)',
6965
+ required: ['did'],
6966
+ properties: {
6967
+ did: {
6968
+ type: 'string',
6969
+ format: 'did',
6970
+ },
6971
+ following: {
6972
+ type: 'string',
6973
+ format: 'at-uri',
6974
+ description:
6975
+ 'if the actor follows this DID, this is the AT-URI of the follow record',
6976
+ },
6977
+ followedBy: {
6978
+ type: 'string',
6979
+ format: 'at-uri',
6980
+ description:
6981
+ 'if the actor is followed by this DID, contains the AT-URI of the follow record',
6982
+ },
6983
+ },
6984
+ },
6928
6985
  },
6929
6986
  },
6930
6987
  AppBskyGraphFollow: {
@@ -7328,6 +7385,65 @@ export const schemaDict = {
7328
7385
  },
7329
7386
  },
7330
7387
  },
7388
+ AppBskyGraphGetRelationships: {
7389
+ lexicon: 1,
7390
+ id: 'app.bsky.graph.getRelationships',
7391
+ defs: {
7392
+ main: {
7393
+ type: 'query',
7394
+ description:
7395
+ 'Enumerates public relationships between one account, and a list of other accounts',
7396
+ parameters: {
7397
+ type: 'params',
7398
+ required: ['actor'],
7399
+ properties: {
7400
+ actor: {
7401
+ type: 'string',
7402
+ format: 'at-identifier',
7403
+ },
7404
+ others: {
7405
+ type: 'array',
7406
+ maxLength: 30,
7407
+ items: {
7408
+ type: 'string',
7409
+ format: 'at-identifier',
7410
+ },
7411
+ },
7412
+ },
7413
+ },
7414
+ output: {
7415
+ encoding: 'application/json',
7416
+ schema: {
7417
+ type: 'object',
7418
+ required: ['relationships'],
7419
+ properties: {
7420
+ actor: {
7421
+ type: 'string',
7422
+ format: 'did',
7423
+ },
7424
+ relationships: {
7425
+ type: 'array',
7426
+ items: {
7427
+ type: 'union',
7428
+ refs: [
7429
+ 'lex:app.bsky.graph.defs#relationship',
7430
+ 'lex:app.bsky.graph.defs#notFoundActor',
7431
+ ],
7432
+ },
7433
+ },
7434
+ },
7435
+ },
7436
+ },
7437
+ errors: [
7438
+ {
7439
+ name: 'ActorNotFound',
7440
+ description:
7441
+ 'the primary actor at-identifier could not be resolved',
7442
+ },
7443
+ ],
7444
+ },
7445
+ },
7446
+ },
7331
7447
  AppBskyGraphGetSuggestedFollowsByActor: {
7332
7448
  lexicon: 1,
7333
7449
  id: 'app.bsky.graph.getSuggestedFollowsByActor',
@@ -7908,6 +8024,54 @@ export const schemaDict = {
7908
8024
  },
7909
8025
  },
7910
8026
  },
8027
+ AppBskyUnspeccedGetTaggedSuggestions: {
8028
+ lexicon: 1,
8029
+ id: 'app.bsky.unspecced.getTaggedSuggestions',
8030
+ defs: {
8031
+ main: {
8032
+ type: 'query',
8033
+ description:
8034
+ 'Get a list of suggestions (feeds and users) tagged with categories',
8035
+ parameters: {
8036
+ type: 'params',
8037
+ properties: {},
8038
+ },
8039
+ output: {
8040
+ encoding: 'application/json',
8041
+ schema: {
8042
+ type: 'object',
8043
+ required: ['suggestions'],
8044
+ properties: {
8045
+ suggestions: {
8046
+ type: 'array',
8047
+ items: {
8048
+ type: 'ref',
8049
+ ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',
8050
+ },
8051
+ },
8052
+ },
8053
+ },
8054
+ },
8055
+ },
8056
+ suggestion: {
8057
+ type: 'object',
8058
+ required: ['tag', 'subjectType', 'subject'],
8059
+ properties: {
8060
+ tag: {
8061
+ type: 'string',
8062
+ },
8063
+ subjectType: {
8064
+ type: 'string',
8065
+ knownValues: ['actor', 'feed'],
8066
+ },
8067
+ subject: {
8068
+ type: 'string',
8069
+ format: 'uri',
8070
+ },
8071
+ },
8072
+ },
8073
+ },
8074
+ },
7911
8075
  AppBskyUnspeccedGetTimelineSkeleton: {
7912
8076
  lexicon: 1,
7913
8077
  id: 'app.bsky.unspecced.getTimelineSkeleton',
@@ -8224,6 +8388,7 @@ export const ids = {
8224
8388
  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',
8225
8389
  AppBskyGraphGetLists: 'app.bsky.graph.getLists',
8226
8390
  AppBskyGraphGetMutes: 'app.bsky.graph.getMutes',
8391
+ AppBskyGraphGetRelationships: 'app.bsky.graph.getRelationships',
8227
8392
  AppBskyGraphGetSuggestedFollowsByActor:
8228
8393
  'app.bsky.graph.getSuggestedFollowsByActor',
8229
8394
  AppBskyGraphList: 'app.bsky.graph.list',
@@ -8242,6 +8407,8 @@ export const ids = {
8242
8407
  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',
8243
8408
  AppBskyUnspeccedGetPopularFeedGenerators:
8244
8409
  'app.bsky.unspecced.getPopularFeedGenerators',
8410
+ AppBskyUnspeccedGetTaggedSuggestions:
8411
+ 'app.bsky.unspecced.getTaggedSuggestions',
8245
8412
  AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton',
8246
8413
  AppBskyUnspeccedSearchActorsSkeleton:
8247
8414
  'app.bsky.unspecced.searchActorsSkeleton',
@@ -112,6 +112,7 @@ export type Preferences = (
112
112
  | PersonalDetailsPref
113
113
  | FeedViewPref
114
114
  | ThreadViewPref
115
+ | InterestsPref
115
116
  | { $type: string; [k: string]: unknown }
116
117
  )[]
117
118
 
@@ -233,3 +234,21 @@ export function isThreadViewPref(v: unknown): v is ThreadViewPref {
233
234
  export function validateThreadViewPref(v: unknown): ValidationResult {
234
235
  return lexicons.validate('app.bsky.actor.defs#threadViewPref', v)
235
236
  }
237
+
238
+ export interface InterestsPref {
239
+ /** A list of tags which describe the account owner's interests gathered during onboarding. */
240
+ tags: string[]
241
+ [k: string]: unknown
242
+ }
243
+
244
+ export function isInterestsPref(v: unknown): v is InterestsPref {
245
+ return (
246
+ isObj(v) &&
247
+ hasProp(v, '$type') &&
248
+ v.$type === 'app.bsky.actor.defs#interestsPref'
249
+ )
250
+ }
251
+
252
+ export function validateInterestsPref(v: unknown): ValidationResult {
253
+ return lexicons.validate('app.bsky.actor.defs#interestsPref', v)
254
+ }
@@ -102,3 +102,44 @@ export function isListViewerState(v: unknown): v is ListViewerState {
102
102
  export function validateListViewerState(v: unknown): ValidationResult {
103
103
  return lexicons.validate('app.bsky.graph.defs#listViewerState', v)
104
104
  }
105
+
106
+ /** indicates that a handle or DID could not be resolved */
107
+ export interface NotFoundActor {
108
+ actor: string
109
+ notFound: true
110
+ [k: string]: unknown
111
+ }
112
+
113
+ export function isNotFoundActor(v: unknown): v is NotFoundActor {
114
+ return (
115
+ isObj(v) &&
116
+ hasProp(v, '$type') &&
117
+ v.$type === 'app.bsky.graph.defs#notFoundActor'
118
+ )
119
+ }
120
+
121
+ export function validateNotFoundActor(v: unknown): ValidationResult {
122
+ return lexicons.validate('app.bsky.graph.defs#notFoundActor', v)
123
+ }
124
+
125
+ /** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) */
126
+ export interface Relationship {
127
+ did: string
128
+ /** if the actor follows this DID, this is the AT-URI of the follow record */
129
+ following?: string
130
+ /** if the actor is followed by this DID, contains the AT-URI of the follow record */
131
+ followedBy?: string
132
+ [k: string]: unknown
133
+ }
134
+
135
+ export function isRelationship(v: unknown): v is Relationship {
136
+ return (
137
+ isObj(v) &&
138
+ hasProp(v, '$type') &&
139
+ v.$type === 'app.bsky.graph.defs#relationship'
140
+ )
141
+ }
142
+
143
+ export function validateRelationship(v: unknown): ValidationResult {
144
+ return lexicons.validate('app.bsky.graph.defs#relationship', v)
145
+ }