@atcute/bluesky-moderation 2.0.3 → 3.0.0
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/README.md +126 -128
- package/dist/_test-util/mock.d.ts +2735 -0
- package/dist/_test-util/mock.d.ts.map +1 -0
- package/dist/_test-util/mock.js +109 -0
- package/dist/_test-util/mock.js.map +1 -0
- package/dist/_test-util/moderation-behavior.d.ts +57 -0
- package/dist/_test-util/moderation-behavior.d.ts.map +1 -0
- package/dist/_test-util/moderation-behavior.js +158 -0
- package/dist/_test-util/moderation-behavior.js.map +1 -0
- package/dist/behaviors.d.ts +22 -18
- package/dist/behaviors.d.ts.map +1 -0
- package/dist/behaviors.js +18 -21
- package/dist/behaviors.js.map +1 -1
- package/dist/decision.d.ts +26 -24
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +14 -16
- package/dist/decision.js.map +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/internal/keyword-filter.d.ts +4 -3
- package/dist/internal/keyword-filter.d.ts.map +1 -0
- package/dist/internal/keyword-filter.js.map +1 -1
- package/dist/keyword-filter.d.ts +8 -6
- package/dist/keyword-filter.d.ts.map +1 -0
- package/dist/keyword-filter.js +8 -7
- package/dist/keyword-filter.js.map +1 -1
- package/dist/label.d.ts +31 -27
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +24 -28
- package/dist/label.js.map +1 -1
- package/dist/subjects/feed-generator.d.ts +4 -3
- package/dist/subjects/feed-generator.d.ts.map +1 -0
- package/dist/subjects/feed-generator.js.map +1 -1
- package/dist/subjects/list.d.ts +3 -2
- package/dist/subjects/list.d.ts.map +1 -0
- package/dist/subjects/list.js.map +1 -1
- package/dist/subjects/notification.d.ts +4 -3
- package/dist/subjects/notification.d.ts.map +1 -0
- package/dist/subjects/notification.js.map +1 -1
- package/dist/subjects/post.d.ts +5 -3
- package/dist/subjects/post.d.ts.map +1 -0
- package/dist/subjects/post.js.map +1 -1
- package/dist/subjects/profile.d.ts +3 -2
- package/dist/subjects/profile.d.ts.map +1 -0
- package/dist/subjects/profile.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -0
- package/dist/ui.d.ts +3 -2
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +4 -4
- package/dist/ui.js.map +1 -1
- package/lib/_test-util/mock.ts +214 -0
- package/lib/_test-util/moderation-behavior.ts +259 -0
- package/lib/behaviors.ts +21 -18
- package/lib/decision.ts +26 -26
- package/lib/index.ts +11 -11
- package/lib/internal/keyword-filter.ts +1 -1
- package/lib/keyword-filter.ts +9 -6
- package/lib/label.ts +32 -28
- package/lib/subjects/feed-generator.ts +4 -4
- package/lib/subjects/list.ts +4 -4
- package/lib/subjects/notification.ts +4 -4
- package/lib/subjects/post.ts +6 -7
- package/lib/subjects/profile.ts +3 -3
- package/lib/types.ts +2 -2
- package/lib/ui.ts +7 -7
- package/package.json +14 -12
package/dist/ui.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DisplayContext } from './behaviors.
|
|
2
|
-
import { type ModerationCause, type ModerationDecision } from './decision.
|
|
1
|
+
import { DisplayContext } from './behaviors.ts';
|
|
2
|
+
import { type ModerationCause, type ModerationDecision } from './decision.ts';
|
|
3
3
|
export interface DisplayRestrictions {
|
|
4
4
|
noOverride: boolean;
|
|
5
5
|
filters: ModerationCause[];
|
|
@@ -8,3 +8,4 @@ export interface DisplayRestrictions {
|
|
|
8
8
|
informs: ModerationCause[];
|
|
9
9
|
}
|
|
10
10
|
export declare const getDisplayRestrictions: (decision: ModerationDecision, context: DisplayContext) => DisplayRestrictions;
|
|
11
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/dist/ui.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../lib/ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,cAAc,EAMd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAuB,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGnG,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,OAAO,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,eAAO,MAAM,sBAAsB,gFA8JlC,CAAC"}
|
package/dist/ui.js
CHANGED
|
@@ -133,10 +133,10 @@ export const getDisplayRestrictions = (decision, context) => {
|
|
|
133
133
|
}
|
|
134
134
|
return {
|
|
135
135
|
noOverride,
|
|
136
|
-
filters: filters.sort(sortByPriority),
|
|
137
|
-
blurs: blurs.sort(sortByPriority),
|
|
138
|
-
alerts: alerts.sort(sortByPriority),
|
|
139
|
-
informs: informs.sort(sortByPriority),
|
|
136
|
+
filters: filters.sort(sortByPriority), // oxlint-disable-line unicorn/no-array-sort -- local array
|
|
137
|
+
blurs: blurs.sort(sortByPriority), // oxlint-disable-line unicorn/no-array-sort -- local array
|
|
138
|
+
alerts: alerts.sort(sortByPriority), // oxlint-disable-line unicorn/no-array-sort -- local array
|
|
139
|
+
informs: informs.sort(sortByPriority), // oxlint-disable-line unicorn/no-array-sort -- local array
|
|
140
140
|
};
|
|
141
141
|
};
|
|
142
142
|
const sortByPriority = (a, b) => {
|
package/dist/ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../lib/ui.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,cAAc,EACd,cAAc,EACd,aAAa,EACb,qBAAqB,EACrB,WAAW,EACX,gBAAgB,EAChB,aAAa,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAiD,MAAM,eAAe,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAU7C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACrC,QAA4B,EAC5B,OAAuB,EACD,EAAE;
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../lib/ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { ComAtprotoLabelDefs } from '@atcute/atproto';
|
|
2
|
+
import type {
|
|
3
|
+
AppBskyActorDefs,
|
|
4
|
+
AppBskyEmbedRecord,
|
|
5
|
+
AppBskyFeedDefs,
|
|
6
|
+
AppBskyFeedPost,
|
|
7
|
+
AppBskyGraphDefs,
|
|
8
|
+
AppBskyNotificationListNotifications,
|
|
9
|
+
} from '@atcute/bluesky';
|
|
10
|
+
import type { $type, Did, GenericUri, Handle, ResourceUri } from '@atcute/lexicons';
|
|
11
|
+
|
|
12
|
+
const FAKE_CID = 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq';
|
|
13
|
+
|
|
14
|
+
export const post = ({
|
|
15
|
+
text,
|
|
16
|
+
facets,
|
|
17
|
+
reply,
|
|
18
|
+
embed,
|
|
19
|
+
}: {
|
|
20
|
+
text: string;
|
|
21
|
+
facets?: AppBskyFeedPost.Main['facets'];
|
|
22
|
+
reply?: AppBskyFeedPost.ReplyRef;
|
|
23
|
+
embed?: AppBskyFeedPost.Main['embed'];
|
|
24
|
+
}): AppBskyFeedPost.Main => {
|
|
25
|
+
return {
|
|
26
|
+
$type: 'app.bsky.feed.post',
|
|
27
|
+
text,
|
|
28
|
+
facets,
|
|
29
|
+
reply,
|
|
30
|
+
embed,
|
|
31
|
+
langs: ['en'],
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const postView = ({
|
|
37
|
+
record,
|
|
38
|
+
author,
|
|
39
|
+
embed,
|
|
40
|
+
replyCount,
|
|
41
|
+
repostCount,
|
|
42
|
+
likeCount,
|
|
43
|
+
viewer,
|
|
44
|
+
labels,
|
|
45
|
+
}: {
|
|
46
|
+
record: AppBskyFeedPost.Main;
|
|
47
|
+
author: AppBskyActorDefs.ProfileViewBasic;
|
|
48
|
+
embed?: AppBskyFeedDefs.PostView['embed'];
|
|
49
|
+
replyCount?: number;
|
|
50
|
+
repostCount?: number;
|
|
51
|
+
likeCount?: number;
|
|
52
|
+
viewer?: AppBskyFeedDefs.ViewerState;
|
|
53
|
+
labels?: ComAtprotoLabelDefs.Label[];
|
|
54
|
+
}): $type.enforce<AppBskyFeedDefs.PostView> => {
|
|
55
|
+
return {
|
|
56
|
+
$type: 'app.bsky.feed.defs#postView',
|
|
57
|
+
uri: `at://${author.did}/app.bsky.feed.post/fake`,
|
|
58
|
+
cid: FAKE_CID,
|
|
59
|
+
author,
|
|
60
|
+
record,
|
|
61
|
+
embed,
|
|
62
|
+
replyCount,
|
|
63
|
+
repostCount,
|
|
64
|
+
likeCount,
|
|
65
|
+
indexedAt: new Date().toISOString(),
|
|
66
|
+
viewer,
|
|
67
|
+
labels,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const embedRecordView = ({
|
|
72
|
+
record,
|
|
73
|
+
author,
|
|
74
|
+
labels,
|
|
75
|
+
}: {
|
|
76
|
+
record: AppBskyFeedPost.Main;
|
|
77
|
+
author: AppBskyActorDefs.ProfileViewBasic;
|
|
78
|
+
labels?: ComAtprotoLabelDefs.Label[];
|
|
79
|
+
}): $type.enforce<AppBskyEmbedRecord.View> => {
|
|
80
|
+
return {
|
|
81
|
+
$type: 'app.bsky.embed.record#view',
|
|
82
|
+
record: {
|
|
83
|
+
$type: 'app.bsky.embed.record#viewRecord',
|
|
84
|
+
uri: `at://${author.did}/app.bsky.feed.post/fake`,
|
|
85
|
+
cid: FAKE_CID,
|
|
86
|
+
author,
|
|
87
|
+
value: record,
|
|
88
|
+
labels,
|
|
89
|
+
indexedAt: new Date().toISOString(),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const profileView = ({
|
|
95
|
+
handle,
|
|
96
|
+
displayName,
|
|
97
|
+
viewer,
|
|
98
|
+
labels,
|
|
99
|
+
}: {
|
|
100
|
+
handle: Handle;
|
|
101
|
+
displayName?: string;
|
|
102
|
+
viewer?: AppBskyActorDefs.ViewerState;
|
|
103
|
+
labels?: ComAtprotoLabelDefs.Label[];
|
|
104
|
+
}): $type.omit<AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic> => {
|
|
105
|
+
return {
|
|
106
|
+
did: `did:web:${handle}`,
|
|
107
|
+
handle,
|
|
108
|
+
displayName,
|
|
109
|
+
viewer,
|
|
110
|
+
labels,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const actorViewerState = ({
|
|
115
|
+
muted,
|
|
116
|
+
mutedByList,
|
|
117
|
+
blockedBy,
|
|
118
|
+
blocking,
|
|
119
|
+
blockingByList,
|
|
120
|
+
following,
|
|
121
|
+
followedBy,
|
|
122
|
+
}: {
|
|
123
|
+
muted?: boolean;
|
|
124
|
+
mutedByList?: AppBskyGraphDefs.ListViewBasic;
|
|
125
|
+
blockedBy?: boolean;
|
|
126
|
+
blocking?: ResourceUri;
|
|
127
|
+
blockingByList?: AppBskyGraphDefs.ListViewBasic;
|
|
128
|
+
following?: ResourceUri;
|
|
129
|
+
followedBy?: ResourceUri;
|
|
130
|
+
}): AppBskyActorDefs.ViewerState => {
|
|
131
|
+
return {
|
|
132
|
+
muted,
|
|
133
|
+
mutedByList,
|
|
134
|
+
blockedBy,
|
|
135
|
+
blocking,
|
|
136
|
+
blockingByList,
|
|
137
|
+
following,
|
|
138
|
+
followedBy,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const listViewBasic = ({ name }: { name: string }): AppBskyGraphDefs.ListViewBasic => {
|
|
143
|
+
return {
|
|
144
|
+
uri: 'at://did:plc:fake/app.bsky.graph.list/fake',
|
|
145
|
+
cid: FAKE_CID,
|
|
146
|
+
name,
|
|
147
|
+
purpose: 'app.bsky.graph.defs#modlist',
|
|
148
|
+
indexedAt: new Date().toISOString(),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const replyNotification = ({
|
|
153
|
+
author,
|
|
154
|
+
record,
|
|
155
|
+
labels,
|
|
156
|
+
}: {
|
|
157
|
+
record: AppBskyFeedPost.Main;
|
|
158
|
+
author: AppBskyActorDefs.ProfileView;
|
|
159
|
+
labels?: ComAtprotoLabelDefs.Label[];
|
|
160
|
+
}): AppBskyNotificationListNotifications.Notification => {
|
|
161
|
+
return {
|
|
162
|
+
uri: `at://${author.did}/app.bsky.feed.post/fake`,
|
|
163
|
+
cid: FAKE_CID,
|
|
164
|
+
author,
|
|
165
|
+
reason: 'reply',
|
|
166
|
+
reasonSubject: `at://${author.did}/app.bsky.feed.post/fake-parent`,
|
|
167
|
+
record,
|
|
168
|
+
isRead: false,
|
|
169
|
+
indexedAt: new Date().toISOString(),
|
|
170
|
+
labels,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const followNotification = ({
|
|
175
|
+
author,
|
|
176
|
+
subjectDid,
|
|
177
|
+
labels,
|
|
178
|
+
}: {
|
|
179
|
+
author: AppBskyActorDefs.ProfileView;
|
|
180
|
+
subjectDid: string;
|
|
181
|
+
labels?: ComAtprotoLabelDefs.Label[];
|
|
182
|
+
}): AppBskyNotificationListNotifications.Notification => {
|
|
183
|
+
return {
|
|
184
|
+
uri: `at://${author.did}/app.bsky.graph.follow/fake`,
|
|
185
|
+
cid: FAKE_CID,
|
|
186
|
+
author,
|
|
187
|
+
reason: 'follow',
|
|
188
|
+
record: {
|
|
189
|
+
$type: 'app.bsky.graph.follow',
|
|
190
|
+
createdAt: new Date().toISOString(),
|
|
191
|
+
subject: subjectDid,
|
|
192
|
+
},
|
|
193
|
+
isRead: false,
|
|
194
|
+
indexedAt: new Date().toISOString(),
|
|
195
|
+
labels,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const label = ({
|
|
200
|
+
val,
|
|
201
|
+
uri,
|
|
202
|
+
src,
|
|
203
|
+
}: {
|
|
204
|
+
val: string;
|
|
205
|
+
uri: GenericUri;
|
|
206
|
+
src?: Did;
|
|
207
|
+
}): ComAtprotoLabelDefs.Label => {
|
|
208
|
+
return {
|
|
209
|
+
src: src || 'did:plc:fake-labeler',
|
|
210
|
+
uri,
|
|
211
|
+
val,
|
|
212
|
+
cts: new Date().toISOString(),
|
|
213
|
+
};
|
|
214
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ComAtprotoLabelDefs } from '@atcute/atproto';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { LabelPreference, type DisplayRestrictions, type ModerationOptions } from '../index.ts';
|
|
6
|
+
|
|
7
|
+
import * as m from './mock.ts';
|
|
8
|
+
|
|
9
|
+
export type ModerationTestSuiteResultFlag = 'filter' | 'blur' | 'alert' | 'inform' | 'noOverride';
|
|
10
|
+
|
|
11
|
+
export interface ModerationTestSuiteScenario {
|
|
12
|
+
cfg: string;
|
|
13
|
+
subject: 'post' | 'profile' | 'userlist' | 'feedgen';
|
|
14
|
+
author: string;
|
|
15
|
+
quoteAuthor?: string;
|
|
16
|
+
labels: {
|
|
17
|
+
post?: string[];
|
|
18
|
+
profile?: string[];
|
|
19
|
+
account?: string[];
|
|
20
|
+
quotedPost?: string[];
|
|
21
|
+
quotedAccount?: string[];
|
|
22
|
+
};
|
|
23
|
+
behaviors: {
|
|
24
|
+
profileList?: ModerationTestSuiteResultFlag[];
|
|
25
|
+
profileView?: ModerationTestSuiteResultFlag[];
|
|
26
|
+
profileMedia?: ModerationTestSuiteResultFlag[];
|
|
27
|
+
contentList?: ModerationTestSuiteResultFlag[];
|
|
28
|
+
contentView?: ModerationTestSuiteResultFlag[];
|
|
29
|
+
contentMedia?: ModerationTestSuiteResultFlag[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SuiteUsers = Record<
|
|
34
|
+
string,
|
|
35
|
+
{
|
|
36
|
+
blocking: boolean;
|
|
37
|
+
blockingByList: boolean;
|
|
38
|
+
blockedBy: boolean;
|
|
39
|
+
muted: boolean;
|
|
40
|
+
mutedByList: boolean;
|
|
41
|
+
}
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export type SuiteConfigurations = Record<
|
|
45
|
+
string,
|
|
46
|
+
{
|
|
47
|
+
authed?: boolean;
|
|
48
|
+
adultContentEnabled?: boolean;
|
|
49
|
+
settings?: Record<string, LabelPreference>;
|
|
50
|
+
}
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
export type SuiteScenarios = Record<string, ModerationTestSuiteScenario>;
|
|
54
|
+
|
|
55
|
+
expect.extend({
|
|
56
|
+
toBeModerationResult(
|
|
57
|
+
actual: DisplayRestrictions,
|
|
58
|
+
expected: ModerationTestSuiteResultFlag[] | undefined,
|
|
59
|
+
context = '',
|
|
60
|
+
stringifiedResult: string | undefined = undefined,
|
|
61
|
+
_ignoreCause = false,
|
|
62
|
+
) {
|
|
63
|
+
const fail = (msg: string) => ({
|
|
64
|
+
pass: false,
|
|
65
|
+
message: () => `${msg}.${stringifiedResult ? ` Full result: ${stringifiedResult}` : ''}`,
|
|
66
|
+
});
|
|
67
|
+
// let cause = actual.causes?.type as string
|
|
68
|
+
// if (actual.cause?.type === 'label') {
|
|
69
|
+
// cause = `label:${actual.cause.labelDef.id}`
|
|
70
|
+
// } else if (actual.cause?.type === 'muted') {
|
|
71
|
+
// if (actual.cause.source.type === 'list') {
|
|
72
|
+
// cause = 'muted-by-list'
|
|
73
|
+
// }
|
|
74
|
+
// } else if (actual.cause?.type === 'blocking') {
|
|
75
|
+
// if (actual.cause.source.type === 'list') {
|
|
76
|
+
// cause = 'blocking-by-list'
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
79
|
+
if (!expected) {
|
|
80
|
+
// if (!ignoreCause && actual.cause) {
|
|
81
|
+
// return fail(`${context} expected to be a no-op, got ${cause}`)
|
|
82
|
+
// }
|
|
83
|
+
if (actual.informs.length > 0) {
|
|
84
|
+
return fail(`${context} expected to be a no-op, got inform=true`);
|
|
85
|
+
}
|
|
86
|
+
if (actual.alerts.length > 0) {
|
|
87
|
+
return fail(`${context} expected to be a no-op, got alert=true`);
|
|
88
|
+
}
|
|
89
|
+
if (actual.blurs.length > 0) {
|
|
90
|
+
return fail(`${context} expected to be a no-op, got blur=true`);
|
|
91
|
+
}
|
|
92
|
+
if (actual.filters.length > 0) {
|
|
93
|
+
return fail(`${context} expected to be a no-op, got filter=true`);
|
|
94
|
+
}
|
|
95
|
+
if (actual.noOverride) {
|
|
96
|
+
return fail(`${context} expected to be a no-op, got noOverride=true`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// if (!ignoreCause && cause !== expected.cause) {
|
|
100
|
+
// return fail(`${context} expected to be ${expected.cause}, got ${cause}`)
|
|
101
|
+
// }
|
|
102
|
+
const expectedInform = expected.includes('inform');
|
|
103
|
+
if (!!(actual.informs.length > 0) !== expectedInform) {
|
|
104
|
+
return fail(
|
|
105
|
+
`${context} expected to be inform=${expectedInform}, got ${actual.informs.length > 0 || false}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const expectedAlert = expected.includes('alert');
|
|
109
|
+
if (!!(actual.alerts.length > 0) !== expectedAlert) {
|
|
110
|
+
return fail(
|
|
111
|
+
`${context} expected to be alert=${expectedAlert}, got ${actual.alerts.length > 0 || false}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const expectedBlur = expected.includes('blur');
|
|
115
|
+
if (!!(actual.blurs.length > 0) !== expectedBlur) {
|
|
116
|
+
return fail(
|
|
117
|
+
`${context} expected to be blur=${expectedBlur}, got ${actual.blurs.length > 0 || false}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const expectedFilter = expected.includes('filter');
|
|
121
|
+
if (!!(actual.filters.length > 0) !== expectedFilter) {
|
|
122
|
+
return fail(
|
|
123
|
+
`${context} expected to be filter=${expectedFilter}, got ${actual.filters.length > 0 || false}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const expectedNoOverride = expected.includes('noOverride');
|
|
127
|
+
if (!!actual.noOverride !== expectedNoOverride) {
|
|
128
|
+
return fail(
|
|
129
|
+
`${context} expected to be noOverride=${expectedNoOverride}, got ${actual.noOverride || false}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { pass: true, message: () => '' };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
declare module 'vitest' {
|
|
138
|
+
// oxlint-disable-next-line no-unused-vars -- required for module augmentation
|
|
139
|
+
interface Assertion<T = any> {
|
|
140
|
+
toBeModerationResult(
|
|
141
|
+
expected?: ModerationTestSuiteResultFlag[],
|
|
142
|
+
context?: string,
|
|
143
|
+
stringifiedResult?: string,
|
|
144
|
+
ignoreCause?: boolean,
|
|
145
|
+
): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface AsymmetricMatchers {
|
|
149
|
+
toBeModerationResult(
|
|
150
|
+
expected?: ModerationTestSuiteResultFlag[],
|
|
151
|
+
context?: string,
|
|
152
|
+
stringifiedResult?: string,
|
|
153
|
+
ignoreCause?: boolean,
|
|
154
|
+
): any;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class ModerationBehaviorSuiteRunner {
|
|
159
|
+
users: SuiteUsers;
|
|
160
|
+
configurations: SuiteConfigurations;
|
|
161
|
+
scenarios: SuiteScenarios;
|
|
162
|
+
|
|
163
|
+
constructor(users: SuiteUsers, configurations: SuiteConfigurations, scenarios: SuiteScenarios) {
|
|
164
|
+
this.users = users;
|
|
165
|
+
this.configurations = configurations;
|
|
166
|
+
this.scenarios = scenarios;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
postScenario(scenario: ModerationTestSuiteScenario) {
|
|
170
|
+
if (scenario.subject !== 'post') {
|
|
171
|
+
throw new Error('Scenario subject must be "post"');
|
|
172
|
+
}
|
|
173
|
+
const author = this.profileView(scenario.author, scenario.labels);
|
|
174
|
+
return m.postView({
|
|
175
|
+
record: m.post({
|
|
176
|
+
text: 'Post text',
|
|
177
|
+
}),
|
|
178
|
+
author,
|
|
179
|
+
labels: (scenario.labels.post || []).map((val) =>
|
|
180
|
+
m.label({ val, uri: `at://${author.did}/app.bsky.feed.post/fake` }),
|
|
181
|
+
),
|
|
182
|
+
embed: scenario.quoteAuthor
|
|
183
|
+
? m.embedRecordView({
|
|
184
|
+
record: m.post({
|
|
185
|
+
text: 'Quoted post text',
|
|
186
|
+
}),
|
|
187
|
+
labels: (scenario.labels.quotedPost || []).map((val) =>
|
|
188
|
+
m.label({
|
|
189
|
+
val,
|
|
190
|
+
uri: `at://${author.did}/app.bsky.feed.post/fake`,
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
author: this.profileView(scenario.quoteAuthor, {
|
|
194
|
+
account: scenario.labels.quotedAccount,
|
|
195
|
+
}),
|
|
196
|
+
})
|
|
197
|
+
: undefined,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
profileScenario(scenario: ModerationTestSuiteScenario) {
|
|
202
|
+
if (scenario.subject !== 'profile') {
|
|
203
|
+
throw new Error('Scenario subject must be "profile"');
|
|
204
|
+
}
|
|
205
|
+
return this.profileView(scenario.author, scenario.labels);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
profileView(name: string, scenarioLabels: ModerationTestSuiteScenario['labels']) {
|
|
209
|
+
const def = this.users[name];
|
|
210
|
+
|
|
211
|
+
const labels: ComAtprotoLabelDefs.Label[] = [];
|
|
212
|
+
if (scenarioLabels.account) {
|
|
213
|
+
for (const l of scenarioLabels.account) {
|
|
214
|
+
labels.push(m.label({ val: l, uri: `did:web:${name}` }));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (scenarioLabels.profile) {
|
|
218
|
+
for (const l of scenarioLabels.profile) {
|
|
219
|
+
labels.push(
|
|
220
|
+
m.label({
|
|
221
|
+
val: l,
|
|
222
|
+
uri: `at://did:web:${name}/app.bsky.actor.profile/self`,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return m.profileView({
|
|
229
|
+
handle: `${name}.test`,
|
|
230
|
+
labels,
|
|
231
|
+
viewer: m.actorViewerState({
|
|
232
|
+
muted: def.muted || def.mutedByList,
|
|
233
|
+
mutedByList: def.mutedByList ? m.listViewBasic({ name: 'Fake List' }) : undefined,
|
|
234
|
+
blockedBy: def.blockedBy,
|
|
235
|
+
blocking:
|
|
236
|
+
def.blocking || def.blockingByList ? 'at://did:web:self.test/app.bsky.graph.block/fake' : undefined,
|
|
237
|
+
blockingByList: def.blockingByList ? m.listViewBasic({ name: 'Fake List' }) : undefined,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
moderationOptions(scenario: ModerationTestSuiteScenario): ModerationOptions {
|
|
243
|
+
return {
|
|
244
|
+
viewerDid: this.configurations[scenario.cfg].authed === false ? undefined : 'did:web:self.test',
|
|
245
|
+
prefs: {
|
|
246
|
+
adultContentEnabled: Boolean(this.configurations[scenario.cfg]?.adultContentEnabled),
|
|
247
|
+
globalLabelPrefs: this.configurations[scenario.cfg].settings || {},
|
|
248
|
+
prefsByLabelers: {
|
|
249
|
+
'did:plc:fake-labeler': {
|
|
250
|
+
labelPrefs: {},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
hiddenPosts: [],
|
|
254
|
+
keywordFilters: [],
|
|
255
|
+
temporaryMutes: [],
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
package/lib/behaviors.ts
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const LabelTarget = {
|
|
2
2
|
/** label is intended for account's content */
|
|
3
|
-
Content
|
|
3
|
+
Content: 'content',
|
|
4
4
|
/** label is intended for account's profile */
|
|
5
|
-
Profile
|
|
5
|
+
Profile: 'profile',
|
|
6
6
|
/** label is intended for account's content and profile */
|
|
7
|
-
Account
|
|
8
|
-
}
|
|
7
|
+
Account: 'account',
|
|
8
|
+
} as const;
|
|
9
|
+
export type LabelTarget = (typeof LabelTarget)[keyof typeof LabelTarget];
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
+
export const DisplayContext = {
|
|
11
12
|
/** content in expanded view */
|
|
12
|
-
ContentView
|
|
13
|
+
ContentView: 'contentView',
|
|
13
14
|
/** images or video contained in content */
|
|
14
|
-
ContentMedia
|
|
15
|
+
ContentMedia: 'contentMedia',
|
|
15
16
|
/** content in a list/feed */
|
|
16
|
-
ContentList
|
|
17
|
+
ContentList: 'contentList',
|
|
17
18
|
|
|
18
19
|
/** profile in expanded view */
|
|
19
|
-
ProfileView
|
|
20
|
+
ProfileView: 'profileView',
|
|
20
21
|
/** profile's avatar or banner */
|
|
21
|
-
ProfileMedia
|
|
22
|
+
ProfileMedia: 'profileMedia',
|
|
22
23
|
/** profile in a list */
|
|
23
|
-
ProfileList
|
|
24
|
-
}
|
|
24
|
+
ProfileList: 'profileList',
|
|
25
|
+
} as const;
|
|
26
|
+
export type DisplayContext = (typeof DisplayContext)[keyof typeof DisplayContext];
|
|
25
27
|
|
|
26
|
-
export
|
|
28
|
+
export const ModerationAction = {
|
|
27
29
|
/** should cause blurring */
|
|
28
|
-
Blur
|
|
30
|
+
Blur: 'blur',
|
|
29
31
|
/** should cause an alert */
|
|
30
|
-
Alert
|
|
32
|
+
Alert: 'alert',
|
|
31
33
|
/** should cause a notice */
|
|
32
|
-
Inform
|
|
33
|
-
}
|
|
34
|
+
Inform: 'inform',
|
|
35
|
+
} as const;
|
|
36
|
+
export type ModerationAction = (typeof ModerationAction)[keyof typeof ModerationAction];
|
|
34
37
|
|
|
35
38
|
export type BehaviorMapping = { [C in DisplayContext]?: ModerationAction };
|
|
36
39
|
export type LabelBehaviorMatrix = { [T in LabelTarget]: BehaviorMapping };
|