refactor(dm): update private dm to use ndk
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 52s

This commit is contained in:
en 2025-01-24 14:53:17 +01:00
parent 4b5955fa9c
commit efe3c2c9c7
5 changed files with 142 additions and 151 deletions

View File

@ -12,7 +12,9 @@ import {
import _ from 'lodash'
import {
Event,
finalizeEvent,
generateSecretKey,
getEventHash,
getPublicKey,
kinds,
UnsignedEvent
@ -40,17 +42,21 @@ import {
getDTagForUserAppData,
getUserAppDataFromBlossom,
hexToNpub,
nip44Encrypt,
parseJson,
randomTimeUpTo2DaysInThePast,
SIGIT_RELAY,
unixNow,
uploadUserAppDataToBlossom
} from '../utils'
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
export const useNDK = () => {
const dispatch = useAppDispatch()
const {
ndk,
fetchEvent,
fetchEventFromUserRelays,
fetchEventsFromUserRelays,
publish,
getNDKRelayList
@ -503,10 +509,139 @@ export const useNDK = () => {
[ndk, usersPubkey, getNDKRelayList]
)
/**
* Modified {@link UnsignedEvent Unsigned Event} that includes an id
*
* Fields id and created_at are required.
* @see {@link UnsignedEvent}
* @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
*/
type UnsignedEventWithId = UnsignedEvent & {
id?: string
}
const sendPrivateDirectMessage = useCallback(
async (message: string, receiver: string, subject?: string) => {
if (!receiver) throw new SendDMError(SendDMErrorType.MISSING_RECIEVER)
// Get the direct message preferred relays list
// https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
const preferredRelaysListEvent = await fetchEventFromUserRelays(
{
kinds: [NDKKind.DirectMessageReceiveRelayList],
authors: [receiver]
},
receiver,
UserRelaysType.Read
)
const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
const finalRelaysList: string[] = []
if (preferredRelaysListEvent) {
const preferredRelaysList = preferredRelaysListEvent.tags
.filter((t) => isRelayTag(t))
.map((t) => t[1])
finalRelaysList.push(...preferredRelaysList)
}
if (!finalRelaysList.length) {
// Get receiver's read relay list
const ndkRelayList = await getNDKRelayList(receiver).catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err
)
return null
})
if (ndkRelayList?.readRelayUrls) {
finalRelaysList.push(...ndkRelayList.readRelayUrls)
}
}
if (!finalRelaysList.length) {
finalRelaysList.push(SIGIT_RELAY)
}
// Generate "sender"
const senderSecret = generateSecretKey()
const senderPubkey = getPublicKey(senderSecret)
// Prepare tags for the message
const tags: string[][] = [['p', receiver]]
// Conversation title
if (subject) tags.push(['subject', subject])
// Create private DM event containing the message and relevant metadata
// TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const dm: UnsignedEventWithId = {
pubkey: senderPubkey,
created_at: unixNow(),
kind: 14,
tags,
content: message
}
// Calculate the hash based on the UnverifiedEvent
dm.id = getEventHash(dm)
// Encrypt the private dm using the sender secret and the receiver's public key
const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
if (!encryptedDm) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
message,
kind: dm.kind
}
})
}
// Seal the message
// TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const sealedMessage: UnsignedEvent = {
kind: 13, // seal
pubkey: senderPubkey,
content: encryptedDm,
created_at: randomTimeUpTo2DaysInThePast(),
tags: [] // no tags
}
// Finalize and sign the sealed event
const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
// Encrypt the seal and gift wrap
const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
const ndkEvent = new NDKEvent(ndk, finalizedGiftWrap)
// Publish the finalized gift wrap event (the encrypted DM) to the relays
const publishedOnRelays = await ndkEvent.publish(
NDKRelaySet.fromRelayUrls(finalRelaysList, ndk, true)
)
// Handle cases where publishing to the relays failed
if (publishedOnRelays.size === 0) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
count: publishedOnRelays.size
}
})
}
// Return true indicating that the DM was successfully sent
return true
},
[fetchEventFromUserRelays, getNDKRelayList, ndk]
)
return {
getUsersAppData,
subscribeForSigits,
updateUsersAppData,
sendNotification
sendNotification,
sendPrivateDirectMessage
}
}

View File

@ -45,7 +45,6 @@ import {
uploadToFileStorage,
DEFAULT_TOOLBOX,
settleAllFullfilfedPromises,
sendPrivateDirectMessage,
parseNostrEvent,
uploadMetaToFileStorage
} from '../../utils'
@ -89,7 +88,8 @@ export const CreatePage = () => {
const navigate = useNavigate()
const location = useLocation()
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
const { updateUsersAppData, sendNotification } = useNDK()
const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
useNDK()
const { uploadedFiles } = location.state || {}
const [currentFile, setCurrentFile] = useState<File>()

View File

@ -29,7 +29,6 @@ import {
unixNow,
updateMarks,
uploadMetaToFileStorage,
sendPrivateDirectMessage,
parseNostrEvent
} from '../../utils'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
@ -44,7 +43,8 @@ export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
const params = useParams()
const { updateUsersAppData, sendNotification } = useNDK()
const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
useNDK()
const usersAppData = useAppSelector((state) => state.userAppData)
@ -607,7 +607,7 @@ export const SignPage = () => {
// Send DMs
setLoadingSpinnerDesc('Sending DMs')
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
const createSignatureEvent = parseNostrEvent(meta.createSignature)
const { id } = createSignatureEvent
if (isLastSigner) {

View File

@ -1,8 +1,7 @@
import { Jsonable } from '.'
export enum SendDMErrorType {
'METADATA_FETCH_FAILED' = 'Sending DM failed. An error occured while fetching user metadata.',
'RELAY_READ_EMPTY' = `Sending DM failed. The user's relay read set is empty.`,
'MISSING_RECIEVER' = 'Sending DM failed. Reciever is required.',
'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
'RELAY_PUBLISH_FAILED' = 'Sending DM failed. Publishing events failed.'
}

View File

@ -23,7 +23,6 @@ import { Meta, SignedEvent } from '../types'
import { SIGIT_BLOSSOM } from './const.ts'
import { getHash } from './hash'
import { parseJson, removeLeadingSlash } from './string'
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError.ts'
/**
* Generates a `d` tag for userAppData
@ -514,148 +513,6 @@ export const getProfileUsername = (
length: 16
})
/**
* Modified {@link UnsignedEvent Unsigned Event} that includes an id
*
* Fields id and created_at are required.
* @see {@link UnsignedEvent}
* @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
*/
type UnsignedEventWithId = UnsignedEvent & {
id?: string
}
export const sendPrivateDirectMessage = async (
message: string,
receiver: string,
subject?: string
) => {
// Instantiate the MetadataController to retrieve relay list metadata to look for preferred DM relays
const metadataController = MetadataController.getInstance()
const relaySet = await metadataController
.findRelayListMetadata(receiver)
.catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err
)
return null
})
// Throw if metadata retrieval failed
if (!relaySet) {
throw new SendDMError(SendDMErrorType.METADATA_FETCH_FAILED, {
context: {
receiver
}
})
}
// Ensure relay list is not empty
if (relaySet.read.length === 0) {
throw new SendDMError(SendDMErrorType.RELAY_READ_EMPTY, {
context: {
receiver,
relaySet: JSON.stringify(relaySet)
}
})
}
// Get the direct message preferred relays list
// TODO: kinds.DirectMessageRelaysList (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
// https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
const eventFilter: Filter = {
kinds: [10050],
authors: [receiver]
}
const preferredRelaysListEvents =
await RelayController.getInstance().fetchEvents(eventFilter, relaySet.read)
const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
const preferredRelaysList = preferredRelaysListEvents.reduce(
(previous: string[], current: Event) => {
const relaysList = current.tags
.filter((t) => isRelayTag(t) && !previous.includes(t[1]))
.map((t) => t[1])
return [...previous, ...relaysList]
},
[]
)
// Empty preferred relays list
const finalRelaysList: string[] =
preferredRelaysList?.length > 0 ? preferredRelaysList : [...relaySet.write]
// Generate "sender"
const senderSecret = generateSecretKey()
const senderPubkey = getPublicKey(senderSecret)
// Prepare tags for the message
const tags: string[][] = [['p', receiver]]
// Conversation title
if (subject) tags.push(['subject', subject])
// Create private DM event containing the message and relevant metadata
// TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const dm: UnsignedEventWithId = {
pubkey: senderPubkey,
created_at: unixNow(),
kind: 14,
tags,
content: message
}
// Calculate the hash based on the UnverifiedEvent
dm.id = getEventHash(dm)
// Encrypt the private dm using the sender secret and the receiver's public key
const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
if (!encryptedDm) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
message,
kind: dm.kind
}
})
}
// Seal the message
// TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const sealedMessage: UnsignedEvent = {
kind: 13, // seal
pubkey: senderPubkey,
content: encryptedDm,
created_at: randomTimeUpTo2DaysInThePast(),
tags: [] // no tags
}
// Finalize and sign the sealed event
const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
// Encrypt the seal and gift wrap
const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
// Publish the finalized gift wrap event (the encrypted DM) to the relays
const publishedOnRelays = await relayController.publish(
finalizedGiftWrap,
finalRelaysList
)
// Handle cases where publishing to the relays failed
if (publishedOnRelays.length === 0) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
count: publishedOnRelays.length
}
})
}
// Return true indicating that the DM was successfully sent
return true
}
/**
* Orders an array of NDKEvent objects chronologically based on their `created_at` property.
*