sigit.io/src/utils/misc.ts

201 lines
6.6 KiB
TypeScript
Raw Normal View History

import axios from 'axios'
import { EventTemplate } from 'nostr-tools'
import { MetadataController, NostrController } from '../controllers'
import { toast } from 'react-toastify'
import { appPrivateRoutes } from '../routes'
/**
* Uploads a file to a file storage service.
* @param blob The Blob object representing the file to upload.
* @param nostrController The NostrController instance for handling authentication.
* @returns The URL of the uploaded file.
*/
export const uploadToFileStorage = async (
blob: Blob,
nostrController: NostrController
) => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
// Create a File object with the Blob data
const file = new File([blob], `zipped-${unixNow}.zip`, {
type: 'application/zip'
})
// Define event metadata for authorization
const event: EventTemplate = {
kind: 24242,
content: 'Authorize Upload',
created_at: Math.floor(Date.now() / 1000),
tags: [
['t', 'upload'],
['expiration', String(unixNow + 60 * 5)], // Set expiration time to 5 minutes from now
['name', file.name],
['size', String(file.size)]
]
}
// Sign the authorization event using the NostrController
const authEvent = await nostrController.signEvent(event)
// URL of the file storage service
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
// Upload the file to the file storage service using Axios
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
'Content-Type': 'application/zip' // Set content type header
}
})
// Return the URL of the uploaded file
return response.data.url as string
}
/**
* Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
* @param fileUrl The URL of the encrypted zip file to be included in the DM.
* @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
* @param pubkey The public key of the recipient.
* @param nostrController The NostrController instance for handling authentication and encryption.
* @param isSigner Boolean indicating whether the recipient is a signer or viewer.
* @param setAuthUrl Function to set the authentication URL in the component state.
*/
export const sendDM = async (
fileUrl: string,
encryptionKey: string,
pubkey: string,
nostrController: NostrController,
isSigner: boolean,
setAuthUrl: (value: React.SetStateAction<string | undefined>) => void
) => {
// Construct the content of the DM
const initialLine = isSigner
? 'You have been requested for a signature.'
: 'You have received a signed document.'
const decryptionUrl = `http://app.sigit.io${appPrivateRoutes.decryptZip}?file=${fileUrl}&key=${encryptionKey}`
const content = `${initialLine}\nHere is the URL for the zip file that you can download.\n${fileUrl}\nHowever, this zip file is encrypted and you need to decrypt it using ${decryptionUrl}`
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
// Set up timeout promise to handle encryption timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout occurred'))
}, 15000) // Timeout duration = 15 seconds
})
// Encrypt the DM content, with timeout
const encrypted = await Promise.race([
nostrController.nip04Encrypt(pubkey, content),
timeoutPromise
])
.then((res) => {
return res
})
.catch((err) => {
console.log('err :>> ', err)
toast.error(
err.message || 'An error occurred while encrypting DM content'
)
return null
})
.finally(() => {
setAuthUrl(undefined) // Clear authentication URL
})
// Return if encryption failed
if (!encrypted) return
// Construct event metadata for the DM
const event: EventTemplate = {
kind: 4, // DM event type
content: encrypted, // Encrypted DM content
created_at: Math.floor(Date.now() / 1000), // Current timestamp
tags: [['p', pubkey]] // Tag with recipient's public key
}
// Sign the DM event
const signedEvent = await nostrController.signEvent(event).catch((err) => {
console.log('err :>> ', err)
toast.error(err.message || 'An error occurred while signing event for DM')
return null
})
// Return if event signing failed
if (!signedEvent) return
// Get relay list metadata
const metadataController = new MetadataController()
const relaySet = await metadataController
.findRelayListMetadata(pubkey)
.catch((err) => {
toast.error(
err.message || 'An error occurred while finding relay list metadata'
)
return null
})
// Return if metadata retrieval failed
if (!relaySet) return
// Ensure relay list is not empty
if (relaySet.read.length === 0) {
toast.error('No relay found for publishing encrypted DM')
return
}
// Publish the signed DM event to the recipient's read relays
await nostrController
.publishEvent(signedEvent, relaySet.read)
.then((relays) => {
toast.success(`Encrypted DM sent on: ${relays.join('\n')}`)
})
.catch((err) => {
console.log('err :>> ', err)
toast.error(err.message || 'An error occurred while publishing DM')
return null
})
}
/**
* Signs an event for a meta.json file.
* @param receiver The recipient's public key.
* @param fileHashes Object containing file hashes.
* @param nostrController The NostrController instance for signing the event.
* @param setIsLoading Function to set loading state in the component.
* @returns A Promise resolving to the signed event, or null if signing fails.
*/
export const signEventForMetaFile = async (
receiver: string,
fileHashes: {
[key: string]: string
},
nostrController: NostrController,
setIsLoading: (value: React.SetStateAction<boolean>) => void
) => {
// Construct the event metadata for the meta file
const event: EventTemplate = {
kind: 1, // Event type for meta file
tags: [['r', receiver]], // Tag with recipient's public key
content: JSON.stringify(fileHashes), // Convert file hashes to JSON string
created_at: Math.floor(Date.now() / 1000) // Current timestamp
}
// Sign the event
const signedEvent = await nostrController.signEvent(event).catch((err) => {
console.error(err)
toast.error(err.message || 'Error occurred in signing nostr event')
setIsLoading(false) // Set loading state to false
return null
})
return signedEvent // Return the signed event
}