Skip to content

Commit

Permalink
✨ Upgrade atproto/api sdk to 0.14.2 and refactor (#294)
Browse files Browse the repository at this point in the history
* ✨ Upgrade atproto/api sdk to 0.14.2 and refactor

* ✨ Use asPredicate for type guard

* 🔨 Refactor

* ♻️ Refactor asPredicate checks

* ♻️ Use asPredicate helper for type guards

* Remove a bunch ov validation functions

* Remove un-necessary type cast

* Avoid use of 'in' operator

* Avoid use of 'in' operator

* Avoid unsafe csting of un-specd fields

* ✨ Add priority score to workspace

* ⬆️ Upgrade pacakges

---------

Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
  • Loading branch information
foysalit and matthieusieben authored Feb 21, 2025
1 parent 1cf86e8 commit 3876678
Show file tree
Hide file tree
Showing 37 changed files with 749 additions and 513 deletions.
145 changes: 37 additions & 108 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
AtUri,
ComAtprotoModerationDefs,
ToolsOzoneModerationDefs,
ToolsOzoneModerationEmitEvent,
} from '@atproto/api'
Expand All @@ -20,7 +19,6 @@ import {
toLabelVal,
isSelfLabel,
ModerationLabel,
LabelChip,
} from '@/common/labels'
import { FullScreenActionPanel } from '@/common/FullScreenActionPanel'
import { PreviewCard } from '@/common/PreviewCard'
Expand All @@ -29,8 +27,6 @@ import {
ArrowLeftIcon,
ArrowRightIcon,
CheckCircleIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@heroicons/react/24/outline'
import { LabelSelector } from '@/common/labels/Selector'
import { takesKeyboardEvt } from '@/lib/util'
Expand All @@ -44,10 +40,8 @@ import { useCreateSubjectFromId } from '@/reports/helpers/subject'
import { getProfileUriForDid } from '@/reports/helpers/subject'
import { Dialog } from '@headlessui/react'
import { SubjectSwitchButton } from '@/common/SubjectSwitchButton'
import { diffTags } from 'components/tags/utils'
import { ActionError } from '@/reports/ModerationForm/ActionError'
import { Card } from '@/common/Card'
import { DM_DISABLE_TAG, VIDEO_UPLOAD_DISABLE_TAG } from '@/lib/constants'
import { MessageActorMeta } from '@/dms/MessageActorMeta'
import { ModEventDetailsPopover } from '@/mod-event/DetailsPopover'
import { LastReviewedTimestamp } from '@/subject/LastReviewedTimestamp'
Expand All @@ -61,8 +55,8 @@ import { SubjectTag } from 'components/tags/SubjectTag'
import { HighProfileWarning } from '@/repositories/HighProfileWarning'
import { EmailComposer } from 'components/email/Composer'
import { ActionPolicySelector } from '@/reports/ModerationForm/ActionPolicySelector'
import { HandRaisedIcon } from '@heroicons/react/24/solid'
import { PriorityScore } from '@/subject/PriorityScore'
import { getEventFromFormData } from '@/mod-event/helpers/emitEvent'

const FORM_ID = 'mod-action-panel'
const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 })
Expand Down Expand Up @@ -264,91 +258,28 @@ function Form(
setSubmission({ isSubmitting: true, error: '' })
const formData = new FormData(ev.currentTarget)
const nextLabels = String(formData.get('labels'))!.split(',')
const coreEvent: Parameters<typeof onSubmit>[0]['event'] = {
$type: modEventType,
}
const shouldMoveToNextSubject = formData.get('moveToNextSubject') === '1'

if (formData.get('durationInHours')) {
coreEvent.durationInHours = Number(formData.get('durationInHours'))
}

if (isTakedownEvent && formData.get('policies')) {
coreEvent.policies = [String(formData.get('policies'))]
}

if (
(isTakedownEvent || isAckEvent) &&
formData.get('acknowledgeAccountSubjects')
) {
coreEvent.acknowledgeAccountSubjects = true
}

if (formData.get('comment')) {
coreEvent.comment = formData.get('comment')
}

if (formData.get('sticky')) {
coreEvent.sticky = true
}

if (isPriorityScoreEvent) {
coreEvent.score = Number(formData.get('priorityScore'))
}

if (formData.get('tags')) {
const tags = String(formData.get('tags'))
.split(',')
.map((tag) => tag.trim())
const { add, remove } = diffTags(subjectStatus?.tags || [], tags)
coreEvent.add = add
coreEvent.remove = remove
}

// Appeal type doesn't really exist, behind the scenes, it's just a report event with special reason
if (coreEvent.$type === MOD_EVENTS.APPEAL) {
coreEvent.$type = MOD_EVENTS.REPORT
coreEvent.reportType = ComAtprotoModerationDefs.REASONAPPEAL
}

// Enable and disable dm/video-upload actions are just tag operations behind the scenes
// so, for those events, we rebuild the coreEvent with the appropriate $type and tags
if (
MOD_EVENTS.DISABLE_DMS === coreEvent.$type ||
MOD_EVENTS.ENABLE_DMS === coreEvent.$type ||
MOD_EVENTS.DISABLE_VIDEO_UPLOAD === coreEvent.$type ||
MOD_EVENTS.ENABLE_VIDEO_UPLOAD === coreEvent.$type
) {
if (coreEvent.$type === MOD_EVENTS.DISABLE_DMS) {
coreEvent.add = [DM_DISABLE_TAG]
coreEvent.remove = []
}
if (coreEvent.$type === MOD_EVENTS.ENABLE_DMS) {
coreEvent.add = []
coreEvent.remove = [DM_DISABLE_TAG]
}
if (coreEvent.$type === MOD_EVENTS.DISABLE_VIDEO_UPLOAD) {
coreEvent.add = [VIDEO_UPLOAD_DISABLE_TAG]
coreEvent.remove = []
}
if (coreEvent.$type === MOD_EVENTS.ENABLE_VIDEO_UPLOAD) {
coreEvent.add = []
coreEvent.remove = [VIDEO_UPLOAD_DISABLE_TAG]
}
coreEvent.$type = MOD_EVENTS.TAG
}
const { subject: subjectInfo, record: recordInfo } =
await createSubjectFromId(subject)

const subjectBlobCids = formData
.getAll('subjectBlobCids')
.map((cid) => String(cid))

const coreEvent = getEventFromFormData(
modEventType,
formData,
subjectStatus || undefined,
)
if (isDivertEvent && !subjectBlobCids.length) {
throw new Error('blob-selection-required')
}

if (isTakedownEvent && !coreEvent.policies) {
if (
ToolsOzoneModerationDefs.isModEventTakedown(coreEvent) &&
!coreEvent.policies
) {
throw new Error('policy-selection-required')
}

Expand All @@ -358,7 +289,7 @@ function Form(
// To work around that, this block checks if any label is being reverted and if so, it checks if the event's CID is different than the CID
// associated with the label that's being negated. If yes, it emits separate events for each such label and after that, if there are more labels
// left to be created/negated for the current CID, it emits the original event separate event for that.
if (isLabelEvent) {
if (ToolsOzoneModerationDefs.isModEventLabel(coreEvent)) {
const labels = diffLabels(
// Make sure we don't try to negate self labels
currentLabels.filter((label) => !isSelfLabel(label)),
Expand Down Expand Up @@ -398,35 +329,30 @@ function Form(
const labelSubmissions: Promise<void>[] = []

Object.keys(negatingLabelsByCid).forEach((labelCid) => {
labelSubmissions.push(
onSubmit({
subject: { ...subjectInfo, cid: labelCid },
createdBy: accountDid,
subjectBlobCids: formData
.getAll('subjectBlobCids')
.map((cid) => String(cid)),
event: {
...coreEvent,
// Here we'd never want to create labels associated with different CID than the current one
createLabelVals: [],
negateLabelVals: negatingLabelsByCid[labelCid],
},
}),
)
const negateLabelEvent = {
subject: { ...subjectInfo, cid: labelCid },
createdBy: accountDid,
subjectBlobCids,
event: {
...coreEvent,
// Here we'd never want to create labels associated with different CID than the current one
createLabelVals: [],
negateLabelVals: negatingLabelsByCid[labelCid],
},
}

labelSubmissions.push(onSubmit(negateLabelEvent))
})

// TODO: Typecasting here is not ideal
if (
(coreEvent.negateLabelVals as string[]).length ||
(coreEvent.createLabelVals as string[]).length
coreEvent.negateLabelVals.length ||
coreEvent.createLabelVals.length
) {
labelSubmissions.push(
onSubmit({
subject: subjectInfo,
createdBy: accountDid,
subjectBlobCids: formData
.getAll('subjectBlobCids')
.map((cid) => String(cid)),
subjectBlobCids,
event: coreEvent,
}),
)
Expand All @@ -446,11 +372,13 @@ function Form(
await onSubmit({
subject: subjectInfo,
createdBy: accountDid,
subjectBlobCids: formData
.getAll('subjectBlobCids')
.map((cid) => String(cid)),
subjectBlobCids,
// We want the comment from label and other params like label val etc. to NOT be associated with the ack event
event: { $type: MOD_EVENTS.ACKNOWLEDGE },
// But leave a specific keyword to indicate that the previous action was definitive
event: {
$type: MOD_EVENTS.ACKNOWLEDGE,
comment: '[DEFINITIVE_PREVIOUS_ACTION]',
},
})
}

Expand All @@ -463,9 +391,10 @@ function Form(
// This state is not kept in the form and driven by state so we need to reset it manually after submission
// If previous event was takedown and not immediately moving to next subject, moderators are most like to send a follow up email so default to email event
const eventMayNeedEmail =
coreEvent.$type === MOD_EVENTS.TAKEDOWN ||
coreEvent.$type === MOD_EVENTS.REVERSE_TAKEDOWN ||
coreEvent.$type === MOD_EVENTS.LABEL
ToolsOzoneModerationDefs.isModEventTakedown(coreEvent) ||
ToolsOzoneModerationDefs.isModEventReverseTakedown(coreEvent) ||
ToolsOzoneModerationDefs.isModEventLabel(coreEvent)

setModEventType(
eventMayNeedEmail && !shouldMoveToNextSubject && canSendEmail
? MOD_EVENTS.EMAIL
Expand Down
8 changes: 6 additions & 2 deletions app/repositories/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const getRepos =
},
options: { signal?: AbortSignal } = {},
): Promise<{
repos: ToolsOzoneModerationDefs.RepoView[]
repos: (
| ToolsOzoneModerationDefs.RepoViewDetail
| ToolsOzoneModerationDefs.RepoView
)[]
cursor?: string
}> => {
const limit = 25
Expand Down Expand Up @@ -82,10 +85,11 @@ const getRepos =
return { repos: [], cursor: data.cursor }
}

const repos: Record<string, ToolsOzoneModerationDefs.RepoView> = {}
const repos: Record<string, ToolsOzoneModerationDefs.RepoViewDetail> = {}
data.accounts.forEach((account) => {
repos[account.did] = {
...account,
$type: 'tools.ozone.moderation.defs#repoViewDetail',
// Set placeholder properties that will be later filled in with data from ozone
relatedRecords: [],
indexedAt: account.indexedAt,
Expand Down
78 changes: 47 additions & 31 deletions components/common/RecordCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ToolsOzoneModerationDefs,
AppBskyActorDefs,
ComAtprotoLabelDefs,
AppBskyActorProfile,
asPredicate,
} from '@atproto/api'
import { buildBlueSkyAppUrl, parseAtUri, pluralize } from '@/lib/util'
import { PostAsCard } from './posts/PostsFeed'
Expand All @@ -16,6 +18,7 @@ import { ProfileAvatar } from '@/repositories/ProfileAvatar'
import { ShieldCheckIcon } from '@heroicons/react/24/solid'
import { StarterPackRecordCard } from './starterpacks/RecordCard'
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { getProfileFromRepo } from '@/repositories/helpers'

export function RecordCard(props: {
uri: string
Expand Down Expand Up @@ -70,6 +73,8 @@ export function RecordCard(props: {
)
}

const isValidSelfLabels = asPredicate(ComAtprotoLabelDefs.validateSelfLabels)

function PostCard({
uri,
showLabels,
Expand All @@ -87,10 +92,10 @@ function PostCard({
retry: false,
queryKey: ['postCard', { uri }],
queryFn: async () => {
// @TODO when unifying admin auth, ensure admin can see taken-down posts
const { data: post } = await labelerAgent.api.app.bsky.feed.getPostThread(
{ uri, depth: 0 },
)
const { data: post } = await labelerAgent.app.bsky.feed.getPostThread({
uri,
depth: 0,
})
return post
},
})
Expand All @@ -106,33 +111,45 @@ function PostCard({

// When the author of the post blocks the viewer, getPostThread won't return the necessary properties
// to build the post view so we manually build the post view from the raw record data
if (data?.thread?.blocked) {
if (AppBskyFeedDefs.isBlockedPost(data?.thread)) {
return (
<BaseRecordCard
uri={uri}
renderRecord={(record) => (
<PostAsCard
dense
controls={[]}
item={{
post: {
uri: record.uri,
cid: record.cid,
author: record.repo,
record: record.value,
labels: ComAtprotoLabelDefs.isSelfLabels(record.value['labels'])
? record.value['labels'].values.map(({ val }) => ({
val,
uri: record.uri,
src: record.repo.did,
cts: new Date(0).toISOString(),
}))
: [],
indexedAt: new Date(0).toISOString(),
},
}}
/>
)}
renderRecord={(record) => {
const author = getProfileFromRepo(record.repo.relatedRecords)
const selfLabels = isValidSelfLabels(record.value?.labels)
? record.value.labels.values
: []
const labels = selfLabels.map(({ val }) => ({
val,
uri: record.uri,
src: record.repo.did,
cts: new Date(0).toISOString(),
}))
return (
<PostAsCard
dense
controls={[]}
item={{
post: {
author: {
did: record.repo.did,
handle: record.repo.handle,
...author,
avatar: undefined,
labels: [],
$type: 'app.bsky.actor.defs#profileViewBasic',
},
labels,
uri: record.uri,
cid: record.cid,
record: record.value,
indexedAt: new Date(0).toISOString(),
},
}}
/>
)
}}
/>
)
}
Expand Down Expand Up @@ -213,18 +230,17 @@ const useRepoAndProfile = ({ did }: { did: string }) => {
retry: false,
queryKey: ['repoCard', { did }],
queryFn: async () => {
// @TODO when unifying admin auth, ensure admin can see taken-down profiles
const getRepo = async () => {
const { data: repo } =
await labelerAgent.api.tools.ozone.moderation.getRepo({
await labelerAgent.tools.ozone.moderation.getRepo({
did,
})
return repo
}
const getProfile = async () => {
try {
const { data: profile } =
await labelerAgent.api.app.bsky.actor.getProfile({
await labelerAgent.app.bsky.actor.getProfile({
actor: did,
})
return profile
Expand Down
Loading

0 comments on commit 3876678

Please sign in to comment.