refactor: flat meta and add useSigitProfile

This commit is contained in:
enes 2024-08-13 13:32:32 +02:00
parent 115a3974e2
commit d9779c10bd
5 changed files with 155 additions and 161 deletions

View File

@ -1,9 +1,6 @@
import { useEffect, useState } from 'react' import { Meta } from '../../types'
import { Meta, ProfileMetadata } from '../../types'
import { SigitCardDisplayInfo, SigitStatus } from '../../utils' import { SigitCardDisplayInfo, SigitStatus } from '../../utils'
import { Event, kinds } from 'nostr-tools'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { MetadataController } from '../../controllers'
import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils'
import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { appPublicRoutes, appPrivateRoutes } from '../../routes'
import { Button, Divider, Tooltip } from '@mui/material' import { Button, Divider, Tooltip } from '@mui/material'
@ -22,6 +19,7 @@ import { UserAvatarGroup } from '../UserAvatarGroup'
import styles from './style.module.scss' import styles from './style.module.scss'
import { TooltipChild } from '../TooltipChild' import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel' import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
type SigitProps = { type SigitProps = {
meta: Meta meta: Meta
@ -38,61 +36,10 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
fileExtensions fileExtensions
} = parsedMeta } = parsedMeta
const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( const profiles = useSigitProfiles([
{} ...(submittedBy ? [submittedBy] : []),
) ...signers
])
useEffect(() => {
const hexKeys = new Set<string>([
...signers.map((signer) => npubToHex(signer)!)
])
if (submittedBy) {
hexKeys.add(npubToHex(submittedBy)!)
}
const metadataController = new MetadataController()
const handleMetadataEvent = (key: string) => (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent) {
setProfiles((prev) => ({
...prev,
[key]: metadataContent
}))
}
}
const handleEventListener =
(key: string) => (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(key)(event)
}
}
hexKeys.forEach((key) => {
if (!(key in profiles)) {
metadataController.on(key, handleEventListener(key))
metadataController
.findMetadata(key)
.then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(key)(metadataEvent)
})
.catch((err) => {
console.error(`error occurred in finding metadata for: ${key}`, err)
})
}
})
return () => {
hexKeys.forEach((key) => {
metadataController.off(key, handleEventListener(key))
})
}
}, [submittedBy, signers, profiles])
return ( return (
<div className={styles.itemWrapper}> <div className={styles.itemWrapper}>

View File

@ -3,6 +3,7 @@ import { CreateSignatureEventContent, Meta } from '../types'
import { Mark } from '../types/mark' import { Mark } from '../types/mark'
import { import {
fromUnixTimestamp, fromUnixTimestamp,
hexToNpub,
parseCreateSignatureEvent, parseCreateSignatureEvent,
parseCreateSignatureEventContent, parseCreateSignatureEventContent,
SigitMetaParseError, SigitMetaParseError,
@ -12,11 +13,30 @@ import {
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { verifyEvent } from 'nostr-tools' import { verifyEvent } from 'nostr-tools'
import { Event } 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)
*/
interface FlatMeta
extends Meta,
CreateSignatureEventContent,
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
// Remove pubkey and use submittedBy as `npub1${string}`
submittedBy?: `npub1${string}`
// Remove created_at and replace with createdAt
createdAt?: number
interface FlatMeta extends Meta, CreateSignatureEventContent, Partial<Event> {
// Validated create signature event // Validated create signature event
isValid: boolean isValid: boolean
// Decryption
encryptionKey: string | null
// Calculated status fields // Calculated status fields
signedStatus: SigitStatus signedStatus: SigitStatus
signersStatus: { signersStatus: {
@ -33,8 +53,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
const [isValid, setIsValid] = useState(false) const [isValid, setIsValid] = useState(false)
const [kind, setKind] = useState<number>() const [kind, setKind] = useState<number>()
const [tags, setTags] = useState<string[][]>() const [tags, setTags] = useState<string[][]>()
const [created_at, setCreatedAt] = useState<number>() const [createdAt, setCreatedAt] = useState<number>()
const [pubkey, setPubkey] = useState<string>() // submittedBy, pubkey from nostr event const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
const [id, setId] = useState<string>() const [id, setId] = useState<string>()
const [sig, setSig] = useState<string>() const [sig, setSig] = useState<string>()
@ -54,6 +74,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
[signer: `npub1${string}`]: SignStatus [signer: `npub1${string}`]: SignStatus
}>({}) }>({})
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (!meta) return if (!meta) return
;(async function () { ;(async function () {
@ -70,7 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setTags(tags) setTags(tags)
// created_at in nostr events are stored in seconds // created_at in nostr events are stored in seconds
setCreatedAt(fromUnixTimestamp(created_at)) setCreatedAt(fromUnixTimestamp(created_at))
setPubkey(pubkey) setSubmittedBy(pubkey as `npub1${string}`)
setId(id) setId(id)
setSig(sig) setSig(sig)
@ -84,6 +106,31 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setMarkConfig(markConfig) setMarkConfig(markConfig)
setZipUrl(zipUrl) 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)
}
}
// Parse each signature event and set signer status // Parse each signature event and set signer status
for (const npub in meta.docSignatures) { for (const npub in meta.docSignatures) {
try { try {
@ -125,15 +172,15 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
}, [meta]) }, [meta])
return { return {
modifiedAt: meta.modifiedAt, modifiedAt: meta?.modifiedAt,
createSignature: meta.createSignature, createSignature: meta?.createSignature,
docSignatures: meta.docSignatures, docSignatures: meta?.docSignatures,
keys: meta.keys, keys: meta?.keys,
isValid, isValid,
kind, kind,
tags, tags,
created_at, createdAt,
pubkey, submittedBy,
id, id,
sig, sig,
signers, signers,
@ -143,6 +190,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
title, title,
zipUrl, zipUrl,
signedStatus, signedStatus,
signersStatus signersStatus,
encryptionKey
} }
} }

View File

@ -0,0 +1,70 @@
import { useEffect, useState } from 'react'
import { ProfileMetadata } from '../types'
import { MetadataController } from '../controllers'
import { npubToHex } from '../utils'
import { Event, kinds } from 'nostr-tools'
/**
* Extracts profiles from metadata events
* @param pubkeys Array of npubs to check
* @returns ProfileMetadata
*/
export const useSigitProfiles = (
pubkeys: `npub1${string}`[]
): { [key: string]: ProfileMetadata } => {
const [profileMetadata, setProfileMetadata] = useState<{
[key: string]: ProfileMetadata
}>({})
useEffect(() => {
if (pubkeys.length) {
const metadataController = new MetadataController()
// Remove duplicate keys
const users = new Set<string>([...pubkeys])
const handleMetadataEvent = (key: string) => (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent) {
setProfileMetadata((prev) => ({
...prev,
[key]: metadataContent
}))
}
}
users.forEach((user) => {
const hexKey = npubToHex(user)
if (hexKey && !(hexKey in profileMetadata)) {
metadataController.on(hexKey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(hexKey)(event)
}
})
metadataController
.findMetadata(hexKey)
.then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent)
})
.catch((err) => {
console.error(
`error occurred in finding metadata for: ${user}`,
err
)
})
}
})
return () => {
users.forEach((key) => {
metadataController.off(key, handleMetadataEvent(key))
})
}
}
}, [pubkeys, profileMetadata])
return profileMetadata
}

View File

@ -10,22 +10,20 @@ import {
} from '@mui/material' } from '@mui/material'
import JSZip from 'jszip' import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, kinds, verifyEvent } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserAvatar } from '../../components/UserAvatar' import { UserAvatar } from '../../components/UserAvatar'
import { MetadataController, NostrController } from '../../controllers' import { NostrController } from '../../controllers'
import { import {
CreateSignatureEventContent, CreateSignatureEventContent,
Meta, Meta,
ProfileMetadata,
SignedEventContent SignedEventContent
} from '../../types' } from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
extractMarksFromSignedMeta, extractMarksFromSignedMeta,
extractZipUrlAndEncryptionKey,
getHash, getHash,
hexToNpub, hexToNpub,
unixNow, unixNow,
@ -51,6 +49,8 @@ import { useSelector } from 'react-redux'
import { getLastSignersSig } from '../../utils/sign.ts' import { getLastSignersSig } from '../../utils/sign.ts'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
export const VerifyPage = () => { export const VerifyPage = () => {
const theme = useTheme() const theme = useTheme()
@ -63,52 +63,35 @@ export const VerifyPage = () => {
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
* meta will be received in navigation from create & home page in online mode * meta will be received in navigation from create & home page in online mode
*/ */
const { uploadedZip, meta: metaInNavState } = location.state || {} const { uploadedZip, meta } = location.state || {}
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers,
...viewers
])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [meta, setMeta] = useState<Meta | null>(null)
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
const [currentFileHashes, setCurrentFileHashes] = useState<{ const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null [key: string]: string | null
}>({}) }>(fileHashes)
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
useEffect(() => { useEffect(() => {
if (uploadedZip) { if (uploadedZip) {
setSelectedFile(uploadedZip) setSelectedFile(uploadedZip)
} else if (metaInNavState) { } else if (meta && encryptionKey) {
const processSigit = async () => { const processSigit = async () => {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta')
const res = await extractZipUrlAndEncryptionKey(metaInNavState)
if (!res) {
setIsLoading(false)
return
}
const {
zipUrl,
encryptionKey,
createSignatureEvent,
createSignatureContent
} = res
setLoadingSpinnerDesc('Fetching file from file server') setLoadingSpinnerDesc('Fetching file from file server')
axios axios
@ -175,12 +158,6 @@ export const VerifyPage = () => {
setCurrentFileHashes(fileHashes) setCurrentFileHashes(fileHashes)
setFiles(files) setFiles(files)
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMeta(metaInNavState)
setIsLoading(false) setIsLoading(false)
} }
}) })
@ -197,49 +174,7 @@ export const VerifyPage = () => {
processSigit() processSigit()
} }
}, [uploadedZip, metaInNavState]) }, [encryptionKey, meta, uploadedZip, zipUrl])
useEffect(() => {
if (submittedBy) {
const metadataController = new MetadataController()
const users = [submittedBy, ...signers, ...viewers]
users.forEach((user) => {
const pubkey = npubToHex(user)!
if (!(pubkey in metadata)) {
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[pubkey]: metadataContent
}))
}
metadataController.on(pubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController
.findMetadata(pubkey)
.then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent)
})
.catch((err) => {
console.error(
`error occurred in finding metadata for: ${user}`,
err
)
})
}
})
}
}, [submittedBy, signers, viewers, metadata])
const handleVerify = async () => { const handleVerify = async () => {
if (!selectedFile) return if (!selectedFile) return
@ -345,12 +280,6 @@ export const VerifyPage = () => {
if (!createSignatureContent) return if (!createSignatureContent) return
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMeta(parsedMetaJson)
setIsLoading(false) setIsLoading(false)
} }
@ -451,7 +380,7 @@ export const VerifyPage = () => {
} }
const displayUser = (pubkey: string, verifySignature = false) => { const displayUser = (pubkey: string, verifySignature = false) => {
const profile = metadata[pubkey] const profile = profiles[pubkey]
let isValidSignature = false let isValidSignature = false
@ -682,7 +611,7 @@ export const VerifyPage = () => {
<Box className={styles.filesWrapper}> <Box className={styles.filesWrapper}>
{Object.entries(currentFileHashes).map( {Object.entries(currentFileHashes).map(
([filename, hash], index) => { ([filename, hash], index) => {
const isValidHash = creatorFileHashes[filename] === hash const isValidHash = fileHashes[filename] === hash
return ( return (
<Box key={`file-${index}`} className={styles.file}> <Box key={`file-${index}`} className={styles.file}>

View File

@ -69,7 +69,7 @@ export enum SigitMetaParseErrorType {
export interface SigitCardDisplayInfo { export interface SigitCardDisplayInfo {
createdAt?: number createdAt?: number
title?: string title?: string
submittedBy?: string submittedBy?: `npub1${string}`
signers: `npub1${string}`[] signers: `npub1${string}`[]
fileExtensions: string[] fileExtensions: string[]
signedStatus: SigitStatus signedStatus: SigitStatus
@ -161,7 +161,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
) )
sigitInfo.title = createSignatureContent.title sigitInfo.title = createSignatureContent.title
sigitInfo.submittedBy = createSignatureEvent.pubkey sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}`
sigitInfo.signers = createSignatureContent.signers sigitInfo.signers = createSignatureContent.signers
sigitInfo.fileExtensions = extensions sigitInfo.fileExtensions = extensions