import axios from 'axios' import { EventTemplate, generateSecretKey, getPublicKey, nip04 } 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 ( file: File, nostrController: NostrController ) => { // Get the current timestamp in seconds const unixNow = Math.floor(Date.now() / 1000) // 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' // REFACTOR: should be an env // 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/sigit' // 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 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, pubkey: string, nostrController: NostrController, isSigner: boolean, setAuthUrl: (value: React.SetStateAction) => void ) => { // Construct the content of the DM const initialLine = isSigner ? 'Your signature is requested on the document below!' : 'You have received a signed document.' const decryptionUrl = `${window.location.origin}/#${ appPrivateRoutes.sign }?file=${encodeURIComponent(fileUrl)}` const content = `${initialLine}\n\n${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((_, reject) => { setTimeout(() => { reject(new Error('Timeout occurred')) }, 60000) // Timeout duration = 60 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((errResults) => { console.log('err :>> ', errResults) toast.error('An error occurred while publishing DM') errResults.forEach((errResult: any) => { toast.error( `Publishing to ${errResult.relay} caused the following error: ${errResult.error}` ) }) return null }) } /** * Signs an event for a meta.json file. * @param content contains content for event. * @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 ( content: string, nostrController: NostrController, setIsLoading: (value: React.SetStateAction) => void ) => { // Construct the event metadata for the meta file const event: EventTemplate = { kind: 27235, // Event type for meta file content: content, // content for event created_at: Math.floor(Date.now() / 1000), // Current timestamp tags: [] } // 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 } /** * Generates the content for keys.json file. * * @param users - An array of public keys. * @param key - The key that will be encrypted for each user. * @returns A promise that resolves to a JSON string containing the sender's public key and encrypted keys, or null if an error occurs. */ export const generateKeysFile = async ( users: string[], key: string ): Promise => { // Generate a random private key to act as the sender const privateKey = generateSecretKey() // Calculate the required length to be a multiple of 10 const requiredLength = Math.ceil(users.length / 10) * 10 const additionalKeysCount = requiredLength - users.length if (additionalKeysCount > 0) { // generate random public keys to make the keys array multiple of 10 const additionalPubkeys = Array.from({ length: additionalKeysCount }, () => getPublicKey(generateSecretKey()) ) users.push(...additionalPubkeys) } // Encrypt the key for each user's public key const promises = users.map((pubkey) => nip04.encrypt(privateKey, pubkey, key)) // Wait for all encryption promises to resolve const keys = await Promise.all(promises).catch((err) => { console.log('Error while generating keys :>> ', err) toast.error(err.message || 'An error occurred while generating key') return null }) // If any encryption promise failed, return null if (!keys) return null try { // Return a JSON string containing the sender's public key and encrypted keys return JSON.stringify({ sender: getPublicKey(privateKey), keys }) } catch (error) { // Return null if an error occurs during JSON stringification return null } }