refactor(dm): update private dm to use ndk
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 52s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 52s
This commit is contained in:
parent
4b5955fa9c
commit
efe3c2c9c7
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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) {
|
||||
|
@ -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.'
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user