2024-04-18 11:12:11 +00:00
|
|
|
import axios from 'axios'
|
2024-06-12 10:02:26 +00:00
|
|
|
import {
|
|
|
|
EventTemplate,
|
|
|
|
generateSecretKey,
|
|
|
|
getPublicKey,
|
|
|
|
nip04
|
|
|
|
} from 'nostr-tools'
|
2024-04-18 11:12:11 +00:00
|
|
|
import { MetadataController, NostrController } from '../controllers'
|
|
|
|
import { toast } from 'react-toastify'
|
2024-04-19 10:57:44 +00:00
|
|
|
import { appPrivateRoutes } from '../routes'
|
2024-04-18 11:12:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 (
|
2024-06-12 10:02:26 +00:00
|
|
|
file: File,
|
2024-04-18 11:12:11 +00:00
|
|
|
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
|
2024-05-16 06:23:26 +00:00
|
|
|
const FILE_STORAGE_URL = 'https://blossom.sigit.io' // REFACTOR: should be an env
|
2024-04-18 11:12:11 +00:00
|
|
|
|
|
|
|
// 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
|
2024-05-16 06:23:26 +00:00
|
|
|
'Content-Type': 'application/sigit' // Set content type header
|
2024-04-18 11:12:11 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// 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.
|
2024-06-12 14:44:06 +00:00
|
|
|
* @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
|
2024-04-18 11:12:11 +00:00
|
|
|
* @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,
|
2024-06-12 14:44:06 +00:00
|
|
|
encryptionKey: string,
|
2024-04-18 11:12:11 +00:00
|
|
|
pubkey: string,
|
|
|
|
nostrController: NostrController,
|
|
|
|
isSigner: boolean,
|
|
|
|
setAuthUrl: (value: React.SetStateAction<string | undefined>) => void
|
|
|
|
) => {
|
|
|
|
// Construct the content of the DM
|
|
|
|
const initialLine = isSigner
|
2024-05-08 14:11:06 +00:00
|
|
|
? 'Your signature is requested on the document below!'
|
2024-04-18 11:12:11 +00:00
|
|
|
: 'You have received a signed document.'
|
2024-04-19 10:57:44 +00:00
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
const decryptionUrl = `${window.location.origin}/#${
|
2024-05-15 11:11:57 +00:00
|
|
|
appPrivateRoutes.sign
|
2024-06-12 14:44:06 +00:00
|
|
|
}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(
|
|
|
|
encryptionKey
|
|
|
|
)}`
|
2024-04-19 10:57:44 +00:00
|
|
|
|
2024-05-17 05:51:28 +00:00
|
|
|
const content = `${initialLine}\n\n${decryptionUrl}`
|
2024-04-18 11:12:11 +00:00
|
|
|
|
|
|
|
// 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'))
|
2024-05-13 05:50:32 +00:00
|
|
|
}, 60000) // Timeout duration = 60 seconds
|
2024-04-18 11:12:11 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// 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')}`)
|
|
|
|
})
|
2024-05-13 11:11:52 +00:00
|
|
|
.catch((errResults) => {
|
|
|
|
console.log('err :>> ', errResults)
|
|
|
|
toast.error('An error occurred while publishing DM')
|
|
|
|
|
|
|
|
errResults.forEach((errResult: any) => {
|
2024-05-15 11:11:57 +00:00
|
|
|
toast.error(
|
|
|
|
`Publishing to ${errResult.relay} caused the following error: ${errResult.error}`
|
|
|
|
)
|
2024-05-13 11:11:52 +00:00
|
|
|
})
|
|
|
|
|
2024-04-18 11:12:11 +00:00
|
|
|
return null
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Signs an event for a meta.json file.
|
2024-05-22 06:19:40 +00:00
|
|
|
* @param content contains content for event.
|
2024-04-18 11:12:11 +00:00
|
|
|
* @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 (
|
2024-05-22 06:19:40 +00:00
|
|
|
content: string,
|
2024-04-18 11:12:11 +00:00
|
|
|
nostrController: NostrController,
|
|
|
|
setIsLoading: (value: React.SetStateAction<boolean>) => void
|
|
|
|
) => {
|
|
|
|
// Construct the event metadata for the meta file
|
|
|
|
const event: EventTemplate = {
|
2024-05-27 10:56:52 +00:00
|
|
|
kind: 27235, // Event type for meta file
|
2024-05-22 06:19:40 +00:00
|
|
|
content: content, // content for event
|
2024-05-14 09:27:05 +00:00
|
|
|
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
|
|
|
tags: []
|
2024-04-18 11:12:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2024-06-12 10:02:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<string | null> => {
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|