tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
host-configurable moderation policy
whey.party
1 month ago
589d29f8
511e5391
+151
-61
3 changed files
expand all
collapse all
unified
split
policy.ts
src
components
ModerationInitializer.tsx
UniversalPostRenderer.tsx
+17
policy.ts
···
1
1
+
export const FORCED_LABELER_DIDS = [
2
2
+
"did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation
3
3
+
];
4
4
+
5
5
+
export const UNAUTHED_FORCE_WARN_LABELS = new Set([
6
6
+
// i dont know if some of these are even valid labels
7
7
+
"porn",
8
8
+
"sexual",
9
9
+
"graphic-media",
10
10
+
"nudity",
11
11
+
"nsfl",
12
12
+
"corpse",
13
13
+
"gore",
14
14
+
"!no-unauthenticated"
15
15
+
]);
16
16
+
17
17
+
export const UNAUTHED_PREVENT_OPENING_WARNS = true;
+113
-56
src/components/ModerationInitializer.tsx
···
1
1
import { useQueries } from "@tanstack/react-query";
2
2
-
import { useSetAtom } from "jotai";
3
3
-
import { useEffect } from "react";
2
2
+
import { useAtom, useSetAtom } from "jotai";
3
3
+
import { useEffect, useRef } from "react";
4
4
5
5
+
import { FORCED_LABELER_DIDS, UNAUTHED_FORCE_WARN_LABELS } from "~/../policy";
5
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
7
import { labelerConfigAtom } from "~/state/moderationAtoms";
7
8
import type { LabelerDefinition, LabelPreference, LabelValueDefinition } from "~/types/moderation";
9
9
+
import { slingshotURLAtom } from "~/utils/atoms";
8
10
import { useQueryIdentity } from "~/utils/useQuery";
9
11
import { useQueryPreferences } from "~/utils/useQuery";
10
12
11
11
-
export const BSKY_LABELER_DID = "did:plc:ar7c4by46qjdydhdevvrndac";
12
12
-
13
13
// Manual DID document resolution
14
14
const fetchDidDocument = async (did: string): Promise<any> => {
15
15
if (did.startsWith("did:plc:")) {
16
16
-
// For PLC DIDs, fetch from plc.directory
17
16
const response = await fetch(
18
17
`https://plc.directory/${encodeURIComponent(did)}`,
19
18
);
···
21
20
throw new Error(`Failed to fetch PLC DID document for ${did}`);
22
21
return response.json();
23
22
} else if (did.startsWith("did:web:")) {
24
24
-
// For web DIDs, fetch from well-known
25
23
const handle = did.replace("did:web:", "");
26
24
const url = `https://${handle}/.well-known/did.json`;
27
25
const response = await fetch(url);
···
36
34
};
37
35
38
36
export const ModerationInitializer = () => {
39
39
-
const { agent } = useAuth();
37
37
+
const { agent, status } = useAuth();
40
38
const setLabelerConfig = useSetAtom(labelerConfigAtom);
39
39
+
const [slingshoturl] = useAtom(slingshotURLAtom);
41
40
42
42
-
// 1. Get User Identity to get PDS URL
41
41
+
// Define clear boolean for mode
42
42
+
const isUnauthed = status === "signedOut" || !agent;
43
43
+
44
44
+
// Track previous status to detect transitions
45
45
+
const prevStatusRef = useRef(status);
46
46
+
47
47
+
// --- 1. THE HARD FLUSH ---
48
48
+
// When Auth Status changes (Logged In <-> Logged Out), immediately wipe the config.
49
49
+
// This prevents "Authed" prefs from bleeding into "Unauthed" state and vice versa
50
50
+
// while the async queries are spinning up.
51
51
+
useEffect(() => {
52
52
+
if (prevStatusRef.current !== status) {
53
53
+
console.log(`[Moderation] Auth status changed (${prevStatusRef.current} -> ${status}). Flushing config.`);
54
54
+
setLabelerConfig([]); // <--- WIPE CLEAN
55
55
+
prevStatusRef.current = status;
56
56
+
}
57
57
+
}, [status, setLabelerConfig]);
58
58
+
59
59
+
// 2. Get User Identity (Only if authed)
43
60
const { data: identity } = useQueryIdentity(agent?.did);
44
61
45
45
-
// 2. Get User Preferences (Global: "porn" -> "hide")
62
62
+
// 3. Get User Preferences (Only if authed)
46
63
const { data: prefs } = useQueryPreferences({
47
64
agent: agent ?? undefined,
48
65
pdsUrl: identity?.pds,
49
66
});
50
67
51
51
-
// 3. Identify Labeler DIDs from prefs
52
52
-
const userPrefDids =
53
53
-
prefs?.preferences
54
54
-
?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref")
55
55
-
?.labelers?.map((l: any) => l.did) ?? [];
68
68
+
// 4. Identify Labeler DIDs
69
69
+
// Important: If unauthed, userPrefDids MUST be empty, even if cache exists.
70
70
+
const userPrefDids = !isUnauthed
71
71
+
? prefs?.preferences
72
72
+
?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref")
73
73
+
?.labelers?.map((l: any) => l.did) ?? []
74
74
+
: [];
56
75
57
57
-
// 2. MERGE: Force Bsky DID + User DIDs (Set removes duplicates)
76
76
+
// 5. Force Bsky DID + User DIDs
58
77
const activeLabelerDids = Array.from(
59
59
-
new Set([BSKY_LABELER_DID, ...userPrefDids])
78
78
+
new Set([...FORCED_LABELER_DIDS, ...userPrefDids])
60
79
);
61
80
62
62
-
// 4. Parallel fetch all Labeler DID Documents and Service Records
81
81
+
// 6. Parallel fetch DID Docs
63
82
const labelerDidDocQueries = useQueries({
64
83
queries: activeLabelerDids.map((did: string) => ({
65
84
queryKey: ["labelerDidDoc", did],
66
85
queryFn: () => fetchDidDocument(did),
67
67
-
staleTime: 5 * 60 * 1000, // 5 minutes
68
68
-
retry: 1, // Only retry once for DID docs
86
86
+
staleTime: 1000 * 60 * 60 * 24,
69
87
})),
70
88
});
71
89
90
90
+
// 7. Parallel fetch Service Records
72
91
const labelerServiceQueries = useQueries({
73
92
queries: activeLabelerDids.map((did: string) => ({
74
93
queryKey: ["labelerService", did],
75
94
queryFn: async () => {
76
76
-
if (!identity?.pds) throw new Error("No PDS URL");
95
95
+
const host = slingshoturl || "public.api.bsky.app";
77
96
const response = await fetch(
78
78
-
`${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`,
97
97
+
`https://${host}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`,
79
98
);
80
99
if (!response.ok) throw new Error("Failed to fetch labeler service");
81
100
return response.json();
82
101
},
83
83
-
enabled: !!identity?.pds && !!agent,
84
84
-
staleTime: 5 * 60 * 1000, // 5 minutes
102
102
+
staleTime: 1000 * 60 * 60,
85
103
})),
86
104
});
87
105
88
106
useEffect(() => {
107
107
+
// Guard: Wait for queries
89
108
if (
90
90
-
!prefs ||
91
109
labelerDidDocQueries.some((q) => q.isLoading) ||
92
92
-
labelerDidDocQueries.some((q) => q.isFetching) ||
93
93
-
labelerServiceQueries.some((q) => q.isLoading) ||
94
94
-
labelerServiceQueries.some((q) => q.isFetching)
95
95
-
)
110
110
+
labelerServiceQueries.some((q) => q.isLoading)
111
111
+
) {
96
112
return;
113
113
+
}
97
114
98
98
-
// Extract content label preferences
99
99
-
const contentLabelPrefs =
100
100
-
prefs.preferences?.filter(
101
101
-
(pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref",
102
102
-
) ?? [];
115
115
+
// Guard: If we are supposed to be Authed, but prefs haven't loaded yet,
116
116
+
// DO NOT run the logic. Wait. This prevents falling back to defaults temporarily.
117
117
+
if (!isUnauthed && !prefs) {
118
118
+
return;
119
119
+
}
103
120
121
121
+
// A. Extract User Global Overrides
122
122
+
// STRICT SEPARATION: If unauthed, force this to be empty to ensure no leakage.
104
123
const globalPrefs: Record<string, LabelPreference> = {};
105
105
-
contentLabelPrefs.forEach((pref: any) => {
106
106
-
globalPrefs[pref.label] = pref.visibility as LabelPreference;
107
107
-
});
124
124
+
125
125
+
if (!isUnauthed && prefs?.preferences) {
126
126
+
const contentLabelPrefs = prefs.preferences.filter(
127
127
+
(pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref",
128
128
+
);
129
129
+
contentLabelPrefs.forEach((pref: any) => {
130
130
+
globalPrefs[pref.label] = pref.visibility as LabelPreference;
131
131
+
});
132
132
+
}
108
133
109
134
const definitions: LabelerDefinition[] = activeLabelerDids
110
135
.map((did: string, index: number) => {
···
113
138
114
139
if (!didDocQuery.data || !serviceQuery.data) return null;
115
140
116
116
-
// Extract service endpoint from DID document
117
141
const didDoc = didDocQuery.data as any;
118
142
const atprotoLabelerService = didDoc?.service?.find(
119
143
(s: any) => s.id === "#atproto_labeler",
120
144
);
121
145
122
122
-
const record = (serviceQuery.data as any).value; // The raw ATProto record
146
146
+
const record = (serviceQuery.data as any).value;
123
147
124
124
-
// 1. Create the Metadata Map
148
148
+
// B. Gather ALL identifiers
149
149
+
const allIdentifiers = new Set<string>();
150
150
+
record.policies?.labelValues?.forEach((val: string) => allIdentifiers.add(val));
151
151
+
record.policies?.labelValueDefinitions?.forEach((def: any) => allIdentifiers.add(def.identifier));
152
152
+
153
153
+
// C. Create Metadata Map
125
154
const labelDefs: Record<string, LabelValueDefinition> = {};
126
126
-
127
155
if (record.policies.labelValueDefinitions) {
128
156
record.policies.labelValueDefinitions.forEach((def: any) => {
129
157
labelDefs[def.identifier] = {
···
132
160
blurs: def.blurs,
133
161
adultOnly: def.adultOnly,
134
162
defaultSetting: def.defaultSetting,
135
135
-
locales: def.locales || [] // <--- Capture the locales array
163
163
+
locales: def.locales || []
136
164
};
137
165
});
138
166
}
139
167
140
140
-
// RESOLUTION LOGIC:
141
141
-
// Map record.policies.labelValueDefinitions to a lookup map.
142
142
-
// Priority: User Global Pref > Labeler Default > 'ignore'
168
168
+
// D. Resolve Preferences
143
169
const supportedLabels: Record<string, LabelPreference> = {};
144
170
145
145
-
record.policies?.labelValues?.forEach((val: string) => {
146
146
-
// Does user have a global override for this string?
171
171
+
allIdentifiers.forEach((val) => {
172
172
+
// todo this works but with how useModeration hooks works right now old verdicts wont get stale-d
173
173
+
// it only works right now because these are warns and warns are negligable i guess
174
174
+
// --- BRANCH 1: UNAUTHED MODE ---
175
175
+
if (isUnauthed) {
176
176
+
// 1. Strict Force Overrides
177
177
+
if (UNAUTHED_FORCE_WARN_LABELS.has(val)) {
178
178
+
supportedLabels[val] = "warn"; // or 'hide' if that's what your policy constant implies
179
179
+
return;
180
180
+
}
181
181
+
182
182
+
// 2. Default Labeler Settings
183
183
+
const def = labelDefs[val];
184
184
+
const rawDefault = def?.defaultSetting || "ignore";
185
185
+
186
186
+
// 3. Apply Unauthed-Specific Aliasing (Optional)
187
187
+
// e.g., if you want to hide 'inform' labels for unauthed users
188
188
+
supportedLabels[val] = rawDefault as LabelPreference;
189
189
+
return;
190
190
+
}
191
191
+
192
192
+
// --- BRANCH 2: AUTHED MODE ---
193
193
+
// 1. User Global Override (Highest Priority)
147
194
const globalPref = globalPrefs[val];
148
148
-
// Or use labeler default
149
149
-
const defaultPref =
150
150
-
record.policies?.labelValueDefinitions?.find(
151
151
-
(d: any) => d.identifier === val,
152
152
-
)?.defaultSetting || "ignore";
195
195
+
if (globalPref) {
196
196
+
supportedLabels[val] = globalPref;
197
197
+
return;
198
198
+
}
153
199
154
154
-
supportedLabels[val] = (globalPref || defaultPref) as LabelPreference;
200
200
+
// 2. Labeler Default
201
201
+
const def = labelDefs[val];
202
202
+
const rawDefault = def?.defaultSetting || "ignore";
203
203
+
204
204
+
supportedLabels[val] = rawDefault as LabelPreference;
155
205
});
156
206
157
207
return {
158
208
did: did,
159
209
url: atprotoLabelerService?.serviceEndpoint || record.serviceEndpoint,
160
160
-
isDefault: false, // logic to determine if this is a default Bluesky labeler
210
210
+
isDefault: FORCED_LABELER_DIDS.includes(did),
161
211
supportedLabels,
162
212
labelDefs,
163
213
};
···
165
215
.filter(Boolean) as LabelerDefinition[];
166
216
167
217
setLabelerConfig(definitions);
168
168
-
}, [prefs, labelerDidDocQueries, labelerServiceQueries, setLabelerConfig, identity?.pds, activeLabelerDids]);
218
218
+
}, [
219
219
+
prefs,
220
220
+
labelerDidDocQueries,
221
221
+
labelerServiceQueries,
222
222
+
setLabelerConfig,
223
223
+
activeLabelerDids,
224
224
+
isUnauthed // <--- Critical dependency triggers re-eval on login/out
225
225
+
]);
169
226
170
170
-
return null; // Headless component
171
171
-
};
227
227
+
return null;
228
228
+
};
+21
-5
src/components/UniversalPostRenderer.tsx
···
15
15
import * as React from "react";
16
16
import { useEffect, useState } from "react";
17
17
18
18
+
import { UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy";
18
19
import defaultpfp from "~/../public/defaultpfp.png";
19
20
import { useLabelInfo } from "~/hooks/useLabelInfo";
20
21
import { useModeration } from "~/hooks/useModeration";
···
599
600
post.viewer?.repost ? true : false,
600
601
);
601
602
const [, setComposerPost] = useAtom(composerAtom);
602
602
-
const { agent } = useAuth();
603
603
+
const { agent, status } = useAuth();
603
604
const [retweetUri, setRetweetUri] = useState<string | undefined>(
604
605
post.viewer?.repost,
605
606
);
···
676
677
const isMainItem = false;
677
678
const setMainItem = (any: any) => { };
678
679
680
680
+
const hideWarnsWhenUnauthed = UNAUTHED_PREVENT_OPENING_WARNS && status === "signedOut";
681
681
+
679
682
const showContentWarning = warnContentLabels.length > 0;
680
683
681
684
const [isOpen, setIsOpen] = useState(!showContentWarning);
685
685
+
const [hasUserTouchedToggleYet, setHasUserTouchedToggleYet] = useState(false);
686
686
+
687
687
+
useEffect(()=>{
688
688
+
if(!hasUserTouchedToggleYet && showContentWarning) {
689
689
+
setIsOpen(false);
690
690
+
}
691
691
+
},[hasUserTouchedToggleYet, showContentWarning])
682
692
683
693
684
694
if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0) {
···
977
987
{/* <ModerationInner subject={post.uri} /> */}
978
988
{showContentWarning && (
979
989
<ContentWarning
990
990
+
unauthedgate={hideWarnsWhenUnauthed}
980
991
labels={warnContentLabels}
981
992
isOpen={isOpen}
982
993
onPress={(e) => {
983
994
e.stopPropagation();
984
984
-
setIsOpen(!isOpen)
995
995
+
setHasUserTouchedToggleYet(true);
996
996
+
if (!hideWarnsWhenUnauthed) {
997
997
+
setIsOpen(!isOpen)
998
998
+
}
985
999
}}
986
1000
/>
987
1001
)}
···
1214
1228
}
1215
1229
1216
1230
export function ContentWarning({
1231
1231
+
unauthedgate,
1217
1232
labels,
1218
1233
isOpen,
1219
1234
onPress,
1220
1235
}: {
1236
1236
+
unauthedgate?: boolean;
1221
1237
labels: ContentLabel[];
1222
1238
isOpen: boolean;
1223
1239
onPress: React.MouseEventHandler<HTMLDivElement>;
···
1257
1273
1258
1274
{/* Chevron */}
1259
1275
<div className="flex items-center justify-center text-gray-500 dark:text-gray-400 pl-2 gap-2 text-sm">
1260
1260
-
{isOpen ? "hide" : "show"}
1261
1261
-
<IconMdiChevronDown
1276
1276
+
{unauthedgate ? "please login to view" : isOpen ? "hide" : "show"}
1277
1277
+
{!unauthedgate && (<IconMdiChevronDown
1262
1278
className={`text-xl transition-transform duration-300 ease-[cubic-bezier(0.2,0,0,1)] ${isOpen ? "rotate-180" : ""
1263
1279
}`}
1264
1264
-
/>
1280
1280
+
/>)}
1265
1281
</div>
1266
1282
</div>
1267
1283
</div>