forward labelers to chat
This commit is contained in:
@@ -38,7 +38,7 @@ import {
|
||||
type UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAppviewClient, usePdsClient} from '#/state/session'
|
||||
import {useAppviewClient, useChatClient, usePdsClient} from '#/state/session'
|
||||
import {applyLabelersToClient, saveLabelers} from '#/state/session/moderation'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
|
||||
@@ -58,6 +58,7 @@ export const preferencesQueryKey = createQueryKey(
|
||||
export function usePreferencesQuery() {
|
||||
const client = usePdsClient()
|
||||
const appviewClient = useAppviewClient()
|
||||
const chatClient = useChatClient()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
const query = useQuery({
|
||||
@@ -85,12 +86,12 @@ export function usePreferencesQuery() {
|
||||
* from a labeler would not affect server-attached labels until the
|
||||
* session bundle was rebuilt.
|
||||
*
|
||||
* The subscriptions go on the appview client, which is what stamps
|
||||
* `atproto-accept-labelers` on its own requests. The Bluesky moderation
|
||||
* DID is dropped so the globally redacted authority is not also listed
|
||||
* unredacted.
|
||||
* The subscriptions go on both services that hydrate moderated content.
|
||||
* The Bluesky moderation DID is dropped so the globally redacted
|
||||
* authority is not also listed unredacted.
|
||||
*/
|
||||
applyLabelersToClient(appviewClient, labelerDids)
|
||||
applyLabelersToClient(chatClient, labelerDids)
|
||||
|
||||
/*
|
||||
* `BskyPreferences` is now the sdk's own type, so the assembled
|
||||
|
||||
@@ -251,19 +251,35 @@ describe('buildChatClient', () => {
|
||||
expect(headers.get('authorization')).toBe('Bearer access-jwt')
|
||||
})
|
||||
|
||||
it('emits no labeler header', async () => {
|
||||
/* the global authorities do not apply: a chat call is not an appview read */
|
||||
it('emits a global app labeler once, redacted', async () => {
|
||||
const client = buildChatClient(makeSession(fetchMock))
|
||||
configureGlobalAppLabelers(['did:plc:global-labeler'])
|
||||
|
||||
await buildChatClient(makeSession(fetchMock))
|
||||
.call(chat.bsky.convo.listConvos, {})
|
||||
.catch(() => {})
|
||||
await client.call(chat.bsky.convo.listConvos, {}).catch(() => {})
|
||||
|
||||
expect(
|
||||
headersFor(fetchMock, 'chat.bsky.convo.listConvos').get(
|
||||
'atproto-accept-labelers',
|
||||
),
|
||||
).toBeNull()
|
||||
const labelers = headersFor(fetchMock, 'chat.bsky.convo.listConvos').get(
|
||||
'atproto-accept-labelers',
|
||||
)
|
||||
const entries = labelers!
|
||||
.split(',')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.includes('did:plc:global-labeler'))
|
||||
expect(entries).toEqual(['did:plc:global-labeler;redact'])
|
||||
})
|
||||
|
||||
it('emits an account subscription exactly once', async () => {
|
||||
const client = buildChatClient(makeSession(fetchMock))
|
||||
client.setLabelers(['did:plc:labeler'])
|
||||
|
||||
await client.call(chat.bsky.convo.listConvos, {}).catch(() => {})
|
||||
|
||||
const labelers = headersFor(fetchMock, 'chat.bsky.convo.listConvos').get(
|
||||
'atproto-accept-labelers',
|
||||
)
|
||||
const entries = labelers!
|
||||
.split(',')
|
||||
.filter(l => l.includes('did:plc:labeler'))
|
||||
expect(entries).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {type Client} from '@atproto/lex'
|
||||
import {api} from '@bsky/sdk'
|
||||
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
jest.mock('#/storage', () => ({
|
||||
account: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
device: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import {account} from '#/storage'
|
||||
import {configureGlobalAppLabelers} from '../additional-moderation-authorities'
|
||||
import {configureModerationForAccount} from '../moderation'
|
||||
import {makeAccount} from './mock-fetch'
|
||||
|
||||
describe('configureModerationForAccount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
configureGlobalAppLabelers([])
|
||||
})
|
||||
|
||||
it('applies cached account labelers to appview and chat', () => {
|
||||
const appviewClient = {setLabelers: jest.fn()} as unknown as Client
|
||||
const chatClient = {setLabelers: jest.fn()} as unknown as Client
|
||||
jest
|
||||
.mocked(account.get)
|
||||
.mockReturnValue(['did:plc:account-labeler', api.moderation.did])
|
||||
|
||||
configureModerationForAccount(
|
||||
{appviewClient, chatClient},
|
||||
makeAccount({handle: 'alice.example.com'}),
|
||||
)
|
||||
|
||||
expect(appviewClient.setLabelers).toHaveBeenCalledWith([
|
||||
'did:plc:account-labeler',
|
||||
])
|
||||
expect(chatClient.setLabelers).toHaveBeenCalledWith([
|
||||
'did:plc:account-labeler',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -98,10 +98,10 @@ export function configureAdditionalModerationAuthorities() {
|
||||
* reads, so a request carries the same `;redact` authorities whether or not
|
||||
* there is a session behind it.
|
||||
*
|
||||
* It is a single global producer by design. The PDS and chat clients opt out
|
||||
* with `appLabelers: null` (see `clients.ts`) because those services take no
|
||||
* moderation authorities, leaving exactly one producer on an appview request and
|
||||
* none elsewhere.
|
||||
* It is a single global producer by design. The PDS client opts out with
|
||||
* `appLabelers: null` (see `clients.ts`) because that service takes no
|
||||
* moderation authorities. Appview and chat requests each carry one copy from
|
||||
* their respective clients.
|
||||
*/
|
||||
export function configureGlobalAppLabelers(dids: string[]) {
|
||||
Client.configure({appLabelers: dids as `did:${string}:${string}`[]})
|
||||
|
||||
@@ -54,12 +54,13 @@ export function buildPdsClient(agent: Agent): Client {
|
||||
* env-configurable `CHAT_PROXY_DID` rather than a hard-coded constant, so it can
|
||||
* be retargeted per environment.
|
||||
*
|
||||
* `appLabelers: null` for the same reason as the PDS client: the chat service
|
||||
* takes no moderation authorities.
|
||||
* Unlike the PDS client, chat carries moderation authorities. The service uses
|
||||
* them to hydrate labels on profiles embedded in conversation responses, so
|
||||
* this client reads the global `Client.appLabelers` and receives the account's
|
||||
* subscriptions through `configureModerationForAccount`.
|
||||
*/
|
||||
export function buildChatClient(agent: Agent): Client {
|
||||
return createLexClient(agent, {
|
||||
appLabelers: null,
|
||||
service: CHAT_PROXY_SERVICE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import {type SessionAccount} from './types'
|
||||
|
||||
/** The moderation surface of a session bundle. */
|
||||
type ModerationSession = {appviewClient: Client}
|
||||
type ModerationSession = {appviewClient: Client; chatClient: Client}
|
||||
|
||||
/**
|
||||
* Cache an account's subscribed labeler DIDs. Called on every preferences
|
||||
@@ -45,8 +45,8 @@ export function readLabelers(did: string): string[] | undefined {
|
||||
* lex collects the two lists into a `Set` keyed on the suffixed string, so
|
||||
* neither dedupes against the other.
|
||||
*
|
||||
* Only the appview client takes subscriptions - the PDS and chat clients suppress
|
||||
* labelers entirely (see clients.ts).
|
||||
* Appview and chat both take subscriptions. The PDS suppresses labelers because
|
||||
* repo and identity requests do not hydrate moderated content (see clients.ts).
|
||||
*/
|
||||
export function applyLabelersToClient(
|
||||
client: Client,
|
||||
@@ -85,6 +85,7 @@ export function configureModerationForAccount(
|
||||
const labelerDids = readLabelers(account.did)
|
||||
if (labelerDids) {
|
||||
applyLabelersToClient(bundle.appviewClient, labelerDids)
|
||||
applyLabelersToClient(bundle.chatClient, labelerDids)
|
||||
} else {
|
||||
// If there are no headers in the storage, we'll not send them on the initial requests.
|
||||
// If we wanted to fix this, we could block on the preferences query here.
|
||||
|
||||
Reference in New Issue
Block a user