[SDK] Swap moderation to @bsky.app/sdk and move labeler config to Client statics (#11383)

This commit is contained in:
Samuel Newman
2026-08-13 22:26:21 +03:00
committed by GitHub
parent dc31e7fd12
commit 59d2bf09d7
138 changed files with 601 additions and 416 deletions
+3 -5
View File
@@ -1,12 +1,10 @@
import {useCallback} from 'react'
import {
type AppBskyActorDefs,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {type AppBskyActorDefs} from '@atproto/api'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session'
+2 -6
View File
@@ -1,10 +1,5 @@
import {useMemo, useRef} from 'react'
import {
type AppBskyActorDefs,
AppBskyFeedDefs,
AtUri,
moderatePost,
} from '@atproto/api'
import {type AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -17,6 +12,7 @@ import {
import {CustomFeedAPI} from '#/lib/api/feed/custom'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner} from '#/lib/api/feed-manip'
import {moderatePost} from '#/lib/moderation/subjects'
import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
+1 -1
View File
@@ -4,7 +4,6 @@ import {
type AppBskyFeedDefs,
type AppBskyGraphDefs,
AtUri,
moderateFeedGenerator,
} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext'
@@ -20,6 +19,7 @@ import {
} from '@tanstack/react-query'
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {moderateFeedGenerator} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
+27 -23
View File
@@ -1,4 +1,3 @@
import {type AppBskyLabelerDefs} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {addLabeler, removeLabeler} from '@bsky.app/sdk'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
@@ -11,7 +10,8 @@ import {
usePreferencesQuery,
} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, usePdsClient} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app} from '#/lexicons'
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
@@ -35,45 +35,47 @@ export function useLabelerInfoQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!did && enabled !== false,
queryKey: labelerInfoQueryKey(did as string),
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
dids: [did!],
const res = await client.call(app.bsky.labeler.getServices, {
dids: [did! as DidString],
detailed: true,
})
return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
return res.views[0] as app.bsky.labeler.defs.LabelerViewDetailed
},
})
}
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!dids.length,
queryKey: labelersInfoQueryKey(dids),
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({dids})
return res.data.views as AppBskyLabelerDefs.LabelerView[]
const res = await client.call(app.bsky.labeler.getServices, {
dids: dids as DidString[],
})
return res.views as app.bsky.labeler.defs.LabelerView[]
},
})
}
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!dids.length,
queryKey: createLabelersDetailedInfoQueryKey(dids),
gcTime: GCTIME.INFINITY,
staleTime: STALE.MINUTES.ONE,
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
dids,
const res = await client.call(app.bsky.labeler.getServices, {
dids: dids as DidString[],
detailed: true,
})
return res.data.views as AppBskyLabelerDefs.LabelerViewDetailed[]
return res.views as app.bsky.labeler.defs.LabelerViewDetailed[]
},
})
}
@@ -98,7 +100,7 @@ export function useRemoveLabelersMutation() {
export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const preferences = usePreferencesQuery()
@@ -122,28 +124,30 @@ export function useLabelerSubscriptionMutation() {
const labelerDids = (
preferences.data?.moderationPrefs?.labelers ?? []
).map(l => l.did)
const invalidLabelers: string[] = []
const invalidLabelers: DidString[] = []
if (labelerDids.length) {
const profiles = await agent.getProfiles({actors: labelerDids})
if (profiles.data) {
for (const did of labelerDids) {
const exists = profiles.data.profiles.find(p => p.did === did)
const profiles = await appviewClient.call(app.bsky.actor.getProfiles, {
actors: labelerDids,
})
if (profiles) {
for (const labelerDid of labelerDids) {
const exists = profiles.profiles.find(p => p.did === labelerDid)
if (exists) {
// profile came back but it's not a valid labeler
if (exists.associated && !exists.associated.labeler) {
invalidLabelers.push(did)
invalidLabelers.push(labelerDid)
}
} else {
// no response came back, might be deactivated or takendown
invalidLabelers.push(did)
invalidLabelers.push(labelerDid)
}
}
}
}
if (invalidLabelers.length) {
await Promise.all(
invalidLabelers.map(did =>
pdsClient.call(removeLabeler, did as DidString),
invalidLabelers.map(labelerDid =>
pdsClient.call(removeLabeler, labelerDid),
),
)
}
+2 -6
View File
@@ -17,12 +17,7 @@
*/
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
moderatePost,
} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -31,6 +26,7 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {moderatePost} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session'
+2 -3
View File
@@ -6,16 +6,15 @@ import {
type AppBskyGraphDefs,
AppBskyGraphStarterpack,
type AppBskyNotificationListNotifications,
hasMutedWord,
moderateNotification,
type ModerationOpts,
} from '@atproto/api'
import {type Client} from '@atproto/lex'
import {type AtUriString} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {type QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
import {hasMutedWord, moderateNotification} from '#/lib/moderation/subjects'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {precacheProfile} from '../profile'
+5 -3
View File
@@ -5,12 +5,13 @@ import {
AppBskyFeedDefs,
type AppBskyFeedPost,
AtUri,
moderatePost,
type ModerationDecision,
type ModerationPrefs,
} from '@atproto/api'
import {type Client} from '@atproto/lex'
import {type AtIdentifierString, type AtUriString} from '@atproto/syntax'
import {
type ModerationDecision,
type ModerationPrefs,
} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryClient,
@@ -31,6 +32,7 @@ import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {moderatePost} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
+1 -1
View File
@@ -1,4 +1,4 @@
import {DEFAULT_LABEL_SETTINGS} from '@atproto/api'
import {DEFAULT_LABEL_SETTINGS} from '@bsky.app/sdk/moderation'
import {
type ThreadViewPreferences,
+23 -33
View File
@@ -40,7 +40,7 @@ import {
} from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, usePdsClient} from '#/state/session'
import {saveLabelers} from '#/state/session/moderation'
import {applyLabelersToClient, saveLabelers} from '#/state/session/moderation'
import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
@@ -81,21 +81,23 @@ export function usePreferencesQuery() {
/*
* `BskyAgent.getPreferences` used to call `configureLabelers` itself,
* so subscribing to a labeler took effect on the very next appview
* read. The sdk action has no such side effect, and appview reads still
* inherit `atproto-accept-labelers` from the agent's fetch handler, so
* apply the subscriptions there rather than on the wrapping client -
* setting them on both would emit the header twice.
* read. The sdk action has no such side effect, so apply the
* subscriptions here - without this, subscribing to or unsubscribing
* from a labeler would not affect server-attached labels until the
* session bundle was rebuilt.
*
* `applyLabelersToClient` writes to the agent, which is what stamps the
* header on the requests the wrapping appview client issues, and it
* drops the Bluesky moderation DID so the globally redacted authority is
* not also listed unredacted.
*/
agent.configureLabelers(labelerDids)
applyLabelersToClient(agent, labelerDids)
/*
* The sdk's `BskyPreferences` is field-for-field identical to the
* legacy one that `UsePreferencesQueryResponse` is still derived from;
* only its strings are branded (`AtUriString`, `DidString`). Cast at
* this one seam so every downstream consumer of the response - notably
* the `moderationPrefs` readers - keeps its current types.
* `BskyPreferences` is now the sdk's own type, so the assembled
* response types structurally with no cast at this seam.
*/
const preferences = {
const preferences: UsePreferencesQueryResponse = {
...res,
savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'),
/**
@@ -111,7 +113,7 @@ export function usePreferencesQuery() {
...(res.threadViewPrefs ?? {}),
},
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
} as UsePreferencesQueryResponse
}
return preferences
}
},
@@ -381,11 +383,8 @@ export function useUpsertMutedWordsMutation() {
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await client.call(
upsertMutedWords,
mutedWords as app.bsky.actor.defs.MutedWord[],
)
mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => {
await client.call(upsertMutedWords, mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -399,11 +398,8 @@ export function useUpdateMutedWordMutation() {
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await client.call(
updateMutedWord,
mutedWord as app.bsky.actor.defs.MutedWord,
)
mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => {
await client.call(updateMutedWord, mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -417,11 +413,8 @@ export function useRemoveMutedWordMutation() {
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await client.call(
removeMutedWord,
mutedWord as app.bsky.actor.defs.MutedWord,
)
mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => {
await client.call(removeMutedWord, mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -435,11 +428,8 @@ export function useRemoveMutedWordsMutation() {
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await client.call(
removeMutedWords,
mutedWords as app.bsky.actor.defs.MutedWord[],
)
mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => {
await client.call(removeMutedWords, mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
+2 -1
View File
@@ -1,5 +1,6 @@
import {useMemo} from 'react'
import {AtpAgent, interpretLabelValueDefinitions} from '@atproto/api'
import {AtpAgent} from '@atproto/api'
import {interpretLabelValueDefinitions} from '@bsky.app/sdk/moderation'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useLabelersDetailedInfoQuery} from '../labeler'
+1 -1
View File
@@ -1,4 +1,4 @@
import {type BskyFeedViewPreference, type BskyPreferences} from '@atproto/api'
import {type BskyFeedViewPreference, type BskyPreferences} from '@bsky.app/sdk'
export type UsePreferencesQueryResponse = Omit<
BskyPreferences,
+1 -1
View File
@@ -1,4 +1,3 @@
import {moderateFeedGenerator} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {
type InfiniteData,
@@ -6,6 +5,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {moderateFeedGenerator} from '#/lib/moderation/subjects'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
+1 -1
View File
@@ -1,4 +1,3 @@
import {moderateUserList} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {
type InfiniteData,
@@ -6,6 +5,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {moderateUserList} from '#/lib/moderation/subjects'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
+2 -1
View File
@@ -1,5 +1,5 @@
import {useCallback, useMemo, useRef} from 'react'
import {type AppBskyFeedDefs, AtUri, moderatePost} from '@atproto/api'
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -7,6 +7,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {moderatePost} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAppviewClient} from '#/state/session'
import {type SearchFilters} from '#/screens/Search/searchParams'
@@ -1,5 +1,5 @@
import {useCallback, useMemo} from 'react'
import {hasMutedWord} from '@atproto/api'
import {hasMutedWord} from '@bsky.app/sdk/moderation'
import {useQuery} from '@tanstack/react-query'
import {
+2 -1
View File
@@ -1,4 +1,5 @@
import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
import {AppBskyUnspeccedDefs} from '@atproto/api'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {
type ApiThreadItem,
+1 -1
View File
@@ -5,8 +5,8 @@ import {
type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadOtherV2,
type AppBskyUnspeccedGetPostThreadV2,
type ModerationDecision,
} from '@atproto/api'
import {type ModerationDecision} from '@bsky.app/sdk/moderation'
export type ApiThreadItem =
| AppBskyUnspeccedGetPostThreadV2.ThreadItem
+2 -2
View File
@@ -5,10 +5,10 @@ import {
type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
moderatePost,
type ModerationOpts,
} from '@atproto/api'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {
type ApiThreadItem,