@atproto/api 0.20.23 → 0.20.26
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 +34 -0
- package/dist/agent.d.ts +4 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +17 -0
- package/dist/agent.js.map +1 -1
- package/dist/client/lexicons.d.ts +8 -0
- package/dist/client/lexicons.d.ts.map +1 -1
- package/dist/client/lexicons.js +4 -0
- package/dist/client/lexicons.js.map +1 -1
- package/dist/client/types/app/bsky/actor/defs.d.ts +2 -0
- package/dist/client/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/client/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/rich-text/detection.d.ts.map +1 -1
- package/dist/rich-text/detection.js +4 -1
- package/dist/rich-text/detection.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +22 -16
- package/definitions/labels.json +0 -170
- package/docs/moderation.md +0 -260
- package/jest.config.cjs +0 -23
- package/jest.d.ts +0 -20
- package/jest.setup.ts +0 -97
- package/scripts/code/labels.mjs +0 -77
- package/scripts/generate-code.mjs +0 -4
- package/src/age-assurance.test.ts +0 -218
- package/src/age-assurance.ts +0 -137
- package/src/agent.ts +0 -1645
- package/src/atp-agent.ts +0 -570
- package/src/bsky-agent.ts +0 -14
- package/src/const.ts +0 -1
- package/src/index.ts +0 -44
- package/src/mocker.ts +0 -220
- package/src/moderation/decision.ts +0 -389
- package/src/moderation/index.ts +0 -69
- package/src/moderation/mutewords.ts +0 -170
- package/src/moderation/subjects/account.ts +0 -44
- package/src/moderation/subjects/feed-generator.ts +0 -24
- package/src/moderation/subjects/notification.ts +0 -25
- package/src/moderation/subjects/post.ts +0 -433
- package/src/moderation/subjects/profile.ts +0 -26
- package/src/moderation/subjects/status.ts +0 -27
- package/src/moderation/subjects/user-list.ts +0 -48
- package/src/moderation/types.ts +0 -191
- package/src/moderation/ui.ts +0 -21
- package/src/moderation/util.ts +0 -111
- package/src/predicate.ts +0 -52
- package/src/rich-text/detection.ts +0 -146
- package/src/rich-text/rich-text.ts +0 -418
- package/src/rich-text/sanitization.ts +0 -42
- package/src/rich-text/unicode.ts +0 -47
- package/src/rich-text/util.ts +0 -15
- package/src/session-manager.ts +0 -5
- package/src/types.ts +0 -165
- package/src/util.ts +0 -96
- package/tests/atp-agent.test.ts +0 -3863
- package/tests/dispatcher.test.ts +0 -527
- package/tests/errors.test.ts +0 -29
- package/tests/moderation-behaviors.test.ts +0 -951
- package/tests/moderation-custom-labels.test.ts +0 -334
- package/tests/moderation-mutewords.test.ts +0 -1299
- package/tests/moderation-prefs.test.ts +0 -402
- package/tests/moderation-quoteposts.test.ts +0 -277
- package/tests/moderation.test.ts +0 -739
- package/tests/rich-text-detection.test.ts +0 -456
- package/tests/rich-text-sanitization.test.ts +0 -216
- package/tests/rich-text.test.ts +0 -705
- package/tests/util/echo-server.ts +0 -37
- package/tests/util/moderation-behavior.ts +0 -268
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -10
package/docs/moderation.md
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# Moderation API
|
|
2
|
-
|
|
3
|
-
Applying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including:
|
|
4
|
-
|
|
5
|
-
- Moderator labeling
|
|
6
|
-
- User muting (including mutelists)
|
|
7
|
-
- User blocking
|
|
8
|
-
- Mutewords
|
|
9
|
-
- Hidden posts
|
|
10
|
-
|
|
11
|
-
## Configuration
|
|
12
|
-
|
|
13
|
-
Every moderation function takes a set of options which look like this:
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
{
|
|
17
|
-
// the logged-in user's DID
|
|
18
|
-
userDid: 'did:plc:1234...',
|
|
19
|
-
|
|
20
|
-
moderationPrefs: {
|
|
21
|
-
// is adult content allowed?
|
|
22
|
-
adultContentEnabled: true,
|
|
23
|
-
|
|
24
|
-
// the global label settings (used on self-labels)
|
|
25
|
-
labels: {
|
|
26
|
-
porn: 'hide',
|
|
27
|
-
sexual: 'warn',
|
|
28
|
-
nudity: 'ignore',
|
|
29
|
-
// ...
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
// the subscribed labelers and their label settings
|
|
33
|
-
labelers: [
|
|
34
|
-
{
|
|
35
|
-
did: 'did:plc:1234...',
|
|
36
|
-
labels: {
|
|
37
|
-
porn: 'hide',
|
|
38
|
-
sexual: 'warn',
|
|
39
|
-
nudity: 'ignore',
|
|
40
|
-
// ...
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
],
|
|
44
|
-
|
|
45
|
-
mutedWords: [/* ... */],
|
|
46
|
-
hiddenPosts: [/* ... */]
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
// custom label definitions
|
|
50
|
-
labelDefs: {
|
|
51
|
-
// labelerDid => defs[]
|
|
52
|
-
'did:plc:1234...': [
|
|
53
|
-
/* ... */
|
|
54
|
-
]
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
This should match the following interfaces:
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
export interface ModerationPrefsLabeler {
|
|
63
|
-
did: string
|
|
64
|
-
labels: Record<string, LabelPreference>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ModerationPrefs {
|
|
68
|
-
adultContentEnabled: boolean
|
|
69
|
-
labels: Record<string, LabelPreference>
|
|
70
|
-
labelers: ModerationPrefsLabeler[]
|
|
71
|
-
mutedWords: AppBskyActorDefs.MutedWord[]
|
|
72
|
-
hiddenPosts: string[]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface ModerationOpts {
|
|
76
|
-
userDid: string | undefined
|
|
77
|
-
prefs: ModerationPrefs
|
|
78
|
-
/**
|
|
79
|
-
* Map of labeler did -> custom definitions
|
|
80
|
-
*/
|
|
81
|
-
labelDefs?: Record<string, InterpretedLabelValueDefinition[]>
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
You can quickly grab the `ModerationPrefs` using the `agent.getPreferences()` method:
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
const prefs = await agent.getPreferences()
|
|
89
|
-
moderatePost(post, {
|
|
90
|
-
userDid: /*...*/,
|
|
91
|
-
prefs: prefs.moderationPrefs,
|
|
92
|
-
labelDefs: /*...*/
|
|
93
|
-
})
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
To gather the label definitions (`labelDefs`) see the _Labelers_ section below.
|
|
97
|
-
|
|
98
|
-
## Labelers
|
|
99
|
-
|
|
100
|
-
Labelers are services that provide moderation labels. Your application will typically have 1+ top-level labelers set with the ability to do "takedowns" on content. This is controlled via this static function, though the default is to use Bluesky's moderation:
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
BskyAgent.configure({
|
|
104
|
-
appLabelers: ['did:web:my-labeler.com'],
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Users may also add their own labelers. The active labelers are controlled via an HTTP header which is automatically set by the agent when `getPreferences` is called, or when the labeler preferences are changed.
|
|
109
|
-
|
|
110
|
-
Labelers publish a `app.bsky.labeler.service` record that looks like this:
|
|
111
|
-
|
|
112
|
-
```js
|
|
113
|
-
{
|
|
114
|
-
$type: 'app.bsky.labeler.service',
|
|
115
|
-
policies: {
|
|
116
|
-
// the list of label values the labeler will publish
|
|
117
|
-
labelValues: [
|
|
118
|
-
'rude',
|
|
119
|
-
],
|
|
120
|
-
// any custom definitions the labeler will be using
|
|
121
|
-
labelValueDefinitions: [
|
|
122
|
-
{
|
|
123
|
-
identifier: 'rude',
|
|
124
|
-
blurs: 'content',
|
|
125
|
-
severity: 'alert',
|
|
126
|
-
defaultSetting: 'warn',
|
|
127
|
-
adultOnly: false,
|
|
128
|
-
locales: [
|
|
129
|
-
{
|
|
130
|
-
lang: 'en',
|
|
131
|
-
name: 'Rude',
|
|
132
|
-
description: 'Not keeping things civil.',
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
createdAt: '2024-03-12T17:17:17.215Z'
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
The label value definition are custom labels which only apply to that labeler. Your client needs to sync those definitions in order to correctly interpret them. To do that, call `app.bsky.labeler.getService()` (or the `getServices` batch variant) periodically to fetch their definitions. We recommend caching the response (at time our writing the official client uses a TTL of 6 hours).
|
|
143
|
-
|
|
144
|
-
Here is how to do this:
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
import { AtpAgent } from '@atproto/api'
|
|
148
|
-
|
|
149
|
-
const agent = new AtpAgent({ service: 'https://example.com' })
|
|
150
|
-
// assume `agent` is a signed in session
|
|
151
|
-
const prefs = await agent.getPreferences()
|
|
152
|
-
const labelDefs = await agent.getLabelDefinitions(prefs)
|
|
153
|
-
|
|
154
|
-
moderatePost(post, {
|
|
155
|
-
userDid: agent.session.did,
|
|
156
|
-
prefs: prefs.moderationPrefs,
|
|
157
|
-
labelDefs,
|
|
158
|
-
})
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## The `moderate*()` APIs
|
|
162
|
-
|
|
163
|
-
The SDK exports methods to moderate the different kinds of content on the network.
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
import {
|
|
167
|
-
moderateProfile,
|
|
168
|
-
moderatePost,
|
|
169
|
-
moderateNotification,
|
|
170
|
-
moderateFeedGen,
|
|
171
|
-
moderateUserList,
|
|
172
|
-
moderateLabeler,
|
|
173
|
-
} from '@atproto/api'
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Each of these follows the same API signature:
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
const res = moderatePost(post, moderationOptions)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The response object provides an API for figuring out what your UI should do in different contexts.
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
res.ui(context) /* =>
|
|
186
|
-
|
|
187
|
-
ModerationUI {
|
|
188
|
-
filter: boolean // should the content be removed from the interface?
|
|
189
|
-
blur: boolean // should the content be put behind a cover?
|
|
190
|
-
alert: boolean // should an alert be put on the content? (negative)
|
|
191
|
-
inform: boolean // should an informational notice be put on the content? (neutral)
|
|
192
|
-
noOverride: boolean // if blur=true, should the UI disable opening the cover?
|
|
193
|
-
|
|
194
|
-
// the reasons for each of the flags:
|
|
195
|
-
filters: ModerationCause[]
|
|
196
|
-
blurs: ModerationCause[]
|
|
197
|
-
alerts: ModerationCause[]
|
|
198
|
-
informs: ModerationCause[]
|
|
199
|
-
}
|
|
200
|
-
*/
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
There are multiple UI contexts available:
|
|
204
|
-
|
|
205
|
-
- `profileList` A profile being listed, eg in search or a follower list
|
|
206
|
-
- `profileView` A profile being viewed directly
|
|
207
|
-
- `avatar` The user's avatar in any context
|
|
208
|
-
- `banner` The user's banner in any context
|
|
209
|
-
- `displayName` The user's display name in any context
|
|
210
|
-
- `contentList` Content being listed, eg posts in a feed, posts as replies, a user list list, a feed generator list, etc
|
|
211
|
-
- `contentView` Content being viewed direct, eg an opened post, the user list page, the feedgen page, etc
|
|
212
|
-
- `contentMedia ` Media inside the content, eg a picture embedded in a post
|
|
213
|
-
|
|
214
|
-
Here's how a post in a feed would use these tools to make a decision:
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
const mod = moderatePost(post, moderationOptions)
|
|
218
|
-
|
|
219
|
-
if (mod.ui('contentList').filter) {
|
|
220
|
-
// dont show the post
|
|
221
|
-
}
|
|
222
|
-
if (mod.ui('contentList').blur) {
|
|
223
|
-
// cover the post with the explanation from mod.ui('contentList').blurs[0]
|
|
224
|
-
if (mod.ui('contentList').noOverride) {
|
|
225
|
-
// dont allow the cover to be removed
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (mod.ui('contentMedia').blur) {
|
|
229
|
-
// cover the post's embedded images with the explanation from mod.ui('contentMedia').blurs[0]
|
|
230
|
-
if (mod.ui('contentMedia').noOverride) {
|
|
231
|
-
// dont allow the cover to be removed
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (mod.ui('avatar').blur) {
|
|
235
|
-
// cover the avatar with the explanation from mod.ui('avatar').blurs[0]
|
|
236
|
-
if (mod.ui('avatar').noOverride) {
|
|
237
|
-
// dont allow the cover to be removed
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
for (const alert of mod.ui('contentList').alerts) {
|
|
241
|
-
// render this alert
|
|
242
|
-
}
|
|
243
|
-
for (const inform of mod.ui('contentList').informs) {
|
|
244
|
-
// render this inform
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## Sending moderation reports
|
|
249
|
-
|
|
250
|
-
Any Labeler is capable of receiving moderation reports. As a result, you need to specify which labeler should receive the report. You do this with the `Atproto-Proxy` header:
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
agent
|
|
254
|
-
.withProxy('atproto_labeler', 'did:web:my-labeler.com')
|
|
255
|
-
.createModerationReport({
|
|
256
|
-
reasonType: 'com.atproto.moderation.defs#reasonViolation',
|
|
257
|
-
reason: 'They were being such a jerk to me!',
|
|
258
|
-
subject: { did: 'did:web:bob.com' },
|
|
259
|
-
})
|
|
260
|
-
```
|
package/jest.config.cjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
displayName: 'API',
|
|
4
|
-
transform: {
|
|
5
|
-
'^.+\\.(t|j)s$': [
|
|
6
|
-
'@swc/jest',
|
|
7
|
-
{
|
|
8
|
-
jsc: {
|
|
9
|
-
parser: { syntax: 'typescript', importAttributes: true },
|
|
10
|
-
experimental: { keepImportAttributes: true },
|
|
11
|
-
transform: {},
|
|
12
|
-
},
|
|
13
|
-
module: { type: 'es6' },
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
extensionsToTreatAsEsm: ['.ts'],
|
|
18
|
-
transformIgnorePatterns: [],
|
|
19
|
-
testTimeout: 60000,
|
|
20
|
-
setupFiles: ['<rootDir>/../../test.setup.ts'],
|
|
21
|
-
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
22
|
-
moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
|
|
23
|
-
}
|
package/jest.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
declare namespace jest {
|
|
2
|
-
// eslint-disable-next-line
|
|
3
|
-
interface Matchers<R, T = {}> {
|
|
4
|
-
toBeModerationResult(
|
|
5
|
-
expected: ModerationTestSuiteResultFlag[] | undefined,
|
|
6
|
-
context?: string,
|
|
7
|
-
stringifiedResult?: string,
|
|
8
|
-
ignoreCause?: boolean,
|
|
9
|
-
): R
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface Expect {
|
|
13
|
-
toBeModerationResult(
|
|
14
|
-
expected: ModerationTestSuiteResultFlag[] | undefined,
|
|
15
|
-
context?: string,
|
|
16
|
-
stringifiedResult?: string,
|
|
17
|
-
ignoreCause?: boolean,
|
|
18
|
-
): void
|
|
19
|
-
}
|
|
20
|
-
}
|
package/jest.setup.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { ModerationUI } from './src/index.js'
|
|
2
|
-
import { ModerationTestSuiteResultFlag } from './tests/util/moderation-behavior.js'
|
|
3
|
-
|
|
4
|
-
expect.extend({
|
|
5
|
-
toBeModerationResult(
|
|
6
|
-
actual: ModerationUI,
|
|
7
|
-
expected: ModerationTestSuiteResultFlag[] | undefined,
|
|
8
|
-
context = '',
|
|
9
|
-
stringifiedResult: string | undefined = undefined,
|
|
10
|
-
_ignoreCause = false,
|
|
11
|
-
) {
|
|
12
|
-
const fail = (msg: string) => ({
|
|
13
|
-
pass: false,
|
|
14
|
-
message: () =>
|
|
15
|
-
`${msg}.${
|
|
16
|
-
stringifiedResult ? ` Full result: ${stringifiedResult}` : ''
|
|
17
|
-
}`,
|
|
18
|
-
})
|
|
19
|
-
// let cause = actual.causes?.type as string
|
|
20
|
-
// if (actual.cause?.type === 'label') {
|
|
21
|
-
// cause = `label:${actual.cause.labelDef.id}`
|
|
22
|
-
// } else if (actual.cause?.type === 'muted') {
|
|
23
|
-
// if (actual.cause.source.type === 'list') {
|
|
24
|
-
// cause = 'muted-by-list'
|
|
25
|
-
// }
|
|
26
|
-
// } else if (actual.cause?.type === 'blocking') {
|
|
27
|
-
// if (actual.cause.source.type === 'list') {
|
|
28
|
-
// cause = 'blocking-by-list'
|
|
29
|
-
// }
|
|
30
|
-
// }
|
|
31
|
-
if (!expected) {
|
|
32
|
-
// if (!ignoreCause && actual.cause) {
|
|
33
|
-
// return fail(`${context} expected to be a no-op, got ${cause}`)
|
|
34
|
-
// }
|
|
35
|
-
if (actual.inform) {
|
|
36
|
-
return fail(`${context} expected to be a no-op, got inform=true`)
|
|
37
|
-
}
|
|
38
|
-
if (actual.alert) {
|
|
39
|
-
return fail(`${context} expected to be a no-op, got alert=true`)
|
|
40
|
-
}
|
|
41
|
-
if (actual.blur) {
|
|
42
|
-
return fail(`${context} expected to be a no-op, got blur=true`)
|
|
43
|
-
}
|
|
44
|
-
if (actual.filter) {
|
|
45
|
-
return fail(`${context} expected to be a no-op, got filter=true`)
|
|
46
|
-
}
|
|
47
|
-
if (actual.noOverride) {
|
|
48
|
-
return fail(`${context} expected to be a no-op, got noOverride=true`)
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// if (!ignoreCause && cause !== expected.cause) {
|
|
52
|
-
// return fail(`${context} expected to be ${expected.cause}, got ${cause}`)
|
|
53
|
-
// }
|
|
54
|
-
const expectedInform = expected.includes('inform')
|
|
55
|
-
if (!!actual.inform !== expectedInform) {
|
|
56
|
-
return fail(
|
|
57
|
-
`${context} expected to be inform=${expectedInform}, got ${
|
|
58
|
-
actual.inform || false
|
|
59
|
-
}`,
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
const expectedAlert = expected.includes('alert')
|
|
63
|
-
if (!!actual.alert !== expectedAlert) {
|
|
64
|
-
return fail(
|
|
65
|
-
`${context} expected to be alert=${expectedAlert}, got ${
|
|
66
|
-
actual.alert || false
|
|
67
|
-
}`,
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
const expectedBlur = expected.includes('blur')
|
|
71
|
-
if (!!actual.blur !== expectedBlur) {
|
|
72
|
-
return fail(
|
|
73
|
-
`${context} expected to be blur=${expectedBlur}, got ${
|
|
74
|
-
actual.blur || false
|
|
75
|
-
}`,
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
const expectedFilter = expected.includes('filter')
|
|
79
|
-
if (!!actual.filter !== expectedFilter) {
|
|
80
|
-
return fail(
|
|
81
|
-
`${context} expected to be filter=${expectedFilter}, got ${
|
|
82
|
-
actual.filter || false
|
|
83
|
-
}`,
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
const expectedNoOverride = expected.includes('noOverride')
|
|
87
|
-
if (!!actual.noOverride !== expectedNoOverride) {
|
|
88
|
-
return fail(
|
|
89
|
-
`${context} expected to be noOverride=${expectedNoOverride}, got ${
|
|
90
|
-
actual.noOverride || false
|
|
91
|
-
}`,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return { pass: true, message: () => '' }
|
|
96
|
-
},
|
|
97
|
-
})
|
package/scripts/code/labels.mjs
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import * as prettier from 'prettier'
|
|
5
|
-
import labelsDef from '../../definitions/labels.json' with { type: 'json' }
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
|
|
9
|
-
export async function labels() {
|
|
10
|
-
const content = await gen()
|
|
11
|
-
|
|
12
|
-
const path = join(
|
|
13
|
-
__dirname,
|
|
14
|
-
'..',
|
|
15
|
-
'..',
|
|
16
|
-
'src',
|
|
17
|
-
'moderation',
|
|
18
|
-
'const',
|
|
19
|
-
'labels.ts',
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
await mkdir(dirname(path), { recursive: true })
|
|
23
|
-
await writeFile(path, content, 'utf8')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function gen() {
|
|
27
|
-
const knownValues = new Set()
|
|
28
|
-
const flattenedLabelDefs = []
|
|
29
|
-
|
|
30
|
-
for (const { alias: aliases, ...label } of labelsDef) {
|
|
31
|
-
knownValues.add(label.identifier)
|
|
32
|
-
flattenedLabelDefs.push([label.identifier, { ...label, locales: [] }])
|
|
33
|
-
|
|
34
|
-
if (aliases) {
|
|
35
|
-
for (const alias of aliases) {
|
|
36
|
-
knownValues.add(alias)
|
|
37
|
-
flattenedLabelDefs.push([
|
|
38
|
-
alias,
|
|
39
|
-
{
|
|
40
|
-
...label,
|
|
41
|
-
identifier: alias,
|
|
42
|
-
locales: [],
|
|
43
|
-
comment: `@deprecated alias for \`${label.identifier}\``,
|
|
44
|
-
},
|
|
45
|
-
])
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let labelDefsStr = `{`
|
|
51
|
-
for (const [key, { comment, ...value }] of flattenedLabelDefs) {
|
|
52
|
-
const commentStr = comment ? `\n/** ${comment} */\n` : ''
|
|
53
|
-
labelDefsStr += `${commentStr}'${key}': ${JSON.stringify(value, null, 2)},`
|
|
54
|
-
}
|
|
55
|
-
labelDefsStr += `}`
|
|
56
|
-
|
|
57
|
-
return prettier.format(
|
|
58
|
-
`/** this doc is generated by ./scripts/code/labels.mjs **/
|
|
59
|
-
import {InterpretedLabelValueDefinition, LabelPreference} from '../types.js'
|
|
60
|
-
|
|
61
|
-
export type KnownLabelValue = ${Array.from(knownValues)
|
|
62
|
-
.map((value) => `"${value}"`)
|
|
63
|
-
.join(' | ')}
|
|
64
|
-
|
|
65
|
-
export const DEFAULT_LABEL_SETTINGS: Record<string, LabelPreference> = ${JSON.stringify(
|
|
66
|
-
Object.fromEntries(
|
|
67
|
-
labelsDef
|
|
68
|
-
.filter((label) => label.configurable)
|
|
69
|
-
.map((label) => [label.identifier, label.defaultSetting]),
|
|
70
|
-
),
|
|
71
|
-
)}
|
|
72
|
-
|
|
73
|
-
export const LABELS: Record<KnownLabelValue, InterpretedLabelValueDefinition> = ${labelDefsStr}
|
|
74
|
-
`,
|
|
75
|
-
{ semi: false, parser: 'typescript', singleQuote: true },
|
|
76
|
-
)
|
|
77
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from '@jest/globals'
|
|
2
|
-
import {
|
|
3
|
-
ageAssuranceRuleIDs,
|
|
4
|
-
computeAgeAssuranceRegionAccess,
|
|
5
|
-
getAgeAssuranceRegionConfig,
|
|
6
|
-
} from './age-assurance.js'
|
|
7
|
-
import { AppBskyAgeassuranceDefs } from './client/index.js'
|
|
8
|
-
|
|
9
|
-
describe('age-assurance', () => {
|
|
10
|
-
describe('getAgeAssuranceRegionConfig', () => {
|
|
11
|
-
const config: AppBskyAgeassuranceDefs.Config = {
|
|
12
|
-
regions: [
|
|
13
|
-
{
|
|
14
|
-
countryCode: 'US',
|
|
15
|
-
regionCode: 'CA',
|
|
16
|
-
minAccessAge: 13,
|
|
17
|
-
rules: [],
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
countryCode: 'US',
|
|
21
|
-
minAccessAge: 13,
|
|
22
|
-
rules: [],
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
it('should find region by country code only', () => {
|
|
28
|
-
const result = getAgeAssuranceRegionConfig(config, {
|
|
29
|
-
countryCode: 'US',
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
expect(result).toEqual({
|
|
33
|
-
countryCode: 'US',
|
|
34
|
-
minAccessAge: 13,
|
|
35
|
-
rules: [],
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should find region by country code and region code', () => {
|
|
40
|
-
const result = getAgeAssuranceRegionConfig(config, {
|
|
41
|
-
countryCode: 'US',
|
|
42
|
-
regionCode: 'CA',
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
expect(result).toEqual({
|
|
46
|
-
countryCode: 'US',
|
|
47
|
-
regionCode: 'CA',
|
|
48
|
-
minAccessAge: 13,
|
|
49
|
-
rules: [],
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('should return undefined when no matching region found', () => {
|
|
54
|
-
const result = getAgeAssuranceRegionConfig(config, {
|
|
55
|
-
countryCode: 'GB',
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
expect(result).toBeUndefined()
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe('computeAgeAssuranceRegionAccess', () => {
|
|
63
|
-
const region: AppBskyAgeassuranceDefs.ConfigRegion = {
|
|
64
|
-
countryCode: 'US',
|
|
65
|
-
minAccessAge: 13,
|
|
66
|
-
rules: [
|
|
67
|
-
{
|
|
68
|
-
$type: ageAssuranceRuleIDs.IfAccountNewerThan,
|
|
69
|
-
date: '2025-12-10T00:00:00Z',
|
|
70
|
-
access: 'none',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
$type: ageAssuranceRuleIDs.IfAssuredOverAge,
|
|
74
|
-
age: 18,
|
|
75
|
-
access: 'full',
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
$type: ageAssuranceRuleIDs.IfAssuredOverAge,
|
|
79
|
-
age: 16,
|
|
80
|
-
access: 'safe',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
$type: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
84
|
-
age: 16,
|
|
85
|
-
access: 'safe',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
$type: ageAssuranceRuleIDs.Default,
|
|
89
|
-
access: 'none',
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
it('should apply default if no data provided', () => {
|
|
95
|
-
const result = computeAgeAssuranceRegionAccess(region, {})
|
|
96
|
-
|
|
97
|
-
expect(result).toEqual({
|
|
98
|
-
access: 'none',
|
|
99
|
-
reason: ageAssuranceRuleIDs.Default,
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('IfAccountNewerThan', () => {
|
|
104
|
-
it('should block accounts created after threshold', () => {
|
|
105
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
106
|
-
accountCreatedAt: new Date(2025, 11, 15).toISOString(),
|
|
107
|
-
declaredAge: 18,
|
|
108
|
-
})
|
|
109
|
-
expect(result).toEqual({
|
|
110
|
-
access: 'none',
|
|
111
|
-
reason: ageAssuranceRuleIDs.IfAccountNewerThan,
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should allow accounts created before threshold', () => {
|
|
116
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
117
|
-
accountCreatedAt: new Date(2025, 10, 1).toISOString(),
|
|
118
|
-
declaredAge: 18,
|
|
119
|
-
})
|
|
120
|
-
expect(result).toEqual({
|
|
121
|
-
access: 'safe',
|
|
122
|
-
reason: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('should allow accounts created exactly at threshold', () => {
|
|
127
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
128
|
-
accountCreatedAt: new Date(2025, 11, 1).toISOString(),
|
|
129
|
-
declaredAge: 18,
|
|
130
|
-
})
|
|
131
|
-
expect(result).toEqual({
|
|
132
|
-
access: 'safe',
|
|
133
|
-
reason: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('should not apply rule when accountCreatedAt is not provided', () => {
|
|
138
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
139
|
-
declaredAge: 15,
|
|
140
|
-
})
|
|
141
|
-
expect(result).toEqual({
|
|
142
|
-
access: 'none',
|
|
143
|
-
reason: ageAssuranceRuleIDs.Default,
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should not apply rule when assuredAge is present', () => {
|
|
148
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
149
|
-
accountCreatedAt: new Date(2025, 11, 15).toISOString(),
|
|
150
|
-
assuredAge: 20,
|
|
151
|
-
})
|
|
152
|
-
expect(result).toEqual({
|
|
153
|
-
access: 'full',
|
|
154
|
-
reason: ageAssuranceRuleIDs.IfAssuredOverAge,
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
describe('IfDeclaredOverAge rule', () => {
|
|
160
|
-
it('should allow users at or above age threshold', () => {
|
|
161
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
162
|
-
declaredAge: 18,
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
expect(result).toEqual({
|
|
166
|
-
access: 'safe',
|
|
167
|
-
reason: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should allow users above age threshold', () => {
|
|
172
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
173
|
-
declaredAge: 25,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
expect(result).toEqual({
|
|
177
|
-
access: 'safe',
|
|
178
|
-
reason: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('should not allow users below age threshold', () => {
|
|
183
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
184
|
-
declaredAge: 17,
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
expect(result).toEqual({
|
|
188
|
-
access: 'safe',
|
|
189
|
-
reason: ageAssuranceRuleIDs.IfDeclaredOverAge,
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
describe('IfAssuredOverAge rule', () => {
|
|
195
|
-
it('should allow users at or above assured age threshold', () => {
|
|
196
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
197
|
-
assuredAge: 18,
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
expect(result).toEqual({
|
|
201
|
-
access: 'full',
|
|
202
|
-
reason: ageAssuranceRuleIDs.IfAssuredOverAge,
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('should not allow users below assured age threshold', () => {
|
|
207
|
-
const result = computeAgeAssuranceRegionAccess(region, {
|
|
208
|
-
assuredAge: 17,
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
expect(result).toEqual({
|
|
212
|
-
access: 'safe',
|
|
213
|
-
reason: ageAssuranceRuleIDs.IfAssuredOverAge,
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
})
|