import { useEffect, useState } from 'react' import { CreateSignatureEventContent, Meta } from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, hexToNpub, parseNostrEvent, parseCreateSignatureEventContent, SigitMetaParseError, SigitStatus, SignStatus } from '../utils' import { toast } from 'react-toastify' import { verifyEvent } from 'nostr-tools' import { Event } from 'nostr-tools' import store from '../store/store' import { AuthState } from '../store/auth/types' import { NostrController } from '../controllers' /** * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) */ export interface FlatMeta extends Meta, CreateSignatureEventContent, Partial> { // Remove pubkey and use submittedBy as `npub1${string}` submittedBy?: `npub1${string}` // Remove created_at and replace with createdAt createdAt?: number // Validated create signature event isValid: boolean // Decryption encryptionKey: string | null // Parsed Document Signatures parsedSignatureEvents: { [signer: `npub1${string}`]: Event } // Calculated completion time completedAt?: number // Calculated status fields signedStatus: SigitStatus signersStatus: { [signer: `npub1${string}`]: SignStatus } } /** * Custom use hook for parsing the Sigit Meta * @param meta Sigit Meta * @returns flattened Meta object with calculated signed status */ export const useSigitMeta = (meta: Meta): FlatMeta => { const [isValid, setIsValid] = useState(false) const [kind, setKind] = useState() const [tags, setTags] = useState() const [createdAt, setCreatedAt] = useState() const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event const [id, setId] = useState() const [sig, setSig] = useState() const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) const [fileHashes, setFileHashes] = useState<{ [user: `npub1${string}`]: string }>({}) const [markConfig, setMarkConfig] = useState([]) const [title, setTitle] = useState('') const [zipUrl, setZipUrl] = useState('') const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ [signer: `npub1${string}`]: Event }>({}) const [completedAt, setCompletedAt] = useState() const [signedStatus, setSignedStatus] = useState( SigitStatus.Partial ) const [signersStatus, setSignersStatus] = useState<{ [signer: `npub1${string}`]: SignStatus }>({}) const [encryptionKey, setEncryptionKey] = useState(null) useEffect(() => { if (!meta) return ;(async function () { try { const createSignatureEvent = await parseNostrEvent(meta.createSignature) const { kind, tags, created_at, pubkey, id, sig, content } = createSignatureEvent setIsValid(verifyEvent(createSignatureEvent)) setKind(kind) setTags(tags) // created_at in nostr events are stored in seconds setCreatedAt(fromUnixTimestamp(created_at)) setSubmittedBy(pubkey as `npub1${string}`) setId(id) setSig(sig) const { title, signers, viewers, fileHashes, markConfig, zipUrl } = await parseCreateSignatureEventContent(content) setTitle(title) setSigners(signers) setViewers(viewers) setFileHashes(fileHashes) setMarkConfig(markConfig) setZipUrl(zipUrl) if (meta.keys) { const { sender, keys } = meta.keys // Retrieve the user's public key from the state const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersNpub = hexToNpub(usersPubkey) // Check if the user's public key is in the keys object if (usersNpub in keys) { // Instantiate the NostrController to decrypt the encryption key const nostrController = NostrController.getInstance() const decrypted = await nostrController .nip04Decrypt(sender, keys[usersNpub]) .catch((err) => { console.log( 'An error occurred in decrypting encryption key', err ) return null }) setEncryptionKey(decrypted) } } // Temp. map to hold events const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>() for (const npub in meta.docSignatures) { try { // Parse each signature event const event = await parseNostrEvent( meta.docSignatures[npub as `npub1${string}`] ) const isValidSignature = verifyEvent(event) // Save events to a map, to save all at once outside loop // We need the object to find completedAt // Avoided using parsedSignatureEvents due to useEffect deps parsedSignatureEventsMap.set(npub as `npub1${string}`, event) setSignersStatus((prev) => { return { ...prev, [npub]: isValidSignature ? SignStatus.Signed : SignStatus.Invalid } }) } catch (error) { setSignersStatus((prev) => { return { ...prev, [npub]: SignStatus.Invalid } }) } } setParsedSignatureEvents( Object.fromEntries(parsedSignatureEventsMap.entries()) ) const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[] const isCompletelySigned = signers.every((signer) => signedBy.includes(signer) ) setSignedStatus( isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial ) // Check if all signers signed and are valid if (isCompletelySigned) { setCompletedAt( fromUnixTimestamp( signedBy.reduce((p, c) => { return Math.max( p, parsedSignatureEventsMap.get(c)?.created_at || 0 ) }, 0) ) ) } } catch (error) { if (error instanceof SigitMetaParseError) { toast.error(error.message) } console.error(error) } })() }, [meta]) return { modifiedAt: meta?.modifiedAt, createSignature: meta?.createSignature, docSignatures: meta?.docSignatures, keys: meta?.keys, isValid, kind, tags, createdAt, submittedBy, id, sig, signers, viewers, fileHashes, markConfig, title, zipUrl, parsedSignatureEvents, completedAt, signedStatus, signersStatus, encryptionKey } }