diff --git a/policy.ts b/policy.ts new file mode 100644 index 0000000..a478365 --- /dev/null +++ b/policy.ts @@ -0,0 +1,17 @@ +export const FORCED_LABELER_DIDS = [ + "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation +]; + +export const UNAUTHED_FORCE_WARN_LABELS = new Set([ + // i dont know if some of these are even valid labels + "porn", + "sexual", + "graphic-media", + "nudity", + "nsfl", + "corpse", + "gore", + "!no-unauthenticated" +]); + +export const UNAUTHED_PREVENT_OPENING_WARNS = true; \ No newline at end of file diff --git a/src/components/ModerationInitializer.tsx b/src/components/ModerationInitializer.tsx index 3af0486..b9ec4a8 100644 --- a/src/components/ModerationInitializer.tsx +++ b/src/components/ModerationInitializer.tsx @@ -1,19 +1,18 @@ import { useQueries } from "@tanstack/react-query"; -import { useSetAtom } from "jotai"; -import { useEffect } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { useEffect, useRef } from "react"; +import { FORCED_LABELER_DIDS, UNAUTHED_FORCE_WARN_LABELS } from "~/../policy"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { labelerConfigAtom } from "~/state/moderationAtoms"; import type { LabelerDefinition, LabelPreference, LabelValueDefinition } from "~/types/moderation"; +import { slingshotURLAtom } from "~/utils/atoms"; import { useQueryIdentity } from "~/utils/useQuery"; import { useQueryPreferences } from "~/utils/useQuery"; -export const BSKY_LABELER_DID = "did:plc:ar7c4by46qjdydhdevvrndac"; - // Manual DID document resolution const fetchDidDocument = async (did: string): Promise => { if (did.startsWith("did:plc:")) { - // For PLC DIDs, fetch from plc.directory const response = await fetch( `https://plc.directory/${encodeURIComponent(did)}`, ); @@ -21,7 +20,6 @@ const fetchDidDocument = async (did: string): Promise => { throw new Error(`Failed to fetch PLC DID document for ${did}`); return response.json(); } else if (did.startsWith("did:web:")) { - // For web DIDs, fetch from well-known const handle = did.replace("did:web:", ""); const url = `https://${handle}/.well-known/did.json`; const response = await fetch(url); @@ -36,75 +34,102 @@ const fetchDidDocument = async (did: string): Promise => { }; export const ModerationInitializer = () => { - const { agent } = useAuth(); + const { agent, status } = useAuth(); const setLabelerConfig = useSetAtom(labelerConfigAtom); - - // 1. Get User Identity to get PDS URL + const [slingshoturl] = useAtom(slingshotURLAtom); + + // Define clear boolean for mode + const isUnauthed = status === "signedOut" || !agent; + + // Track previous status to detect transitions + const prevStatusRef = useRef(status); + + // --- 1. THE HARD FLUSH --- + // When Auth Status changes (Logged In <-> Logged Out), immediately wipe the config. + // This prevents "Authed" prefs from bleeding into "Unauthed" state and vice versa + // while the async queries are spinning up. + useEffect(() => { + if (prevStatusRef.current !== status) { + console.log(`[Moderation] Auth status changed (${prevStatusRef.current} -> ${status}). Flushing config.`); + setLabelerConfig([]); // <--- WIPE CLEAN + prevStatusRef.current = status; + } + }, [status, setLabelerConfig]); + + // 2. Get User Identity (Only if authed) const { data: identity } = useQueryIdentity(agent?.did); - // 2. Get User Preferences (Global: "porn" -> "hide") + // 3. Get User Preferences (Only if authed) const { data: prefs } = useQueryPreferences({ agent: agent ?? undefined, pdsUrl: identity?.pds, }); - // 3. Identify Labeler DIDs from prefs - const userPrefDids = - prefs?.preferences - ?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref") - ?.labelers?.map((l: any) => l.did) ?? []; + // 4. Identify Labeler DIDs + // Important: If unauthed, userPrefDids MUST be empty, even if cache exists. + const userPrefDids = !isUnauthed + ? prefs?.preferences + ?.find((pref: any) => pref.$type === "app.bsky.actor.defs#labelersPref") + ?.labelers?.map((l: any) => l.did) ?? [] + : []; - // 2. MERGE: Force Bsky DID + User DIDs (Set removes duplicates) + // 5. Force Bsky DID + User DIDs const activeLabelerDids = Array.from( - new Set([BSKY_LABELER_DID, ...userPrefDids]) + new Set([...FORCED_LABELER_DIDS, ...userPrefDids]) ); - // 4. Parallel fetch all Labeler DID Documents and Service Records + // 6. Parallel fetch DID Docs const labelerDidDocQueries = useQueries({ queries: activeLabelerDids.map((did: string) => ({ queryKey: ["labelerDidDoc", did], queryFn: () => fetchDidDocument(did), - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 1, // Only retry once for DID docs + staleTime: 1000 * 60 * 60 * 24, })), }); + // 7. Parallel fetch Service Records const labelerServiceQueries = useQueries({ queries: activeLabelerDids.map((did: string) => ({ queryKey: ["labelerService", did], queryFn: async () => { - if (!identity?.pds) throw new Error("No PDS URL"); + const host = slingshoturl || "public.api.bsky.app"; const response = await fetch( - `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`, + `https://${host}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent("app.bsky.labeler.service")}&rkey=self`, ); if (!response.ok) throw new Error("Failed to fetch labeler service"); return response.json(); }, - enabled: !!identity?.pds && !!agent, - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: 1000 * 60 * 60, })), }); useEffect(() => { + // Guard: Wait for queries if ( - !prefs || labelerDidDocQueries.some((q) => q.isLoading) || - labelerDidDocQueries.some((q) => q.isFetching) || - labelerServiceQueries.some((q) => q.isLoading) || - labelerServiceQueries.some((q) => q.isFetching) - ) + labelerServiceQueries.some((q) => q.isLoading) + ) { return; + } - // Extract content label preferences - const contentLabelPrefs = - prefs.preferences?.filter( - (pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref", - ) ?? []; + // Guard: If we are supposed to be Authed, but prefs haven't loaded yet, + // DO NOT run the logic. Wait. This prevents falling back to defaults temporarily. + if (!isUnauthed && !prefs) { + return; + } + // A. Extract User Global Overrides + // STRICT SEPARATION: If unauthed, force this to be empty to ensure no leakage. const globalPrefs: Record = {}; - contentLabelPrefs.forEach((pref: any) => { - globalPrefs[pref.label] = pref.visibility as LabelPreference; - }); + + if (!isUnauthed && prefs?.preferences) { + const contentLabelPrefs = prefs.preferences.filter( + (pref: any) => pref.$type === "app.bsky.actor.defs#contentLabelPref", + ); + contentLabelPrefs.forEach((pref: any) => { + globalPrefs[pref.label] = pref.visibility as LabelPreference; + }); + } const definitions: LabelerDefinition[] = activeLabelerDids .map((did: string, index: number) => { @@ -113,17 +138,20 @@ export const ModerationInitializer = () => { if (!didDocQuery.data || !serviceQuery.data) return null; - // Extract service endpoint from DID document const didDoc = didDocQuery.data as any; const atprotoLabelerService = didDoc?.service?.find( (s: any) => s.id === "#atproto_labeler", ); - const record = (serviceQuery.data as any).value; // The raw ATProto record + const record = (serviceQuery.data as any).value; + + // B. Gather ALL identifiers + const allIdentifiers = new Set(); + record.policies?.labelValues?.forEach((val: string) => allIdentifiers.add(val)); + record.policies?.labelValueDefinitions?.forEach((def: any) => allIdentifiers.add(def.identifier)); - // 1. Create the Metadata Map + // C. Create Metadata Map const labelDefs: Record = {}; - if (record.policies.labelValueDefinitions) { record.policies.labelValueDefinitions.forEach((def: any) => { labelDefs[def.identifier] = { @@ -132,32 +160,54 @@ export const ModerationInitializer = () => { blurs: def.blurs, adultOnly: def.adultOnly, defaultSetting: def.defaultSetting, - locales: def.locales || [] // <--- Capture the locales array + locales: def.locales || [] }; }); } - // RESOLUTION LOGIC: - // Map record.policies.labelValueDefinitions to a lookup map. - // Priority: User Global Pref > Labeler Default > 'ignore' + // D. Resolve Preferences const supportedLabels: Record = {}; - record.policies?.labelValues?.forEach((val: string) => { - // Does user have a global override for this string? + allIdentifiers.forEach((val) => { + // todo this works but with how useModeration hooks works right now old verdicts wont get stale-d + // it only works right now because these are warns and warns are negligable i guess + // --- BRANCH 1: UNAUTHED MODE --- + if (isUnauthed) { + // 1. Strict Force Overrides + if (UNAUTHED_FORCE_WARN_LABELS.has(val)) { + supportedLabels[val] = "warn"; // or 'hide' if that's what your policy constant implies + return; + } + + // 2. Default Labeler Settings + const def = labelDefs[val]; + const rawDefault = def?.defaultSetting || "ignore"; + + // 3. Apply Unauthed-Specific Aliasing (Optional) + // e.g., if you want to hide 'inform' labels for unauthed users + supportedLabels[val] = rawDefault as LabelPreference; + return; + } + + // --- BRANCH 2: AUTHED MODE --- + // 1. User Global Override (Highest Priority) const globalPref = globalPrefs[val]; - // Or use labeler default - const defaultPref = - record.policies?.labelValueDefinitions?.find( - (d: any) => d.identifier === val, - )?.defaultSetting || "ignore"; + if (globalPref) { + supportedLabels[val] = globalPref; + return; + } + + // 2. Labeler Default + const def = labelDefs[val]; + const rawDefault = def?.defaultSetting || "ignore"; - supportedLabels[val] = (globalPref || defaultPref) as LabelPreference; + supportedLabels[val] = rawDefault as LabelPreference; }); return { did: did, url: atprotoLabelerService?.serviceEndpoint || record.serviceEndpoint, - isDefault: false, // logic to determine if this is a default Bluesky labeler + isDefault: FORCED_LABELER_DIDS.includes(did), supportedLabels, labelDefs, }; @@ -165,7 +215,14 @@ export const ModerationInitializer = () => { .filter(Boolean) as LabelerDefinition[]; setLabelerConfig(definitions); - }, [prefs, labelerDidDocQueries, labelerServiceQueries, setLabelerConfig, identity?.pds, activeLabelerDids]); - - return null; // Headless component -}; + }, [ + prefs, + labelerDidDocQueries, + labelerServiceQueries, + setLabelerConfig, + activeLabelerDids, + isUnauthed // <--- Critical dependency triggers re-eval on login/out + ]); + + return null; +}; \ No newline at end of file diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 8cca6d8..111c735 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -15,6 +15,7 @@ import { HoverCard } from "radix-ui"; import * as React from "react"; import { useEffect, useState } from "react"; +import { UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy"; import defaultpfp from "~/../public/defaultpfp.png"; import { useLabelInfo } from "~/hooks/useLabelInfo"; import { useModeration } from "~/hooks/useModeration"; @@ -599,7 +600,7 @@ export function UniversalPostRenderer({ post.viewer?.repost ? true : false, ); const [, setComposerPost] = useAtom(composerAtom); - const { agent } = useAuth(); + const { agent, status } = useAuth(); const [retweetUri, setRetweetUri] = useState( post.viewer?.repost, ); @@ -676,9 +677,18 @@ export function UniversalPostRenderer({ const isMainItem = false; const setMainItem = (any: any) => { }; + const hideWarnsWhenUnauthed = UNAUTHED_PREVENT_OPENING_WARNS && status === "signedOut"; + const showContentWarning = warnContentLabels.length > 0; const [isOpen, setIsOpen] = useState(!showContentWarning); + const [hasUserTouchedToggleYet, setHasUserTouchedToggleYet] = useState(false); + + useEffect(()=>{ + if(!hasUserTouchedToggleYet && showContentWarning) { + setIsOpen(false); + } + },[hasUserTouchedToggleYet, showContentWarning]) if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0) { @@ -977,11 +987,15 @@ export function UniversalPostRenderer({ {/* */} {showContentWarning && ( { e.stopPropagation(); - setIsOpen(!isOpen) + setHasUserTouchedToggleYet(true); + if (!hideWarnsWhenUnauthed) { + setIsOpen(!isOpen) + } }} /> )} @@ -1214,10 +1228,12 @@ enum PostEmbedViewContext { } export function ContentWarning({ + unauthedgate, labels, isOpen, onPress, }: { + unauthedgate?: boolean; labels: ContentLabel[]; isOpen: boolean; onPress: React.MouseEventHandler; @@ -1257,11 +1273,11 @@ export function ContentWarning({ {/* Chevron */}
- {isOpen ? "hide" : "show"} - + />)}