@atcute/bluesky-moderation 2.0.4 → 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 +21 -18
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/behaviors.js +18 -21
- package/dist/behaviors.js.map +1 -1
- package/dist/decision.d.ts +25 -24
- package/dist/decision.d.ts.map +1 -1
- package/dist/decision.js +14 -16
- package/dist/decision.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/internal/keyword-filter.d.ts +3 -3
- package/dist/internal/keyword-filter.d.ts.map +1 -1
- package/dist/internal/keyword-filter.js.map +1 -1
- package/dist/keyword-filter.d.ts +7 -6
- package/dist/keyword-filter.d.ts.map +1 -1
- package/dist/keyword-filter.js +8 -7
- package/dist/keyword-filter.js.map +1 -1
- package/dist/label.d.ts +30 -27
- package/dist/label.d.ts.map +1 -1
- package/dist/label.js +24 -28
- package/dist/label.js.map +1 -1
- package/dist/subjects/feed-generator.d.ts +3 -3
- package/dist/subjects/feed-generator.d.ts.map +1 -1
- package/dist/subjects/feed-generator.js.map +1 -1
- package/dist/subjects/list.d.ts +2 -2
- package/dist/subjects/list.d.ts.map +1 -1
- package/dist/subjects/list.js.map +1 -1
- package/dist/subjects/notification.d.ts +3 -3
- package/dist/subjects/notification.d.ts.map +1 -1
- package/dist/subjects/notification.js.map +1 -1
- package/dist/subjects/post.d.ts +4 -3
- package/dist/subjects/post.d.ts.map +1 -1
- package/dist/subjects/post.js.map +1 -1
- package/dist/subjects/profile.d.ts +2 -2
- package/dist/subjects/profile.d.ts.map +1 -1
- package/dist/subjects/profile.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/ui.d.ts +2 -2
- package/dist/ui.d.ts.map +1 -1
- 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/README.md
CHANGED
|
@@ -1,167 +1,165 @@
|
|
|
1
1
|
# @atcute/bluesky-moderation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interpret Bluesky content moderation labels and user preferences.
|
|
4
4
|
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
```sh
|
|
6
|
+
npm install @atcute/bluesky-moderation
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
evaluates posts, profiles, lists, and other content against moderation labels, mutes, blocks, and
|
|
10
|
+
keyword filters to determine how they should be displayed.
|
|
11
|
+
|
|
12
|
+
## usage
|
|
8
13
|
|
|
14
|
+
### basic flow
|
|
15
|
+
|
|
16
|
+
1. fetch user preferences and labeler definitions
|
|
17
|
+
2. run moderation functions on content
|
|
18
|
+
3. get display restrictions for your UI context
|
|
19
|
+
|
|
20
|
+
```ts
|
|
9
21
|
import {
|
|
10
22
|
DisplayContext,
|
|
11
23
|
getDisplayRestrictions,
|
|
12
24
|
interpretLabelerDefinitions,
|
|
13
|
-
interpretMutedWordPreferences,
|
|
14
|
-
LabelPreference,
|
|
15
25
|
moderatePost,
|
|
16
26
|
type ModerationPreferences,
|
|
17
27
|
} from '@atcute/bluesky-moderation';
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const labelerDids = new Set<At.Did>([
|
|
23
|
-
// Bluesky moderation service
|
|
24
|
-
'did:plc:ar7c4by46qjdydhdevvrndac',
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
const modPrefs: ModerationPreferences = {
|
|
28
|
-
adultContentEnabled: false,
|
|
29
|
-
globalLabelPrefs: {},
|
|
30
|
-
prefsByLabelers: {
|
|
31
|
-
'did:plc:ar7c4by46qjdydhdevvrndac': {
|
|
32
|
-
labelPrefs: {},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
keywordFilters: [],
|
|
36
|
-
hiddenPosts: [],
|
|
37
|
-
temporaryMutes: [],
|
|
38
|
-
};
|
|
29
|
+
// 1. set up preferences (see "loading preferences" below)
|
|
30
|
+
const prefs: ModerationPreferences = { ... };
|
|
31
|
+
const labelDefs = interpretLabelerDefinitions(labelers);
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
// 2. moderate content
|
|
34
|
+
const decision = moderatePost(post, {
|
|
35
|
+
viewerDid: 'did:plc:...',
|
|
36
|
+
prefs,
|
|
37
|
+
labelDefs,
|
|
38
|
+
});
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
// 3. get display restrictions for your context
|
|
41
|
+
const ui = getDisplayRestrictions(decision, DisplayContext.ContentList);
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (ui.filters.length > 0) {
|
|
44
|
+
// don't show this post in feeds
|
|
45
|
+
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'app.bsky.actor.defs#adultContentPref': {
|
|
51
|
-
modPrefs.adultContentEnabled = pref.enabled;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
case 'app.bsky.actor.defs#labelersPref': {
|
|
55
|
-
for (const labeler of pref.labelers) {
|
|
56
|
-
prefsByLabelers[labeler.did] ??= { labelPrefs: {} };
|
|
57
|
-
labelerDids.add(labeler.did);
|
|
58
|
-
}
|
|
47
|
+
if (ui.blurs.length > 0) {
|
|
48
|
+
// hide behind a content warning
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
case 'app.bsky.actor.defs#contentLabelPref': {
|
|
63
|
-
labelPrefs.push(pref);
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
case 'app.bsky.actor.defs#mutedWordsPref': {
|
|
67
|
-
modPrefs.keywordFilters = interpretMutedWordPreferences(pref);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
case 'app.bsky.actor.defs#hiddenPostsPref': {
|
|
71
|
-
modPrefs.hiddenPosts = pref.items as At.CanonicalResourceUri[];
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
50
|
+
if (ui.noOverride) {
|
|
51
|
+
// don't allow user to reveal
|
|
75
52
|
}
|
|
53
|
+
}
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case 'ignore': {
|
|
82
|
-
pref = LabelPreference.Ignore;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
case 'warn': {
|
|
86
|
-
pref = LabelPreference.Warn;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
case 'hide': {
|
|
90
|
-
pref = LabelPreference.Hide;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
55
|
+
if (ui.alerts.length > 0 || ui.informs.length > 0) {
|
|
56
|
+
// show warning badges
|
|
57
|
+
}
|
|
58
|
+
```
|
|
94
59
|
|
|
95
|
-
|
|
96
|
-
globalLabelPrefs[label] = pref;
|
|
97
|
-
} else if (labelerDid in prefsByLabelers) {
|
|
98
|
-
const labelerPref = prefsByLabelers[labelerDid]!;
|
|
60
|
+
### display contexts
|
|
99
61
|
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
62
|
+
use different contexts depending on where content appears:
|
|
104
63
|
|
|
105
|
-
|
|
106
|
-
|
|
64
|
+
```ts
|
|
65
|
+
// content in feeds/lists
|
|
66
|
+
getDisplayRestrictions(decision, DisplayContext.ContentList);
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
params: {
|
|
111
|
-
dids: [...labelerDids],
|
|
112
|
-
detailed: true,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
68
|
+
// content in expanded view
|
|
69
|
+
getDisplayRestrictions(decision, DisplayContext.ContentView);
|
|
115
70
|
|
|
116
|
-
|
|
117
|
-
|
|
71
|
+
// images/videos in content
|
|
72
|
+
getDisplayRestrictions(decision, DisplayContext.ContentMedia);
|
|
118
73
|
|
|
119
|
-
//
|
|
120
|
-
|
|
74
|
+
// profile in lists
|
|
75
|
+
getDisplayRestrictions(decision, DisplayContext.ProfileList);
|
|
121
76
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
declare const post: AppBskyFeedDefs.PostView;
|
|
77
|
+
// profile in expanded view
|
|
78
|
+
getDisplayRestrictions(decision, DisplayContext.ProfileView);
|
|
125
79
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
prefs: modPrefs,
|
|
130
|
-
});
|
|
80
|
+
// profile avatar/banner
|
|
81
|
+
getDisplayRestrictions(decision, DisplayContext.ProfileMedia);
|
|
82
|
+
```
|
|
131
83
|
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
const ui = getDisplayRestrictions(mod, DisplayContext.ContentList);
|
|
84
|
+
### loading preferences
|
|
135
85
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
interpretLabelerDefinitions,
|
|
89
|
+
interpretMutedWordPreferences,
|
|
90
|
+
LabelPreference,
|
|
91
|
+
type ModerationPreferences,
|
|
92
|
+
} from '@atcute/bluesky-moderation';
|
|
139
93
|
|
|
140
|
-
|
|
141
|
-
|
|
94
|
+
// fetch user preferences
|
|
95
|
+
const { data } = await rpc.get('app.bsky.actor.getPreferences', {});
|
|
96
|
+
|
|
97
|
+
const prefs: ModerationPreferences = {
|
|
98
|
+
adultContentEnabled: false,
|
|
99
|
+
globalLabelPrefs: {},
|
|
100
|
+
prefsByLabelers: {},
|
|
101
|
+
keywordFilters: [],
|
|
102
|
+
hiddenPosts: [],
|
|
103
|
+
temporaryMutes: [],
|
|
104
|
+
};
|
|
142
105
|
|
|
143
|
-
|
|
144
|
-
|
|
106
|
+
for (const pref of data.preferences) {
|
|
107
|
+
switch (pref.$type) {
|
|
108
|
+
case 'app.bsky.actor.defs#adultContentPref':
|
|
109
|
+
prefs.adultContentEnabled = pref.enabled;
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'app.bsky.actor.defs#contentLabelPref':
|
|
113
|
+
// map visibility to LabelPreference
|
|
114
|
+
const labelPref =
|
|
115
|
+
pref.visibility === 'hide'
|
|
116
|
+
? LabelPreference.Hide
|
|
117
|
+
: pref.visibility === 'warn'
|
|
118
|
+
? LabelPreference.Warn
|
|
119
|
+
: LabelPreference.Ignore;
|
|
120
|
+
|
|
121
|
+
if (pref.labelerDid) {
|
|
122
|
+
prefs.prefsByLabelers[pref.labelerDid] ??= { labelPrefs: {} };
|
|
123
|
+
prefs.prefsByLabelers[pref.labelerDid].labelPrefs[pref.label] = labelPref;
|
|
124
|
+
} else {
|
|
125
|
+
prefs.globalLabelPrefs[pref.label] = labelPref;
|
|
145
126
|
}
|
|
146
|
-
|
|
127
|
+
break;
|
|
147
128
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
case 'app.bsky.actor.defs#mutedWordsPref':
|
|
130
|
+
prefs.keywordFilters = interpretMutedWordPreferences(pref);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'app.bsky.actor.defs#hiddenPostsPref':
|
|
134
|
+
prefs.hiddenPosts = pref.items;
|
|
135
|
+
break;
|
|
151
136
|
}
|
|
137
|
+
}
|
|
152
138
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
139
|
+
// fetch labeler definitions
|
|
140
|
+
const { data: labelerData } = await rpc.get('app.bsky.labeler.getServices', {
|
|
141
|
+
params: { dids: [...labelerDids], detailed: true },
|
|
142
|
+
});
|
|
156
143
|
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
const labelDefs = interpretLabelerDefinitions(
|
|
145
|
+
labelerData.views.filter((v) => v.$type === 'app.bsky.labeler.defs#labelerViewDetailed'),
|
|
146
|
+
);
|
|
147
|
+
```
|
|
159
148
|
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
const ui = getDisplayRestrictions(mod, DisplayContext.ProfileMedia);
|
|
149
|
+
### moderating different content types
|
|
163
150
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
```ts
|
|
152
|
+
import {
|
|
153
|
+
moderateFeedGenerator,
|
|
154
|
+
moderateList,
|
|
155
|
+
moderateNotification,
|
|
156
|
+
moderatePost,
|
|
157
|
+
moderateProfile,
|
|
158
|
+
} from '@atcute/bluesky-moderation';
|
|
159
|
+
|
|
160
|
+
const postDecision = moderatePost(post, opts);
|
|
161
|
+
const profileDecision = moderateProfile(profile, opts);
|
|
162
|
+
const listDecision = moderateList(list, opts);
|
|
163
|
+
const feedDecision = moderateFeedGenerator(feed, opts);
|
|
164
|
+
const notifDecision = moderateNotification(notification, opts);
|
|
167
165
|
```
|