Send notifications with blossom url to meta.json #276
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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(() => {
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
}
|
||||
|
26
src/types/errors/MetaStorageError.ts
Normal file
26
src/types/errors/MetaStorageError.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user