import { useEffect, useState } from 'react' import { CreateSignatureEventContent, DocSignatureEvent, Meta, SignedEventContent } 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}`]: DocSignatureEvent } // 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}`]: DocSignatureEvent }>({}) 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 and signers const parsedSignatureEventsMap = new Map< `npub1${string}`, DocSignatureEvent >() const signerStatusMap = new Map<`npub1${string}`, SignStatus>() const getPrevSignerSig = (npub: `npub1${string}`) => { if (signers[0] === npub) { return sig } // find the index of signer const currentSignerIndex = signers.findIndex( (signer) => signer === npub ) // return if could not found user in signer's list if (currentSignerIndex === -1) return // find prev signer const prevSigner = signers[currentSignerIndex - 1] // get the signature of prev signer return parsedSignatureEventsMap.get(prevSigner)?.sig } for (const npub in meta.docSignatures) { try { // Parse each signature event const event = await parseNostrEvent( meta.docSignatures[npub as `npub1${string}`] ) // 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) } catch (error) { signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) } } parsedSignatureEventsMap.forEach((event, npub) => { const isValidSignature = verifyEvent(event) if (isValidSignature) { // get the signature of prev signer from the content of current signers signedEvent const prevSignersSig = getPrevSignerSig(npub) try { const obj: SignedEventContent = JSON.parse(event.content) parsedSignatureEventsMap.set(npub, { ...event, parsedContent: obj }) if ( obj.prevSig && prevSignersSig && obj.prevSig === prevSignersSig ) { signerStatusMap.set(npub as `npub1${string}`, SignStatus.Signed) } } catch (error) { signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) } } }) signers .filter((s) => !parsedSignatureEventsMap.has(s)) .forEach((s) => signerStatusMap.set(s, SignStatus.Pending)) // Get the first signer that hasn't signed const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s)) if (nextSigner) { signerStatusMap.set(nextSigner, SignStatus.Awaiting) } setSignersStatus(Object.fromEntries(signerStatusMap)) setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap)) 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 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 } }