[SDK] Route moderation reports through the appview client (#11368)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ToolsOzoneReportDefs} from '@atproto/api'
|
||||
import {type DidString} from '@atproto/syntax'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {MOD_PROXY_SERVICE} from '#/lib/constants'
|
||||
import {useAppviewClient, useSession} from '#/state/session'
|
||||
import {atoms as a, useBreakpoints, web} from '#/alf'
|
||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -17,6 +18,7 @@ import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {logger} from '#/ageAssurance'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {com} from '#/lexicons'
|
||||
|
||||
export function AgeAssuranceAppealDialog({
|
||||
control,
|
||||
@@ -42,7 +44,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
|
||||
const ax = useAnalytics()
|
||||
const {currentAccount} = useSession()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const agent = useAgent()
|
||||
const client = useAppviewClient()
|
||||
|
||||
const [details, setDetails] = useState('')
|
||||
const isInvalid = details.length > 1000
|
||||
@@ -51,19 +53,22 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
|
||||
mutationFn: async () => {
|
||||
ax.metric('ageAssurance:appealDialogSubmit', {})
|
||||
|
||||
await agent.createModerationReport(
|
||||
if (!currentAccount) {
|
||||
throw new Error('No current account, should be unreachable')
|
||||
}
|
||||
|
||||
await client.call(
|
||||
com.atproto.moderation.createReport,
|
||||
{
|
||||
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: currentAccount?.did,
|
||||
// the persisted account did is already resolved
|
||||
did: currentAccount.did as DidString,
|
||||
},
|
||||
reason: `AGE_ASSURANCE_INQUIRY: ` + details,
|
||||
},
|
||||
{
|
||||
encoding: 'application/json',
|
||||
headers: BLUESKY_MOD_SERVICE_HEADERS,
|
||||
},
|
||||
{service: MOD_PROXY_SERVICE},
|
||||
)
|
||||
},
|
||||
onError: err => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {XrpcResponseError} from '@atproto/lex'
|
||||
import {type AtUriString, type DidString} from '@atproto/syntax'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -12,7 +13,7 @@ import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useAppviewClient} from '#/state/session'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -22,6 +23,7 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_ANDROID} from '#/env'
|
||||
import {com} from '#/lexicons'
|
||||
|
||||
export function AppealForm({
|
||||
label,
|
||||
@@ -38,7 +40,7 @@ export function AppealForm({
|
||||
const [details, setDetails] = useState('')
|
||||
const {subject} = useLabelSubject({label})
|
||||
const isAccountReport = 'did' in subject
|
||||
const agent = useAgent()
|
||||
const client = useAppviewClient()
|
||||
const sourceName = labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src
|
||||
@@ -46,28 +48,38 @@ export function AppealForm({
|
||||
|
||||
const {mutate, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
const $type = !isAccountReport
|
||||
? 'com.atproto.repo.strongRef'
|
||||
: 'com.atproto.admin.defs#repoRef'
|
||||
await agent.createModerationReport(
|
||||
await client.call(
|
||||
com.atproto.moderation.createReport,
|
||||
{
|
||||
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type,
|
||||
...subject,
|
||||
},
|
||||
/*
|
||||
* `useLabelSubject` derives one shape or the other from the label's
|
||||
* `cid`: an at-uri plus cid for a record, or the label's `uri` reused
|
||||
* as the account did.
|
||||
*/
|
||||
subject: isAccountReport
|
||||
? {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: subject.did as DidString,
|
||||
}
|
||||
: {
|
||||
$type: 'com.atproto.repo.strongRef',
|
||||
uri: subject.uri as AtUriString,
|
||||
cid: subject.cid,
|
||||
},
|
||||
reason: details,
|
||||
},
|
||||
{
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${label.src}#atproto_labeler`,
|
||||
},
|
||||
},
|
||||
// the appeal goes to the labeler that applied the label
|
||||
{service: `${label.src as DidString}#atproto_labeler`},
|
||||
)
|
||||
},
|
||||
onError: err => {
|
||||
if (err instanceof XRPCError && err.error === 'AlreadyAppealed') {
|
||||
/*
|
||||
* `AlreadyAppealed` is real server behavior that createReport's lexicon
|
||||
* does NOT declare, so `matchXrpcError` cannot see it and the raw error
|
||||
* code is checked instead. Worth an upstream PR to declare it.
|
||||
*/
|
||||
if (err instanceof XrpcResponseError && err.error === 'AlreadyAppealed') {
|
||||
setError(
|
||||
_(
|
||||
msg`You've already appealed this label and it's being reviewed by our moderation team.`,
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {
|
||||
type $Typed,
|
||||
BSKY_LABELER_DID,
|
||||
type ChatBskyConvoDefs,
|
||||
type ComAtprotoModerationCreateReport,
|
||||
} from '@atproto/api'
|
||||
import {BSKY_LABELER_DID} from '@atproto/api'
|
||||
import {type AtUriString, type DidString} from '@atproto/syntax'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useAppviewClient} from '#/state/session'
|
||||
import {com} from '#/lexicons'
|
||||
import {NEW_TO_OLD_REASONS_MAP, REPORT_MOD_TOOL_NAME} from './const'
|
||||
import {type ReportState} from './state'
|
||||
import {type ParsedReportSubject} from './types'
|
||||
|
||||
type ReportInput = com.atproto.moderation.createReport.$InputBody
|
||||
|
||||
export function useSubmitReportMutation() {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const client = useAppviewClient()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn({
|
||||
@@ -59,13 +58,7 @@ export function useSubmitReportMutation() {
|
||||
reasonType = backwardsCompatibleReasonType
|
||||
}
|
||||
|
||||
let report:
|
||||
| ComAtprotoModerationCreateReport.InputSchema
|
||||
| (Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'> & {
|
||||
subject:
|
||||
| $Typed<ChatBskyConvoDefs.MessageRef>
|
||||
| $Typed<ChatBskyConvoDefs.ConvoRef>
|
||||
})
|
||||
let report: ReportInput
|
||||
|
||||
switch (subject.type) {
|
||||
case 'account': {
|
||||
@@ -74,7 +67,8 @@ export function useSubmitReportMutation() {
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: subject.did,
|
||||
// the parsed subject carries an already-resolved did
|
||||
did: subject.did as DidString,
|
||||
},
|
||||
}
|
||||
break
|
||||
@@ -89,7 +83,8 @@ export function useSubmitReportMutation() {
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'com.atproto.repo.strongRef',
|
||||
uri: subject.uri,
|
||||
// the parsed subject carries an at-uri read off a view
|
||||
uri: subject.uri as AtUriString,
|
||||
cid: subject.cid,
|
||||
},
|
||||
}
|
||||
@@ -99,12 +94,12 @@ export function useSubmitReportMutation() {
|
||||
report = {
|
||||
reasonType,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
subject: toOpenSubject({
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: subject.message.id,
|
||||
convoId: subject.convoId,
|
||||
did: subject.message.sender.did,
|
||||
},
|
||||
}),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -112,11 +107,11 @@ export function useSubmitReportMutation() {
|
||||
report = {
|
||||
reasonType,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
subject: toOpenSubject({
|
||||
$type: 'chat.bsky.convo.defs#convoRef',
|
||||
convoId: subject.convoId,
|
||||
did: subject.did,
|
||||
},
|
||||
}),
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -145,13 +140,30 @@ export function useSubmitReportMutation() {
|
||||
report,
|
||||
})
|
||||
} else {
|
||||
await agent.createModerationReport(report, {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${labeler.creator.did}#atproto_labeler`,
|
||||
},
|
||||
/*
|
||||
* Reports go to the labeler the user selected rather than Bluesky's, so
|
||||
* the proxy target is built per call from that labeler's creator did.
|
||||
*/
|
||||
await client.call(com.atproto.moderation.createReport, report, {
|
||||
service: `${labeler.creator.did as DidString}#atproto_labeler`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Widen a chat convo ref into `createReport`'s subject union.
|
||||
*
|
||||
* The lexicon declares only `com.atproto.admin.defs#repoRef` and
|
||||
* `com.atproto.repo.strongRef`, but leaves the union OPEN, and the chat service
|
||||
* accepts its own refs there. An open union types its unknown arm as
|
||||
* `{$type: Unknown$Type}`, which a concrete chat ref does not structurally
|
||||
* satisfy, so the widening is asserted here once rather than at each call site.
|
||||
*/
|
||||
function toOpenSubject(ref: {
|
||||
$type: string
|
||||
[key: string]: unknown
|
||||
}): ReportInput['subject'] {
|
||||
return ref as unknown as ReportInput['subject']
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user