@atproto/bsky 0.0.38 → 0.0.40

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 (65) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/hydration/hydrator.d.ts +12 -2
  3. package/dist/hydration/label.d.ts +7 -3
  4. package/dist/hydration/util.d.ts +5 -2
  5. package/dist/index.js +216 -104
  6. package/dist/index.js.map +2 -2
  7. package/dist/lexicon/lexicons.d.ts +13 -0
  8. package/dist/lexicon/types/com/atproto/server/describeServer.d.ts +7 -0
  9. package/package.json +5 -5
  10. package/src/api/app/bsky/actor/getProfile.ts +10 -6
  11. package/src/api/app/bsky/actor/getProfiles.ts +2 -2
  12. package/src/api/app/bsky/actor/getSuggestions.ts +2 -2
  13. package/src/api/app/bsky/actor/searchActors.ts +7 -3
  14. package/src/api/app/bsky/actor/searchActorsTypeahead.ts +2 -2
  15. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -2
  16. package/src/api/app/bsky/feed/getActorLikes.ts +2 -2
  17. package/src/api/app/bsky/feed/getAuthorFeed.ts +6 -2
  18. package/src/api/app/bsky/feed/getFeed.ts +2 -2
  19. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -6
  20. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -2
  21. package/src/api/app/bsky/feed/getLikes.ts +2 -2
  22. package/src/api/app/bsky/feed/getListFeed.ts +2 -2
  23. package/src/api/app/bsky/feed/getPostThread.ts +2 -2
  24. package/src/api/app/bsky/feed/getPosts.ts +2 -2
  25. package/src/api/app/bsky/feed/getRepostedBy.ts +2 -2
  26. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +3 -5
  27. package/src/api/app/bsky/feed/getTimeline.ts +6 -3
  28. package/src/api/app/bsky/feed/searchPosts.ts +2 -2
  29. package/src/api/app/bsky/graph/getBlocks.ts +6 -3
  30. package/src/api/app/bsky/graph/getFollowers.ts +6 -2
  31. package/src/api/app/bsky/graph/getFollows.ts +6 -2
  32. package/src/api/app/bsky/graph/getList.ts +2 -2
  33. package/src/api/app/bsky/graph/getListBlocks.ts +6 -3
  34. package/src/api/app/bsky/graph/getListMutes.ts +6 -3
  35. package/src/api/app/bsky/graph/getLists.ts +2 -2
  36. package/src/api/app/bsky/graph/getMutes.ts +6 -3
  37. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +3 -3
  38. package/src/api/app/bsky/labeler/getServices.ts +3 -3
  39. package/src/api/app/bsky/notification/listNotifications.ts +6 -3
  40. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +2 -2
  41. package/src/data-plane/server/routes/labels.ts +26 -8
  42. package/src/hydration/hydrator.ts +55 -10
  43. package/src/hydration/label.ts +33 -7
  44. package/src/hydration/util.ts +6 -2
  45. package/src/index.ts +1 -1
  46. package/src/lexicon/lexicons.ts +13 -0
  47. package/src/lexicon/types/com/atproto/server/describeServer.ts +18 -0
  48. package/src/views/index.ts +8 -8
  49. package/tests/__snapshots__/feed-generation.test.ts.snap +0 -45
  50. package/tests/data-plane/__snapshots__/indexing.test.ts.snap +0 -8
  51. package/tests/label-hydration.test.ts +31 -0
  52. package/tests/views/__snapshots__/author-feed.test.ts.snap +0 -46
  53. package/tests/views/__snapshots__/block-lists.test.ts.snap +0 -17
  54. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  55. package/tests/views/__snapshots__/labeler-service.test.ts.snap +0 -4
  56. package/tests/views/__snapshots__/list-feed.test.ts.snap +0 -20
  57. package/tests/views/__snapshots__/mute-lists.test.ts.snap +0 -18
  58. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -4
  59. package/tests/views/__snapshots__/notifications.test.ts.snap +0 -9
  60. package/tests/views/__snapshots__/posts.test.ts.snap +0 -7
  61. package/tests/views/__snapshots__/profile.test.ts.snap +0 -6
  62. package/tests/views/__snapshots__/thread.test.ts.snap +0 -38
  63. package/tests/views/__snapshots__/timeline.test.ts.snap +0 -145
  64. package/tests/views/labeler-service.test.ts +2 -7
  65. package/tests/views/takedown-labels.test.ts +52 -0
@@ -10,11 +10,11 @@ export default function (server: Server, ctx: AppContext) {
10
10
  const { dids, detailed } = params
11
11
  const viewer = auth.credentials.iss
12
12
  const labelers = ctx.reqLabelers(req)
13
-
14
- const hydration = await ctx.hydrator.hydrateLabelers(dids, {
13
+ const hydrateCtx = await ctx.hydrator.createContext({
15
14
  viewer,
16
15
  labelers,
17
16
  })
17
+ const hydration = await ctx.hydrator.hydrateLabelers(dids, hydrateCtx)
18
18
 
19
19
  const views = mapDefined(dids, (did) => {
20
20
  if (detailed) {
@@ -39,7 +39,7 @@ export default function (server: Server, ctx: AppContext) {
39
39
  body: {
40
40
  views,
41
41
  },
42
- headers: resHeaders({ labelers }),
42
+ headers: resHeaders({ labelers: hydrateCtx.labelers }),
43
43
  }
44
44
  },
45
45
  })
@@ -28,12 +28,15 @@ export default function (server: Server, ctx: AppContext) {
28
28
  handler: async ({ params, auth, req }) => {
29
29
  const viewer = auth.credentials.iss
30
30
  const labelers = ctx.reqLabelers(req)
31
- const hydrateCtx = { labelers, viewer }
32
- const result = await listNotifications({ ...params, hydrateCtx }, ctx)
31
+ const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })
32
+ const result = await listNotifications(
33
+ { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },
34
+ ctx,
35
+ )
33
36
  return {
34
37
  encoding: 'application/json',
35
38
  body: result,
36
- headers: resHeaders({ labelers }),
39
+ headers: resHeaders({ labelers: hydrateCtx.labelers }),
37
40
  }
38
41
  },
39
42
  })
@@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) {
13
13
  handler: async ({ auth, params, req }) => {
14
14
  const viewer = auth.credentials.iss
15
15
  const labelers = ctx.reqLabelers(req)
16
- const hydrateCtx = { viewer, labelers }
16
+ const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers })
17
17
 
18
18
  if (clearlyBadCursor(params.cursor)) {
19
19
  return {
@@ -53,7 +53,7 @@ export default function (server: Server, ctx: AppContext) {
53
53
  feeds: feedViews,
54
54
  cursor,
55
55
  },
56
- headers: resHeaders({ labelers }),
56
+ headers: resHeaders({ labelers: hydrateCtx.labelers }),
57
57
  }
58
58
  },
59
59
  })
@@ -1,28 +1,46 @@
1
1
  import * as ui8 from 'uint8arrays'
2
+ import { noUndefinedVals } from '@atproto/common'
2
3
  import { ServiceImpl } from '@connectrpc/connect'
3
4
  import { Service } from '../../../proto/bsky_connect'
4
5
  import { Database } from '../db'
6
+ import { Selectable } from 'kysely'
7
+ import { Label } from '../db/tables/label'
8
+
9
+ type LabelRow = Selectable<Label>
5
10
 
6
11
  export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
7
12
  async getLabels(req) {
8
13
  const { subjects, issuers } = req
9
14
  if (subjects.length === 0 || issuers.length === 0) {
10
- return { records: [] }
15
+ return { labels: [] }
11
16
  }
12
- const res = await db.db
17
+ const res: LabelRow[] = await db.db
13
18
  .selectFrom('label')
14
19
  .where('uri', 'in', subjects)
15
20
  .where('src', 'in', issuers)
16
21
  .selectAll()
17
22
  .execute()
18
23
 
19
- const labels = res.map((l) => {
20
- const formatted = {
21
- ...l,
22
- cid: l.cid === '' ? undefined : l.cid,
23
- }
24
- return ui8.fromString(JSON.stringify(formatted), 'utf8')
24
+ const labelsBySubject = new Map<string, LabelRow[]>()
25
+ res.forEach((l) => {
26
+ const labels = labelsBySubject.get(l.uri) ?? []
27
+ labels.push(l)
28
+ labelsBySubject.set(l.uri, labels)
29
+ })
30
+
31
+ // intentionally duplicate label results, appview frontend should be defensive to this
32
+ const labels = subjects.flatMap((sub) => {
33
+ const labelsForSub = labelsBySubject.get(sub) ?? []
34
+ return labelsForSub.map((l) => {
35
+ const formatted = noUndefinedVals({
36
+ ...l,
37
+ cid: l.cid === '' ? undefined : l.cid,
38
+ neg: l.neg === true ? true : undefined,
39
+ })
40
+ return ui8.fromString(JSON.stringify(formatted), 'utf8')
41
+ })
25
42
  })
43
+
26
44
  return { labels }
27
45
  },
28
46
  })
@@ -29,7 +29,13 @@ import {
29
29
  Labelers,
30
30
  Labels,
31
31
  } from './label'
32
- import { HydrationMap, RecordInfo, didFromUri, urisByCollection } from './util'
32
+ import {
33
+ HydrationMap,
34
+ Merges,
35
+ RecordInfo,
36
+ didFromUri,
37
+ urisByCollection,
38
+ } from './util'
33
39
  import {
34
40
  FeedGenAggs,
35
41
  FeedGens,
@@ -47,7 +53,17 @@ import {
47
53
  } from './feed'
48
54
  import { ParsedLabelers } from '../util'
49
55
 
50
- export type HydrateCtx = {
56
+ export class HydrateCtx {
57
+ labelers = this.vals.labelers
58
+ viewer = this.vals.viewer
59
+ includeTakedowns = this.vals.includeTakedowns
60
+ constructor(private vals: HydrateCtxVals) {}
61
+ copy<V extends Partial<HydrateCtxVals>>(vals?: V): HydrateCtx & V {
62
+ return new HydrateCtx({ ...this.vals, ...vals }) as HydrateCtx & V
63
+ }
64
+ }
65
+
66
+ export type HydrateCtxVals = {
51
67
  labelers: ParsedLabelers
52
68
  viewer: string | null
53
69
  includeTakedowns?: boolean
@@ -91,12 +107,17 @@ export class Hydrator {
91
107
  feed: FeedHydrator
92
108
  graph: GraphHydrator
93
109
  label: LabelHydrator
110
+ serviceLabelers: Set<string>
94
111
 
95
- constructor(public dataplane: DataPlaneClient) {
112
+ constructor(
113
+ public dataplane: DataPlaneClient,
114
+ serviceLabelers: string[] = [],
115
+ ) {
96
116
  this.actor = new ActorHydrator(dataplane)
97
117
  this.feed = new FeedHydrator(dataplane)
98
118
  this.graph = new GraphHydrator(dataplane)
99
119
  this.label = new LabelHydrator(dataplane)
120
+ this.serviceLabelers = new Set(serviceLabelers)
100
121
  }
101
122
 
102
123
  // app.bsky.actor.defs#profileView
@@ -549,13 +570,14 @@ export class Hydrator {
549
570
  ): Promise<HydrationState> {
550
571
  const [labelers, labelerAggs, labelerViewers, profileState] =
551
572
  await Promise.all([
552
- this.label.getLabelers(dids),
573
+ this.label.getLabelers(dids, ctx.includeTakedowns),
553
574
  this.label.getLabelerAggregates(dids),
554
575
  ctx.viewer
555
576
  ? this.label.getLabelerViewerStates(dids, ctx.viewer)
556
577
  : undefined,
557
- this.hydrateProfiles(dids.map(didFromUri), ctx),
578
+ this.hydrateProfiles(dids, ctx),
558
579
  ])
580
+ actionTakedownLabels(dids, labelers, profileState.labels ?? new Labels())
559
581
  return mergeStates(profileState, {
560
582
  labelers,
561
583
  labelerAggs,
@@ -613,8 +635,10 @@ export class Hydrator {
613
635
  undefined
614
636
  )
615
637
  } else if (collection === ids.AppBskyLabelerService) {
638
+ if (parsed.rkey !== 'self') return
639
+ const did = parsed.hostname
616
640
  return (
617
- (await this.label.getLabelers([uri], includeTakedowns)).get(uri) ??
641
+ (await this.label.getLabelers([did], includeTakedowns)).get(did) ??
618
642
  undefined
619
643
  )
620
644
  } else if (collection === ids.AppBskyActorProfile) {
@@ -631,6 +655,30 @@ export class Hydrator {
631
655
  }
632
656
  }
633
657
  }
658
+
659
+ async createContext(vals: HydrateCtxVals) {
660
+ // ensures we're only apply labelers that exist and are not taken down
661
+ const labelers = vals.labelers.dids
662
+ const nonServiceLabelers = labelers.filter(
663
+ (did) => !this.serviceLabelers.has(did),
664
+ )
665
+ const labelerActors = await this.actor.getActors(
666
+ nonServiceLabelers,
667
+ vals.includeTakedowns,
668
+ )
669
+ const availableDids = labelers.filter(
670
+ (did) => this.serviceLabelers.has(did) || !!labelerActors.get(did),
671
+ )
672
+ const availableLabelers = {
673
+ dids: availableDids,
674
+ redact: vals.labelers.redact,
675
+ }
676
+ return new HydrateCtx({
677
+ labelers: availableLabelers,
678
+ viewer: vals.viewer,
679
+ includeTakedowns: vals.includeTakedowns,
680
+ })
681
+ }
634
682
  }
635
683
 
636
684
  const listUrisFromProfileViewer = (item: ProfileViewerState | null) => {
@@ -770,10 +818,7 @@ export const mergeStates = (
770
818
  }
771
819
  }
772
820
 
773
- const mergeMaps = <T>(
774
- mapA?: HydrationMap<T>,
775
- mapB?: HydrationMap<T>,
776
- ): HydrationMap<T> | undefined => {
821
+ const mergeMaps = <M extends Merges>(mapA?: M, mapB?: M): M | undefined => {
777
822
  if (!mapA) return mapB
778
823
  if (!mapB) return mapA
779
824
  return mapA.merge(mapB)
@@ -3,6 +3,7 @@ import { Label } from '../lexicon/types/com/atproto/label/defs'
3
3
  import { Record as LabelerRecord } from '../lexicon/types/app/bsky/labeler/service'
4
4
  import {
5
5
  HydrationMap,
6
+ Merges,
6
7
  RecordInfo,
7
8
  parseJsonBytes,
8
9
  parseRecord,
@@ -16,10 +17,36 @@ export type { Label } from '../lexicon/types/com/atproto/label/defs'
16
17
 
17
18
  export type SubjectLabels = {
18
19
  isTakendown: boolean
19
- labels: Label[]
20
+ labels: HydrationMap<Label> // src + val -> label
20
21
  }
21
22
 
22
- export type Labels = HydrationMap<SubjectLabels>
23
+ export class Labels extends HydrationMap<SubjectLabels> implements Merges {
24
+ static key(label: Label) {
25
+ return `${label.src}::${label.val}`
26
+ }
27
+ merge(map: Labels): this {
28
+ map.forEach((theirs, key) => {
29
+ if (!theirs) return
30
+ const mine = this.get(key)
31
+ if (mine) {
32
+ mine.isTakendown = mine.isTakendown || theirs.isTakendown
33
+ mine.labels = mine.labels.merge(theirs.labels)
34
+ } else {
35
+ this.set(key, theirs)
36
+ }
37
+ })
38
+ return this
39
+ }
40
+ getBySubject(sub: string): Label[] {
41
+ const it = this.get(sub)?.labels.values()
42
+ if (!it) return []
43
+ const labels: Label[] = []
44
+ for (const label of it) {
45
+ if (label) labels.push(label)
46
+ }
47
+ return labels
48
+ }
49
+ }
23
50
 
24
51
  export type LabelerAgg = {
25
52
  likes: number
@@ -43,8 +70,7 @@ export class LabelHydrator {
43
70
  subjects: string[],
44
71
  labelers: ParsedLabelers,
45
72
  ): Promise<Labels> {
46
- if (!subjects.length || !labelers.dids.length)
47
- return new HydrationMap<SubjectLabels>()
73
+ if (!subjects.length || !labelers.dids.length) return new Labels()
48
74
  const res = await this.dataplane.getLabels({
49
75
  subjects,
50
76
  issuers: labelers.dids,
@@ -57,11 +83,11 @@ export class LabelHydrator {
57
83
  if (!entry) {
58
84
  entry = {
59
85
  isTakendown: false,
60
- labels: [],
86
+ labels: new HydrationMap(),
61
87
  }
62
88
  acc.set(label.uri, entry)
63
89
  }
64
- entry.labels.push(label)
90
+ entry.labels.set(Labels.key(label), label)
65
91
  if (
66
92
  TAKEDOWN_LABELS.includes(label.val) &&
67
93
  !label.neg &&
@@ -70,7 +96,7 @@ export class LabelHydrator {
70
96
  entry.isTakendown = true
71
97
  }
72
98
  return acc
73
- }, new HydrationMap<SubjectLabels>())
99
+ }, new Labels())
74
100
  }
75
101
 
76
102
  async getLabelers(
@@ -5,8 +5,8 @@ import * as ui8 from 'uint8arrays'
5
5
  import { lexicons } from '../lexicon/lexicons'
6
6
  import { Record } from '../proto/bsky_pb'
7
7
 
8
- export class HydrationMap<T> extends Map<string, T | null> {
9
- merge(map: HydrationMap<T>): HydrationMap<T> {
8
+ export class HydrationMap<T> extends Map<string, T | null> implements Merges {
9
+ merge(map: HydrationMap<T>): this {
10
10
  map.forEach((val, key) => {
11
11
  this.set(key, val)
12
12
  })
@@ -14,6 +14,10 @@ export class HydrationMap<T> extends Map<string, T | null> {
14
14
  }
15
15
  }
16
16
 
17
+ export interface Merges {
18
+ merge<T extends this>(map: T): this
19
+ }
20
+
17
21
  export type RecordInfo<T> = {
18
22
  record: T
19
23
  cid: string
package/src/index.ts CHANGED
@@ -77,7 +77,7 @@ export class BskyAppView {
77
77
  httpVersion: config.dataplaneHttpVersion,
78
78
  rejectUnauthorized: !config.dataplaneIgnoreBadTls,
79
79
  })
80
- const hydrator = new Hydrator(dataplane)
80
+ const hydrator = new Hydrator(dataplane, config.labelsFromIssuerDids)
81
81
  const views = new Views(imgUriBuilder)
82
82
 
83
83
  const bsyncClient = createBsyncClient({
@@ -2420,6 +2420,11 @@ export const schemaDict = {
2420
2420
  description: 'URLs of service policy documents.',
2421
2421
  ref: 'lex:com.atproto.server.describeServer#links',
2422
2422
  },
2423
+ contact: {
2424
+ type: 'ref',
2425
+ description: 'Contact information',
2426
+ ref: 'lex:com.atproto.server.describeServer#contact',
2427
+ },
2423
2428
  did: {
2424
2429
  type: 'string',
2425
2430
  format: 'did',
@@ -2439,6 +2444,14 @@ export const schemaDict = {
2439
2444
  },
2440
2445
  },
2441
2446
  },
2447
+ contact: {
2448
+ type: 'object',
2449
+ properties: {
2450
+ email: {
2451
+ type: 'string',
2452
+ },
2453
+ },
2454
+ },
2442
2455
  },
2443
2456
  },
2444
2457
  ComAtprotoServerGetAccountInviteCodes: {
@@ -20,6 +20,7 @@ export interface OutputSchema {
20
20
  /** List of domain suffixes that can be used in account handles. */
21
21
  availableUserDomains: string[]
22
22
  links?: Links
23
+ contact?: Contact
23
24
  did: string
24
25
  [k: string]: unknown
25
26
  }
@@ -66,3 +67,20 @@ export function isLinks(v: unknown): v is Links {
66
67
  export function validateLinks(v: unknown): ValidationResult {
67
68
  return lexicons.validate('com.atproto.server.describeServer#links', v)
68
69
  }
70
+
71
+ export interface Contact {
72
+ email?: string
73
+ [k: string]: unknown
74
+ }
75
+
76
+ export function isContact(v: unknown): v is Contact {
77
+ return (
78
+ isObj(v) &&
79
+ hasProp(v, '$type') &&
80
+ v.$type === 'com.atproto.server.describeServer#contact'
81
+ )
82
+ }
83
+
84
+ export function validateContact(v: unknown): ValidationResult {
85
+ return lexicons.validate('com.atproto.server.describeServer#contact', v)
86
+ }
@@ -136,8 +136,8 @@ export class Views {
136
136
  'self',
137
137
  ).toString()
138
138
  const labels = [
139
- ...(state.labels?.get(did)?.labels ?? []),
140
- ...(state.labels?.get(profileUri)?.labels ?? []),
139
+ ...(state.labels?.getBySubject(did) ?? []),
140
+ ...(state.labels?.getBySubject(profileUri) ?? []),
141
141
  ...this.selfLabels({
142
142
  uri: profileUri,
143
143
  cid: actor.profileCid?.toString(),
@@ -225,7 +225,7 @@ export class Views {
225
225
  return undefined
226
226
  }
227
227
  const listViewer = state.listViewers?.get(uri)
228
- const labels = state.labels?.get(uri)?.labels ?? []
228
+ const labels = state.labels?.getBySubject(uri) ?? []
229
229
  const creator = new AtUri(uri).hostname
230
230
  return {
231
231
  uri,
@@ -267,7 +267,7 @@ export class Views {
267
267
  ? normalizeDatetimeAlways(record.createdAt)
268
268
  : new Date(0).toISOString()
269
269
  return record.labels.values.map(({ val }) => {
270
- return { src, uri, cid, val, cts, neg: false }
270
+ return { src, uri, cid, val, cts }
271
271
  })
272
272
  }
273
273
 
@@ -281,7 +281,7 @@ export class Views {
281
281
 
282
282
  const uri = AtUri.make(did, ids.AppBskyLabelerService, 'self').toString()
283
283
  const labels = [
284
- ...(state.labels?.get(uri)?.labels ?? []),
284
+ ...(state.labels?.getBySubject(uri) ?? []),
285
285
  ...this.selfLabels({
286
286
  uri,
287
287
  cid: labeler.cid.toString(),
@@ -351,7 +351,7 @@ export class Views {
351
351
  if (!creator) return
352
352
  const viewer = state.feedgenViewers?.get(uri)
353
353
  const aggs = state.feedgenAggs?.get(uri)
354
- const labels = state.labels?.get(uri)?.labels ?? []
354
+ const labels = state.labels?.getBySubject(uri) ?? []
355
355
 
356
356
  return {
357
357
  uri,
@@ -408,7 +408,7 @@ export class Views {
408
408
  parsedUri.rkey,
409
409
  ).toString()
410
410
  const labels = [
411
- ...(state.labels?.get(uri)?.labels ?? []),
411
+ ...(state.labels?.getBySubject(uri) ?? []),
412
412
  ...this.selfLabels({
413
413
  uri,
414
414
  cid: post.cid,
@@ -886,7 +886,7 @@ export class Views {
886
886
  recordInfo = state.follows?.get(notif.uri)
887
887
  }
888
888
  if (!recordInfo) return
889
- const labels = state.labels?.get(notif.uri)?.labels ?? []
889
+ const labels = state.labels?.getBySubject(notif.uri) ?? []
890
890
  const selfLabels = this.selfLabels({
891
891
  uri: notif.uri,
892
892
  cid: recordInfo.cid,