@atproto/ozone 0.0.12 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/api/label/queryLabels.d.ts +3 -0
- package/dist/api/label/subscribeLabels.d.ts +3 -0
- package/dist/config/config.d.ts +3 -0
- package/dist/config/env.d.ts +3 -0
- package/dist/context.d.ts +3 -0
- package/dist/db/index.js +3 -1
- package/dist/db/index.js.map +2 -2
- package/dist/db/schema/label.d.ts +4 -0
- package/dist/index.js +593 -244
- package/dist/index.js.map +3 -3
- package/dist/logger.d.ts +1 -0
- package/dist/mod-service/util.d.ts +3 -0
- package/dist/sequencer/index.d.ts +2 -0
- package/dist/sequencer/outbox.d.ts +16 -0
- package/dist/sequencer/sequencer.d.ts +33 -0
- package/package.json +10 -10
- package/src/api/admin/emitModerationEvent.ts +16 -10
- package/src/api/index.ts +4 -0
- package/src/api/label/queryLabels.ts +58 -0
- package/src/api/label/subscribeLabels.ts +25 -0
- package/src/api/temp/fetchLabels.ts +2 -4
- package/src/config/config.ts +6 -0
- package/src/config/env.ts +6 -0
- package/src/context.ts +12 -0
- package/src/db/migrations/20231219T205730722Z-init.ts +7 -1
- package/src/db/schema/label.ts +7 -0
- package/src/index.ts +2 -0
- package/src/lexicon/lexicons.ts +1 -1
- package/src/logger.ts +2 -0
- package/src/mod-service/index.ts +73 -72
- package/src/mod-service/status.ts +3 -0
- package/src/mod-service/util.ts +17 -0
- package/src/mod-service/views.ts +2 -5
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/outbox.ts +122 -0
- package/src/sequencer/sequencer.ts +143 -0
- package/tests/__snapshots__/moderation-events.test.ts.snap +53 -75
- package/tests/__snapshots__/moderation.test.ts.snap +4 -4
- package/tests/moderation-appeals.test.ts +19 -7
- package/tests/moderation-events.test.ts +7 -7
- package/tests/moderation-statuses.test.ts +2 -2
- package/tests/moderation.test.ts +14 -13
- package/tests/query-labels.test.ts +163 -0
- package/tests/repo-search.test.ts +0 -1
- package/tests/sequencer.test.ts +222 -0
- package/tests/server.test.ts +2 -0
package/src/mod-service/views.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './types'
|
|
25
25
|
import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs'
|
|
26
26
|
import { subjectFromEventRow, subjectFromStatusRow } from './subject'
|
|
27
|
+
import { formatLabel } from './util'
|
|
27
28
|
|
|
28
29
|
export type AppviewAuth = () => Promise<
|
|
29
30
|
| {
|
|
@@ -403,11 +404,7 @@ export class ModerationViews {
|
|
|
403
404
|
.if(!includeNeg, (qb) => qb.where('neg', '=', false))
|
|
404
405
|
.selectAll()
|
|
405
406
|
.execute()
|
|
406
|
-
return res.map((l) => (
|
|
407
|
-
...l,
|
|
408
|
-
cid: l.cid === '' ? undefined : l.cid,
|
|
409
|
-
neg: l.neg,
|
|
410
|
-
}))
|
|
407
|
+
return res.map((l) => formatLabel(l))
|
|
411
408
|
}
|
|
412
409
|
|
|
413
410
|
async getSubjectStatus(
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AsyncBuffer, AsyncBufferFullError } from '@atproto/common'
|
|
2
|
+
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
|
+
import { Sequencer, LabelsEvt } from './sequencer'
|
|
4
|
+
|
|
5
|
+
export type OutboxOpts = {
|
|
6
|
+
maxBufferSize: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Outbox {
|
|
10
|
+
private caughtUp = false
|
|
11
|
+
lastSeen = -1
|
|
12
|
+
|
|
13
|
+
cutoverBuffer: LabelsEvt[]
|
|
14
|
+
outBuffer: AsyncBuffer<LabelsEvt>
|
|
15
|
+
|
|
16
|
+
constructor(public sequencer: Sequencer, opts: Partial<OutboxOpts> = {}) {
|
|
17
|
+
const { maxBufferSize = 500 } = opts
|
|
18
|
+
this.cutoverBuffer = []
|
|
19
|
+
this.outBuffer = new AsyncBuffer<LabelsEvt>(maxBufferSize)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// event stream occurs in 3 phases
|
|
23
|
+
// 1. backfill events: events that have been added to the DB since the last time a connection was open.
|
|
24
|
+
// The outbox is not yet listening for new events from the sequencer
|
|
25
|
+
// 2. cutover: the outbox has caught up with where the sequencer purports to be,
|
|
26
|
+
// but the sequencer might already be halfway through sending out a round of updates.
|
|
27
|
+
// Therefore, we start accepting the sequencer's events in a buffer, while making our own request to the
|
|
28
|
+
// database to ensure we're caught up. We then dedupe the query & the buffer & stream the events in order
|
|
29
|
+
// 3. streaming: we're all caught up on historic state, so the sequencer outputs events and we
|
|
30
|
+
// immediately yield them
|
|
31
|
+
async *events(
|
|
32
|
+
backfillCursor?: number,
|
|
33
|
+
signal?: AbortSignal,
|
|
34
|
+
): AsyncGenerator<LabelsEvt> {
|
|
35
|
+
// catch up as much as we can
|
|
36
|
+
if (backfillCursor !== undefined) {
|
|
37
|
+
for await (const evt of this.getBackfill(backfillCursor)) {
|
|
38
|
+
if (signal?.aborted) return
|
|
39
|
+
this.lastSeen = evt.seq
|
|
40
|
+
yield evt
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// if not backfill, we don't need to cutover, just start streaming
|
|
44
|
+
this.caughtUp = true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// streams updates from sequencer, but buffers them for cutover as it makes a last request
|
|
48
|
+
|
|
49
|
+
const addToBuffer = (evts) => {
|
|
50
|
+
if (this.caughtUp) {
|
|
51
|
+
this.outBuffer.pushMany(evts)
|
|
52
|
+
} else {
|
|
53
|
+
this.cutoverBuffer = [...this.cutoverBuffer, ...evts]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!signal?.aborted) {
|
|
58
|
+
this.sequencer.on('events', addToBuffer)
|
|
59
|
+
}
|
|
60
|
+
signal?.addEventListener('abort', () =>
|
|
61
|
+
this.sequencer.off('events', addToBuffer),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const cutover = async () => {
|
|
65
|
+
// only need to perform cutover if we've been backfilling
|
|
66
|
+
if (backfillCursor !== undefined) {
|
|
67
|
+
const cutoverEvts = await this.sequencer.requestLabelRange({
|
|
68
|
+
earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,
|
|
69
|
+
})
|
|
70
|
+
this.outBuffer.pushMany(cutoverEvts)
|
|
71
|
+
// dont worry about dupes, we ensure order on yield
|
|
72
|
+
this.outBuffer.pushMany(this.cutoverBuffer)
|
|
73
|
+
this.caughtUp = true
|
|
74
|
+
this.cutoverBuffer = []
|
|
75
|
+
} else {
|
|
76
|
+
this.caughtUp = true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
cutover()
|
|
80
|
+
|
|
81
|
+
while (true) {
|
|
82
|
+
try {
|
|
83
|
+
for await (const evt of this.outBuffer.events()) {
|
|
84
|
+
if (signal?.aborted) return
|
|
85
|
+
if (evt.seq > this.lastSeen) {
|
|
86
|
+
this.lastSeen = evt.seq
|
|
87
|
+
yield evt
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof AsyncBufferFullError) {
|
|
92
|
+
throw new InvalidRequestError(
|
|
93
|
+
'Stream consumer too slow',
|
|
94
|
+
'ConsumerTooSlow',
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// yields only historical events
|
|
104
|
+
async *getBackfill(backfillCursor: number) {
|
|
105
|
+
const PAGE_SIZE = 500
|
|
106
|
+
while (true) {
|
|
107
|
+
const evts = await this.sequencer.requestLabelRange({
|
|
108
|
+
earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,
|
|
109
|
+
limit: PAGE_SIZE,
|
|
110
|
+
})
|
|
111
|
+
for (const evt of evts) {
|
|
112
|
+
yield evt
|
|
113
|
+
}
|
|
114
|
+
// if we're within half a pagesize of the sequencer, we call it good & switch to cutover
|
|
115
|
+
const seqCursor = this.sequencer.lastSeen ?? -1
|
|
116
|
+
if (seqCursor - this.lastSeen < PAGE_SIZE / 2) break
|
|
117
|
+
if (evts.length < 1) break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default Outbox
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import EventEmitter from 'events'
|
|
2
|
+
import TypedEmitter from 'typed-emitter'
|
|
3
|
+
import { seqLogger as log } from '../logger'
|
|
4
|
+
import Database from '../db'
|
|
5
|
+
import { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'
|
|
6
|
+
import { LabelChannel, Label as LabelTable } from '../db/schema/label'
|
|
7
|
+
import { Selectable } from 'kysely'
|
|
8
|
+
import { formatLabel } from '../mod-service/util'
|
|
9
|
+
import { PoolClient } from 'pg'
|
|
10
|
+
|
|
11
|
+
export type { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'
|
|
12
|
+
type LabelRow = Selectable<LabelTable>
|
|
13
|
+
|
|
14
|
+
export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
|
|
15
|
+
destroyed = false
|
|
16
|
+
pollPromise: Promise<void> | undefined
|
|
17
|
+
queued = false
|
|
18
|
+
conn: PoolClient | undefined
|
|
19
|
+
|
|
20
|
+
constructor(public db: Database, public lastSeen = 0) {
|
|
21
|
+
super()
|
|
22
|
+
// note: this does not err when surpassed, just prints a warning to stderr
|
|
23
|
+
this.setMaxListeners(100)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async start() {
|
|
27
|
+
const curr = await this.curr()
|
|
28
|
+
this.lastSeen = curr ?? 0
|
|
29
|
+
this.poll()
|
|
30
|
+
this.conn = await this.db.pool.connect()
|
|
31
|
+
this.conn.query(`listen ${LabelChannel}`) // if this errors, unhandled rejection should cause process to exit
|
|
32
|
+
this.conn.on('notification', (notif) => {
|
|
33
|
+
if (notif.channel === LabelChannel) {
|
|
34
|
+
this.poll()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async destroy() {
|
|
40
|
+
if (this.destroyed) return
|
|
41
|
+
this.destroyed = true
|
|
42
|
+
if (this.conn) {
|
|
43
|
+
this.conn.release()
|
|
44
|
+
this.conn = undefined
|
|
45
|
+
}
|
|
46
|
+
if (this.pollPromise) {
|
|
47
|
+
await this.pollPromise
|
|
48
|
+
}
|
|
49
|
+
this.emit('close')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async curr(): Promise<number | null> {
|
|
53
|
+
const got = await this.db.db
|
|
54
|
+
.selectFrom('label')
|
|
55
|
+
.selectAll()
|
|
56
|
+
.orderBy('id', 'desc')
|
|
57
|
+
.limit(1)
|
|
58
|
+
.executeTakeFirst()
|
|
59
|
+
return got?.id ?? null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async next(cursor: number): Promise<LabelRow | null> {
|
|
63
|
+
const got = await this.db.db
|
|
64
|
+
.selectFrom('label')
|
|
65
|
+
.selectAll()
|
|
66
|
+
.where('id', '>', cursor)
|
|
67
|
+
.limit(1)
|
|
68
|
+
.orderBy('id', 'asc')
|
|
69
|
+
.executeTakeFirst()
|
|
70
|
+
return got || null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async requestLabelRange(opts: {
|
|
74
|
+
earliestId?: number
|
|
75
|
+
limit?: number
|
|
76
|
+
}): Promise<LabelsEvt[]> {
|
|
77
|
+
const { earliestId, limit } = opts
|
|
78
|
+
|
|
79
|
+
let seqQb = this.db.db.selectFrom('label').selectAll().orderBy('id', 'asc')
|
|
80
|
+
if (earliestId !== undefined) {
|
|
81
|
+
seqQb = seqQb.where('id', '>', earliestId)
|
|
82
|
+
}
|
|
83
|
+
if (limit !== undefined) {
|
|
84
|
+
seqQb = seqQb.limit(limit)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rows = await seqQb.execute()
|
|
88
|
+
if (rows.length < 1) {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const evts: LabelsEvt[] = []
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
evts.push({
|
|
95
|
+
seq: row.id,
|
|
96
|
+
labels: [formatLabel(row)],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return evts
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private poll() {
|
|
104
|
+
if (this.destroyed) return
|
|
105
|
+
if (this.pollPromise) {
|
|
106
|
+
this.queued = true
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.queued = false
|
|
110
|
+
this.pollPromise = this.requestLabelRange({
|
|
111
|
+
earliestId: this.lastSeen,
|
|
112
|
+
limit: 500,
|
|
113
|
+
})
|
|
114
|
+
.then((evts) => {
|
|
115
|
+
this.emit('events', evts)
|
|
116
|
+
this.lastSeen = evts.at(-1)?.seq ?? this.lastSeen
|
|
117
|
+
if (evts.length > 0) {
|
|
118
|
+
this.queued = true
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.catch((err) => {
|
|
122
|
+
log.error(
|
|
123
|
+
{ err, lastSeen: this.lastSeen },
|
|
124
|
+
'sequencer failed to poll db',
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
.finally(() => {
|
|
128
|
+
this.pollPromise = undefined
|
|
129
|
+
if (this.queued) {
|
|
130
|
+
this.poll()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type SequencerEvents = {
|
|
137
|
+
events: (evts: LabelsEvt[]) => void
|
|
138
|
+
close: () => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type SequencerEmitter = TypedEmitter<SequencerEvents>
|
|
142
|
+
|
|
143
|
+
export default Sequencer
|
|
@@ -5,85 +5,63 @@ Object {
|
|
|
5
5
|
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
6
6
|
"createdBy": "user(2)",
|
|
7
7
|
"event": Object {
|
|
8
|
-
"$type": "com.atproto.admin.defs#
|
|
9
|
-
"comment": "
|
|
10
|
-
"
|
|
11
|
-
"test-label",
|
|
12
|
-
],
|
|
13
|
-
"negateLabelVals": Array [],
|
|
8
|
+
"$type": "com.atproto.admin.defs#modEventReport",
|
|
9
|
+
"comment": "X",
|
|
10
|
+
"reportType": "com.atproto.moderation.defs#reasonMisleading",
|
|
14
11
|
},
|
|
15
12
|
"id": 1,
|
|
16
13
|
"subject": Object {
|
|
17
|
-
"$type": "com.atproto.admin.defs#
|
|
18
|
-
"
|
|
19
|
-
"
|
|
14
|
+
"$type": "com.atproto.admin.defs#repoView",
|
|
15
|
+
"did": "user(0)",
|
|
16
|
+
"handle": "alice.test",
|
|
20
17
|
"indexedAt": "1970-01-01T00:00:00.000Z",
|
|
21
|
-
"moderation": Object {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"lastReviewedBy": "user(1)",
|
|
33
|
-
"reviewState": "com.atproto.admin.defs#reviewEscalated",
|
|
34
|
-
"subject": Object {
|
|
35
|
-
"$type": "com.atproto.admin.defs#repoRef",
|
|
36
|
-
"did": "user(0)",
|
|
37
|
-
},
|
|
38
|
-
"subjectBlobCids": Array [],
|
|
39
|
-
"subjectRepoHandle": "alice.test",
|
|
40
|
-
"tags": Array [
|
|
41
|
-
"lang:und",
|
|
42
|
-
],
|
|
43
|
-
"takendown": false,
|
|
44
|
-
"updatedAt": "1970-01-01T00:00:00.000Z",
|
|
18
|
+
"moderation": Object {
|
|
19
|
+
"subjectStatus": Object {
|
|
20
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
21
|
+
"id": 1,
|
|
22
|
+
"lastReportedAt": "1970-01-01T00:00:00.000Z",
|
|
23
|
+
"lastReviewedAt": "1970-01-01T00:00:00.000Z",
|
|
24
|
+
"lastReviewedBy": "user(1)",
|
|
25
|
+
"reviewState": "com.atproto.admin.defs#reviewEscalated",
|
|
26
|
+
"subject": Object {
|
|
27
|
+
"$type": "com.atproto.admin.defs#repoRef",
|
|
28
|
+
"did": "user(0)",
|
|
45
29
|
},
|
|
30
|
+
"subjectBlobCids": Array [],
|
|
31
|
+
"subjectRepoHandle": "alice.test",
|
|
32
|
+
"tags": Array [
|
|
33
|
+
"lang:und",
|
|
34
|
+
],
|
|
35
|
+
"takendown": false,
|
|
36
|
+
"updatedAt": "1970-01-01T00:00:00.000Z",
|
|
46
37
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"size": 3976,
|
|
57
|
-
},
|
|
58
|
-
"description": "its me!",
|
|
59
|
-
"displayName": "ali",
|
|
60
|
-
"labels": Object {
|
|
61
|
-
"$type": "com.atproto.label.defs#selfLabels",
|
|
62
|
-
"values": Array [
|
|
63
|
-
Object {
|
|
64
|
-
"val": "self-label-a",
|
|
65
|
-
},
|
|
66
|
-
Object {
|
|
67
|
-
"val": "self-label-b",
|
|
68
|
-
},
|
|
69
|
-
],
|
|
38
|
+
},
|
|
39
|
+
"relatedRecords": Array [
|
|
40
|
+
Object {
|
|
41
|
+
"$type": "app.bsky.actor.profile",
|
|
42
|
+
"avatar": Object {
|
|
43
|
+
"$type": "blob",
|
|
44
|
+
"mimeType": "image/jpeg",
|
|
45
|
+
"ref": Object {
|
|
46
|
+
"$link": "cids(0)",
|
|
70
47
|
},
|
|
48
|
+
"size": 3976,
|
|
71
49
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
50
|
+
"description": "its me!",
|
|
51
|
+
"displayName": "ali",
|
|
52
|
+
"labels": Object {
|
|
53
|
+
"$type": "com.atproto.label.defs#selfLabels",
|
|
54
|
+
"values": Array [
|
|
55
|
+
Object {
|
|
56
|
+
"val": "self-label-a",
|
|
57
|
+
},
|
|
58
|
+
Object {
|
|
59
|
+
"val": "self-label-b",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
83
62
|
},
|
|
84
63
|
},
|
|
85
|
-
|
|
86
|
-
},
|
|
64
|
+
],
|
|
87
65
|
},
|
|
88
66
|
"subjectBlobCids": Array [],
|
|
89
67
|
"subjectBlobs": Array [],
|
|
@@ -101,7 +79,7 @@ Array [
|
|
|
101
79
|
"comment": "X",
|
|
102
80
|
"reportType": "com.atproto.moderation.defs#reasonSpam",
|
|
103
81
|
},
|
|
104
|
-
"id":
|
|
82
|
+
"id": 11,
|
|
105
83
|
"subject": Object {
|
|
106
84
|
"$type": "com.atproto.admin.defs#repoRef",
|
|
107
85
|
"did": "user(0)",
|
|
@@ -120,7 +98,7 @@ Array [
|
|
|
120
98
|
],
|
|
121
99
|
"remove": Array [],
|
|
122
100
|
},
|
|
123
|
-
"id":
|
|
101
|
+
"id": 6,
|
|
124
102
|
"subject": Object {
|
|
125
103
|
"$type": "com.atproto.admin.defs#repoRef",
|
|
126
104
|
"did": "user(0)",
|
|
@@ -137,7 +115,7 @@ Array [
|
|
|
137
115
|
"comment": "X",
|
|
138
116
|
"reportType": "com.atproto.moderation.defs#reasonSpam",
|
|
139
117
|
},
|
|
140
|
-
"id":
|
|
118
|
+
"id": 5,
|
|
141
119
|
"subject": Object {
|
|
142
120
|
"$type": "com.atproto.admin.defs#repoRef",
|
|
143
121
|
"did": "user(0)",
|
|
@@ -159,7 +137,7 @@ Array [
|
|
|
159
137
|
"comment": "X",
|
|
160
138
|
"reportType": "com.atproto.moderation.defs#reasonSpam",
|
|
161
139
|
},
|
|
162
|
-
"id":
|
|
140
|
+
"id": 10,
|
|
163
141
|
"subject": Object {
|
|
164
142
|
"$type": "com.atproto.repo.strongRef",
|
|
165
143
|
"cid": "cids(0)",
|
|
@@ -178,7 +156,7 @@ Array [
|
|
|
178
156
|
],
|
|
179
157
|
"remove": Array [],
|
|
180
158
|
},
|
|
181
|
-
"id":
|
|
159
|
+
"id": 4,
|
|
182
160
|
"subject": Object {
|
|
183
161
|
"$type": "com.atproto.repo.strongRef",
|
|
184
162
|
"cid": "cids(0)",
|
|
@@ -196,7 +174,7 @@ Array [
|
|
|
196
174
|
"comment": "X",
|
|
197
175
|
"reportType": "com.atproto.moderation.defs#reasonSpam",
|
|
198
176
|
},
|
|
199
|
-
"id":
|
|
177
|
+
"id": 3,
|
|
200
178
|
"subject": Object {
|
|
201
179
|
"$type": "com.atproto.repo.strongRef",
|
|
202
180
|
"cid": "cids(0)",
|
|
@@ -4,7 +4,7 @@ exports[`moderation reporting creates reports of a record. 1`] = `
|
|
|
4
4
|
Array [
|
|
5
5
|
Object {
|
|
6
6
|
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
7
|
-
"id":
|
|
7
|
+
"id": 5,
|
|
8
8
|
"reasonType": "com.atproto.moderation.defs#reasonSpam",
|
|
9
9
|
"reportedBy": "user(0)",
|
|
10
10
|
"subject": Object {
|
|
@@ -15,7 +15,7 @@ Array [
|
|
|
15
15
|
},
|
|
16
16
|
Object {
|
|
17
17
|
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
18
|
-
"id":
|
|
18
|
+
"id": 7,
|
|
19
19
|
"reason": "defamation",
|
|
20
20
|
"reasonType": "com.atproto.moderation.defs#reasonOther",
|
|
21
21
|
"reportedBy": "user(1)",
|
|
@@ -32,7 +32,7 @@ exports[`moderation reporting creates reports of a repo. 1`] = `
|
|
|
32
32
|
Array [
|
|
33
33
|
Object {
|
|
34
34
|
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
35
|
-
"id":
|
|
35
|
+
"id": 1,
|
|
36
36
|
"reasonType": "com.atproto.moderation.defs#reasonSpam",
|
|
37
37
|
"reportedBy": "user(0)",
|
|
38
38
|
"subject": Object {
|
|
@@ -42,7 +42,7 @@ Array [
|
|
|
42
42
|
},
|
|
43
43
|
Object {
|
|
44
44
|
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
45
|
-
"id":
|
|
45
|
+
"id": 3,
|
|
46
46
|
"reason": "impersonation",
|
|
47
47
|
"reasonType": "com.atproto.moderation.defs#reasonOther",
|
|
48
48
|
"reportedBy": "user(2)",
|
|
@@ -118,7 +118,7 @@ describe('moderation-appeals', () => {
|
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
// Verify that appeal status changed when appeal report was emitted by moderator
|
|
121
|
-
const status = await assertBobsPostStatus(
|
|
121
|
+
const status = await assertBobsPostStatus(REVIEWESCALATED, true)
|
|
122
122
|
expect(status?.appealedAt).not.toBeNull()
|
|
123
123
|
|
|
124
124
|
// Create a report as normal user for carol's post
|
|
@@ -143,7 +143,11 @@ describe('moderation-appeals', () => {
|
|
|
143
143
|
subject: getCarolPostSubject(),
|
|
144
144
|
})
|
|
145
145
|
// Verify that the appeal status on carol's post is true
|
|
146
|
-
await assertSubjectStatus(
|
|
146
|
+
await assertSubjectStatus(
|
|
147
|
+
getCarolPostSubject().uri,
|
|
148
|
+
REVIEWESCALATED,
|
|
149
|
+
true,
|
|
150
|
+
)
|
|
147
151
|
})
|
|
148
152
|
it('allows multiple appeals and updates last appealed timestamp', async () => {
|
|
149
153
|
// Resolve appeal with acknowledge
|
|
@@ -155,7 +159,7 @@ describe('moderation-appeals', () => {
|
|
|
155
159
|
createdBy: sc.dids.carol,
|
|
156
160
|
})
|
|
157
161
|
|
|
158
|
-
const previousStatus = await assertBobsPostStatus(
|
|
162
|
+
const previousStatus = await assertBobsPostStatus(REVIEWESCALATED, false)
|
|
159
163
|
|
|
160
164
|
await emitModerationEvent({
|
|
161
165
|
event: {
|
|
@@ -167,7 +171,7 @@ describe('moderation-appeals', () => {
|
|
|
167
171
|
})
|
|
168
172
|
|
|
169
173
|
// 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(
|
|
174
|
+
const newStatus = await assertBobsPostStatus(REVIEWESCALATED, true)
|
|
171
175
|
expect(
|
|
172
176
|
new Date(`${previousStatus?.lastAppealedAt}`).getTime(),
|
|
173
177
|
).toBeLessThan(new Date(`${newStatus?.lastAppealedAt}`).getTime())
|
|
@@ -210,7 +214,11 @@ describe('moderation-appeals', () => {
|
|
|
210
214
|
createdBy: sc.dids.alice,
|
|
211
215
|
})
|
|
212
216
|
|
|
213
|
-
await assertSubjectStatus(
|
|
217
|
+
await assertSubjectStatus(
|
|
218
|
+
getAlicesPostSubject().uri,
|
|
219
|
+
REVIEWESCALATED,
|
|
220
|
+
true,
|
|
221
|
+
)
|
|
214
222
|
|
|
215
223
|
// Bob reports it again
|
|
216
224
|
await emitModerationEvent({
|
|
@@ -222,8 +230,12 @@ describe('moderation-appeals', () => {
|
|
|
222
230
|
createdBy: sc.dids.bob,
|
|
223
231
|
})
|
|
224
232
|
|
|
225
|
-
// Assert that the status is still
|
|
226
|
-
await assertSubjectStatus(
|
|
233
|
+
// Assert that the status is still REVIEWESCALATED, as report events are meant to do
|
|
234
|
+
await assertSubjectStatus(
|
|
235
|
+
getAlicesPostSubject().uri,
|
|
236
|
+
REVIEWESCALATED,
|
|
237
|
+
true,
|
|
238
|
+
)
|
|
227
239
|
|
|
228
240
|
// Emit an escalation event
|
|
229
241
|
await emitModerationEvent({
|
|
@@ -22,13 +22,13 @@ describe('moderation-events', () => {
|
|
|
22
22
|
) => {
|
|
23
23
|
return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, {
|
|
24
24
|
encoding: 'application/json',
|
|
25
|
-
headers: network.
|
|
25
|
+
headers: network.ozone.adminAuthHeaders('moderator'),
|
|
26
26
|
})
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const queryModerationEvents = (eventQuery) =>
|
|
30
30
|
agent.api.com.atproto.admin.queryModerationEvents(eventQuery, {
|
|
31
|
-
headers: network.
|
|
31
|
+
headers: network.ozone.adminAuthHeaders('moderator'),
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
const seedEvents = async () => {
|
|
@@ -203,11 +203,11 @@ describe('moderation-events', () => {
|
|
|
203
203
|
const defaultEvents = await getPaginatedEvents()
|
|
204
204
|
const reversedEvents = await getPaginatedEvents('asc')
|
|
205
205
|
|
|
206
|
-
expect(allEvents.data.events.length).toEqual(
|
|
206
|
+
expect(allEvents.data.events.length).toEqual(6)
|
|
207
207
|
expect(defaultEvents.length).toEqual(allEvents.data.events.length)
|
|
208
208
|
expect(reversedEvents.length).toEqual(allEvents.data.events.length)
|
|
209
209
|
// First event in the reversed list is the last item in the default list
|
|
210
|
-
expect(reversedEvents[0].id).toEqual(defaultEvents[
|
|
210
|
+
expect(reversedEvents[0].id).toEqual(defaultEvents[5].id)
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
it('returns report events matching reportType filters', async () => {
|
|
@@ -240,7 +240,7 @@ describe('moderation-events', () => {
|
|
|
240
240
|
|
|
241
241
|
expect(eventsWithX.data.events.length).toEqual(10)
|
|
242
242
|
expect(eventsWithTest.data.events.length).toEqual(0)
|
|
243
|
-
expect(eventsWithComment.data.events.length).toEqual(
|
|
243
|
+
expect(eventsWithComment.data.events.length).toEqual(10)
|
|
244
244
|
})
|
|
245
245
|
|
|
246
246
|
it('returns events matching filter params for labels', async () => {
|
|
@@ -325,7 +325,7 @@ describe('moderation-events', () => {
|
|
|
325
325
|
})
|
|
326
326
|
const addEvent = await tagEvent({ add: ['L1', 'L2'], remove: [] })
|
|
327
327
|
const addAndRemoveEvent = await tagEvent({ add: ['L3'], remove: ['L2'] })
|
|
328
|
-
const [addFinder, addAndRemoveFinder,
|
|
328
|
+
const [addFinder, addAndRemoveFinder, _removeFinder] = await Promise.all([
|
|
329
329
|
queryModerationEvents({
|
|
330
330
|
addedTags: ['L1'],
|
|
331
331
|
}),
|
|
@@ -356,7 +356,7 @@ describe('moderation-events', () => {
|
|
|
356
356
|
it('gets an event by specific id', async () => {
|
|
357
357
|
const { data } = await pdsAgent.api.com.atproto.admin.getModerationEvent(
|
|
358
358
|
{ id: 1 },
|
|
359
|
-
{ headers: network.
|
|
359
|
+
{ headers: network.ozone.adminAuthHeaders('moderator') },
|
|
360
360
|
)
|
|
361
361
|
expect(forSnapshot(data)).toMatchSnapshot()
|
|
362
362
|
})
|
|
@@ -19,13 +19,13 @@ describe('moderation-statuses', () => {
|
|
|
19
19
|
const emitModerationEvent = async (eventData) => {
|
|
20
20
|
return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, {
|
|
21
21
|
encoding: 'application/json',
|
|
22
|
-
headers: network.
|
|
22
|
+
headers: network.ozone.adminAuthHeaders('moderator'),
|
|
23
23
|
})
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const queryModerationStatuses = (statusQuery) =>
|
|
27
27
|
agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, {
|
|
28
|
-
headers: network.
|
|
28
|
+
headers: network.ozone.adminAuthHeaders('moderator'),
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
const seedEvents = async () => {
|