@atproto/ozone 0.1.63 → 0.1.65
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 +24 -0
- package/dist/api/moderation/queryEvents.d.ts.map +1 -1
- package/dist/api/moderation/queryEvents.js +2 -1
- package/dist/api/moderation/queryEvents.js.map +1 -1
- package/dist/daemon/blob-diverter.d.ts +26 -11
- package/dist/daemon/blob-diverter.d.ts.map +1 -1
- package/dist/daemon/blob-diverter.js +109 -52
- package/dist/daemon/blob-diverter.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +30 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +15 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +1 -0
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
- package/dist/mod-service/index.d.ts +1 -0
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +12 -1
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +8 -0
- package/dist/mod-service/views.js.map +1 -1
- package/dist/setting/constants.d.ts +1 -0
- package/dist/setting/constants.d.ts.map +1 -1
- package/dist/setting/constants.js +2 -1
- package/dist/setting/constants.js.map +1 -1
- package/dist/setting/validators.d.ts.map +1 -1
- package/dist/setting/validators.js +19 -0
- package/dist/setting/validators.js.map +1 -1
- package/dist/util.d.ts +2 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +9 -20
- package/dist/util.js.map +1 -1
- package/package.json +10 -11
- package/src/api/moderation/queryEvents.ts +2 -0
- package/src/daemon/blob-diverter.ts +137 -102
- package/src/lexicon/lexicons.ts +17 -0
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
- package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +1 -0
- package/src/mod-service/index.ts +14 -0
- package/src/mod-service/views.ts +11 -0
- package/src/setting/constants.ts +1 -0
- package/src/setting/validators.ts +28 -1
- package/src/util.ts +8 -20
- package/tests/_util.ts +46 -0
- package/tests/blob-divert.test.ts +30 -19
- package/tests/moderation.test.ts +3 -1
- package/tests/server.test.ts +23 -35
- package/tests/takedown.test.ts +64 -0
- package/tsconfig.tests.tsbuildinfo +1 -1
package/dist/util.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.getSigningKeyId = void 0;
|
|
4
|
-
exports.retryHttp = retryHttp;
|
|
5
|
-
exports.retryableHttp = retryableHttp;
|
|
6
|
-
const axios_1 = require("axios");
|
|
7
|
-
const structured_headers_1 = require("structured-headers");
|
|
8
|
-
const xrpc_1 = require("@atproto/xrpc");
|
|
3
|
+
exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.retryHttp = exports.RETRYABLE_HTTP_STATUS_CODES = exports.getSigningKeyId = void 0;
|
|
9
4
|
const common_1 = require("@atproto/common");
|
|
5
|
+
const xrpc_1 = require("@atproto/xrpc");
|
|
6
|
+
const structured_headers_1 = require("structured-headers");
|
|
10
7
|
const getSigningKeyId = async (db, signingKey) => {
|
|
11
8
|
const selectRes = await db.db
|
|
12
9
|
.selectFrom('signing_key')
|
|
@@ -24,25 +21,17 @@ const getSigningKeyId = async (db, signingKey) => {
|
|
|
24
21
|
return insertRes.id;
|
|
25
22
|
};
|
|
26
23
|
exports.getSigningKeyId = getSigningKeyId;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
exports.RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
25
|
+
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
26
|
+
]);
|
|
27
|
+
exports.retryHttp = (0, common_1.createRetryable)((err) => {
|
|
31
28
|
if (err instanceof xrpc_1.XRPCError) {
|
|
32
29
|
if (err.status === xrpc_1.ResponseType.Unknown)
|
|
33
30
|
return true;
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
if (err instanceof axios_1.AxiosError) {
|
|
37
|
-
if (!err.response)
|
|
38
|
-
return true;
|
|
39
|
-
return retryableHttpStatusCodes.has(err.response.status);
|
|
31
|
+
return exports.RETRYABLE_HTTP_STATUS_CODES.has(err.status);
|
|
40
32
|
}
|
|
41
33
|
return false;
|
|
42
|
-
}
|
|
43
|
-
const retryableHttpStatusCodes = new Set([
|
|
44
|
-
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
45
|
-
]);
|
|
34
|
+
});
|
|
46
35
|
exports.LABELER_HEADER_NAME = 'atproto-accept-labelers';
|
|
47
36
|
const parseLabelerHeader = (header, ignoreDid) => {
|
|
48
37
|
if (!header)
|
package/dist/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ozone",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backend service for moderating the Bluesky network.",
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"bin": "dist/bin.js",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@did-plc/lib": "^0.0.1",
|
|
21
|
-
"axios": "^1.6.7",
|
|
22
21
|
"compression": "^1.7.4",
|
|
23
22
|
"cors": "^2.8.5",
|
|
24
23
|
"express": "^4.17.2",
|
|
@@ -32,14 +31,15 @@
|
|
|
32
31
|
"structured-headers": "^1.0.1",
|
|
33
32
|
"typed-emitter": "^2.1.0",
|
|
34
33
|
"uint8arrays": "3.0.0",
|
|
35
|
-
"
|
|
36
|
-
"@atproto/
|
|
34
|
+
"undici": "^6.14.1",
|
|
35
|
+
"@atproto/api": "^0.13.26",
|
|
36
|
+
"@atproto/common": "^0.4.6",
|
|
37
37
|
"@atproto/crypto": "^0.4.2",
|
|
38
|
-
"@atproto/identity": "^0.4.
|
|
39
|
-
"@atproto/lexicon": "^0.4.
|
|
38
|
+
"@atproto/identity": "^0.4.4",
|
|
39
|
+
"@atproto/lexicon": "^0.4.5",
|
|
40
40
|
"@atproto/syntax": "^0.3.1",
|
|
41
|
-
"@atproto/xrpc": "^0.6.
|
|
42
|
-
"@atproto/xrpc-server": "^0.7.
|
|
41
|
+
"@atproto/xrpc": "^0.6.6",
|
|
42
|
+
"@atproto/xrpc-server": "^0.7.5"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@did-plc/server": "^0.0.1",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
"@types/express-serve-static-core": "^4.17.36",
|
|
49
49
|
"@types/pg": "^8.6.6",
|
|
50
50
|
"@types/qs": "^6.9.7",
|
|
51
|
-
"axios": "^0.27.2",
|
|
52
51
|
"jest": "^28.1.2",
|
|
53
52
|
"ts-node": "^10.8.2",
|
|
54
53
|
"typescript": "^5.6.3",
|
|
55
|
-
"@atproto/lex-cli": "^0.5.
|
|
56
|
-
"@atproto/pds": "^0.4.
|
|
54
|
+
"@atproto/lex-cli": "^0.5.5",
|
|
55
|
+
"@atproto/pds": "^0.4.82"
|
|
57
56
|
},
|
|
58
57
|
"scripts": {
|
|
59
58
|
"codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
|
|
@@ -25,6 +25,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
25
25
|
reportTypes,
|
|
26
26
|
collections = [],
|
|
27
27
|
subjectType,
|
|
28
|
+
policies,
|
|
28
29
|
} = params
|
|
29
30
|
const db = ctx.db
|
|
30
31
|
const modService = ctx.modService(db)
|
|
@@ -47,6 +48,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
47
48
|
reportTypes,
|
|
48
49
|
collections,
|
|
49
50
|
subjectType,
|
|
51
|
+
policies,
|
|
50
52
|
})
|
|
51
53
|
return {
|
|
52
54
|
encoding: 'application/json',
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
forwardStreamErrors,
|
|
2
|
+
createDecoders,
|
|
4
3
|
getPdsEndpoint,
|
|
4
|
+
VerifyCidTransform,
|
|
5
|
+
allFulfilled,
|
|
5
6
|
} from '@atproto/common'
|
|
6
7
|
import { IdResolver } from '@atproto/identity'
|
|
7
|
-
import
|
|
8
|
-
import { Readable } from 'stream'
|
|
8
|
+
import { ResponseType, XRPCError } from '@atproto/xrpc'
|
|
9
9
|
import { CID } from 'multiformats/cid'
|
|
10
|
+
import { Readable } from 'node:stream'
|
|
11
|
+
import { finished, pipeline } from 'node:stream/promises'
|
|
12
|
+
import * as undici from 'undici'
|
|
10
13
|
|
|
14
|
+
import { BlobDivertConfig } from '../config'
|
|
11
15
|
import Database from '../db'
|
|
12
16
|
import { retryHttp } from '../util'
|
|
13
|
-
import { BlobDivertConfig } from '../config'
|
|
14
17
|
|
|
15
18
|
export class BlobDiverter {
|
|
16
19
|
serviceConfig: BlobDivertConfig
|
|
@@ -27,126 +30,158 @@ export class BlobDiverter {
|
|
|
27
30
|
this.idResolver = services.idResolver
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
contentType:
|
|
54
|
-
blobResponse.headers['content-type'] || 'application/octet-stream',
|
|
55
|
-
imageStream: imageStream.pipe(verifyCid),
|
|
33
|
+
/**
|
|
34
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
35
|
+
*/
|
|
36
|
+
async getBlob(options: GetBlobOptions): Promise<Blob> {
|
|
37
|
+
const blobUrl = getBlobUrl(options)
|
|
38
|
+
|
|
39
|
+
const blobResponse = await undici
|
|
40
|
+
.request(blobUrl, {
|
|
41
|
+
headersTimeout: 10e3,
|
|
42
|
+
bodyTimeout: 30e3,
|
|
43
|
+
})
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
throw asXrpcClientError(err, `Error fetching blob ${options.cid}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (blobResponse.statusCode !== 200) {
|
|
49
|
+
blobResponse.body.destroy()
|
|
50
|
+
throw new XRPCError(
|
|
51
|
+
blobResponse.statusCode,
|
|
52
|
+
undefined,
|
|
53
|
+
`Error downloading blob ${options.cid}`,
|
|
54
|
+
)
|
|
56
55
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async sendImage({
|
|
60
|
-
url,
|
|
61
|
-
imageStream,
|
|
62
|
-
contentType,
|
|
63
|
-
}: {
|
|
64
|
-
url: string
|
|
65
|
-
imageStream: Readable
|
|
66
|
-
contentType: string
|
|
67
|
-
}) {
|
|
68
|
-
const result = await axios(url, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
data: imageStream,
|
|
71
|
-
headers: {
|
|
72
|
-
Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
|
|
73
|
-
'Content-Type': contentType,
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
56
|
|
|
77
|
-
|
|
57
|
+
try {
|
|
58
|
+
const type = blobResponse.headers['content-type']
|
|
59
|
+
const encoding = blobResponse.headers['content-encoding']
|
|
60
|
+
|
|
61
|
+
const verifier = new VerifyCidTransform(CID.parse(options.cid))
|
|
62
|
+
|
|
63
|
+
void pipeline([
|
|
64
|
+
blobResponse.body,
|
|
65
|
+
...createDecoders(encoding),
|
|
66
|
+
verifier,
|
|
67
|
+
]).catch((_err) => {})
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
type: typeof type === 'string' ? type : 'application/octet-stream',
|
|
71
|
+
stream: verifier,
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Typically un-supported content encoding
|
|
75
|
+
blobResponse.body.destroy()
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
82
|
+
*/
|
|
83
|
+
async uploadBlob(blob: Blob, report: ReportBlobOptions) {
|
|
84
|
+
const uploadUrl = reportBlobUrl(this.serviceConfig.url, report)
|
|
85
|
+
|
|
86
|
+
const result = await undici
|
|
87
|
+
.request(uploadUrl, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: blob.stream,
|
|
90
|
+
headersTimeout: 30e3,
|
|
91
|
+
bodyTimeout: 10e3,
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
|
|
94
|
+
'content-type': blob.type,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
throw asXrpcClientError(err, `Error uploading blob ${report.did}`)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (result.statusCode !== 200) {
|
|
102
|
+
result.body.destroy()
|
|
103
|
+
throw new XRPCError(
|
|
104
|
+
result.statusCode,
|
|
105
|
+
undefined,
|
|
106
|
+
`Error uploading blob ${report.did}`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
await finished(result.body.resume())
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
async uploadBlobOnService({
|
|
105
|
-
subjectDid,
|
|
106
|
-
subjectUri,
|
|
114
|
+
subjectDid: did,
|
|
115
|
+
subjectUri: uri,
|
|
107
116
|
subjectBlobCids,
|
|
108
117
|
}: {
|
|
109
118
|
subjectDid: string
|
|
110
119
|
subjectUri: string | null
|
|
111
120
|
subjectBlobCids: string[]
|
|
112
|
-
}): Promise<
|
|
113
|
-
const didDoc = await this.idResolver.did.resolve(
|
|
114
|
-
|
|
115
|
-
if (!didDoc) {
|
|
116
|
-
throw new Error('Error resolving DID')
|
|
117
|
-
}
|
|
121
|
+
}): Promise<void> {
|
|
122
|
+
const didDoc = await this.idResolver.did.resolve(did)
|
|
123
|
+
if (!didDoc) throw new Error('Error resolving DID')
|
|
118
124
|
|
|
119
125
|
const pds = getPdsEndpoint(didDoc)
|
|
126
|
+
if (!pds) throw new Error('Error resolving PDS')
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
throw new Error('Error resolving PDS')
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// attempt to download and upload within the same retry block since the imageStream is not reusable
|
|
126
|
-
const uploadResult = await Promise.all(
|
|
128
|
+
await allFulfilled(
|
|
127
129
|
subjectBlobCids.map((cid) =>
|
|
128
130
|
retryHttp(async () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
return this.uploadBlob(
|
|
135
|
-
{ imageStream, contentType },
|
|
136
|
-
{ subjectDid, subjectUri },
|
|
137
|
-
)
|
|
131
|
+
// attempt to download and upload within the same retry block since
|
|
132
|
+
// the blob stream is not reusable
|
|
133
|
+
const blob = await this.getBlob({ pds, cid, did })
|
|
134
|
+
return this.uploadBlob(blob, { did, uri })
|
|
138
135
|
}),
|
|
139
136
|
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
).catch((err) => {
|
|
138
|
+
throw new XRPCError(
|
|
139
|
+
ResponseType.UpstreamFailure,
|
|
140
|
+
undefined,
|
|
141
|
+
'Failed to process blobs',
|
|
142
|
+
undefined,
|
|
143
|
+
{ cause: err },
|
|
144
|
+
)
|
|
145
|
+
})
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
const basicAuth = (username: string, password: string) => {
|
|
151
150
|
return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
|
|
152
151
|
}
|
|
152
|
+
|
|
153
|
+
type Blob = {
|
|
154
|
+
type: string
|
|
155
|
+
stream: Readable
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type GetBlobOptions = {
|
|
159
|
+
pds: string
|
|
160
|
+
did: string
|
|
161
|
+
cid: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getBlobUrl({ pds, did, cid }: GetBlobOptions): URL {
|
|
165
|
+
const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds)
|
|
166
|
+
url.searchParams.set('did', did)
|
|
167
|
+
url.searchParams.set('cid', cid)
|
|
168
|
+
return url
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type ReportBlobOptions = {
|
|
172
|
+
did: string
|
|
173
|
+
uri: string | null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reportBlobUrl(service: string, { did, uri }: ReportBlobOptions): URL {
|
|
177
|
+
const url = new URL(`/xrpc/com.atproto.unspecced.reportBlob`, service)
|
|
178
|
+
url.searchParams.set('did', did)
|
|
179
|
+
if (uri != null) url.searchParams.set('uri', uri)
|
|
180
|
+
return url
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function asXrpcClientError(err: unknown, message: string) {
|
|
184
|
+
return new XRPCError(ResponseType.Unknown, undefined, message, undefined, {
|
|
185
|
+
cause: err,
|
|
186
|
+
})
|
|
187
|
+
}
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -11309,6 +11309,15 @@ export const schemaDict = {
|
|
|
11309
11309
|
description:
|
|
11310
11310
|
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
|
|
11311
11311
|
},
|
|
11312
|
+
policies: {
|
|
11313
|
+
type: 'array',
|
|
11314
|
+
maxLength: 5,
|
|
11315
|
+
items: {
|
|
11316
|
+
type: 'string',
|
|
11317
|
+
},
|
|
11318
|
+
description:
|
|
11319
|
+
'Names/Keywords of the policies that drove the decision.',
|
|
11320
|
+
},
|
|
11312
11321
|
},
|
|
11313
11322
|
},
|
|
11314
11323
|
modEventReverseTakedown: {
|
|
@@ -12345,6 +12354,14 @@ export const schemaDict = {
|
|
|
12345
12354
|
type: 'string',
|
|
12346
12355
|
},
|
|
12347
12356
|
},
|
|
12357
|
+
policies: {
|
|
12358
|
+
type: 'array',
|
|
12359
|
+
items: {
|
|
12360
|
+
type: 'string',
|
|
12361
|
+
description:
|
|
12362
|
+
'If specified, only events where the policy matches the given policy are returned',
|
|
12363
|
+
},
|
|
12364
|
+
},
|
|
12348
12365
|
cursor: {
|
|
12349
12366
|
type: 'string',
|
|
12350
12367
|
},
|
|
@@ -174,6 +174,8 @@ export interface ModEventTakedown {
|
|
|
174
174
|
durationInHours?: number
|
|
175
175
|
/** If true, all other reports on content authored by this account will be resolved (acknowledged). */
|
|
176
176
|
acknowledgeAccountSubjects?: boolean
|
|
177
|
+
/** Names/Keywords of the policies that drove the decision. */
|
|
178
|
+
policies?: string[]
|
|
177
179
|
[k: string]: unknown
|
|
178
180
|
}
|
|
179
181
|
|
package/src/mod-service/index.ts
CHANGED
|
@@ -152,6 +152,7 @@ export class ModerationService {
|
|
|
152
152
|
reportTypes?: string[]
|
|
153
153
|
collections: string[]
|
|
154
154
|
subjectType?: string
|
|
155
|
+
policies?: string[]
|
|
155
156
|
}): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
|
|
156
157
|
const {
|
|
157
158
|
subject,
|
|
@@ -172,6 +173,7 @@ export class ModerationService {
|
|
|
172
173
|
reportTypes,
|
|
173
174
|
collections,
|
|
174
175
|
subjectType,
|
|
176
|
+
policies,
|
|
175
177
|
} = opts
|
|
176
178
|
const { ref } = this.db.db.dynamic
|
|
177
179
|
let builder = this.db.db.selectFrom('moderation_event').selectAll()
|
|
@@ -264,6 +266,14 @@ export class ModerationService {
|
|
|
264
266
|
if (reportTypes?.length) {
|
|
265
267
|
builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)
|
|
266
268
|
}
|
|
269
|
+
if (policies?.length) {
|
|
270
|
+
builder = builder.where((qb) => {
|
|
271
|
+
policies.forEach((policy) => {
|
|
272
|
+
qb = qb.orWhere(sql`meta->>'policies'`, 'ilike', `%${policy}%`)
|
|
273
|
+
})
|
|
274
|
+
return qb
|
|
275
|
+
})
|
|
276
|
+
}
|
|
267
277
|
|
|
268
278
|
const keyset = new TimeIdKeyset(
|
|
269
279
|
ref(`moderation_event.createdAt`),
|
|
@@ -435,6 +445,10 @@ export class ModerationService {
|
|
|
435
445
|
meta.acknowledgeAccountSubjects = true
|
|
436
446
|
}
|
|
437
447
|
|
|
448
|
+
if (isModEventTakedown(event) && event.policies?.length) {
|
|
449
|
+
meta.policies = event.policies.join(',')
|
|
450
|
+
}
|
|
451
|
+
|
|
438
452
|
// Keep trace of reports that came in while the reporter was in muted stated
|
|
439
453
|
if (isModEventReport(event)) {
|
|
440
454
|
const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
|
package/src/mod-service/views.ts
CHANGED
|
@@ -137,6 +137,17 @@ export class ModerationViews {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
if (
|
|
141
|
+
event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
|
|
142
|
+
typeof event.meta?.policies === 'string' &&
|
|
143
|
+
event.meta.policies.length > 0
|
|
144
|
+
) {
|
|
145
|
+
eventView.event = {
|
|
146
|
+
...eventView.event,
|
|
147
|
+
policies: event.meta.policies.split(','),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
if (event.action === 'tools.ozone.moderation.defs#modEventLabel') {
|
|
141
152
|
eventView.event = {
|
|
142
153
|
...eventView.event,
|
package/src/setting/constants.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Selectable } from 'kysely'
|
|
2
2
|
import { Setting } from '../db/schema/setting'
|
|
3
|
-
import { ProtectedTagSettingKey } from './constants'
|
|
3
|
+
import { PolicyListSettingKey, ProtectedTagSettingKey } from './constants'
|
|
4
4
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
5
5
|
|
|
6
6
|
export const settingValidators = new Map<
|
|
@@ -58,4 +58,31 @@ export const settingValidators = new Map<
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
],
|
|
61
|
+
[
|
|
62
|
+
PolicyListSettingKey,
|
|
63
|
+
async (setting: Partial<Selectable<Setting>>) => {
|
|
64
|
+
if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
|
|
65
|
+
throw new InvalidRequestError(
|
|
66
|
+
'Only admins should be able to manage policy list',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof setting.value !== 'object') {
|
|
71
|
+
throw new InvalidRequestError('Invalid value')
|
|
72
|
+
}
|
|
73
|
+
for (const [key, val] of Object.entries(setting.value)) {
|
|
74
|
+
if (!val || typeof val !== 'object') {
|
|
75
|
+
throw new InvalidRequestError(
|
|
76
|
+
`Invalid configuration for policy ${key}`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!val['name'] || !val['description']) {
|
|
81
|
+
throw new InvalidRequestError(
|
|
82
|
+
`Must define a name and description for policy ${key}`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
],
|
|
61
88
|
])
|
package/src/util.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRetryable } from '@atproto/common'
|
|
2
|
+
import { ResponseType, XRPCError } from '@atproto/xrpc'
|
|
2
3
|
import { parseList } from 'structured-headers'
|
|
3
|
-
import { XRPCError, ResponseType } from '@atproto/xrpc'
|
|
4
|
-
import { RetryOptions, retry } from '@atproto/common'
|
|
5
4
|
import Database from './db'
|
|
6
5
|
|
|
7
6
|
export const getSigningKeyId = async (
|
|
@@ -24,28 +23,17 @@ export const getSigningKeyId = async (
|
|
|
24
23
|
return insertRes.id
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
): Promise<T> {
|
|
31
|
-
return retry(fn, { retryable: retryableHttp, ...opts })
|
|
32
|
-
}
|
|
26
|
+
export const RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
27
|
+
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
28
|
+
])
|
|
33
29
|
|
|
34
|
-
export
|
|
30
|
+
export const retryHttp = createRetryable((err: unknown) => {
|
|
35
31
|
if (err instanceof XRPCError) {
|
|
36
32
|
if (err.status === ResponseType.Unknown) return true
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if (err instanceof AxiosError) {
|
|
40
|
-
if (!err.response) return true
|
|
41
|
-
return retryableHttpStatusCodes.has(err.response.status)
|
|
33
|
+
return RETRYABLE_HTTP_STATUS_CODES.has(err.status)
|
|
42
34
|
}
|
|
43
35
|
return false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const retryableHttpStatusCodes = new Set([
|
|
47
|
-
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
48
|
-
])
|
|
36
|
+
})
|
|
49
37
|
|
|
50
38
|
export type ParsedLabelers = {
|
|
51
39
|
dids: string[]
|
package/tests/_util.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type Express } from 'express'
|
|
2
|
+
import { Server } from 'node:http'
|
|
3
|
+
import { AddressInfo } from 'node:net'
|
|
1
4
|
import { AtUri } from '@atproto/syntax'
|
|
2
5
|
import { lexToJson } from '@atproto/lexicon'
|
|
3
6
|
import { CID } from 'multiformats/cid'
|
|
@@ -195,3 +198,46 @@ export const stripViewerFromThread = <T>(thread: T): T => {
|
|
|
195
198
|
}
|
|
196
199
|
return thread
|
|
197
200
|
}
|
|
201
|
+
|
|
202
|
+
export async function startServer(app: Express) {
|
|
203
|
+
return new Promise<{
|
|
204
|
+
origin: string
|
|
205
|
+
server: Server
|
|
206
|
+
stop: () => Promise<void>
|
|
207
|
+
}>((resolve, reject) => {
|
|
208
|
+
const onListen = () => {
|
|
209
|
+
const port = (server.address() as AddressInfo).port
|
|
210
|
+
resolve({
|
|
211
|
+
server,
|
|
212
|
+
origin: `http://localhost:${port}`,
|
|
213
|
+
stop: () => stopServer(server),
|
|
214
|
+
})
|
|
215
|
+
cleanup()
|
|
216
|
+
}
|
|
217
|
+
const onError = (err: Error) => {
|
|
218
|
+
reject(err)
|
|
219
|
+
cleanup()
|
|
220
|
+
}
|
|
221
|
+
const cleanup = () => {
|
|
222
|
+
server.removeListener('listening', onListen)
|
|
223
|
+
server.removeListener('error', onError)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const server = app
|
|
227
|
+
.listen(0)
|
|
228
|
+
.once('listening', onListen)
|
|
229
|
+
.once('error', onError)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function stopServer(server: Server) {
|
|
234
|
+
return new Promise<void>((resolve, reject) => {
|
|
235
|
+
server.close((err) => {
|
|
236
|
+
if (err) {
|
|
237
|
+
reject(err)
|
|
238
|
+
} else {
|
|
239
|
+
resolve()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
}
|