From efe3c2c9c77a81a6eb7045945d358e7cbf2ecf78 Mon Sep 17 00:00:00 2001 From: en Date: Fri, 24 Jan 2025 14:53:17 +0100 Subject: [PATCH] refactor(dm): update private dm to use ndk --- src/hooks/useNDK.ts | 137 +++++++++++++++++++++++++++++- src/pages/create/index.tsx | 4 +- src/pages/sign/index.tsx | 6 +- src/types/errors/SendDMError.ts | 3 +- src/utils/nostr.ts | 143 -------------------------------- 5 files changed, 142 insertions(+), 151 deletions(-) diff --git a/src/hooks/useNDK.ts b/src/hooks/useNDK.ts index ad9e54f..2076a29 100644 --- a/src/hooks/useNDK.ts +++ b/src/hooks/useNDK.ts @@ -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 } } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 686f080..89a09b4 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -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() diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 309ee1f..e210cb9 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -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) { diff --git a/src/types/errors/SendDMError.ts b/src/types/errors/SendDMError.ts index 223823b..70cc94a 100644 --- a/src/types/errors/SendDMError.ts +++ b/src/types/errors/SendDMError.ts @@ -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.' } diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 17030ad..3d8aa15 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -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. *