@atproto/bsky 0.0.11 → 0.0.13

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 (158) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +6 -1
  4. package/dist/api/com/atproto/admin/util.d.ts +5 -0
  5. package/dist/context.d.ts +6 -1
  6. package/dist/db/index.js +51 -2
  7. package/dist/db/index.js.map +3 -3
  8. package/dist/db/migrations/20230929T192920807Z-record-cursor-indexes.d.ts +3 -0
  9. package/dist/db/migrations/index.d.ts +1 -0
  10. package/dist/did-cache.d.ts +2 -2
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1630 -755
  13. package/dist/index.js.map +3 -3
  14. package/dist/lexicon/index.d.ts +8 -0
  15. package/dist/lexicon/lexicons.d.ts +243 -3
  16. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -0
  17. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +28 -0
  18. package/dist/lexicon/types/com/atproto/admin/getAccountInfo.d.ts +29 -0
  19. package/dist/lexicon/types/com/atproto/admin/getSubjectStatus.d.ts +39 -0
  20. package/dist/lexicon/types/com/atproto/admin/searchRepos.d.ts +0 -1
  21. package/dist/lexicon/types/com/atproto/admin/updateSubjectStatus.d.ts +46 -0
  22. package/dist/lexicon/types/com/atproto/server/createAccount.d.ts +4 -2
  23. package/dist/lexicon/types/com/atproto/server/createSession.d.ts +1 -0
  24. package/dist/lexicon/types/com/atproto/server/refreshSession.d.ts +1 -0
  25. package/dist/lexicon/types/com/atproto/server/reserveSigningKey.d.ts +36 -0
  26. package/dist/lexicon/types/com/atproto/sync/listRepos.d.ts +1 -0
  27. package/dist/services/actor/types.d.ts +1 -0
  28. package/dist/services/graph/index.d.ts +2 -0
  29. package/dist/services/moderation/index.d.ts +13 -3
  30. package/package.json +14 -15
  31. package/src/api/app/bsky/feed/getAuthorFeed.ts +2 -2
  32. package/src/api/app/bsky/feed/getPostThread.ts +2 -2
  33. package/src/api/app/bsky/notification/listNotifications.ts +33 -22
  34. package/src/api/com/atproto/admin/getModerationAction.ts +28 -2
  35. package/src/api/com/atproto/admin/getModerationReport.ts +27 -2
  36. package/src/api/com/atproto/admin/getRecord.ts +14 -2
  37. package/src/api/com/atproto/admin/getRepo.ts +13 -2
  38. package/src/api/com/atproto/admin/reverseModerationAction.ts +31 -5
  39. package/src/api/com/atproto/admin/searchRepos.ts +2 -5
  40. package/src/api/com/atproto/admin/takeModerationAction.ts +41 -7
  41. package/src/api/com/atproto/admin/util.ts +50 -0
  42. package/src/api/well-known.ts +8 -0
  43. package/src/auth.ts +12 -5
  44. package/src/auto-moderator/index.ts +1 -0
  45. package/src/context.ts +25 -1
  46. package/src/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +40 -0
  47. package/src/db/migrations/index.ts +1 -0
  48. package/src/did-cache.ts +29 -14
  49. package/src/feed-gen/with-friends.ts +2 -2
  50. package/src/index.ts +4 -1
  51. package/src/indexer/subscription.ts +1 -21
  52. package/src/lexicon/index.ts +48 -0
  53. package/src/lexicon/lexicons.ts +259 -5
  54. package/src/lexicon/types/app/bsky/actor/defs.ts +1 -0
  55. package/src/lexicon/types/com/atproto/admin/defs.ts +61 -0
  56. package/src/lexicon/types/com/atproto/admin/getAccountInfo.ts +41 -0
  57. package/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts +54 -0
  58. package/src/lexicon/types/com/atproto/admin/searchRepos.ts +0 -1
  59. package/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts +61 -0
  60. package/src/lexicon/types/com/atproto/server/createAccount.ts +4 -2
  61. package/src/lexicon/types/com/atproto/server/createSession.ts +1 -0
  62. package/src/lexicon/types/com/atproto/server/refreshSession.ts +1 -0
  63. package/src/lexicon/types/com/atproto/server/reserveSigningKey.ts +51 -0
  64. package/src/lexicon/types/com/atproto/sync/listRepos.ts +1 -0
  65. package/src/logger.ts +8 -0
  66. package/src/services/actor/index.ts +7 -1
  67. package/src/services/actor/types.ts +1 -0
  68. package/src/services/actor/views.ts +26 -8
  69. package/src/services/graph/index.ts +26 -7
  70. package/src/services/indexing/index.ts +15 -17
  71. package/src/services/moderation/index.ts +94 -14
  72. package/src/services/moderation/views.ts +1 -0
  73. package/tests/__snapshots__/feed-generation.test.ts.snap +12 -12
  74. package/tests/__snapshots__/indexing.test.ts.snap +4 -4
  75. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +172 -0
  76. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +178 -0
  77. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +177 -0
  78. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +307 -0
  79. package/tests/admin/__snapshots__/get-record.test.ts.snap +275 -0
  80. package/tests/admin/__snapshots__/get-repo.test.ts.snap +103 -0
  81. package/tests/admin/get-moderation-action.test.ts +100 -0
  82. package/tests/admin/get-moderation-actions.test.ts +164 -0
  83. package/tests/admin/get-moderation-report.test.ts +100 -0
  84. package/tests/admin/get-moderation-reports.test.ts +332 -0
  85. package/tests/admin/get-record.test.ts +115 -0
  86. package/tests/admin/get-repo.test.ts +101 -0
  87. package/tests/{moderation.test.ts → admin/moderation.test.ts} +107 -9
  88. package/tests/admin/repo-search.test.ts +124 -0
  89. package/tests/algos/hot-classic.test.ts +3 -5
  90. package/tests/algos/whats-hot.test.ts +3 -5
  91. package/tests/algos/with-friends.test.ts +2 -4
  92. package/tests/auth.test.ts +64 -0
  93. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -3
  94. package/tests/auto-moderator/labeler.test.ts +5 -7
  95. package/tests/auto-moderator/takedowns.test.ts +11 -12
  96. package/tests/blob-resolver.test.ts +1 -3
  97. package/tests/did-cache.test.ts +2 -5
  98. package/tests/feed-generation.test.ts +8 -6
  99. package/tests/handle-invalidation.test.ts +2 -3
  100. package/tests/image/server.test.ts +1 -4
  101. package/tests/image/sharp.test.ts +1 -1
  102. package/tests/indexing.test.ts +4 -4
  103. package/tests/notification-server.test.ts +2 -3
  104. package/tests/pipeline/backpressure.test.ts +2 -3
  105. package/tests/pipeline/reingest.test.ts +7 -4
  106. package/tests/pipeline/repartition.test.ts +2 -3
  107. package/tests/reprocessing.test.ts +2 -6
  108. package/tests/seeds/basic.ts +4 -4
  109. package/tests/seeds/follows.ts +1 -1
  110. package/tests/seeds/likes.ts +1 -1
  111. package/tests/seeds/reposts.ts +1 -1
  112. package/tests/seeds/users-bulk.ts +1 -1
  113. package/tests/seeds/users.ts +1 -1
  114. package/tests/server.test.ts +1 -3
  115. package/tests/subscription/repo.test.ts +2 -4
  116. package/tests/views/__snapshots__/author-feed.test.ts.snap +24 -24
  117. package/tests/views/__snapshots__/block-lists.test.ts.snap +42 -7
  118. package/tests/views/__snapshots__/blocks.test.ts.snap +2 -2
  119. package/tests/views/__snapshots__/list-feed.test.ts.snap +6 -6
  120. package/tests/views/__snapshots__/mute-lists.test.ts.snap +15 -4
  121. package/tests/views/__snapshots__/mutes.test.ts.snap +2 -2
  122. package/tests/views/__snapshots__/notifications.test.ts.snap +2 -2
  123. package/tests/views/__snapshots__/posts.test.ts.snap +8 -8
  124. package/tests/views/__snapshots__/thread.test.ts.snap +10 -10
  125. package/tests/views/__snapshots__/timeline.test.ts.snap +58 -58
  126. package/tests/views/actor-likes.test.ts +2 -3
  127. package/tests/views/actor-search.test.ts +5 -5
  128. package/tests/views/admin/repo-search.test.ts +2 -4
  129. package/tests/views/author-feed.test.ts +2 -4
  130. package/tests/views/block-lists.test.ts +34 -7
  131. package/tests/views/blocks.test.ts +6 -3
  132. package/tests/views/follows.test.ts +2 -4
  133. package/tests/views/likes.test.ts +2 -5
  134. package/tests/views/list-feed.test.ts +2 -4
  135. package/tests/views/mute-lists.test.ts +23 -5
  136. package/tests/views/mutes.test.ts +2 -5
  137. package/tests/views/notifications.test.ts +2 -4
  138. package/tests/views/posts.test.ts +2 -5
  139. package/tests/views/profile.test.ts +4 -5
  140. package/tests/views/reposts.test.ts +2 -4
  141. package/tests/views/suggested-follows.test.ts +2 -3
  142. package/tests/views/suggestions.test.ts +2 -4
  143. package/tests/views/thread.test.ts +2 -4
  144. package/tests/views/threadgating.test.ts +2 -3
  145. package/tests/views/timeline.test.ts +2 -4
  146. package/LICENSE +0 -21
  147. package/dist/env.d.ts +0 -1
  148. package/example.dev.env +0 -5
  149. package/src/env.ts +0 -9
  150. package/tests/seeds/client.ts +0 -466
  151. /package/tests/{__snapshots__ → admin/__snapshots__}/moderation.test.ts.snap +0 -0
  152. /package/tests/{image/fixtures → sample-img}/at.png +0 -0
  153. /package/tests/{image/fixtures → sample-img}/hd-key.jpg +0 -0
  154. /package/tests/{image/fixtures → sample-img}/key-alt.jpg +0 -0
  155. /package/tests/{image/fixtures → sample-img}/key-landscape-large.jpg +0 -0
  156. /package/tests/{image/fixtures → sample-img}/key-landscape-small.jpg +0 -0
  157. /package/tests/{image/fixtures → sample-img}/key-portrait-large.jpg +0 -0
  158. /package/tests/{image/fixtures → sample-img}/key-portrait-small.jpg +0 -0
@@ -0,0 +1,36 @@
1
+ import express from 'express';
2
+ import { HandlerAuth } from '@atproto/xrpc-server';
3
+ export interface QueryParams {
4
+ }
5
+ export interface InputSchema {
6
+ did?: string;
7
+ [k: string]: unknown;
8
+ }
9
+ export interface OutputSchema {
10
+ signingKey: string;
11
+ [k: string]: unknown;
12
+ }
13
+ export interface HandlerInput {
14
+ encoding: 'application/json';
15
+ body: InputSchema;
16
+ }
17
+ export interface HandlerSuccess {
18
+ encoding: 'application/json';
19
+ body: OutputSchema;
20
+ headers?: {
21
+ [key: string]: string;
22
+ };
23
+ }
24
+ export interface HandlerError {
25
+ status: number;
26
+ message?: string;
27
+ }
28
+ export declare type HandlerOutput = HandlerError | HandlerSuccess;
29
+ export declare type HandlerReqCtx<HA extends HandlerAuth = never> = {
30
+ auth: HA;
31
+ params: QueryParams;
32
+ input: HandlerInput;
33
+ req: express.Request;
34
+ res: express.Response;
35
+ };
36
+ export declare type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCtx<HA>) => Promise<HandlerOutput> | HandlerOutput;
@@ -35,6 +35,7 @@ export declare type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCt
35
35
  export interface Repo {
36
36
  did: string;
37
37
  head: string;
38
+ rev: string;
38
39
  [k: string]: unknown;
39
40
  }
40
41
  export declare function isRepo(v: unknown): v is Repo;
@@ -15,6 +15,7 @@ export declare type ActorInfo = {
15
15
  mutedByList?: ListViewBasic;
16
16
  blockedBy?: boolean;
17
17
  blocking?: string;
18
+ blockingByList?: ListViewBasic;
18
19
  following?: string;
19
20
  followedBy?: string;
20
21
  };
@@ -85,6 +85,7 @@ export declare type RelationshipPair = [didA: string, didB: string];
85
85
  export declare class BlockAndMuteState {
86
86
  hasIdx: Map<string, Set<string>>;
87
87
  blockIdx: Map<string, Map<string, string>>;
88
+ blockListIdx: Map<string, Map<string, string>>;
88
89
  muteIdx: Map<string, Set<string>>;
89
90
  muteListIdx: Map<string, Map<string, string>>;
90
91
  constructor(items?: BlockAndMuteInfo[]);
@@ -93,6 +94,7 @@ export declare class BlockAndMuteState {
93
94
  blocking(pair: RelationshipPair): string | null;
94
95
  blockedBy(pair: RelationshipPair): string | null;
95
96
  mute(pair: RelationshipPair): boolean;
97
+ blockList(pair: RelationshipPair): string | null;
96
98
  muteList(pair: RelationshipPair): string | null;
97
99
  has(pair: RelationshipPair): boolean;
98
100
  }
@@ -6,6 +6,8 @@ import { ModerationAction, ModerationReport } from '../../db/tables/moderation';
6
6
  import { ModerationViews } from './views';
7
7
  import { ImageUriBuilder } from '../../image/uri';
8
8
  import { ImageInvalidator } from '../../image/invalidator';
9
+ import { RepoRef, RepoBlobRef } from '../../lexicon/types/com/atproto/admin/defs';
10
+ import { Main as StrongRef } from '../../lexicon/types/com/atproto/repo/strongRef';
9
11
  export declare class ModerationService {
10
12
  db: PrimaryDatabase;
11
13
  imgUriBuilder: ImageUriBuilder;
@@ -74,20 +76,24 @@ export declare class ModerationService {
74
76
  durationInHours?: number;
75
77
  }): Promise<ModerationActionRow>;
76
78
  getActionsDueForReversal(): Promise<ModerationActionRow[]>;
77
- revertAction({ id, createdBy, createdAt, reason, }: ReversibleModerationAction): Promise<ModerationActionRow>;
79
+ revertAction({ id, createdBy, createdAt, reason, }: ReversibleModerationAction): Promise<{
80
+ result: ModerationActionRow;
81
+ restored?: TakedownSubjects;
82
+ }>;
78
83
  logReverseAction(info: ReversibleModerationAction): Promise<ModerationActionRow>;
79
84
  takedownRepo(info: {
80
85
  takedownId: number;
81
86
  did: string;
82
- }): Promise<void>;
87
+ }): Promise<TakedownSubjects>;
83
88
  reverseTakedownRepo(info: {
84
89
  did: string;
85
90
  }): Promise<void>;
86
91
  takedownRecord(info: {
87
92
  takedownId: number;
88
93
  uri: AtUri;
94
+ cid: CID;
89
95
  blobCids?: CID[];
90
- }): Promise<void>;
96
+ }): Promise<TakedownSubjects>;
91
97
  reverseTakedownRecord(info: {
92
98
  uri: AtUri;
93
99
  }): Promise<void>;
@@ -110,6 +116,10 @@ export declare class ModerationService {
110
116
  createdAt?: Date;
111
117
  }): Promise<ModerationReportRow>;
112
118
  }
119
+ export declare type TakedownSubjects = {
120
+ did: string;
121
+ subjects: (RepoRef | RepoBlobRef | StrongRef)[];
122
+ };
113
123
  export declare type ModerationActionRow = Selectable<ModerationAction>;
114
124
  export declare type ReversibleModerationAction = Pick<ModerationActionRow, 'id' | 'createdBy' | 'reason'> & {
115
125
  createdAt?: Date;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -20,7 +20,6 @@
20
20
  "@isaacs/ttlcache": "^1.4.1",
21
21
  "compression": "^1.7.4",
22
22
  "cors": "^2.8.5",
23
- "dotenv": "^16.0.0",
24
23
  "express": "^4.17.2",
25
24
  "express-async-errors": "^3.1.1",
26
25
  "form-data": "^4.0.0",
@@ -36,14 +35,14 @@
36
35
  "sharp": "^0.31.2",
37
36
  "typed-emitter": "^2.1.0",
38
37
  "uint8arrays": "3.0.0",
39
- "@atproto/api": "^0.6.20",
40
- "@atproto/common": "^0.3.1",
41
- "@atproto/crypto": "^0.2.2",
42
- "@atproto/syntax": "^0.1.2",
43
- "@atproto/identity": "^0.2.1",
44
- "@atproto/lexicon": "^0.2.2",
45
- "@atproto/repo": "^0.3.2",
46
- "@atproto/xrpc-server": "^0.3.2"
38
+ "@atproto/api": "^0.6.22",
39
+ "@atproto/common": "^0.3.3",
40
+ "@atproto/crypto": "^0.2.3",
41
+ "@atproto/syntax": "^0.1.4",
42
+ "@atproto/identity": "^0.3.1",
43
+ "@atproto/lexicon": "^0.3.0",
44
+ "@atproto/repo": "^0.3.4",
45
+ "@atproto/xrpc-server": "^0.4.0"
47
46
  },
48
47
  "devDependencies": {
49
48
  "@did-plc/server": "^0.0.1",
@@ -54,11 +53,11 @@
54
53
  "@types/qs": "^6.9.7",
55
54
  "@types/sharp": "^0.31.0",
56
55
  "axios": "^0.27.2",
57
- "@atproto/api": "^0.6.20",
58
- "@atproto/dev-env": "^0.2.11",
59
- "@atproto/lex-cli": "^0.2.2",
60
- "@atproto/pds": "^0.1.20",
61
- "@atproto/xrpc": "^0.3.2"
56
+ "@atproto/api": "^0.6.22",
57
+ "@atproto/dev-env": "^0.2.13",
58
+ "@atproto/lex-cli": "^0.2.4",
59
+ "@atproto/pds": "^0.3.1",
60
+ "@atproto/xrpc": "^0.4.0"
62
61
  },
63
62
  "scripts": {
64
63
  "codegen": "lex gen-server ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*",
@@ -89,8 +89,8 @@ export const skeleton = async (
89
89
 
90
90
  if (filter === 'posts_with_media') {
91
91
  feedItemsQb = feedItemsQb
92
- // and only your own posts/reposts
93
- .where('post.creator', '=', actorDid)
92
+ // only your own posts
93
+ .where('type', '=', 'post')
94
94
  // only posts with media
95
95
  .whereExists((qb) =>
96
96
  qb
@@ -37,9 +37,9 @@ export default function (server: Server, ctx: AppContext) {
37
37
  presentation,
38
38
  )
39
39
  server.app.bsky.feed.getPostThread({
40
- auth: ctx.authOptionalVerifier,
40
+ auth: ctx.authOptionalAccessOrRoleVerifier,
41
41
  handler: async ({ params, auth, res }) => {
42
- const viewer = auth.credentials.did
42
+ const viewer = 'did' in auth.credentials ? auth.credentials.did : null
43
43
  const db = ctx.db.getReplica('thread')
44
44
  const feedService = ctx.services.feed(db)
45
45
  const actorService = ctx.services.actor(db)
@@ -53,10 +53,6 @@ const skeleton = async (
53
53
  }
54
54
  let notifBuilder = db.db
55
55
  .selectFrom('notification as notif')
56
- .innerJoin('record', 'record.uri', 'notif.recordUri')
57
- .innerJoin('actor as author', 'author.did', 'notif.author')
58
- .where(notSoftDeletedClause(ref('record')))
59
- .where(notSoftDeletedClause(ref('author')))
60
56
  .where('notif.did', '=', viewer)
61
57
  .where((clause) =>
62
58
  clause
@@ -69,16 +65,12 @@ const skeleton = async (
69
65
  ),
70
66
  )
71
67
  .select([
68
+ 'notif.author as authorDid',
72
69
  'notif.recordUri as uri',
73
70
  'notif.recordCid as cid',
74
- 'author.did as authorDid',
75
- 'author.handle as authorHandle',
76
- 'author.indexedAt as authorIndexedAt',
77
- 'author.takedownId as authorTakedownId',
78
71
  'notif.reason as reason',
79
72
  'notif.reasonSubject as reasonSubject',
80
73
  'notif.sortAt as indexedAt',
81
- 'record.json as recordJson',
82
74
  ])
83
75
 
84
76
  const keyset = new NotifsKeyset(ref('notif.sortAt'), ref('notif.recordCid'))
@@ -86,6 +78,7 @@ const skeleton = async (
86
78
  cursor,
87
79
  limit,
88
80
  keyset,
81
+ tryIndex: true,
89
82
  })
90
83
 
91
84
  const actorStateQuery = db.db
@@ -107,17 +100,18 @@ const skeleton = async (
107
100
  }
108
101
 
109
102
  const hydration = async (state: SkeletonState, ctx: Context) => {
110
- const { graphService, actorService, labelService } = ctx
103
+ const { graphService, actorService, labelService, db } = ctx
111
104
  const { params, notifs } = state
112
105
  const { viewer } = params
113
106
  const dids = notifs.map((notif) => notif.authorDid)
114
107
  const uris = notifs.map((notif) => notif.uri)
115
- const [actors, labels, bam] = await Promise.all([
108
+ const [actors, records, labels, bam] = await Promise.all([
116
109
  actorService.views.profiles(dids, viewer),
110
+ getRecordMap(db, uris),
117
111
  labelService.getLabelsForUris(uris),
118
112
  graphService.getBlockAndMuteState(dids.map((did) => [viewer, did])),
119
113
  ])
120
- return { ...state, actors, labels, bam }
114
+ return { ...state, actors, records, labels, bam }
121
115
  }
122
116
 
123
117
  const noBlockOrMutes = (state: HydrationState) => {
@@ -131,11 +125,11 @@ const noBlockOrMutes = (state: HydrationState) => {
131
125
  }
132
126
 
133
127
  const presentation = (state: HydrationState) => {
134
- const { notifs, cursor, actors, labels, lastSeenNotifs } = state
128
+ const { notifs, cursor, actors, records, labels, lastSeenNotifs } = state
135
129
  const notifications = mapDefined(notifs, (notif) => {
136
130
  const author = actors[notif.authorDid]
137
- if (!author) return undefined
138
- const record = jsonStringToLex(notif.recordJson) as Record<string, unknown>
131
+ const record = records[notif.uri]
132
+ if (!author || !record) return undefined
139
133
  const recordLabels = labels[notif.uri] ?? []
140
134
  const recordSelfLabels = getSelfLabels({
141
135
  uri: notif.uri,
@@ -157,6 +151,24 @@ const presentation = (state: HydrationState) => {
157
151
  return { notifications, cursor }
158
152
  }
159
153
 
154
+ const getRecordMap = async (
155
+ db: Database,
156
+ uris: string[],
157
+ ): Promise<RecordMap> => {
158
+ if (!uris.length) return {}
159
+ const { ref } = db.db.dynamic
160
+ const recordRows = await db.db
161
+ .selectFrom('record')
162
+ .select(['uri', 'json'])
163
+ .where('uri', 'in', uris)
164
+ .where(notSoftDeletedClause(ref('record')))
165
+ .execute()
166
+ return recordRows.reduce((acc, { uri, json }) => {
167
+ acc[uri] = jsonStringToLex(json) as Record<string, unknown>
168
+ return acc
169
+ }, {} as RecordMap)
170
+ }
171
+
160
172
  type Context = {
161
173
  db: Database
162
174
  actorService: ActorService
@@ -178,20 +190,19 @@ type SkeletonState = {
178
190
  type HydrationState = SkeletonState & {
179
191
  bam: BlockAndMuteState
180
192
  actors: ActorInfoMap
193
+ records: RecordMap
181
194
  labels: Labels
182
195
  }
183
196
 
197
+ type RecordMap = { [uri: string]: Record<string, unknown> }
198
+
184
199
  type NotifRow = {
185
- indexedAt: string
186
- cid: string
187
- uri: string
188
200
  authorDid: string
189
- authorHandle: string | null
190
- authorIndexedAt: string
191
- authorTakedownId: number | null
201
+ uri: string
202
+ cid: string
192
203
  reason: string
193
204
  reasonSubject: string | null
194
- recordJson: string
205
+ indexedAt: string
195
206
  }
196
207
 
197
208
  class NotifsKeyset extends TimeCidKeyset<NotifRow> {
@@ -1,17 +1,43 @@
1
1
  import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
+ import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
4
+ import {
5
+ isRecordView,
6
+ isRepoView,
7
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
3
8
 
4
9
  export default function (server: Server, ctx: AppContext) {
5
10
  server.com.atproto.admin.getModerationAction({
6
11
  auth: ctx.roleVerifier,
7
- handler: async ({ params }) => {
12
+ handler: async ({ params, auth }) => {
8
13
  const { id } = params
9
14
  const db = ctx.db.getPrimary()
10
15
  const moderationService = ctx.services.moderation(db)
11
16
  const result = await moderationService.getActionOrThrow(id)
17
+
18
+ const [action, accountInfo] = await Promise.all([
19
+ moderationService.views.actionDetail(result),
20
+ getPdsAccountInfo(ctx, result.subjectDid),
21
+ ])
22
+
23
+ // add in pds account info if available
24
+ if (isRepoView(action.subject)) {
25
+ action.subject = addAccountInfoToRepoView(
26
+ action.subject,
27
+ accountInfo,
28
+ auth.credentials.moderator,
29
+ )
30
+ } else if (isRecordView(action.subject)) {
31
+ action.subject.repo = addAccountInfoToRepoView(
32
+ action.subject.repo,
33
+ accountInfo,
34
+ auth.credentials.moderator,
35
+ )
36
+ }
37
+
12
38
  return {
13
39
  encoding: 'application/json',
14
- body: await moderationService.views.actionDetail(result),
40
+ body: action,
15
41
  }
16
42
  },
17
43
  })
@@ -1,17 +1,42 @@
1
1
  import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
+ import {
4
+ isRecordView,
5
+ isRepoView,
6
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
7
+ import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
3
8
 
4
9
  export default function (server: Server, ctx: AppContext) {
5
10
  server.com.atproto.admin.getModerationReport({
6
11
  auth: ctx.roleVerifier,
7
- handler: async ({ params }) => {
12
+ handler: async ({ params, auth }) => {
8
13
  const { id } = params
9
14
  const db = ctx.db.getPrimary()
10
15
  const moderationService = ctx.services.moderation(db)
11
16
  const result = await moderationService.getReportOrThrow(id)
17
+ const [report, accountInfo] = await Promise.all([
18
+ moderationService.views.reportDetail(result),
19
+ getPdsAccountInfo(ctx, result.subjectDid),
20
+ ])
21
+
22
+ // add in pds account info if available
23
+ if (isRepoView(report.subject)) {
24
+ report.subject = addAccountInfoToRepoView(
25
+ report.subject,
26
+ accountInfo,
27
+ auth.credentials.moderator,
28
+ )
29
+ } else if (isRecordView(report.subject)) {
30
+ report.subject.repo = addAccountInfoToRepoView(
31
+ report.subject.repo,
32
+ accountInfo,
33
+ auth.credentials.moderator,
34
+ )
35
+ }
36
+
12
37
  return {
13
38
  encoding: 'application/json',
14
- body: await moderationService.views.reportDetail(result),
39
+ body: report,
15
40
  }
16
41
  },
17
42
  })
@@ -1,11 +1,12 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../../../lexicon'
3
3
  import AppContext from '../../../../context'
4
+ import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
4
5
 
5
6
  export default function (server: Server, ctx: AppContext) {
6
7
  server.com.atproto.admin.getRecord({
7
8
  auth: ctx.roleVerifier,
8
- handler: async ({ params }) => {
9
+ handler: async ({ params, auth }) => {
9
10
  const { uri, cid } = params
10
11
  const db = ctx.db.getPrimary()
11
12
  const result = await db.db
@@ -17,9 +18,20 @@ export default function (server: Server, ctx: AppContext) {
17
18
  if (!result) {
18
19
  throw new InvalidRequestError('Record not found', 'RecordNotFound')
19
20
  }
21
+ const [record, accountInfo] = await Promise.all([
22
+ ctx.services.moderation(db).views.recordDetail(result),
23
+ getPdsAccountInfo(ctx, result.did),
24
+ ])
25
+
26
+ record.repo = addAccountInfoToRepoView(
27
+ record.repo,
28
+ accountInfo,
29
+ auth.credentials.moderator,
30
+ )
31
+
20
32
  return {
21
33
  encoding: 'application/json',
22
- body: await ctx.services.moderation(db).views.recordDetail(result),
34
+ body: record,
23
35
  }
24
36
  },
25
37
  })
@@ -1,20 +1,31 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../../../lexicon'
3
3
  import AppContext from '../../../../context'
4
+ import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util'
4
5
 
5
6
  export default function (server: Server, ctx: AppContext) {
6
7
  server.com.atproto.admin.getRepo({
7
8
  auth: ctx.roleVerifier,
8
- handler: async ({ params }) => {
9
+ handler: async ({ params, auth }) => {
9
10
  const { did } = params
10
11
  const db = ctx.db.getPrimary()
11
12
  const result = await ctx.services.actor(db).getActor(did, true)
12
13
  if (!result) {
13
14
  throw new InvalidRequestError('Repo not found', 'RepoNotFound')
14
15
  }
16
+ const [partialRepo, accountInfo] = await Promise.all([
17
+ ctx.services.moderation(db).views.repoDetail(result),
18
+ getPdsAccountInfo(ctx, result.did),
19
+ ])
20
+
21
+ const repo = addAccountInfoToRepoViewDetail(
22
+ partialRepo,
23
+ accountInfo,
24
+ auth.credentials.moderator,
25
+ )
15
26
  return {
16
27
  encoding: 'application/json',
17
- body: await ctx.services.moderation(db).views.repoDetail(result),
28
+ body: repo,
18
29
  }
19
30
  },
20
31
  })
@@ -1,4 +1,8 @@
1
- import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
1
+ import {
2
+ AuthRequiredError,
3
+ InvalidRequestError,
4
+ UpstreamFailureError,
5
+ } from '@atproto/xrpc-server'
2
6
  import {
3
7
  ACKNOWLEDGE,
4
8
  ESCALATE,
@@ -6,6 +10,7 @@ import {
6
10
  } from '../../../../lexicon/types/com/atproto/admin/defs'
7
11
  import { Server } from '../../../../lexicon'
8
12
  import AppContext from '../../../../context'
13
+ import { retryHttp } from '../../../../util/retry'
9
14
 
10
15
  export default function (server: Server, ctx: AppContext) {
11
16
  server.com.atproto.admin.reverseModerationAction({
@@ -16,7 +21,7 @@ export default function (server: Server, ctx: AppContext) {
16
21
  const moderationService = ctx.services.moderation(db)
17
22
  const { id, createdBy, reason } = input.body
18
23
 
19
- const moderationAction = await db.transaction(async (dbTxn) => {
24
+ const { result, restored } = await db.transaction(async (dbTxn) => {
20
25
  const moderationTxn = ctx.services.moderation(dbTxn)
21
26
  const labelTxn = ctx.services.label(dbTxn)
22
27
  const now = new Date()
@@ -53,7 +58,7 @@ export default function (server: Server, ctx: AppContext) {
53
58
  )
54
59
  }
55
60
 
56
- const result = await moderationTxn.revertAction({
61
+ const { result, restored } = await moderationTxn.revertAction({
57
62
  id,
58
63
  createdAt: now,
59
64
  createdBy,
@@ -77,12 +82,33 @@ export default function (server: Server, ctx: AppContext) {
77
82
  { create, negate },
78
83
  )
79
84
 
80
- return result
85
+ return { result, restored }
81
86
  })
82
87
 
88
+ if (restored) {
89
+ const { did, subjects } = restored
90
+ const agent = await ctx.pdsAdminAgent(did)
91
+ const results = await Promise.allSettled(
92
+ subjects.map((subject) =>
93
+ retryHttp(() =>
94
+ agent.api.com.atproto.admin.updateSubjectStatus({
95
+ subject,
96
+ takedown: {
97
+ applied: false,
98
+ },
99
+ }),
100
+ ),
101
+ ),
102
+ )
103
+ const hadFailure = results.some((r) => r.status === 'rejected')
104
+ if (hadFailure) {
105
+ throw new UpstreamFailureError('failed to revert action on PDS')
106
+ }
107
+ }
108
+
83
109
  return {
84
110
  encoding: 'application/json',
85
- body: await moderationService.views.action(moderationAction),
111
+ body: await moderationService.views.action(result),
86
112
  }
87
113
  },
88
114
  })
@@ -1,4 +1,3 @@
1
- import { InvalidRequestError } from '@atproto/xrpc-server'
2
1
  import { Server } from '../../../../lexicon'
3
2
  import AppContext from '../../../../context'
4
3
 
@@ -8,16 +7,14 @@ export default function (server: Server, ctx: AppContext) {
8
7
  handler: async ({ params }) => {
9
8
  const db = ctx.db.getPrimary()
10
9
  const moderationService = ctx.services.moderation(db)
11
- const { invitedBy, limit, cursor } = params
12
- if (invitedBy) {
13
- throw new InvalidRequestError('The invitedBy parameter is unsupported')
14
- }
10
+ const { limit, cursor } = params
15
11
  // prefer new 'q' query param over deprecated 'term'
16
12
  const query = params.q ?? params.term
17
13
 
18
14
  const { results, cursor: resCursor } = await ctx.services
19
15
  .actor(db)
20
16
  .getSearchResults({ query, limit, cursor, includeSoftDeleted: true })
17
+
21
18
  return {
22
19
  encoding: 'application/json',
23
20
  body: {