@atproto/bsky 0.0.20 → 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.
- package/CHANGELOG.md +14 -0
- package/dist/api/com/atproto/moderation/util.d.ts +1 -1
- package/dist/db/index.js +17 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20231213T181744386Z-moderation-subject-appeal.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/tables/moderation.d.ts +3 -1
- package/dist/index.js +213 -32
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +1 -0
- package/dist/lexicon/lexicons.d.ts +35 -0
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +10 -1
- package/dist/lexicon/types/com/atproto/admin/queryModerationStatuses.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/moderation/defs.d.ts +2 -1
- package/dist/services/feed/views.d.ts +1 -1
- package/dist/services/moderation/index.d.ts +7 -2
- package/package.json +5 -5
- package/src/api/app/bsky/feed/getPostThread.ts +4 -3
- package/src/api/app/bsky/feed/getTimeline.ts +54 -0
- package/src/api/com/atproto/admin/queryModerationStatuses.ts +2 -0
- package/src/api/com/atproto/moderation/createReport.ts +14 -3
- package/src/api/com/atproto/moderation/util.ts +2 -0
- package/src/api/health.ts +14 -0
- package/src/auto-moderator/index.ts +19 -0
- package/src/db/migrations/20231213T181744386Z-moderation-subject-appeal.ts +23 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/tables/moderation.ts +3 -0
- package/src/lexicon/index.ts +1 -0
- package/src/lexicon/lexicons.ts +40 -0
- package/src/lexicon/types/com/atproto/admin/defs.ts +28 -0
- package/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts +2 -0
- package/src/lexicon/types/com/atproto/admin/sendEmail.ts +2 -0
- package/src/lexicon/types/com/atproto/moderation/defs.ts +3 -0
- package/src/services/feed/views.ts +6 -3
- package/src/services/moderation/index.ts +9 -0
- package/src/services/moderation/status.ts +24 -0
- package/src/services/moderation/views.ts +2 -0
- package/tests/admin/moderation-appeals.test.ts +269 -0
- package/tests/auto-moderator/labeler.test.ts +39 -0
- package/tests/views/blocks.test.ts +14 -1
- package/tests/views/timeline.test.ts +14 -0
package/dist/lexicon/index.d.ts
CHANGED
|
@@ -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;
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
56
|
-
"@atproto/dev-env": "^0.2.
|
|
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.
|
|
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
|
|
131
|
+
const violatesBlock = (post && blocks[post.uri]?.reply) ?? false
|
|
132
|
+
const omitBadReply = !isAnchorPost && (badReply || violatesBlock)
|
|
132
133
|
|
|
133
|
-
if (!post ||
|
|
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:
|
|
39
|
+
reasonType: reportReasonType,
|
|
29
40
|
reason,
|
|
30
|
-
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
|
|
package/src/lexicon/index.ts
CHANGED
|
@@ -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',
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -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
|
|