Send notifications with blossom url to meta.json #276
@ -45,7 +45,7 @@ export interface FlatMeta
|
|||||||
isValid: boolean
|
isValid: boolean
|
||||||
|
|
||||||
// Decryption
|
// Decryption
|
||||||
encryptionKey: string | null
|
encryptionKey: string | undefined
|
||||||
|
|
||||||
// Parsed Document Signatures
|
// Parsed Document Signatures
|
||||||
parsedSignatureEvents: {
|
parsedSignatureEvents: {
|
||||||
@ -101,7 +101,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
[signer: `npub1${string}`]: SignStatus
|
[signer: `npub1${string}`]: SignStatus
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [encryptionKey, setEncryptionKey] = useState<string | null>(null)
|
const [encryptionKey, setEncryptionKey] = useState<string | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!meta) return
|
if (!meta) return
|
||||||
@ -143,7 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setMarkConfig(markConfig)
|
setMarkConfig(markConfig)
|
||||||
setZipUrl(zipUrl)
|
setZipUrl(zipUrl)
|
||||||
|
|
||||||
let encryptionKey: string | null = null
|
let encryptionKey: string | undefined
|
||||||
if (meta.keys) {
|
if (meta.keys) {
|
||||||
const { sender, keys } = meta.keys
|
const { sender, keys } = meta.keys
|
||||||
// Retrieve the user's public key from the state
|
// 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',
|
'An error occurred in decrypting encryption key',
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
return null
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
encryptionKey = decrypted
|
encryptionKey = decrypted
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
KeyboardCode,
|
KeyboardCode,
|
||||||
Meta,
|
Meta,
|
||||||
ProfileMetadata,
|
ProfileMetadata,
|
||||||
|
SigitNotification,
|
||||||
SignedEvent,
|
SignedEvent,
|
||||||
User,
|
User,
|
||||||
UserRole
|
UserRole
|
||||||
@ -52,7 +53,8 @@ import {
|
|||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
uploadToFileStorage,
|
uploadToFileStorage,
|
||||||
DEFAULT_TOOLBOX,
|
DEFAULT_TOOLBOX,
|
||||||
settleAllFullfilfedPromises
|
settleAllFullfilfedPromises,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||||
@ -782,7 +784,7 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send notifications to signers and viewers
|
// 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
|
// no need to send notification to self so remove it from the list
|
||||||
const receivers = (
|
const receivers = (
|
||||||
signers.length > 0
|
signers.length > 0
|
||||||
@ -790,7 +792,7 @@ export const CreatePage = () => {
|
|||||||
: viewers.map((viewer) => viewer.pubkey)
|
: viewers.map((viewer) => viewer.pubkey)
|
||||||
).filter((receiver) => receiver !== usersPubkey)
|
).filter((receiver) => receiver !== usersPubkey)
|
||||||
|
|
||||||
return receivers.map((receiver) => sendNotification(receiver, meta))
|
return receivers.map((receiver) => sendNotification(receiver, notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractNostrId = (stringifiedEvent: string): string => {
|
const extractNostrId = (stringifiedEvent: string): string => {
|
||||||
@ -865,11 +867,17 @@ export const CreatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating user app data')
|
setLoadingSpinnerDesc('Updating user app data')
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||||
|
|
||||||
const event = await updateUsersAppData(meta)
|
const event = await updateUsersAppData(meta)
|
||||||
if (!event) return
|
if (!event) return
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||||
const promises = sendNotifications(meta)
|
const promises = sendNotifications({
|
||||||
|
metaUrl,
|
||||||
|
keys: meta.keys
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -34,7 +34,8 @@ import {
|
|||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
findOtherUserMarks,
|
findOtherUserMarks,
|
||||||
timeout,
|
timeout,
|
||||||
processMarks
|
processMarks,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { DisplayMeta } from './internal/displayMeta'
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
@ -635,7 +636,7 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (await isOnline()) {
|
if (await isOnline()) {
|
||||||
await handleOnlineFlow(updatedMeta)
|
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||||
} else {
|
} else {
|
||||||
setMeta(updatedMeta)
|
setMeta(updatedMeta)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -741,7 +742,10 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle the online flow: update users app data and send notifications
|
// 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')
|
setLoadingSpinnerDesc('Updating users app data')
|
||||||
const updatedEvent = await updateUsersAppData(meta)
|
const updatedEvent = await updateUsersAppData(meta)
|
||||||
if (!updatedEvent) {
|
if (!updatedEvent) {
|
||||||
@ -749,6 +753,8 @@ export const SignPage = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||||
|
|
||||||
const userSet = new Set<`npub1${string}`>()
|
const userSet = new Set<`npub1${string}`>()
|
||||||
if (submittedBy && submittedBy !== usersPubkey) {
|
if (submittedBy && submittedBy !== usersPubkey) {
|
||||||
userSet.add(hexToNpub(submittedBy))
|
userSet.add(hexToNpub(submittedBy))
|
||||||
@ -781,7 +787,7 @@ export const SignPage = () => {
|
|||||||
setLoadingSpinnerDesc('Sending notifications')
|
setLoadingSpinnerDesc('Sending notifications')
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, meta)
|
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys })
|
||||||
)
|
)
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -23,7 +23,8 @@ import {
|
|||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
sendNotification
|
sendNotification,
|
||||||
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
@ -351,6 +352,11 @@ export const VerifyPage = () => {
|
|||||||
const updatedEvent = await updateUsersAppData(updatedMeta)
|
const updatedEvent = await updateUsersAppData(updatedMeta)
|
||||||
if (!updatedEvent) return
|
if (!updatedEvent) return
|
||||||
|
|
||||||
|
const metaUrl = await uploadMetaToFileStorage(
|
||||||
|
updatedMeta,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
const userSet = new Set<`npub1${string}`>()
|
const userSet = new Set<`npub1${string}`>()
|
||||||
signers.forEach((signer) => {
|
signers.forEach((signer) => {
|
||||||
if (signer !== usersPubkey) {
|
if (signer !== usersPubkey) {
|
||||||
@ -364,7 +370,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, updatedMeta)
|
sendNotification(npubToHex(user)!, {
|
||||||
|
metaUrl,
|
||||||
|
keys: meta.keys!
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
@ -83,3 +83,12 @@ export interface UserAppData {
|
|||||||
export interface DocSignatureEvent extends Event {
|
export interface DocSignatureEvent extends Event {
|
||||||
parsedContent?: SignedEventContent
|
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 { CreateSignatureEventContent, Meta } from '../types'
|
||||||
import { fromUnixTimestamp, parseJson } from '.'
|
import {
|
||||||
|
decryptArrayBuffer,
|
||||||
|
encryptArrayBuffer,
|
||||||
|
fromUnixTimestamp,
|
||||||
|
getHash,
|
||||||
|
parseJson,
|
||||||
|
uploadToFileStorage
|
||||||
|
} from '.'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractFileExtensions } from './file'
|
import { extractFileExtensions } from './file'
|
||||||
@ -8,6 +15,11 @@ import {
|
|||||||
MetaParseError,
|
MetaParseError,
|
||||||
MetaParseErrorType
|
MetaParseErrorType
|
||||||
} from '../types/errors/MetaParseError'
|
} from '../types/errors/MetaParseError'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
MetaStorageError,
|
||||||
|
MetaStorageErrorType
|
||||||
|
} from '../types/errors/MetaStorageError'
|
||||||
|
|
||||||
export enum SignStatus {
|
export enum SignStatus {
|
||||||
Signed = 'Signed',
|
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'
|
} from '../store/actions'
|
||||||
import { Keys } from '../store/auth/types'
|
import { Keys } from '../store/auth/types'
|
||||||
import store from '../store/store'
|
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 { getDefaultRelayMap } from './relays'
|
||||||
import { parseJson, removeLeadingSlash } from './string'
|
import { parseJson, removeLeadingSlash } from './string'
|
||||||
import { timeout } from './utils'
|
import { timeout } from './utils'
|
||||||
import { getHash } from './hash'
|
import { getHash } from './hash'
|
||||||
import { SIGIT_BLOSSOM } from './const.ts'
|
import { SIGIT_BLOSSOM } from './const.ts'
|
||||||
|
import { fetchMetaFromFileStorage } from './meta.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a `d` tag for userAppData
|
* Generates a `d` tag for userAppData
|
||||||
@ -908,17 +916,44 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
|||||||
|
|
||||||
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
||||||
|
|
||||||
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
|
const parsedContent = await parseJson<Meta | SigitNotification>(
|
||||||
(err) => {
|
internalUnsignedEvent.content
|
||||||
console.log(
|
).catch((err) => {
|
||||||
'An error occurred in parsing the internal unsigned event',
|
console.log('An error occurred in parsing the internal unsigned event', err)
|
||||||
err
|
return null
|
||||||
)
|
})
|
||||||
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)
|
await updateUsersAppData(meta)
|
||||||
}
|
}
|
||||||
@ -926,9 +961,12 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
|||||||
/**
|
/**
|
||||||
* Function to send a notification to a specified receiver.
|
* Function to send a notification to a specified receiver.
|
||||||
* @param receiver - The recipient's public key.
|
* @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
|
// Retrieve the user's public key from the state
|
||||||
const usersPubkey = store.getState().auth.usersPubkey!
|
const usersPubkey = store.getState().auth.usersPubkey!
|
||||||
|
|
||||||
@ -936,7 +974,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
kind: 938,
|
kind: 938,
|
||||||
pubkey: usersPubkey,
|
pubkey: usersPubkey,
|
||||||
content: JSON.stringify(meta),
|
content: JSON.stringify(notification),
|
||||||
tags: [],
|
tags: [],
|
||||||
created_at: unixNow()
|
created_at: unixNow()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user