@atproto/bsky 0.0.21 → 0.0.22

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 (42) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +1 -1
  3. package/dist/db/index.js +17 -1
  4. package/dist/db/index.js.map +3 -3
  5. package/dist/db/migrations/20231213T181744386Z-moderation-subject-appeal.d.ts +3 -0
  6. package/dist/db/migrations/index.d.ts +1 -0
  7. package/dist/db/tables/moderation.d.ts +3 -1
  8. package/dist/index.js +213 -32
  9. package/dist/index.js.map +3 -3
  10. package/dist/lexicon/index.d.ts +1 -0
  11. package/dist/lexicon/lexicons.d.ts +35 -0
  12. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +10 -1
  13. package/dist/lexicon/types/com/atproto/admin/queryModerationStatuses.d.ts +1 -0
  14. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  15. package/dist/lexicon/types/com/atproto/moderation/defs.d.ts +2 -1
  16. package/dist/services/feed/views.d.ts +1 -1
  17. package/dist/services/moderation/index.d.ts +7 -2
  18. package/package.json +5 -5
  19. package/src/api/app/bsky/feed/getPostThread.ts +4 -3
  20. package/src/api/app/bsky/feed/getTimeline.ts +54 -0
  21. package/src/api/com/atproto/admin/queryModerationStatuses.ts +2 -0
  22. package/src/api/com/atproto/moderation/createReport.ts +14 -3
  23. package/src/api/com/atproto/moderation/util.ts +2 -0
  24. package/src/api/health.ts +14 -0
  25. package/src/auto-moderator/index.ts +19 -0
  26. package/src/db/migrations/20231213T181744386Z-moderation-subject-appeal.ts +23 -0
  27. package/src/db/migrations/index.ts +1 -0
  28. package/src/db/tables/moderation.ts +3 -0
  29. package/src/lexicon/index.ts +1 -0
  30. package/src/lexicon/lexicons.ts +40 -0
  31. package/src/lexicon/types/com/atproto/admin/defs.ts +28 -0
  32. package/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts +2 -0
  33. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +2 -0
  34. package/src/lexicon/types/com/atproto/moderation/defs.ts +3 -0
  35. package/src/services/feed/views.ts +6 -3
  36. package/src/services/moderation/index.ts +9 -0
  37. package/src/services/moderation/status.ts +24 -0
  38. package/src/services/moderation/views.ts +2 -0
  39. package/tests/admin/moderation-appeals.test.ts +269 -0
  40. package/tests/auto-moderator/labeler.test.ts +39 -0
  41. package/tests/views/blocks.test.ts +14 -1
  42. package/tests/views/timeline.test.ts +14 -0
@@ -124,6 +124,7 @@ export declare const COM_ATPROTO_MODERATION: {
124
124
  DefsReasonSexual: string;
125
125
  DefsReasonRude: string;
126
126
  DefsReasonOther: string;
127
+ DefsReasonAppeal: string;
127
128
  };
128
129
  export declare const APP_BSKY_GRAPH: {
129
130
  DefsModlist: string;
@@ -177,9 +177,18 @@ export declare const schemaDict: {
177
177
  type: string;
178
178
  format: string;
179
179
  };
180
+ lastAppealedAt: {
181
+ type: string;
182
+ format: string;
183
+ description: string;
184
+ };
180
185
  takendown: {
181
186
  type: string;
182
187
  };
188
+ appealed: {
189
+ type: string;
190
+ description: string;
191
+ };
183
192
  suspendUntil: {
184
193
  type: string;
185
194
  format: string;
@@ -606,6 +615,16 @@ export declare const schemaDict: {
606
615
  };
607
616
  };
608
617
  };
618
+ modEventResolveAppeal: {
619
+ type: string;
620
+ description: string;
621
+ properties: {
622
+ comment: {
623
+ type: string;
624
+ description: string;
625
+ };
626
+ };
627
+ };
609
628
  modEventComment: {
610
629
  type: string;
611
630
  description: string;
@@ -705,6 +724,10 @@ export declare const schemaDict: {
705
724
  type: string;
706
725
  description: string;
707
726
  };
727
+ comment: {
728
+ type: string;
729
+ description: string;
730
+ };
708
731
  };
709
732
  };
710
733
  };
@@ -1214,6 +1237,10 @@ export declare const schemaDict: {
1214
1237
  type: string;
1215
1238
  description: string;
1216
1239
  };
1240
+ appealed: {
1241
+ type: string;
1242
+ description: string;
1243
+ };
1217
1244
  limit: {
1218
1245
  type: string;
1219
1246
  minimum: number;
@@ -1324,6 +1351,10 @@ export declare const schemaDict: {
1324
1351
  type: string;
1325
1352
  format: string;
1326
1353
  };
1354
+ comment: {
1355
+ type: string;
1356
+ description: string;
1357
+ };
1327
1358
  };
1328
1359
  };
1329
1360
  };
@@ -1777,6 +1808,10 @@ export declare const schemaDict: {
1777
1808
  type: string;
1778
1809
  description: string;
1779
1810
  };
1811
+ reasonAppeal: {
1812
+ type: string;
1813
+ description: string;
1814
+ };
1780
1815
  };
1781
1816
  };
1782
1817
  ComAtprotoRepoApplyWrites: {
@@ -31,7 +31,7 @@ export declare function isModEventView(v: unknown): v is ModEventView;
31
31
  export declare function validateModEventView(v: unknown): ValidationResult;
32
32
  export interface ModEventViewDetail {
33
33
  id: number;
34
- event: ModEventTakedown | ModEventReverseTakedown | ModEventComment | ModEventReport | ModEventLabel | ModEventAcknowledge | ModEventEscalate | ModEventMute | {
34
+ event: ModEventTakedown | ModEventReverseTakedown | ModEventComment | ModEventReport | ModEventLabel | ModEventAcknowledge | ModEventEscalate | ModEventMute | ModEventResolveAppeal | {
35
35
  $type: string;
36
36
  [k: string]: unknown;
37
37
  };
@@ -78,7 +78,9 @@ export interface SubjectStatusView {
78
78
  lastReviewedBy?: string;
79
79
  lastReviewedAt?: string;
80
80
  lastReportedAt?: string;
81
+ lastAppealedAt?: string;
81
82
  takendown?: boolean;
83
+ appealed?: boolean;
82
84
  suspendUntil?: string;
83
85
  [k: string]: unknown;
84
86
  }
@@ -254,6 +256,12 @@ export interface ModEventReverseTakedown {
254
256
  }
255
257
  export declare function isModEventReverseTakedown(v: unknown): v is ModEventReverseTakedown;
256
258
  export declare function validateModEventReverseTakedown(v: unknown): ValidationResult;
259
+ export interface ModEventResolveAppeal {
260
+ comment?: string;
261
+ [k: string]: unknown;
262
+ }
263
+ export declare function isModEventResolveAppeal(v: unknown): v is ModEventResolveAppeal;
264
+ export declare function validateModEventResolveAppeal(v: unknown): ValidationResult;
257
265
  export interface ModEventComment {
258
266
  comment: string;
259
267
  sticky?: boolean;
@@ -303,6 +311,7 @@ export declare function isModEventUnmute(v: unknown): v is ModEventUnmute;
303
311
  export declare function validateModEventUnmute(v: unknown): ValidationResult;
304
312
  export interface ModEventEmail {
305
313
  subjectLine: string;
314
+ comment?: string;
306
315
  [k: string]: unknown;
307
316
  }
308
317
  export declare function isModEventEmail(v: unknown): v is ModEventEmail;
@@ -15,6 +15,7 @@ export interface QueryParams {
15
15
  sortField: 'lastReviewedAt' | 'lastReportedAt';
16
16
  sortDirection: 'asc' | 'desc';
17
17
  takendown?: boolean;
18
+ appealed?: boolean;
18
19
  limit: number;
19
20
  cursor?: string;
20
21
  }
@@ -7,6 +7,7 @@ export interface InputSchema {
7
7
  content: string;
8
8
  subject?: string;
9
9
  senderDid: string;
10
+ comment?: string;
10
11
  [k: string]: unknown;
11
12
  }
12
13
  export interface OutputSchema {
@@ -1,7 +1,8 @@
1
- export declare type ReasonType = 'com.atproto.moderation.defs#reasonSpam' | 'com.atproto.moderation.defs#reasonViolation' | 'com.atproto.moderation.defs#reasonMisleading' | 'com.atproto.moderation.defs#reasonSexual' | 'com.atproto.moderation.defs#reasonRude' | 'com.atproto.moderation.defs#reasonOther' | (string & {});
1
+ export declare type ReasonType = 'com.atproto.moderation.defs#reasonSpam' | 'com.atproto.moderation.defs#reasonViolation' | 'com.atproto.moderation.defs#reasonMisleading' | 'com.atproto.moderation.defs#reasonSexual' | 'com.atproto.moderation.defs#reasonRude' | 'com.atproto.moderation.defs#reasonOther' | 'com.atproto.moderation.defs#reasonAppeal' | (string & {});
2
2
  export declare const REASONSPAM = "com.atproto.moderation.defs#reasonSpam";
3
3
  export declare const REASONVIOLATION = "com.atproto.moderation.defs#reasonViolation";
4
4
  export declare const REASONMISLEADING = "com.atproto.moderation.defs#reasonMisleading";
5
5
  export declare const REASONSEXUAL = "com.atproto.moderation.defs#reasonSexual";
6
6
  export declare const REASONRUDE = "com.atproto.moderation.defs#reasonRude";
7
7
  export declare const REASONOTHER = "com.atproto.moderation.defs#reasonOther";
8
+ export declare const REASONAPPEAL = "com.atproto.moderation.defs#reasonAppeal";
@@ -27,7 +27,7 @@ export declare class FeedViews {
27
27
  }): FeedViewPost[];
28
28
  formatPostView(uri: string, actors: ActorInfoMap, posts: PostInfoMap, threadgates: ThreadgateInfoMap, embeds: PostEmbedViews, labels: Labels, lists: ListInfoMap, viewer: string | null): PostView | undefined;
29
29
  userReplyDisabled(uri: string, actors: ActorInfoMap, posts: PostInfoMap, threadgates: ThreadgateInfoMap, lists: ListInfoMap, viewer: string | null): boolean | undefined;
30
- formatMaybePostView(uri: string, actors: ActorInfoMap, posts: PostInfoMap, threadgates: ThreadgateInfoMap, embeds: PostEmbedViews, labels: Labels, lists: ListInfoMap, blocks: PostBlocksMap, viewer: string | null, opts?: {
30
+ formatMaybePostView(uri: string, replyUri: string | null, actors: ActorInfoMap, posts: PostInfoMap, threadgates: ThreadgateInfoMap, embeds: PostEmbedViews, labels: Labels, lists: ListInfoMap, blocks: PostBlocksMap, viewer: string | null, opts?: {
31
31
  usePostViewUnion?: boolean;
32
32
  }): MaybePostView | undefined;
33
33
  blockedPost(post: PostView): {
@@ -48,7 +48,9 @@ export declare class ModerationService {
48
48
  lastReviewedAt: string | null;
49
49
  muteUntil: string | null;
50
50
  lastReviewedBy: string | null;
51
+ lastAppealedAt: string | null;
51
52
  takendown: boolean;
53
+ appealed: boolean | null;
52
54
  suspendUntil: string | null;
53
55
  recordCid: string | null;
54
56
  recordPath: string;
@@ -79,7 +81,7 @@ export declare class ModerationService {
79
81
  createLabelVals: string | null;
80
82
  negateLabelVals: string | null;
81
83
  durationInHours: number | null;
82
- action: "com.atproto.admin.defs#modEventTakedown" | "com.atproto.admin.defs#modEventReverseTakedown" | "com.atproto.admin.defs#modEventComment" | "com.atproto.admin.defs#modEventReport" | "com.atproto.admin.defs#modEventLabel" | "com.atproto.admin.defs#modEventAcknowledge" | "com.atproto.admin.defs#modEventEscalate" | "com.atproto.admin.defs#modEventMute" | "com.atproto.admin.defs#modEventEmail";
84
+ action: "com.atproto.admin.defs#modEventTakedown" | "com.atproto.admin.defs#modEventReverseTakedown" | "com.atproto.admin.defs#modEventResolveAppeal" | "com.atproto.admin.defs#modEventComment" | "com.atproto.admin.defs#modEventReport" | "com.atproto.admin.defs#modEventLabel" | "com.atproto.admin.defs#modEventAcknowledge" | "com.atproto.admin.defs#modEventEscalate" | "com.atproto.admin.defs#modEventMute" | "com.atproto.admin.defs#modEventEmail";
83
85
  subjectCid: string | null;
84
86
  subjectDid: string;
85
87
  subjectType: "com.atproto.repo.strongRef" | "com.atproto.admin.defs#repoRef";
@@ -122,10 +124,11 @@ export declare class ModerationService {
122
124
  reportedBy: string;
123
125
  createdAt?: Date;
124
126
  }): Promise<ModerationEventRow>;
125
- getSubjectStatuses({ cursor, limit, takendown, reviewState, reviewedAfter, reviewedBefore, reportedAfter, reportedBefore, includeMuted, ignoreSubjects, sortDirection, lastReviewedBy, sortField, subject, }: {
127
+ getSubjectStatuses({ cursor, limit, takendown, appealed, reviewState, reviewedAfter, reviewedBefore, reportedAfter, reportedBefore, includeMuted, ignoreSubjects, sortDirection, lastReviewedBy, sortField, subject, }: {
126
128
  cursor?: string;
127
129
  limit?: number;
128
130
  takendown?: boolean;
131
+ appealed?: boolean | null;
129
132
  reviewedBefore?: string;
130
133
  reviewState?: ModerationSubjectStatusRow['reviewState'];
131
134
  reviewedAfter?: string;
@@ -185,7 +188,9 @@ export declare class ModerationService {
185
188
  lastReviewedAt: string | null;
186
189
  muteUntil: string | null;
187
190
  lastReviewedBy: string | null;
191
+ lastAppealedAt: string | null;
188
192
  takendown: boolean;
193
+ appealed: boolean | null;
189
194
  suspendUntil: string | null;
190
195
  recordCid: string | null;
191
196
  recordPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  "sharp": "^0.32.6",
36
36
  "typed-emitter": "^2.1.0",
37
37
  "uint8arrays": "3.0.0",
38
- "@atproto/api": "^0.7.4",
38
+ "@atproto/api": "^0.8.0",
39
39
  "@atproto/common": "^0.3.3",
40
40
  "@atproto/crypto": "^0.3.0",
41
41
  "@atproto/syntax": "^0.1.5",
@@ -52,10 +52,10 @@
52
52
  "@types/pg": "^8.6.6",
53
53
  "@types/qs": "^6.9.7",
54
54
  "axios": "^0.27.2",
55
- "@atproto/api": "^0.7.4",
56
- "@atproto/dev-env": "^0.2.21",
55
+ "@atproto/api": "^0.8.0",
56
+ "@atproto/dev-env": "^0.2.22",
57
57
  "@atproto/lex-cli": "^0.2.5",
58
- "@atproto/pds": "^0.3.9",
58
+ "@atproto/pds": "^0.3.10",
59
59
  "@atproto/xrpc": "^0.4.1"
60
60
  },
61
61
  "scripts": {
@@ -128,9 +128,10 @@ const composeThread = (
128
128
  // @TODO re-enable invalidReplyRoot check
129
129
  // const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate
130
130
  const badReply = !!info?.violatesThreadGate
131
- const omitBadReply = !isAnchorPost && badReply
131
+ const violatesBlock = (post && blocks[post.uri]?.reply) ?? false
132
+ const omitBadReply = !isAnchorPost && (badReply || violatesBlock)
132
133
 
133
- if (!post || blocks[post.uri]?.reply || omitBadReply) {
134
+ if (!post || omitBadReply) {
134
135
  return {
135
136
  $type: 'app.bsky.feed.defs#notFoundPost',
136
137
  uri: threadData.post.postUri,
@@ -156,7 +157,7 @@ const composeThread = (
156
157
  }
157
158
 
158
159
  let parent
159
- if (threadData.parent && !badReply) {
160
+ if (threadData.parent && !badReply && !violatesBlock) {
160
161
  if (threadData.parent instanceof ParentNotFoundError) {
161
162
  parent = {
162
163
  $type: 'app.bsky.feed.defs#notFoundPost',
@@ -1,3 +1,4 @@
1
+ import { sql } from 'kysely'
1
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
3
  import { Server } from '../../../../lexicon'
3
4
  import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed'
@@ -55,6 +56,11 @@ export const skeleton = async (
55
56
  throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`)
56
57
  }
57
58
 
59
+ if (limit === 1 && !cursor) {
60
+ // special case for limit=1, which is often used to check if there are new items at the top of the timeline.
61
+ return skeletonLimit1(params, ctx)
62
+ }
63
+
58
64
  const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid'))
59
65
  const sortFrom = keyset.unpack(cursor)?.primary
60
66
 
@@ -117,6 +123,54 @@ export const skeleton = async (
117
123
  }
118
124
  }
119
125
 
126
+ // The limit=1 case is used commonly to check if there are new items at the top of the timeline.
127
+ // Since it's so common, it's optimized here. The most common strategy that postgres takes to
128
+ // build a timeline is to grab all recent content from each of the user's follow, then paginate it.
129
+ // The downside here is that it requires grabbing all recent content from all follows, even if you
130
+ // only want a single result. The approach here instead takes the single most recent post from
131
+ // each of the user's follows, then sorts only those and takes the top item.
132
+ const skeletonLimit1 = async (params: Params, ctx: Context) => {
133
+ const { viewer } = params
134
+ const { db } = ctx
135
+ const { ref } = db.db.dynamic
136
+ const creatorsQb = db.db
137
+ .selectFrom('follow')
138
+ .where('creator', '=', viewer)
139
+ .select('subjectDid as did')
140
+ .unionAll(sql`select ${viewer} as did`)
141
+ const feedItemsQb = db.db
142
+ .selectFrom(creatorsQb.as('creator'))
143
+ .innerJoinLateral(
144
+ (eb) => {
145
+ const keyset = new FeedKeyset(
146
+ ref('feed_item.sortAt'),
147
+ ref('feed_item.cid'),
148
+ )
149
+ const creatorFeedItemQb = eb
150
+ .selectFrom('feed_item')
151
+ .innerJoin('post', 'post.uri', 'feed_item.postUri')
152
+ .whereRef('feed_item.originatorDid', '=', 'creator.did')
153
+ .where('feed_item.sortAt', '>', getFeedDateThreshold(undefined, 2))
154
+ .selectAll('feed_item')
155
+ .select([
156
+ 'post.replyRoot',
157
+ 'post.replyParent',
158
+ 'post.creator as postAuthorDid',
159
+ ])
160
+ return paginate(creatorFeedItemQb, { limit: 1, keyset }).as('result')
161
+ },
162
+ (join) => join.onTrue(),
163
+ )
164
+ .selectAll('result')
165
+ const keyset = new FeedKeyset(ref('result.sortAt'), ref('result.cid'))
166
+ const feedItems = await paginate(feedItemsQb, { limit: 1, keyset }).execute()
167
+ return {
168
+ params,
169
+ feedItems,
170
+ cursor: keyset.packFromResult(feedItems),
171
+ }
172
+ }
173
+
120
174
  const hydration = async (
121
175
  state: SkeletonState,
122
176
  ctx: Context,
@@ -9,6 +9,7 @@ export default function (server: Server, ctx: AppContext) {
9
9
  const {
10
10
  subject,
11
11
  takendown,
12
+ appealed,
12
13
  reviewState,
13
14
  reviewedAfter,
14
15
  reviewedBefore,
@@ -28,6 +29,7 @@ export default function (server: Server, ctx: AppContext) {
28
29
  reviewState: getReviewState(reviewState),
29
30
  subject,
30
31
  takendown,
32
+ appealed,
31
33
  reviewedAfter,
32
34
  reviewedBefore,
33
35
  reportedAfter,
@@ -1,8 +1,9 @@
1
- import { AuthRequiredError } from '@atproto/xrpc-server'
1
+ import { AuthRequiredError, ForbiddenError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../../../lexicon'
3
3
  import AppContext from '../../../../context'
4
4
  import { getReasonType, getSubject } from './util'
5
5
  import { softDeleted } from '../../../../db/util'
6
+ import { REASONAPPEAL } from '../../../../lexicon/types/com/atproto/moderation/defs'
6
7
 
7
8
  export default function (server: Server, ctx: AppContext) {
8
9
  server.com.atproto.moderation.createReport({
@@ -22,12 +23,22 @@ export default function (server: Server, ctx: AppContext) {
22
23
  }
23
24
  }
24
25
 
26
+ const reportReasonType = getReasonType(reasonType)
27
+ const reportSubject = getSubject(subject)
28
+ const subjectDid =
29
+ 'did' in reportSubject ? reportSubject.did : reportSubject.uri.host
30
+
31
+ // If the report is an appeal, the requester must be the author of the subject
32
+ if (reasonType === REASONAPPEAL && requester !== subjectDid) {
33
+ throw new ForbiddenError('You cannot appeal this report')
34
+ }
35
+
25
36
  const report = await db.transaction(async (dbTxn) => {
26
37
  const moderationTxn = ctx.services.moderation(dbTxn)
27
38
  return moderationTxn.report({
28
- reasonType: getReasonType(reasonType),
39
+ reasonType: reportReasonType,
29
40
  reason,
30
- subject: getSubject(subject),
41
+ subject: reportSubject,
31
42
  reportedBy: requester || ctx.cfg.serverDid,
32
43
  })
33
44
  })
@@ -10,6 +10,7 @@ import {
10
10
  REASONRUDE,
11
11
  REASONSEXUAL,
12
12
  REASONVIOLATION,
13
+ REASONAPPEAL,
13
14
  } from '../../../../lexicon/types/com/atproto/moderation/defs'
14
15
  import {
15
16
  REVIEWCLOSED,
@@ -73,6 +74,7 @@ const reasonTypes = new Set([
73
74
  REASONRUDE,
74
75
  REASONSEXUAL,
75
76
  REASONVIOLATION,
77
+ REASONAPPEAL,
76
78
  ])
77
79
 
78
80
  const eventTypes = new Set([
package/src/api/health.ts CHANGED
@@ -5,6 +5,20 @@ import AppContext from '../context'
5
5
  export const createRouter = (ctx: AppContext): express.Router => {
6
6
  const router = express.Router()
7
7
 
8
+ router.get('/', function (req, res) {
9
+ res.type('text/plain')
10
+ res.send(
11
+ 'This is an AT Protocol Application View (AppView) for the "bsky.app" application: https://github.com/bluesky-social/atproto\n\nMost API routes are under /xrpc/',
12
+ )
13
+ })
14
+
15
+ router.get('/robots.txt', function (req, res) {
16
+ res.type('text/plain')
17
+ res.send(
18
+ '# Hello Friends!\n\n# Crawling the public parts of the API is allowed. HTTP 429 ("backoff") status codes are used for rate-limiting. Up to a handful concurrent requests should be ok.\nUser-agent: *\nAllow: /',
19
+ )
20
+ })
21
+
8
22
  router.get('/xrpc/_health', async function (req, res) {
9
23
  const { version } = ctx.cfg
10
24
  const db = ctx.db.getPrimary()
@@ -287,6 +287,25 @@ export class AutoModerator {
287
287
 
288
288
  async storeLabels(uri: AtUri, cid: CID, labels: string[]): Promise<void> {
289
289
  if (labels.length < 1) return
290
+
291
+ // Given that moderation service is available, log the labeling event for historical purposes
292
+ if (this.services.moderation) {
293
+ await this.ctx.db.transaction(async (dbTxn) => {
294
+ if (!this.services.moderation) return
295
+ const modSrvc = this.services.moderation(dbTxn)
296
+ await modSrvc.logEvent({
297
+ event: {
298
+ $type: 'com.atproto.admin.defs#modEventLabel',
299
+ createLabelVals: labels,
300
+ negateLabelVals: [],
301
+ comment: '[AutoModerator]: Applying labels',
302
+ },
303
+ subject: { uri, cid },
304
+ createdBy: this.ctx.cfg.labelerDid,
305
+ })
306
+ })
307
+ }
308
+
290
309
  const labelSrvc = this.services.label(this.ctx.db)
291
310
  await labelSrvc.formatAndCreate(
292
311
  this.ctx.cfg.labelerDid,
@@ -0,0 +1,23 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .alterTable('moderation_subject_status')
6
+ .addColumn('lastAppealedAt', 'varchar')
7
+ .execute()
8
+ await db.schema
9
+ .alterTable('moderation_subject_status')
10
+ .addColumn('appealed', 'boolean')
11
+ .execute()
12
+ }
13
+
14
+ export async function down(db: Kysely<unknown>): Promise<void> {
15
+ await db.schema
16
+ .alterTable('moderation_subject_status')
17
+ .dropColumn('lastAppealedAt')
18
+ .execute()
19
+ await db.schema
20
+ .alterTable('moderation_subject_status')
21
+ .dropColumn('appealed')
22
+ .execute()
23
+ }
@@ -32,3 +32,4 @@ export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post'
32
32
  export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes'
33
33
  export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status'
34
34
  export * as _20231205T000257238Z from './20231205T000257238Z-remove-did-cache'
35
+ export * as _20231213T181744386Z from './20231213T181744386Z-moderation-subject-appeal'
@@ -20,6 +20,7 @@ export interface ModerationEvent {
20
20
  | 'com.atproto.admin.defs#modEventMute'
21
21
  | 'com.atproto.admin.defs#modEventReverseTakedown'
22
22
  | 'com.atproto.admin.defs#modEventEmail'
23
+ | 'com.atproto.admin.defs#modEventResolveAppeal'
23
24
  subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef'
24
25
  subjectDid: string
25
26
  subjectUri: string | null
@@ -47,9 +48,11 @@ export interface ModerationSubjectStatus {
47
48
  lastReviewedBy: string | null
48
49
  lastReviewedAt: string | null
49
50
  lastReportedAt: string | null
51
+ lastAppealedAt: string | null
50
52
  muteUntil: string | null
51
53
  suspendUntil: string | null
52
54
  takendown: boolean
55
+ appealed: boolean | null
53
56
  comment: string | null
54
57
  }
55
58
 
@@ -135,6 +135,7 @@ export const COM_ATPROTO_MODERATION = {
135
135
  DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',
136
136
  DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',
137
137
  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
138
+ DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
138
139
  }
139
140
  export const APP_BSKY_GRAPH = {
140
141
  DefsModlist: 'app.bsky.graph.defs#modlist',
@@ -102,6 +102,7 @@ export const schemaDict = {
102
102
  'lex:com.atproto.admin.defs#modEventAcknowledge',
103
103
  'lex:com.atproto.admin.defs#modEventEscalate',
104
104
  'lex:com.atproto.admin.defs#modEventMute',
105
+ 'lex:com.atproto.admin.defs#modEventResolveAppeal',
105
106
  ],
106
107
  },
107
108
  subject: {
@@ -237,9 +238,20 @@ export const schemaDict = {
237
238
  type: 'string',
238
239
  format: 'datetime',
239
240
  },
241
+ lastAppealedAt: {
242
+ type: 'string',
243
+ format: 'datetime',
244
+ description:
245
+ 'Timestamp referencing when the author of the subject appealed a moderation action',
246
+ },
240
247
  takendown: {
241
248
  type: 'boolean',
242
249
  },
250
+ appealed: {
251
+ type: 'boolean',
252
+ description:
253
+ 'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.',
254
+ },
243
255
  suspendUntil: {
244
256
  type: 'string',
245
257
  format: 'datetime',
@@ -717,6 +729,16 @@ export const schemaDict = {
717
729
  },
718
730
  },
719
731
  },
732
+ modEventResolveAppeal: {
733
+ type: 'object',
734
+ description: 'Resolve appeal on a subject',
735
+ properties: {
736
+ comment: {
737
+ type: 'string',
738
+ description: 'Describe resolution.',
739
+ },
740
+ },
741
+ },
720
742
  modEventComment: {
721
743
  type: 'object',
722
744
  description: 'Add a comment to a subject',
@@ -816,6 +838,10 @@ export const schemaDict = {
816
838
  type: 'string',
817
839
  description: 'The subject line of the email sent to the user.',
818
840
  },
841
+ comment: {
842
+ type: 'string',
843
+ description: 'Additional comment about the outgoing comm.',
844
+ },
819
845
  },
820
846
  },
821
847
  },
@@ -1357,6 +1383,10 @@ export const schemaDict = {
1357
1383
  type: 'boolean',
1358
1384
  description: 'Get subjects that were taken down',
1359
1385
  },
1386
+ appealed: {
1387
+ type: 'boolean',
1388
+ description: 'Get subjects in unresolved appealed status',
1389
+ },
1360
1390
  limit: {
1361
1391
  type: 'integer',
1362
1392
  minimum: 1,
@@ -1467,6 +1497,11 @@ export const schemaDict = {
1467
1497
  type: 'string',
1468
1498
  format: 'did',
1469
1499
  },
1500
+ comment: {
1501
+ type: 'string',
1502
+ description:
1503
+ "Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers",
1504
+ },
1470
1505
  },
1471
1506
  },
1472
1507
  },
@@ -1937,6 +1972,7 @@ export const schemaDict = {
1937
1972
  'com.atproto.moderation.defs#reasonSexual',
1938
1973
  'com.atproto.moderation.defs#reasonRude',
1939
1974
  'com.atproto.moderation.defs#reasonOther',
1975
+ 'com.atproto.moderation.defs#reasonAppeal',
1940
1976
  ],
1941
1977
  },
1942
1978
  reasonSpam: {
@@ -1964,6 +2000,10 @@ export const schemaDict = {
1964
2000
  type: 'token',
1965
2001
  description: 'Other: reports not falling under another report category',
1966
2002
  },
2003
+ reasonAppeal: {
2004
+ type: 'token',
2005
+ description: 'Appeal: appeal a previously taken moderation action',
2006
+ },
1967
2007
  },
1968
2008
  },
1969
2009
  ComAtprotoRepoApplyWrites: {
@@ -76,6 +76,7 @@ export interface ModEventViewDetail {
76
76
  | ModEventAcknowledge
77
77
  | ModEventEscalate
78
78
  | ModEventMute
79
+ | ModEventResolveAppeal
79
80
  | { $type: string; [k: string]: unknown }
80
81
  subject:
81
82
  | RepoView
@@ -147,7 +148,11 @@ export interface SubjectStatusView {
147
148
  lastReviewedBy?: string
148
149
  lastReviewedAt?: string
149
150
  lastReportedAt?: string
151
+ /** Timestamp referencing when the author of the subject appealed a moderation action */
152
+ lastAppealedAt?: string
150
153
  takendown?: boolean
154
+ /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */
155
+ appealed?: boolean
151
156
  suspendUntil?: string
152
157
  [k: string]: unknown
153
158
  }
@@ -538,6 +543,27 @@ export function validateModEventReverseTakedown(v: unknown): ValidationResult {
538
543
  return lexicons.validate('com.atproto.admin.defs#modEventReverseTakedown', v)
539
544
  }
540
545
 
546
+ /** Resolve appeal on a subject */
547
+ export interface ModEventResolveAppeal {
548
+ /** Describe resolution. */
549
+ comment?: string
550
+ [k: string]: unknown
551
+ }
552
+
553
+ export function isModEventResolveAppeal(
554
+ v: unknown,
555
+ ): v is ModEventResolveAppeal {
556
+ return (
557
+ isObj(v) &&
558
+ hasProp(v, '$type') &&
559
+ v.$type === 'com.atproto.admin.defs#modEventResolveAppeal'
560
+ )
561
+ }
562
+
563
+ export function validateModEventResolveAppeal(v: unknown): ValidationResult {
564
+ return lexicons.validate('com.atproto.admin.defs#modEventResolveAppeal', v)
565
+ }
566
+
541
567
  /** Add a comment to a subject */
542
568
  export interface ModEventComment {
543
569
  comment: string
@@ -674,6 +700,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult {
674
700
  export interface ModEventEmail {
675
701
  /** The subject line of the email sent to the user. */
676
702
  subjectLine: string
703
+ /** Additional comment about the outgoing comm. */
704
+ comment?: string
677
705
  [k: string]: unknown
678
706
  }
679
707