import axios from 'axios' import { EventTemplate } from 'nostr-tools' import { MetadataController, NostrController } from '../controllers' import { toast } from 'react-toastify' /** * 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) => void ) => { // Construct the content of the DM const initialLine = isSigner ? 'You have been requested for a signature.' : 'You have received a signed document.' 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 https://app.sigit.io \nEncryption key: ${encryptionKey}` // 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((_, 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) => 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 }