sigit.io/src/utils/meta.ts

181 lines
4.7 KiB
TypeScript

import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.'
import { Event } from 'nostr-tools'
import { toast } from 'react-toastify'
export enum SignStatus {
Signed = 'Signed',
Pending = 'Pending',
Invalid = 'Invalid Sign'
}
export enum SigitStatus {
Partial = 'In-Progress',
Complete = 'Completed'
}
type Jsonable =
| string
| number
| boolean
| null
| undefined
| readonly Jsonable[]
| { readonly [key: string]: Jsonable }
| { toJSON(): Jsonable }
export class SigitMetaParseError extends Error {
public readonly context?: Jsonable
constructor(
message: string,
options: { cause?: Error; context?: Jsonable } = {}
) {
const { cause, context } = options
super(message, { cause })
this.name = this.constructor.name
this.context = context
}
}
/**
* Handle meta errors
* Wraps the errors without message property and stringify to a message so we can use it later
* @param error
* @returns
*/
function handleError(error: unknown): Error {
if (error instanceof Error) return error
// No message error, wrap it and stringify
let stringified = 'Unable to stringify the thrown value'
try {
stringified = JSON.stringify(error)
} catch (error) {
console.error(stringified, error)
}
return new Error(`[SiGit Error]: ${stringified}`)
}
// Reuse common error messages for meta parsing
export enum SigitMetaParseErrorType {
'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
}
export interface SigitCardDisplayInfo {
createdAt?: number
title?: string
submittedBy?: `npub1${string}`
signers: `npub1${string}`[]
fileExtensions: string[]
signedStatus: SigitStatus
}
/**
* Wrapper for event parser that throws custom SigitMetaParseError with cause and context
* @param raw Raw string for parsing
* @returns parsed Event
*/
export const parseNostrEvent = async (raw: string): Promise<Event> => {
try {
const event = await parseJson<Event>(raw)
return event
} catch (error) {
throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, {
cause: handleError(error),
context: raw
})
}
}
/**
* Wrapper for event content parser that throws custom SigitMetaParseError with cause and context
* @param raw Raw string for parsing
* @returns parsed CreateSignatureEventContent
*/
export const parseCreateSignatureEventContent = async (
raw: string
): Promise<CreateSignatureEventContent> => {
try {
const createSignatureEventContent =
await parseJson<CreateSignatureEventContent>(raw)
return createSignatureEventContent
} catch (error) {
throw new SigitMetaParseError(
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
{
cause: handleError(error),
context: raw
}
)
}
}
/**
* Extracts only necessary metadata for the card display
* @param meta Sigit metadata
* @returns SigitCardDisplayInfo
*/
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
if (!meta?.createSignature) return
const sigitInfo: SigitCardDisplayInfo = {
signers: [],
fileExtensions: [],
signedStatus: SigitStatus.Partial
}
try {
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
// created_at in nostr events are stored in seconds
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
const createSignatureContent = await parseCreateSignatureEventContent(
createSignatureEvent.content
)
const files = Object.keys(createSignatureContent.fileHashes)
const extensions = extractFileExtensions(files)
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
signedBy.includes(signer)
)
sigitInfo.title = createSignatureContent.title
sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}`
sigitInfo.signers = createSignatureContent.signers
sigitInfo.fileExtensions = extensions
if (isCompletelySigned) {
sigitInfo.signedStatus = SigitStatus.Complete
}
return sigitInfo
} catch (error) {
if (error instanceof SigitMetaParseError) {
toast.error(error.message)
console.error(error.name, error.message, error.cause, error.context)
} else {
console.error('Unexpected error', error)
}
}
}
export const extractFileExtensions = (fileNames: string[]) => {
const extensions = fileNames.reduce((result: string[], file: string) => {
const extension = file.split('.').pop()
if (extension) {
result.push(extension)
}
return result
}, [])
return extensions
}