@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.
Files changed (67) hide show
  1. package/README.md +126 -128
  2. package/dist/_test-util/mock.d.ts +2735 -0
  3. package/dist/_test-util/mock.d.ts.map +1 -0
  4. package/dist/_test-util/mock.js +109 -0
  5. package/dist/_test-util/mock.js.map +1 -0
  6. package/dist/_test-util/moderation-behavior.d.ts +57 -0
  7. package/dist/_test-util/moderation-behavior.d.ts.map +1 -0
  8. package/dist/_test-util/moderation-behavior.js +158 -0
  9. package/dist/_test-util/moderation-behavior.js.map +1 -0
  10. package/dist/behaviors.d.ts +22 -18
  11. package/dist/behaviors.d.ts.map +1 -0
  12. package/dist/behaviors.js +18 -21
  13. package/dist/behaviors.js.map +1 -1
  14. package/dist/decision.d.ts +26 -24
  15. package/dist/decision.d.ts.map +1 -0
  16. package/dist/decision.js +14 -16
  17. package/dist/decision.js.map +1 -1
  18. package/dist/index.d.ts +12 -11
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/internal/keyword-filter.d.ts +4 -3
  21. package/dist/internal/keyword-filter.d.ts.map +1 -0
  22. package/dist/internal/keyword-filter.js.map +1 -1
  23. package/dist/keyword-filter.d.ts +8 -6
  24. package/dist/keyword-filter.d.ts.map +1 -0
  25. package/dist/keyword-filter.js +8 -7
  26. package/dist/keyword-filter.js.map +1 -1
  27. package/dist/label.d.ts +31 -27
  28. package/dist/label.d.ts.map +1 -0
  29. package/dist/label.js +24 -28
  30. package/dist/label.js.map +1 -1
  31. package/dist/subjects/feed-generator.d.ts +4 -3
  32. package/dist/subjects/feed-generator.d.ts.map +1 -0
  33. package/dist/subjects/feed-generator.js.map +1 -1
  34. package/dist/subjects/list.d.ts +3 -2
  35. package/dist/subjects/list.d.ts.map +1 -0
  36. package/dist/subjects/list.js.map +1 -1
  37. package/dist/subjects/notification.d.ts +4 -3
  38. package/dist/subjects/notification.d.ts.map +1 -0
  39. package/dist/subjects/notification.js.map +1 -1
  40. package/dist/subjects/post.d.ts +5 -3
  41. package/dist/subjects/post.d.ts.map +1 -0
  42. package/dist/subjects/post.js.map +1 -1
  43. package/dist/subjects/profile.d.ts +3 -2
  44. package/dist/subjects/profile.d.ts.map +1 -0
  45. package/dist/subjects/profile.js.map +1 -1
  46. package/dist/types.d.ts +3 -2
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/ui.d.ts +3 -2
  49. package/dist/ui.d.ts.map +1 -0
  50. package/dist/ui.js +4 -4
  51. package/dist/ui.js.map +1 -1
  52. package/lib/_test-util/mock.ts +214 -0
  53. package/lib/_test-util/moderation-behavior.ts +259 -0
  54. package/lib/behaviors.ts +21 -18
  55. package/lib/decision.ts +26 -26
  56. package/lib/index.ts +11 -11
  57. package/lib/internal/keyword-filter.ts +1 -1
  58. package/lib/keyword-filter.ts +9 -6
  59. package/lib/label.ts +32 -28
  60. package/lib/subjects/feed-generator.ts +4 -4
  61. package/lib/subjects/list.ts +4 -4
  62. package/lib/subjects/notification.ts +4 -4
  63. package/lib/subjects/post.ts +6 -7
  64. package/lib/subjects/profile.ts +3 -3
  65. package/lib/types.ts +2 -2
  66. package/lib/ui.ts +7 -7
  67. package/package.json +14 -12
package/README.md CHANGED
@@ -1,167 +1,165 @@
1
1
  # @atcute/bluesky-moderation
2
2
 
3
- interprets Bluesky's content moderation labels.
3
+ interpret Bluesky content moderation labels and user preferences.
4
4
 
5
- ```ts
6
- import type { XRPC } from '@atcute/client';
7
- import type { AppBskyActorDefs, AppBskyFeedDefs, AppBskyLabelerDefs, At } from '@atcute/client/lexicons';
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
- declare const rpc: XRPC;
20
-
21
- // first, let's get the user's preferences
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
- const { data } = await rpc.get('app.bsky.actor.getPreferences', {});
33
+ // 2. moderate content
34
+ const decision = moderatePost(post, {
35
+ viewerDid: 'did:plc:...',
36
+ prefs,
37
+ labelDefs,
38
+ });
42
39
 
43
- const labelPrefs: AppBskyActorDefs.ContentLabelPref[] = [];
40
+ // 3. get display restrictions for your context
41
+ const ui = getDisplayRestrictions(decision, DisplayContext.ContentList);
44
42
 
45
- const globalLabelPrefs = (modPrefs.globalLabelPrefs ??= {});
46
- const prefsByLabelers = (modPrefs.prefsByLabelers ??= {});
43
+ if (ui.filters.length > 0) {
44
+ // don't show this post in feeds
45
+ }
47
46
 
48
- for (const pref of data.preferences) {
49
- switch (pref.$type) {
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
- break;
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
- for (const { labelerDid, label, visibility } of labelPrefs) {
78
- let pref: LabelPreference | undefined;
79
- switch (visibility) {
80
- case 'show':
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
- if (labelerDid === undefined) {
96
- globalLabelPrefs[label] = pref;
97
- } else if (labelerDid in prefsByLabelers) {
98
- const labelerPref = prefsByLabelers[labelerDid]!;
60
+ ### display contexts
99
61
 
100
- labelerPref.labelPrefs[label] = pref;
101
- }
102
- }
103
- }
62
+ use different contexts depending on where content appears:
104
63
 
105
- // grab labeler's definitions
106
- let labelers: AppBskyLabelerDefs.LabelerViewDetailed[] = [];
64
+ ```ts
65
+ // content in feeds/lists
66
+ getDisplayRestrictions(decision, DisplayContext.ContentList);
107
67
 
108
- {
109
- const { data } = await rpc.get('app.bsky.labeler.getServices', {
110
- params: {
111
- dids: [...labelerDids],
112
- detailed: true,
113
- },
114
- });
68
+ // content in expanded view
69
+ getDisplayRestrictions(decision, DisplayContext.ContentView);
115
70
 
116
- labelers = data.views.filter((view) => view.$type === 'app.bsky.labeler.defs#labelerViewDetailed');
117
- }
71
+ // images/videos in content
72
+ getDisplayRestrictions(decision, DisplayContext.ContentMedia);
118
73
 
119
- // interpret the labeler's definitions into something the library can understand
120
- const labelDefs = interpretLabelerDefinitions(labelers);
74
+ // profile in lists
75
+ getDisplayRestrictions(decision, DisplayContext.ProfileList);
121
76
 
122
- // then we call the appropriate moderation functions
123
- {
124
- declare const post: AppBskyFeedDefs.PostView;
77
+ // profile in expanded view
78
+ getDisplayRestrictions(decision, DisplayContext.ProfileView);
125
79
 
126
- const mod = moderatePost(post, {
127
- viewerDid: 'did:plc:xyz',
128
- labelDefs,
129
- prefs: modPrefs,
130
- });
80
+ // profile avatar/banner
81
+ getDisplayRestrictions(decision, DisplayContext.ProfileMedia);
82
+ ```
131
83
 
132
- // when displaying the post in feeds...
133
- {
134
- const ui = getDisplayRestrictions(mod, DisplayContext.ContentList);
84
+ ### loading preferences
135
85
 
136
- if (ui.filters.length > 0) {
137
- // don't include the post in the feed
138
- }
86
+ ```ts
87
+ import {
88
+ interpretLabelerDefinitions,
89
+ interpretMutedWordPreferences,
90
+ LabelPreference,
91
+ type ModerationPreferences,
92
+ } from '@atcute/bluesky-moderation';
139
93
 
140
- if (ui.blurs.length > 0) {
141
- // hide the post behind a cover
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
- if (ui.noOverride) {
144
- // don't allow the cover to be removed
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
- if (ui.alerts.length > 0 || ui.informs.length > 0) {
149
- // show warning/inform badges in the post
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
- // when displaying an expanded version of the post...
154
- {
155
- const ui = getDisplayRestrictions(mod, DisplayContext.ContentView);
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
- // when displaying images/videos of a post...
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
  ```