@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
|
@@ -15,6 +15,8 @@ export interface InputSchema {
|
|
|
15
15
|
content: string
|
|
16
16
|
subject?: string
|
|
17
17
|
senderDid: string
|
|
18
|
+
/** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */
|
|
19
|
+
comment?: string
|
|
18
20
|
[k: string]: unknown
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -13,6 +13,7 @@ export type ReasonType =
|
|
|
13
13
|
| 'com.atproto.moderation.defs#reasonSexual'
|
|
14
14
|
| 'com.atproto.moderation.defs#reasonRude'
|
|
15
15
|
| 'com.atproto.moderation.defs#reasonOther'
|
|
16
|
+
| 'com.atproto.moderation.defs#reasonAppeal'
|
|
16
17
|
| (string & {})
|
|
17
18
|
|
|
18
19
|
/** Spam: frequent unwanted promotion, replies, mentions */
|
|
@@ -27,3 +28,5 @@ export const REASONSEXUAL = 'com.atproto.moderation.defs#reasonSexual'
|
|
|
27
28
|
export const REASONRUDE = 'com.atproto.moderation.defs#reasonRude'
|
|
28
29
|
/** Other: reports not falling under another report category */
|
|
29
30
|
export const REASONOTHER = 'com.atproto.moderation.defs#reasonOther'
|
|
31
|
+
/** Appeal: appeal a previously taken moderation action */
|
|
32
|
+
export const REASONAPPEAL = 'com.atproto.moderation.defs#reasonAppeal'
|
|
@@ -132,8 +132,8 @@ export class FeedViews {
|
|
|
132
132
|
lists,
|
|
133
133
|
viewer,
|
|
134
134
|
)
|
|
135
|
-
// skip over not found
|
|
136
|
-
if (!post
|
|
135
|
+
// skip over not found post
|
|
136
|
+
if (!post) {
|
|
137
137
|
continue
|
|
138
138
|
}
|
|
139
139
|
const feedPost = { post }
|
|
@@ -159,6 +159,7 @@ export class FeedViews {
|
|
|
159
159
|
) {
|
|
160
160
|
const replyParent = this.formatMaybePostView(
|
|
161
161
|
item.replyParent,
|
|
162
|
+
item.uri,
|
|
162
163
|
actors,
|
|
163
164
|
posts,
|
|
164
165
|
threadgates,
|
|
@@ -171,6 +172,7 @@ export class FeedViews {
|
|
|
171
172
|
)
|
|
172
173
|
const replyRoot = this.formatMaybePostView(
|
|
173
174
|
item.replyRoot,
|
|
175
|
+
item.uri,
|
|
174
176
|
actors,
|
|
175
177
|
posts,
|
|
176
178
|
threadgates,
|
|
@@ -291,6 +293,7 @@ export class FeedViews {
|
|
|
291
293
|
|
|
292
294
|
formatMaybePostView(
|
|
293
295
|
uri: string,
|
|
296
|
+
replyUri: string | null,
|
|
294
297
|
actors: ActorInfoMap,
|
|
295
298
|
posts: PostInfoMap,
|
|
296
299
|
threadgates: ThreadgateInfoMap,
|
|
@@ -320,7 +323,7 @@ export class FeedViews {
|
|
|
320
323
|
if (
|
|
321
324
|
post.author.viewer?.blockedBy ||
|
|
322
325
|
post.author.viewer?.blocking ||
|
|
323
|
-
blocks[
|
|
326
|
+
(replyUri !== null && blocks[replyUri]?.reply)
|
|
324
327
|
) {
|
|
325
328
|
if (!opts?.usePostViewUnion) return
|
|
326
329
|
return this.blockedPost(post)
|
|
@@ -539,6 +539,7 @@ export class ModerationService {
|
|
|
539
539
|
cursor,
|
|
540
540
|
limit = 50,
|
|
541
541
|
takendown,
|
|
542
|
+
appealed,
|
|
542
543
|
reviewState,
|
|
543
544
|
reviewedAfter,
|
|
544
545
|
reviewedBefore,
|
|
@@ -554,6 +555,7 @@ export class ModerationService {
|
|
|
554
555
|
cursor?: string
|
|
555
556
|
limit?: number
|
|
556
557
|
takendown?: boolean
|
|
558
|
+
appealed?: boolean | null
|
|
557
559
|
reviewedBefore?: string
|
|
558
560
|
reviewState?: ModerationSubjectStatusRow['reviewState']
|
|
559
561
|
reviewedAfter?: string
|
|
@@ -615,6 +617,13 @@ export class ModerationService {
|
|
|
615
617
|
builder = builder.where('takendown', '=', true)
|
|
616
618
|
}
|
|
617
619
|
|
|
620
|
+
if (appealed !== undefined) {
|
|
621
|
+
builder =
|
|
622
|
+
appealed === null
|
|
623
|
+
? builder.where('appealed', 'is', null)
|
|
624
|
+
: builder.where('appealed', '=', appealed)
|
|
625
|
+
}
|
|
626
|
+
|
|
618
627
|
if (!includeMuted) {
|
|
619
628
|
builder = builder.where((qb) =>
|
|
620
629
|
qb
|
|
@@ -12,6 +12,7 @@ import { ModerationEventRow, ModerationSubjectStatusRow } from './types'
|
|
|
12
12
|
import { HOUR } from '@atproto/common'
|
|
13
13
|
import { CID } from 'multiformats/cid'
|
|
14
14
|
import { sql } from 'kysely'
|
|
15
|
+
import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'
|
|
15
16
|
|
|
16
17
|
const getSubjectStatusForModerationEvent = ({
|
|
17
18
|
action,
|
|
@@ -82,6 +83,10 @@ const getSubjectStatusForModerationEvent = ({
|
|
|
82
83
|
lastReviewedBy: createdBy,
|
|
83
84
|
lastReviewedAt: createdAt,
|
|
84
85
|
}
|
|
86
|
+
case 'com.atproto.admin.defs#modEventResolveAppeal':
|
|
87
|
+
return {
|
|
88
|
+
appealed: false,
|
|
89
|
+
}
|
|
85
90
|
default:
|
|
86
91
|
return null
|
|
87
92
|
}
|
|
@@ -106,6 +111,10 @@ export const adjustModerationSubjectStatus = async (
|
|
|
106
111
|
createdAt,
|
|
107
112
|
} = moderationEvent
|
|
108
113
|
|
|
114
|
+
const isAppealEvent =
|
|
115
|
+
action === 'com.atproto.admin.defs#modEventReport' &&
|
|
116
|
+
meta?.reportType === REASONAPPEAL
|
|
117
|
+
|
|
109
118
|
const subjectStatus = getSubjectStatusForModerationEvent({
|
|
110
119
|
action,
|
|
111
120
|
createdBy,
|
|
@@ -162,6 +171,21 @@ export const adjustModerationSubjectStatus = async (
|
|
|
162
171
|
subjectStatus.takendown = false
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
if (isAppealEvent) {
|
|
175
|
+
newStatus.appealed = true
|
|
176
|
+
subjectStatus.appealed = true
|
|
177
|
+
newStatus.lastAppealedAt = createdAt
|
|
178
|
+
subjectStatus.lastAppealedAt = createdAt
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
action === 'com.atproto.admin.defs#modEventResolveAppeal' &&
|
|
183
|
+
subjectStatus.appealed
|
|
184
|
+
) {
|
|
185
|
+
newStatus.appealed = false
|
|
186
|
+
subjectStatus.appealed = false
|
|
187
|
+
}
|
|
188
|
+
|
|
165
189
|
if (action === 'com.atproto.admin.defs#modEventComment' && meta?.sticky) {
|
|
166
190
|
newStatus.comment = comment
|
|
167
191
|
subjectStatus.comment = comment
|
|
@@ -485,9 +485,11 @@ export class ModerationViews {
|
|
|
485
485
|
lastReviewedBy: subjectStatus.lastReviewedBy ?? undefined,
|
|
486
486
|
lastReviewedAt: subjectStatus.lastReviewedAt ?? undefined,
|
|
487
487
|
lastReportedAt: subjectStatus.lastReportedAt ?? undefined,
|
|
488
|
+
lastAppealedAt: subjectStatus.lastAppealedAt ?? undefined,
|
|
488
489
|
muteUntil: subjectStatus.muteUntil ?? undefined,
|
|
489
490
|
suspendUntil: subjectStatus.suspendUntil ?? undefined,
|
|
490
491
|
takendown: subjectStatus.takendown ?? undefined,
|
|
492
|
+
appealed: subjectStatus.appealed ?? undefined,
|
|
491
493
|
subjectRepoHandle: subjectStatus.handle ?? undefined,
|
|
492
494
|
subjectBlobCids: subjectStatus.blobCids || [],
|
|
493
495
|
subject: !subjectStatus.recordPath
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { TestNetwork, SeedClient } from '@atproto/dev-env'
|
|
2
|
+
import AtpAgent, {
|
|
3
|
+
ComAtprotoAdminDefs,
|
|
4
|
+
ComAtprotoAdminEmitModerationEvent,
|
|
5
|
+
ComAtprotoAdminQueryModerationStatuses,
|
|
6
|
+
} from '@atproto/api'
|
|
7
|
+
import basicSeed from '../seeds/basic'
|
|
8
|
+
import {
|
|
9
|
+
REASONMISLEADING,
|
|
10
|
+
REASONSPAM,
|
|
11
|
+
} from '../../src/lexicon/types/com/atproto/moderation/defs'
|
|
12
|
+
import {
|
|
13
|
+
REVIEWCLOSED,
|
|
14
|
+
REVIEWOPEN,
|
|
15
|
+
} from '@atproto/api/src/client/types/com/atproto/admin/defs'
|
|
16
|
+
import { REASONAPPEAL } from '@atproto/api/src/client/types/com/atproto/moderation/defs'
|
|
17
|
+
import { REVIEWESCALATED } from '../../src/lexicon/types/com/atproto/admin/defs'
|
|
18
|
+
|
|
19
|
+
describe('moderation-appeals', () => {
|
|
20
|
+
let network: TestNetwork
|
|
21
|
+
let agent: AtpAgent
|
|
22
|
+
let pdsAgent: AtpAgent
|
|
23
|
+
let sc: SeedClient
|
|
24
|
+
|
|
25
|
+
const emitModerationEvent = async (
|
|
26
|
+
eventData: ComAtprotoAdminEmitModerationEvent.InputSchema,
|
|
27
|
+
) => {
|
|
28
|
+
return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, {
|
|
29
|
+
encoding: 'application/json',
|
|
30
|
+
headers: network.bsky.adminAuthHeaders('moderator'),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const queryModerationStatuses = (
|
|
35
|
+
statusQuery: ComAtprotoAdminQueryModerationStatuses.QueryParams,
|
|
36
|
+
) =>
|
|
37
|
+
agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, {
|
|
38
|
+
headers: network.bsky.adminAuthHeaders('moderator'),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
network = await TestNetwork.create({
|
|
43
|
+
dbPostgresSchema: 'bsky_moderation_statuses',
|
|
44
|
+
})
|
|
45
|
+
agent = network.bsky.getClient()
|
|
46
|
+
pdsAgent = network.pds.getClient()
|
|
47
|
+
sc = network.getSeedClient()
|
|
48
|
+
await basicSeed(sc)
|
|
49
|
+
await network.processAll()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await network.close()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const assertSubjectStatus = async (
|
|
57
|
+
subject: string,
|
|
58
|
+
status: string,
|
|
59
|
+
appealed: boolean | undefined,
|
|
60
|
+
): Promise<ComAtprotoAdminDefs.SubjectStatusView | undefined> => {
|
|
61
|
+
const { data } = await queryModerationStatuses({
|
|
62
|
+
subject,
|
|
63
|
+
})
|
|
64
|
+
expect(data.subjectStatuses[0]?.reviewState).toEqual(status)
|
|
65
|
+
expect(data.subjectStatuses[0]?.appealed).toEqual(appealed)
|
|
66
|
+
return data.subjectStatuses[0]
|
|
67
|
+
}
|
|
68
|
+
describe('appeals from users', () => {
|
|
69
|
+
const getBobsPostSubject = () => ({
|
|
70
|
+
$type: 'com.atproto.repo.strongRef',
|
|
71
|
+
uri: sc.posts[sc.dids.bob][1].ref.uriStr,
|
|
72
|
+
cid: sc.posts[sc.dids.bob][1].ref.cidStr,
|
|
73
|
+
})
|
|
74
|
+
const getCarolPostSubject = () => ({
|
|
75
|
+
$type: 'com.atproto.repo.strongRef',
|
|
76
|
+
uri: sc.posts[sc.dids.carol][0].ref.uriStr,
|
|
77
|
+
cid: sc.posts[sc.dids.carol][0].ref.cidStr,
|
|
78
|
+
})
|
|
79
|
+
const assertBobsPostStatus = async (
|
|
80
|
+
status: string,
|
|
81
|
+
appealed: boolean | undefined,
|
|
82
|
+
) => assertSubjectStatus(getBobsPostSubject().uri, status, appealed)
|
|
83
|
+
|
|
84
|
+
it('only changes subject status if original author of the content or a moderator is appealing', async () => {
|
|
85
|
+
// Create a report by alice
|
|
86
|
+
await emitModerationEvent({
|
|
87
|
+
event: {
|
|
88
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
89
|
+
reportType: REASONMISLEADING,
|
|
90
|
+
},
|
|
91
|
+
subject: getBobsPostSubject(),
|
|
92
|
+
createdBy: sc.dids.alice,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await assertBobsPostStatus(REVIEWOPEN, undefined)
|
|
96
|
+
|
|
97
|
+
// Create a report as normal user with appeal type
|
|
98
|
+
expect(
|
|
99
|
+
sc.createReport({
|
|
100
|
+
reportedBy: sc.dids.carol,
|
|
101
|
+
reasonType: REASONAPPEAL,
|
|
102
|
+
reason: 'appealing',
|
|
103
|
+
subject: getBobsPostSubject(),
|
|
104
|
+
}),
|
|
105
|
+
).rejects.toThrow('You cannot appeal this report')
|
|
106
|
+
|
|
107
|
+
// Verify that the appeal status did not change
|
|
108
|
+
await assertBobsPostStatus(REVIEWOPEN, undefined)
|
|
109
|
+
|
|
110
|
+
// Emit report event as moderator
|
|
111
|
+
await emitModerationEvent({
|
|
112
|
+
event: {
|
|
113
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
114
|
+
reportType: REASONAPPEAL,
|
|
115
|
+
},
|
|
116
|
+
subject: getBobsPostSubject(),
|
|
117
|
+
createdBy: sc.dids.alice,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Verify that appeal status changed when appeal report was emitted by moderator
|
|
121
|
+
const status = await assertBobsPostStatus(REVIEWOPEN, true)
|
|
122
|
+
expect(status?.appealedAt).not.toBeNull()
|
|
123
|
+
|
|
124
|
+
// Create a report as normal user for carol's post
|
|
125
|
+
await sc.createReport({
|
|
126
|
+
reportedBy: sc.dids.alice,
|
|
127
|
+
reasonType: REASONMISLEADING,
|
|
128
|
+
reason: 'lies!',
|
|
129
|
+
subject: getCarolPostSubject(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Verify that the appeal status on carol's post is undefined
|
|
133
|
+
await assertSubjectStatus(
|
|
134
|
+
getCarolPostSubject().uri,
|
|
135
|
+
REVIEWOPEN,
|
|
136
|
+
undefined,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await sc.createReport({
|
|
140
|
+
reportedBy: sc.dids.carol,
|
|
141
|
+
reasonType: REASONAPPEAL,
|
|
142
|
+
reason: 'appealing',
|
|
143
|
+
subject: getCarolPostSubject(),
|
|
144
|
+
})
|
|
145
|
+
// Verify that the appeal status on carol's post is true
|
|
146
|
+
await assertSubjectStatus(getCarolPostSubject().uri, REVIEWOPEN, true)
|
|
147
|
+
})
|
|
148
|
+
it('allows multiple appeals and updates last appealed timestamp', async () => {
|
|
149
|
+
// Resolve appeal with acknowledge
|
|
150
|
+
await emitModerationEvent({
|
|
151
|
+
event: {
|
|
152
|
+
$type: 'com.atproto.admin.defs#modEventResolveAppeal',
|
|
153
|
+
},
|
|
154
|
+
subject: getBobsPostSubject(),
|
|
155
|
+
createdBy: sc.dids.carol,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const previousStatus = await assertBobsPostStatus(REVIEWOPEN, false)
|
|
159
|
+
|
|
160
|
+
await emitModerationEvent({
|
|
161
|
+
event: {
|
|
162
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
163
|
+
reportType: REASONAPPEAL,
|
|
164
|
+
},
|
|
165
|
+
subject: getBobsPostSubject(),
|
|
166
|
+
createdBy: sc.dids.bob,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Verify that even after the appeal event by bob for his post, the appeal status is true again with new timestamp
|
|
170
|
+
const newStatus = await assertBobsPostStatus(REVIEWOPEN, true)
|
|
171
|
+
expect(
|
|
172
|
+
new Date(`${previousStatus?.lastAppealedAt}`).getTime(),
|
|
173
|
+
).toBeLessThan(new Date(`${newStatus?.lastAppealedAt}`).getTime())
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('appeal resolution', () => {
|
|
178
|
+
const getAlicesPostSubject = () => ({
|
|
179
|
+
$type: 'com.atproto.repo.strongRef',
|
|
180
|
+
uri: sc.posts[sc.dids.alice][1].ref.uriStr,
|
|
181
|
+
cid: sc.posts[sc.dids.alice][1].ref.cidStr,
|
|
182
|
+
})
|
|
183
|
+
it('appeal status is maintained while review state changes based on incoming events', async () => {
|
|
184
|
+
// Bob reports alice's post
|
|
185
|
+
await emitModerationEvent({
|
|
186
|
+
event: {
|
|
187
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
188
|
+
reportType: REASONMISLEADING,
|
|
189
|
+
},
|
|
190
|
+
subject: getAlicesPostSubject(),
|
|
191
|
+
createdBy: sc.dids.bob,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Moderator acknowledges the report, assume a label was applied too
|
|
195
|
+
await emitModerationEvent({
|
|
196
|
+
event: {
|
|
197
|
+
$type: 'com.atproto.admin.defs#modEventAcknowledge',
|
|
198
|
+
},
|
|
199
|
+
subject: getAlicesPostSubject(),
|
|
200
|
+
createdBy: sc.dids.carol,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Alice appeals the report
|
|
204
|
+
await emitModerationEvent({
|
|
205
|
+
event: {
|
|
206
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
207
|
+
reportType: REASONAPPEAL,
|
|
208
|
+
},
|
|
209
|
+
subject: getAlicesPostSubject(),
|
|
210
|
+
createdBy: sc.dids.alice,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await assertSubjectStatus(getAlicesPostSubject().uri, REVIEWOPEN, true)
|
|
214
|
+
|
|
215
|
+
// Bob reports it again
|
|
216
|
+
await emitModerationEvent({
|
|
217
|
+
event: {
|
|
218
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
219
|
+
reportType: REASONSPAM,
|
|
220
|
+
},
|
|
221
|
+
subject: getAlicesPostSubject(),
|
|
222
|
+
createdBy: sc.dids.bob,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Assert that the status is still REVIEWOPEN, as report events are meant to do
|
|
226
|
+
await assertSubjectStatus(getAlicesPostSubject().uri, REVIEWOPEN, true)
|
|
227
|
+
|
|
228
|
+
// Emit an escalation event
|
|
229
|
+
await emitModerationEvent({
|
|
230
|
+
event: {
|
|
231
|
+
$type: 'com.atproto.admin.defs#modEventEscalate',
|
|
232
|
+
},
|
|
233
|
+
subject: getAlicesPostSubject(),
|
|
234
|
+
createdBy: sc.dids.carol,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await assertSubjectStatus(
|
|
238
|
+
getAlicesPostSubject().uri,
|
|
239
|
+
REVIEWESCALATED,
|
|
240
|
+
true,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Emit an acknowledge event
|
|
244
|
+
await emitModerationEvent({
|
|
245
|
+
event: {
|
|
246
|
+
$type: 'com.atproto.admin.defs#modEventAcknowledge',
|
|
247
|
+
},
|
|
248
|
+
subject: getAlicesPostSubject(),
|
|
249
|
+
createdBy: sc.dids.carol,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Assert that status moved on to reviewClosed while appealed status is still true
|
|
253
|
+
await assertSubjectStatus(getAlicesPostSubject().uri, REVIEWCLOSED, true)
|
|
254
|
+
|
|
255
|
+
// Emit a resolveAppeal event
|
|
256
|
+
await emitModerationEvent({
|
|
257
|
+
event: {
|
|
258
|
+
$type: 'com.atproto.admin.defs#modEventResolveAppeal',
|
|
259
|
+
comment: 'lgtm',
|
|
260
|
+
},
|
|
261
|
+
subject: getAlicesPostSubject(),
|
|
262
|
+
createdBy: sc.dids.carol,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Assert that status stayed the same while appealed status is still true
|
|
266
|
+
await assertSubjectStatus(getAlicesPostSubject().uri, REVIEWCLOSED, false)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
})
|
|
@@ -9,6 +9,8 @@ import { LabelService } from '../../src/services/label'
|
|
|
9
9
|
import usersSeed from '../seeds/users'
|
|
10
10
|
import { CID } from 'multiformats/cid'
|
|
11
11
|
import { ImgLabeler } from '../../src/auto-moderator/hive'
|
|
12
|
+
import { ModerationService } from '../../src/services/moderation'
|
|
13
|
+
import { ImageInvalidator } from '../../src/image/invalidator'
|
|
12
14
|
|
|
13
15
|
// outside of test suite so that TestLabeler can access them
|
|
14
16
|
let badCid1: CID | undefined = undefined
|
|
@@ -75,6 +77,10 @@ describe('labeler', () => {
|
|
|
75
77
|
})
|
|
76
78
|
|
|
77
79
|
it('labels text in posts', async () => {
|
|
80
|
+
autoMod.services.moderation = ModerationService.creator(
|
|
81
|
+
new NoopImageUriBuilder(''),
|
|
82
|
+
new NoopInvalidator(),
|
|
83
|
+
)
|
|
78
84
|
const post = {
|
|
79
85
|
$type: 'app.bsky.feed.post',
|
|
80
86
|
text: 'blah blah label_me',
|
|
@@ -93,6 +99,28 @@ describe('labeler', () => {
|
|
|
93
99
|
val: 'test-label',
|
|
94
100
|
neg: false,
|
|
95
101
|
})
|
|
102
|
+
|
|
103
|
+
// Verify that along with applying the labels, we are also leaving trace of the label as moderation event
|
|
104
|
+
// Temporarily assign an instance of moderation service to the autoMod so that we can validate label event
|
|
105
|
+
const modSrvc = autoMod.services.moderation(ctx.db)
|
|
106
|
+
const { events } = await modSrvc.getEvents({
|
|
107
|
+
includeAllUserRecords: false,
|
|
108
|
+
subject: uri.toString(),
|
|
109
|
+
limit: 10,
|
|
110
|
+
types: [],
|
|
111
|
+
})
|
|
112
|
+
expect(events.length).toBe(1)
|
|
113
|
+
expect(events[0]).toMatchObject({
|
|
114
|
+
action: 'com.atproto.admin.defs#modEventLabel',
|
|
115
|
+
subjectUri: uri.toString(),
|
|
116
|
+
createLabelVals: 'test-label',
|
|
117
|
+
negateLabelVals: null,
|
|
118
|
+
comment: `[AutoModerator]: Applying labels`,
|
|
119
|
+
createdBy: labelerDid,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Cleanup the temporary assignment, knowing that by default, moderation service is not available
|
|
123
|
+
autoMod.services.moderation = undefined
|
|
96
124
|
})
|
|
97
125
|
|
|
98
126
|
it('labels embeds in posts', async () => {
|
|
@@ -165,3 +193,14 @@ class TestImgLabeler implements ImgLabeler {
|
|
|
165
193
|
return []
|
|
166
194
|
}
|
|
167
195
|
}
|
|
196
|
+
|
|
197
|
+
class NoopInvalidator implements ImageInvalidator {
|
|
198
|
+
async invalidate() {}
|
|
199
|
+
}
|
|
200
|
+
class NoopImageUriBuilder {
|
|
201
|
+
constructor(public endpoint: string) {}
|
|
202
|
+
|
|
203
|
+
getPresetUri() {
|
|
204
|
+
return ''
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -112,6 +112,18 @@ describe('pds views with blocking', () => {
|
|
|
112
112
|
expect(forSnapshot(thread)).toMatchSnapshot()
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
+
it('loads blocked reply as anchor with no parent', async () => {
|
|
116
|
+
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
|
|
117
|
+
{ depth: 1, uri: carolReplyToDan.ref.uriStr },
|
|
118
|
+
{ headers: await network.serviceHeaders(alice) },
|
|
119
|
+
)
|
|
120
|
+
if (!isThreadViewPost(thread.thread)) {
|
|
121
|
+
throw new Error('Expected thread view post')
|
|
122
|
+
}
|
|
123
|
+
expect(thread.thread.post.uri).toEqual(carolReplyToDan.ref.uriStr)
|
|
124
|
+
expect(thread.thread.parent).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
115
127
|
it('blocks thread parent', async () => {
|
|
116
128
|
// Parent is a post by dan
|
|
117
129
|
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
|
|
@@ -498,7 +510,8 @@ describe('pds views with blocking', () => {
|
|
|
498
510
|
const replyBlockedPost = timeline.feed.find(
|
|
499
511
|
(item) => item.post.uri === replyBlockedUri,
|
|
500
512
|
)
|
|
501
|
-
|
|
513
|
+
assert(replyBlockedPost)
|
|
514
|
+
expect(replyBlockedPost.reply?.parent).toBeUndefined()
|
|
502
515
|
const embedBlockedPost = timeline.feed.find(
|
|
503
516
|
(item) => item.post.uri === embedBlockedUri,
|
|
504
517
|
)
|
|
@@ -154,6 +154,20 @@ describe('timeline views', () => {
|
|
|
154
154
|
expect(results(paginatedAll)).toEqual(results([full.data]))
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
+
it('agrees what the first item is for limit=1 and other limits', async () => {
|
|
158
|
+
const { data: timeline } = await agent.api.app.bsky.feed.getTimeline(
|
|
159
|
+
{ limit: 10 },
|
|
160
|
+
{ headers: await network.serviceHeaders(alice) },
|
|
161
|
+
)
|
|
162
|
+
const { data: timelineLimit1 } = await agent.api.app.bsky.feed.getTimeline(
|
|
163
|
+
{ limit: 1 },
|
|
164
|
+
{ headers: await network.serviceHeaders(alice) },
|
|
165
|
+
)
|
|
166
|
+
expect(timeline.feed.length).toBeGreaterThan(1)
|
|
167
|
+
expect(timelineLimit1.feed.length).toEqual(1)
|
|
168
|
+
expect(timelineLimit1.feed[0].post.uri).toBe(timeline.feed[0].post.uri)
|
|
169
|
+
})
|
|
170
|
+
|
|
157
171
|
it('reflects self-labels', async () => {
|
|
158
172
|
const carolTL = await agent.api.app.bsky.feed.getTimeline(
|
|
159
173
|
{},
|