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 _ from 'lodash'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
|
finalizeEvent,
|
||||||
generateSecretKey,
|
generateSecretKey,
|
||||||
|
getEventHash,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
kinds,
|
kinds,
|
||||||
UnsignedEvent
|
UnsignedEvent
|
||||||
@ -40,17 +42,21 @@ import {
|
|||||||
getDTagForUserAppData,
|
getDTagForUserAppData,
|
||||||
getUserAppDataFromBlossom,
|
getUserAppDataFromBlossom,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
|
nip44Encrypt,
|
||||||
parseJson,
|
parseJson,
|
||||||
|
randomTimeUpTo2DaysInThePast,
|
||||||
SIGIT_RELAY,
|
SIGIT_RELAY,
|
||||||
unixNow,
|
unixNow,
|
||||||
uploadUserAppDataToBlossom
|
uploadUserAppDataToBlossom
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
|
||||||
|
|
||||||
export const useNDK = () => {
|
export const useNDK = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const {
|
const {
|
||||||
ndk,
|
ndk,
|
||||||
fetchEvent,
|
fetchEvent,
|
||||||
|
fetchEventFromUserRelays,
|
||||||
fetchEventsFromUserRelays,
|
fetchEventsFromUserRelays,
|
||||||
publish,
|
publish,
|
||||||
getNDKRelayList
|
getNDKRelayList
|
||||||
@ -503,10 +509,139 @@ export const useNDK = () => {
|
|||||||
[ndk, usersPubkey, getNDKRelayList]
|
[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 {
|
return {
|
||||||
getUsersAppData,
|
getUsersAppData,
|
||||||
subscribeForSigits,
|
subscribeForSigits,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
sendNotification
|
sendNotification,
|
||||||
|
sendPrivateDirectMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,6 @@ import {
|
|||||||
uploadToFileStorage,
|
uploadToFileStorage,
|
||||||
DEFAULT_TOOLBOX,
|
DEFAULT_TOOLBOX,
|
||||||
settleAllFullfilfedPromises,
|
settleAllFullfilfedPromises,
|
||||||
sendPrivateDirectMessage,
|
|
||||||
parseNostrEvent,
|
parseNostrEvent,
|
||||||
uploadMetaToFileStorage
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -89,7 +88,8 @@ export const CreatePage = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
|
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
|
||||||
const { updateUsersAppData, sendNotification } = useNDK()
|
const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
|
||||||
|
useNDK()
|
||||||
|
|
||||||
const { uploadedFiles } = location.state || {}
|
const { uploadedFiles } = location.state || {}
|
||||||
const [currentFile, setCurrentFile] = useState<File>()
|
const [currentFile, setCurrentFile] = useState<File>()
|
||||||
|
@ -29,7 +29,6 @@ import {
|
|||||||
unixNow,
|
unixNow,
|
||||||
updateMarks,
|
updateMarks,
|
||||||
uploadMetaToFileStorage,
|
uploadMetaToFileStorage,
|
||||||
sendPrivateDirectMessage,
|
|
||||||
parseNostrEvent
|
parseNostrEvent
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||||
@ -44,7 +43,8 @@ export const SignPage = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { updateUsersAppData, sendNotification } = useNDK()
|
const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
|
||||||
|
useNDK()
|
||||||
|
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
|
||||||
@ -607,7 +607,7 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
// Send DMs
|
// Send DMs
|
||||||
setLoadingSpinnerDesc('Sending DMs')
|
setLoadingSpinnerDesc('Sending DMs')
|
||||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
const createSignatureEvent = parseNostrEvent(meta.createSignature)
|
||||||
const { id } = createSignatureEvent
|
const { id } = createSignatureEvent
|
||||||
|
|
||||||
if (isLastSigner) {
|
if (isLastSigner) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Jsonable } from '.'
|
import { Jsonable } from '.'
|
||||||
|
|
||||||
export enum SendDMErrorType {
|
export enum SendDMErrorType {
|
||||||
'METADATA_FETCH_FAILED' = 'Sending DM failed. An error occured while fetching user metadata.',
|
'MISSING_RECIEVER' = 'Sending DM failed. Reciever is required.',
|
||||||
'RELAY_READ_EMPTY' = `Sending DM failed. The user's relay read set is empty.`,
|
|
||||||
'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
|
'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
|
||||||
'RELAY_PUBLISH_FAILED' = 'Sending DM failed. Publishing events failed.'
|
'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 { SIGIT_BLOSSOM } from './const.ts'
|
||||||
import { getHash } from './hash'
|
import { getHash } from './hash'
|
||||||
import { parseJson, removeLeadingSlash } from './string'
|
import { parseJson, removeLeadingSlash } from './string'
|
||||||
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError.ts'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a `d` tag for userAppData
|
* Generates a `d` tag for userAppData
|
||||||
@ -514,148 +513,6 @@ export const getProfileUsername = (
|
|||||||
length: 16
|
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.
|
* Orders an array of NDKEvent objects chronologically based on their `created_at` property.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user