206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
import {
|
|
Event,
|
|
EventTemplate,
|
|
SimplePool,
|
|
UnsignedEvent,
|
|
finalizeEvent,
|
|
nip04,
|
|
nip19,
|
|
verifyEvent,
|
|
generateSecretKey,
|
|
getPublicKey
|
|
} from 'nostr-tools'
|
|
import { EventEmitter } from 'tseep'
|
|
import { SignedEvent, Keys } from '../types'
|
|
|
|
export class NostrController extends EventEmitter {
|
|
private static instance: NostrController
|
|
|
|
private generatedKeys: Keys | undefined
|
|
|
|
private constructor() {
|
|
super()
|
|
|
|
this.generatedKeys = this.generateKeys()
|
|
}
|
|
|
|
public static getInstance(): NostrController {
|
|
if (!NostrController.instance) {
|
|
NostrController.instance = new NostrController()
|
|
}
|
|
return NostrController.instance
|
|
}
|
|
|
|
/**
|
|
* Function will publish provided event to the provided relays
|
|
*/
|
|
publishEvent = async (event: Event, relays: string[]): Promise<string[]> => {
|
|
const simplePool = new SimplePool()
|
|
const promises = simplePool.publish(relays, event)
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
|
|
const publishedRelays: string[] = []
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') publishedRelays.push(relays[index])
|
|
})
|
|
|
|
if (publishedRelays.length === 0) {
|
|
const failedPublishes: Array<{ relay: string; error: string }> = []
|
|
const fallbackRejectionReason =
|
|
'Attempt to publish an event has been rejected with unknown reason.'
|
|
|
|
results.forEach((res, index) => {
|
|
if (res.status === 'rejected') {
|
|
failedPublishes.push({
|
|
relay: relays[index],
|
|
error: res.reason
|
|
? res.reason.message || fallbackRejectionReason
|
|
: fallbackRejectionReason
|
|
})
|
|
}
|
|
})
|
|
|
|
throw failedPublishes
|
|
}
|
|
|
|
return publishedRelays
|
|
}
|
|
|
|
/**
|
|
* Signs an event with private key (if it is present in local storage) or
|
|
* with browser extension (if it is present) or
|
|
* with nSecBunker instance.
|
|
* @param event - unsigned nostr event.
|
|
* @returns - a promised that is resolved with signed nostr event.
|
|
*/
|
|
signEvent = async (
|
|
event: UnsignedEvent | EventTemplate
|
|
): Promise<SignedEvent> => {
|
|
if (!this.generatedKeys) {
|
|
throw new Error(`Private & public key pair is not found.`)
|
|
}
|
|
|
|
const { private: nsec } = this.generatedKeys
|
|
const privateKey = nip19.decode(nsec).data as Uint8Array
|
|
|
|
const signedEvent = finalizeEvent(event, privateKey)
|
|
|
|
this.verifySignedEvent(signedEvent)
|
|
|
|
return Promise.resolve(signedEvent)
|
|
}
|
|
|
|
nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
|
|
if (!this.generatedKeys) {
|
|
throw new Error(`Private & public key pair is not found.`)
|
|
}
|
|
|
|
const { private: nsec } = this.generatedKeys
|
|
const privateKey = nip19.decode(nsec).data as Uint8Array
|
|
|
|
const encrypted = await nip04.encrypt(privateKey, receiver, content)
|
|
|
|
return encrypted
|
|
}
|
|
|
|
/**
|
|
* 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 isSigner Boolean indicating whether the recipient is a signer or viewer.
|
|
* @param setAuthUrl Function to set the authentication URL in the component state.
|
|
*/
|
|
sendDM = async (pubkey: string, message: string): Promise<string[]> => {
|
|
// Set up timeout promise to handle encryption timeout
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
setTimeout(() => {
|
|
reject(new Error('Timeout occurred'))
|
|
}, 60000) // Timeout duration = 60 seconds
|
|
})
|
|
|
|
// Encrypt the DM content, with timeout
|
|
const encrypted = await Promise.race([
|
|
this.nip04Encrypt(this.npubToHex(pubkey) as string, message),
|
|
timeoutPromise
|
|
])
|
|
|
|
// Return if encryption failed
|
|
if (!encrypted) throw new Error('Message was not encrypted.')
|
|
|
|
// 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', this.npubToHex(pubkey) as string]] // Tag with recipient's public key
|
|
}
|
|
|
|
// Sign the DM event
|
|
const signedEvent = await this.signEvent(event)
|
|
|
|
// Return if event signing failed
|
|
if (!signedEvent) throw new Error('Message was not signed.')
|
|
|
|
// These relay will be used to send a DM. Recipient has to read from these relays to receive a DM.
|
|
const relays = [
|
|
'wss://relay.damus.io/',
|
|
'wss://nos.lol/',
|
|
'wss://relay.snort.social'
|
|
]
|
|
|
|
// Publish the signed DM event to the recipient's read relays
|
|
return await this.publishEvent(signedEvent, relays)
|
|
}
|
|
|
|
/**
|
|
* @param hexKey hex private or public key
|
|
* @returns whether or not is key valid
|
|
*/
|
|
validateHex = (hexKey: string): RegExpMatchArray | null => {
|
|
return hexKey.match(/^[a-f0-9]{64}$/)
|
|
}
|
|
|
|
/**
|
|
* NPUB provided - it will convert NPUB to HEX
|
|
* HEX provided - it will return HEX
|
|
*
|
|
* @param pubKey in NPUB, HEX format
|
|
* @returns HEX format
|
|
*/
|
|
npubToHex = (pubKey: string): string | null => {
|
|
// If key is NPUB
|
|
if (pubKey.startsWith('npub1')) {
|
|
try {
|
|
return nip19.decode(pubKey).data as string
|
|
} catch (error) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// valid hex key
|
|
if (this.validateHex(pubKey)) return pubKey
|
|
|
|
// Not a valid hex key
|
|
return null
|
|
}
|
|
|
|
generateKeys = (): Keys => {
|
|
const nsec = generateSecretKey()
|
|
|
|
return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
|
|
}
|
|
|
|
verifySignedEvent = (event: SignedEvent): void => {
|
|
const isGood = verifyEvent(event)
|
|
|
|
if (!isGood) {
|
|
throw new Error(
|
|
'Signed event did not pass verification. Check sig, id and pubkey.'
|
|
)
|
|
}
|
|
}
|
|
}
|