Send notifications with blossom url to meta.json #276

Merged
b merged 7 commits from 260-meta-blossom into staging 2024-12-18 11:46:57 +00:00
8 changed files with 210 additions and 29 deletions
Showing only changes of commit 3d1bdece4d - Show all commits

View File

@ -45,7 +45,7 @@ export interface FlatMeta
isValid: boolean
// Decryption
encryptionKey: string | null
encryptionKey: string | undefined
// Parsed Document Signatures
parsedSignatureEvents: {
@ -101,7 +101,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
[signer: `npub1${string}`]: SignStatus
}>({})
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
const [encryptionKey, setEncryptionKey] = useState<string | undefined>()
useEffect(() => {
if (!meta) return
@ -143,7 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setMarkConfig(markConfig)
setZipUrl(zipUrl)
let encryptionKey: string | null = null
let encryptionKey: string | undefined
if (meta.keys) {
const { sender, keys } = meta.keys
// Retrieve the user's public key from the state
@ -161,7 +161,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
'An error occurred in decrypting encryption key',
err
)
return null
return undefined
})
encryptionKey = decrypted

View File

@ -31,6 +31,7 @@ import {
KeyboardCode,
Meta,
ProfileMetadata,
SigitNotification,
SignedEvent,
User,
UserRole
@ -52,7 +53,8 @@ import {
updateUsersAppData,
uploadToFileStorage,
DEFAULT_TOOLBOX,
settleAllFullfilfedPromises
settleAllFullfilfedPromises,
uploadMetaToFileStorage
} from '../../utils'
import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss'
@ -782,7 +784,7 @@ export const CreatePage = () => {
}
// Send notifications to signers and viewers
const sendNotifications = (meta: Meta) => {
const sendNotifications = (notification: SigitNotification) => {
// no need to send notification to self so remove it from the list
const receivers = (
signers.length > 0
@ -790,7 +792,7 @@ export const CreatePage = () => {
: viewers.map((viewer) => viewer.pubkey)
).filter((receiver) => receiver !== usersPubkey)
return receivers.map((receiver) => sendNotification(receiver, meta))
return receivers.map((receiver) => sendNotification(receiver, notification))
}
const extractNostrId = (stringifiedEvent: string): string => {
@ -865,11 +867,17 @@ export const CreatePage = () => {
}
setLoadingSpinnerDesc('Updating user app data')
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
const event = await updateUsersAppData(meta)
if (!event) return
setLoadingSpinnerDesc('Sending notifications to counterparties')
const promises = sendNotifications(meta)
const promises = sendNotifications({
metaUrl,
keys: meta.keys
})
await Promise.all(promises)
.then(() => {

View File

@ -34,7 +34,8 @@ import {
updateUsersAppData,
findOtherUserMarks,
timeout,
processMarks
processMarks,
uploadMetaToFileStorage
} from '../../utils'
import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta'
@ -635,7 +636,7 @@ export const SignPage = () => {
}
if (await isOnline()) {
await handleOnlineFlow(updatedMeta)
await handleOnlineFlow(updatedMeta, encryptionKey)
} else {
setMeta(updatedMeta)
setIsLoading(false)
@ -741,7 +742,10 @@ export const SignPage = () => {
}
// Handle the online flow: update users app data and send notifications
const handleOnlineFlow = async (meta: Meta) => {
const handleOnlineFlow = async (
meta: Meta,
encryptionKey: string | undefined
) => {
setLoadingSpinnerDesc('Updating users app data')
const updatedEvent = await updateUsersAppData(meta)
if (!updatedEvent) {
@ -749,6 +753,8 @@ export const SignPage = () => {
return
}
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
const userSet = new Set<`npub1${string}`>()
if (submittedBy && submittedBy !== usersPubkey) {
userSet.add(hexToNpub(submittedBy))
@ -781,7 +787,7 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Sending notifications')
const users = Array.from(userSet)
const promises = users.map((user) =>
sendNotification(npubToHex(user)!, meta)
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
)
await Promise.all(promises)
.then(() => {

View File

@ -23,7 +23,8 @@ import {
getCurrentUserFiles,
updateUsersAppData,
npubToHex,
sendNotification
sendNotification,
uploadMetaToFileStorage
} from '../../utils'
import styles from './style.module.scss'
import { useLocation, useParams } from 'react-router-dom'
@ -351,6 +352,11 @@ export const VerifyPage = () => {
const updatedEvent = await updateUsersAppData(updatedMeta)
if (!updatedEvent) return
const metaUrl = await uploadMetaToFileStorage(
updatedMeta,
encryptionKey
)
const userSet = new Set<`npub1${string}`>()
signers.forEach((signer) => {
if (signer !== usersPubkey) {
@ -364,7 +370,10 @@ export const VerifyPage = () => {
const users = Array.from(userSet)
const promises = users.map((user) =>
sendNotification(npubToHex(user)!, updatedMeta)
sendNotification(npubToHex(user)!, {
metaUrl,
keys: meta.keys!
})
)
await Promise.all(promises)

View File

@ -83,3 +83,12 @@ export interface UserAppData {
export interface DocSignatureEvent extends Event {
parsedContent?: SignedEventContent
}
export interface SigitNotification {
metaUrl: string
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
}
export function isSigitNotification(obj: unknown): obj is SigitNotification {
return typeof (obj as SigitNotification).metaUrl === 'string'
}

View File

@ -0,0 +1,26 @@
import { Jsonable } from '.'
export enum MetaStorageErrorType {
'ENCRYPTION_KEY_REQUIRED' = 'Encryption key is required.',
'HASHING_FAILED' = "Can't get encrypted file hash.",
'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.',
'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.',
'DECRYPTION_FAILED' = 'Error decryping meta.json.',
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.'
}
export class MetaStorageError extends Error {
public readonly context?: Jsonable
constructor(
message: MetaStorageErrorType,
options: { cause?: Error; context?: Jsonable } = {}
) {
const { cause, context } = options
super(message, { cause })
this.name = this.constructor.name
this.context = context
}
}

View File

@ -1,5 +1,12 @@
import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.'
import {
decryptArrayBuffer,
encryptArrayBuffer,
fromUnixTimestamp,
getHash,
parseJson,
uploadToFileStorage
} from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
@ -8,6 +15,11 @@ import {
MetaParseError,
MetaParseErrorType
} from '../types/errors/MetaParseError'
import axios from 'axios'
import {
MetaStorageError,
MetaStorageErrorType
} from '../types/errors/MetaStorageError'
export enum SignStatus {
Signed = 'Signed',
@ -126,3 +138,76 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
}
}
}
export const uploadMetaToFileStorage = async (
meta: Meta,
encryptionKey: string | undefined
) => {
// Value is the stringified meta object
const value = JSON.stringify(meta)
const encoder = new TextEncoder()
// Encode it to the arrayBuffer
const uint8Array = encoder.encode(value)
if (!encryptionKey) {
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
}
// Encrypt the file contents with the same encryption key from the create signature
const encryptedArrayBuffer = await encryptArrayBuffer(
uint8Array,
encryptionKey
)
const hash = await getHash(encryptedArrayBuffer)
if (!hash) {
throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED)
}
// Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`)
const url = await uploadToFileStorage(file)
return url
}
export const fetchMetaFromFileStorage = async (
url: string,
encryptionKey: string | undefined
) => {
if (!encryptionKey) {
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
}
const encryptedArrayBuffer = await axios.get(url, {
responseType: 'arraybuffer'
})
// Verify hash
const parts = url.split('/')
const urlHash = parts[parts.length - 1]
const hash = await getHash(encryptedArrayBuffer.data)
if (hash !== urlHash) {
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED)
}
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data,
encryptionKey
).catch((err) => {
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
cause: err
})
})
if (arrayBuffer) {
// Decode meta.json and parse
const decoder = new TextDecoder()
const json = decoder.decode(arrayBuffer)
const meta = await parseJson<Meta>(json)
return meta
}
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
}

View File

@ -29,12 +29,20 @@ import {
} from '../store/actions'
import { Keys } from '../store/auth/types'
import store from '../store/store'
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
import {
isSigitNotification,
Meta,
ProfileMetadata,
SigitNotification,
SignedEvent,
UserAppData
} from '../types'
import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
import { getHash } from './hash'
import { SIGIT_BLOSSOM } from './const.ts'
import { fetchMetaFromFileStorage } from './meta.ts'
/**
* Generates a `d` tag for userAppData
@ -908,17 +916,44 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
(err) => {
console.log(
'An error occurred in parsing the internal unsigned event',
err
)
return null
}
)
const parsedContent = await parseJson<Meta | SigitNotification>(
internalUnsignedEvent.content
).catch((err) => {
console.log('An error occurred in parsing the internal unsigned event', err)
return null
})
if (!meta) return
if (!parsedContent) return
let meta: Meta
if (isSigitNotification(parsedContent)) {
const notification = parsedContent
let encryptionKey: string | undefined
if (!notification.keys) return
const { sender, keys } = notification.keys
// Retrieve the user's public key from the state
const usersPubkey = store.getState().auth.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 undefined
})
encryptionKey = decrypted
}
meta = await fetchMetaFromFileStorage(notification.metaUrl, encryptionKey)
} else {
meta = parsedContent
}
await updateUsersAppData(meta)
}
@ -926,9 +961,12 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
/**
* Function to send a notification to a specified receiver.
* @param receiver - The recipient's public key.
* @param meta - Metadata associated with the notification.
* @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt.
*/
export const sendNotification = async (receiver: string, meta: Meta) => {
export const sendNotification = async (
receiver: string,
notification: SigitNotification
) => {
// Retrieve the user's public key from the state
const usersPubkey = store.getState().auth.usersPubkey!
@ -936,7 +974,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
const unsignedEvent: UnsignedEvent = {
kind: 938,
pubkey: usersPubkey,
content: JSON.stringify(meta),
content: JSON.stringify(notification),
tags: [],
created_at: unixNow()
}