From e25c571e037e2510687a82954a29483ea0bee0e6 Mon Sep 17 00:00:00 2001 From: Matthieu Sieben Date: Fri, 22 Nov 2024 17:56:25 +0100 Subject: [PATCH] Correlate accounts from workspace (#239) * Correlate accountds from workspace * Allow correlating from records * revert submission state removal * disable correlation button when not available * retrieve did from strong ref --- components/workspace/Panel.tsx | 149 ++++++++++++++++++++++---- components/workspace/PanelActions.tsx | 16 ++- 2 files changed, 144 insertions(+), 21 deletions(-) diff --git a/components/workspace/Panel.tsx b/components/workspace/Panel.tsx index c14cf5ed..1015623c 100644 --- a/components/workspace/Panel.tsx +++ b/components/workspace/Panel.tsx @@ -1,28 +1,40 @@ +import { ActionPanel } from '@/common/ActionPanel' +import { FullScreenActionPanel } from '@/common/FullScreenActionPanel' +import { LabelChip } from '@/common/labels' +import { PropsOf } from '@/lib/types' +import { MOD_EVENTS } from '@/mod-event/constants' +import { useActionSubjects } from '@/mod-event/helpers/emitEvent' +import { useLabelerAgent, useServerConfig } from '@/shell/ConfigurationContext' import { + AtUri, + ComAtprotoAdminDefs, ComAtprotoModerationDefs, + ComAtprotoRepoStrongRef, + ToolsOzoneModerationDefs, ToolsOzoneModerationEmitEvent, } from '@atproto/api' -import { FormEvent, useRef, useState } from 'react' -import { ActionPanel } from '@/common/ActionPanel' -import { PropsOf } from '@/lib/types' -import { FullScreenActionPanel } from '@/common/FullScreenActionPanel' -import { CheckCircleIcon } from '@heroicons/react/24/outline' -import { MOD_EVENTS } from '@/mod-event/constants' import { Dialog } from '@headlessui/react' +import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' +import { CheckCircleIcon } from '@heroicons/react/24/outline' +import Link from 'next/link' +import { FormEvent, useRef, useState } from 'react' +import { toast } from 'react-toastify' +import { WORKSPACE_FORM_ID } from './constants' import { useWorkspaceEmptyMutation, useWorkspaceList, useWorkspaceRemoveItemsMutation, } from './hooks' -import WorkspaceList from './List' import WorkspaceItemCreator from './ItemCreator' -import { useSubjectStatuses } from '@/subject/useSubjectStatus' -import { WorkspacePanelActions } from './PanelActions' -import { WORKSPACE_FORM_ID } from './constants' +import WorkspaceList from './List' import { WorkspacePanelActionForm } from './PanelActionForm' -import { useActionSubjects } from '@/mod-event/helpers/emitEvent' +import { WorkspacePanelActions } from './PanelActions' import { useWorkspaceListData } from './useWorkspaceListData' +function isNonNullable(v: V): v is NonNullable { + return v != null +} + export function WorkspacePanel(props: PropsOf) { const { onClose, ...others } = props @@ -57,6 +69,103 @@ export function WorkspacePanel(props: PropsOf) { isSubmitting: boolean error: string }>({ isSubmitting: false, error: '' }) + + const labelerAgent = useLabelerAgent() + const supportsCorrelation = useServerConfig().pds != null + const handleFindCorrelation = supportsCorrelation + ? async () => { + const selectedItems = new FormData(formRef.current!) + .getAll('workspaceItem') + .filter((item): item is string => typeof item === 'string') + + // For every selected item, find out which DID it corresponds + const dids = selectedItems + .map((item) => { + if (item.startsWith('did:')) return item + + const status = workspaceListStatuses?.[item] + + if (ToolsOzoneModerationDefs.isRepoViewDetail(status)) { + return status.did + } + + if (ToolsOzoneModerationDefs.isRecordViewDetail(status)) { + return status.repo.did + } + + if (ToolsOzoneModerationDefs.isSubjectStatusView(status)) { + const { subject } = status + if (ComAtprotoAdminDefs.isRepoRef(subject)) { + return subject.did + } + + if (ComAtprotoRepoStrongRef.isMain(subject)) { + return new AtUri(subject.uri).host + } + } + + // Should never happen (future proofing against new item types in workspace) + return undefined + }) + .filter(isNonNullable) + + if (dids.length <= 1) { + toast.warning('Please select at least two accounts to correlate.') + return + } + + if (dids.length !== selectedItems.length) { + toast.info('Only accounts can be correlated (ignoring non-accounts).') + } + + const res = await labelerAgent.tools.ozone.signature.findCorrelation({ + dids, + }) + + const { details } = res.data + + if (!details.length) { + toast.info('No correlation found between the selected accounts.') + } else { + toast.success( +
+ The following correlation were found between the selected + accounts: +
+ {details.map(({ property, value }) => ( + + + + {property} + + + ))} + {details.length > 1 && ( + <> +
+ s.value)), + )}`} + className="text-blue-500 underline" + > + Click here to show all accounts with the same details. + + + )} +
, + { + autoClose: 10_000, + }, + ) + } + } + : undefined + // on form submit const onFormSubmit = async ( ev: FormEvent & { target: HTMLFormElement }, @@ -140,6 +249,7 @@ export function WorkspacePanel(props: PropsOf) { }) } } catch (err) { + console.error(err) setSubmission({ error: (err as Error).message, isSubmitting: false }) } } @@ -195,15 +305,14 @@ export function WorkspacePanel(props: PropsOf) { {!showItemCreator && (
)} diff --git a/components/workspace/PanelActions.tsx b/components/workspace/PanelActions.tsx index 02ec8018..6095d7ad 100644 --- a/components/workspace/PanelActions.tsx +++ b/components/workspace/PanelActions.tsx @@ -1,12 +1,13 @@ import { ActionButton } from '@/common/buttons' import { CopyButton } from '@/common/CopyButton' -import { PlusIcon, NoSymbolIcon, TrashIcon } from '@heroicons/react/24/solid' +import { NoSymbolIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid' import { WorkspaceFilterSelector } from './FilterSelector' import { WorkspaceListData } from './useWorkspaceListData' export const WorkspacePanelActions = ({ handleRemoveSelected, handleEmptyWorkspace, + handleFindCorrelation, setShowActionForm, setShowItemCreator, showActionForm, @@ -15,6 +16,7 @@ export const WorkspacePanelActions = ({ }: { handleRemoveSelected: () => void handleEmptyWorkspace: () => void + handleFindCorrelation?: () => void setShowActionForm: React.Dispatch> setShowItemCreator: React.Dispatch> showActionForm: boolean @@ -50,6 +52,18 @@ export const WorkspacePanelActions = ({ > {showActionForm ? 'Hide Action Form' : 'Show Action Form'} + + {handleFindCorrelation && ( + + Find correlation + + )} +