diff options
| author | Adrien Hopkins <[email protected]> | 2025-12-05 09:51:00 -0500 |
|---|---|---|
| committer | Adrien Hopkins <[email protected]> | 2025-12-05 10:19:23 -0500 |
| commit | 4374655730c3762ea56e9c732a7c0b7ab872ae1f (patch) | |
| tree | 779b77b60ff693010b8c35c7d3e608a1240e0ecc /src | |
| parent | eb6ef86f3d5200967e1d80f6832796348ae7fce5 (diff) | |
| download | coinos-ui-4374655730c3762ea56e9c732a7c0b7ab872ae1f.tar.gz coinos-ui-4374655730c3762ea56e9c732a7c0b7ab872ae1f.zip | |
Move some DM code to $lib
I will be using some of this elsewhere, so it really should be there.
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/nip11.ts | 15 | ||||
| -rw-r--r-- | src/lib/nip17.ts | 105 | ||||
| -rw-r--r-- | src/lib/nip40.ts | 16 | ||||
| -rw-r--r-- | src/routes/(app)/dm/[recipient]/+page.svelte | 174 |
4 files changed, 177 insertions, 133 deletions
diff --git a/src/lib/nip11.ts b/src/lib/nip11.ts new file mode 100644 index 00000000..af254b7e --- /dev/null +++ b/src/lib/nip11.ts @@ -0,0 +1,15 @@ +import { fetchRelayInformation } from 'nostr-tools/nip11'; + +// Accepts a list of relays, and returns a list of the relays that support all the provided NIPs. +export const relaysSupporting = async (relays: string[], nips: number[]) => { + let supportingRelays = []; + + for (const relay of relays) { + const relayInfo = await fetchRelayInformation(relay); + if (nips.every(relay => relayInfo.supported_nips.includes(relay))) { + supportingRelays.push(relay); + } + } + + return supportingRelays; +} diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index cb9a0573..1a78eb5a 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -1,10 +1,23 @@ import { bytesToHex } from '@noble/hashes/utils'; + import { finalizeEvent, getEventHash, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; +import { SimplePool } from 'nostr-tools/pool'; +import { unwrapEvent } from 'nostr-tools/nip17'; + +import { sign, getPrivateKey } from '$lib/nostr'; +import { relaysSupporting } from '$lib/nip11'; +import { expired, expiration } from '$lib/nip40'; import { encrypt, u } from '$lib/nip44'; const TWO_DAYS = 2 * 24 * 60 * 60; const randomTimestamp = () => Math.floor( Date.now() / 1000 - Math.random() * TWO_DAYS); +const pool = new SimplePool(); +let relayListCache = new Map(); + +import { PUBLIC_DM_RELAYS } from '$env/static/public'; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); +const DM_FETCH_LIMIT = 256; // Create a NIP-17 message using a provided sender secret key. // If wrapPK is specified and different than receiverPK, @@ -114,3 +127,95 @@ export const decryptNIP17MessageNIP07 = async (wrapped: object): object => { sealed.pubkey, sealed.content); return JSON.parse(rumourText); } + +// Decrypts the provided message, addressed to the user. +// If NIP-07 is available, it uses that, otherwise it uses getPrivateKey(). +export const decrypt = async (wrapped: object, user: object): object => { + if (await window.nostr.getPublicKey() === user.pubkey) { + return decryptNIP17MessageNIP07(wrapped); + } else { + const sk = await getPrivateKey(user); + return unwrapEvent(wrapped, sk); + } + } + +// Returns the rumours of the messages the provided user has sent or received. +// If the events have an expiration timestamp, +// that timestamp is added to its rumour object as the property `expiration`. +export const getMessageRumours = async (user: object): object[] => { + const relays = await getPreferredRelays(user.pubkey); + const wrapped = await pool.querySync( + relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } + ); + + let rumours = []; + // intentionally decrypting sequentially to avoid having a bunch of popups + for (event of wrapped) { + if (expired(event)) { + console.log("relay(s) contain expired event", event.id); + continue; + } + + const rumour = await decrypt(event, user); + + const expires = expiration(event); + if (expires) { + rumour.expiration = expires; + } + + rumours.push(rumour); + } + + return rumours; +} + +export const getPreferredRelays = async (pubkey: string): string[] => { + if (relayListCache.has(pubkey)) + return relayListCache.get(pubkey); + + const events = await pool.querySync( + DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } + ); + + let relays = []; + for (const event of events) { + for (const tag of event.tags) { + if (tag.length >= 2 && tag[0] == "relay") { + relays.push(tag[1]); + } + } + } + + relayListCache.set(pubkey, relays); + return relays; +} + +const publishToPreferred = async (event: object, pubkey: string, expiryEnabled: boolean): Promise<string> => { + let preferredRelays = await getPreferredRelays(pubkey); + if (expiryEnabled) { + preferredRelays = await relaysSupporting(preferredRelays, [40]); + } + return Promise.any(pool.publish(preferredRelays, event)); +} + +// Sends a NIP-17 message from `user` to `recipient`. +// If expiryDays is set, messages will expire after that many days. +export const send = async (message: string, user: object, recipient: object, expiryDays?: number) => { + let event1, event2; + if (await window.nostr.getPublicKey() === user.pubkey) { + event1 = await createNIP17MessageNIP07( + message, user.pubkey, recipient.pubkey, recipient.pubkey, expiryDays); + event2 = await createNIP17MessageNIP07( + message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); + } else { + const sk = await getPrivateKey(user); + event1 = createNIP17MessageSK( + message, sk, recipient.pubkey, recipient.pubkey, expiryDays); + event2 = createNIP17MessageSK( + message, sk, recipient.pubkey, user.pubkey, expiryDays); + } + + const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); + const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); + return Promise.all([p1, p2]); +} diff --git a/src/lib/nip40.ts b/src/lib/nip40.ts new file mode 100644 index 00000000..030fe0df --- /dev/null +++ b/src/lib/nip40.ts @@ -0,0 +1,16 @@ +// Returns true if the message has an expiration in the past, false otherwise. +export const expired = (event: object): boolean => { + const expiryTime = expiration(event); + return expiryTime !== null && Date.now() / 1000 >= expiryTime; +} + +// Returns the provided event's expiration time, as a Unix ms integer. +// If it has none, returns null. +export const expiration = (event: object): number | null => { + for (const tag of event.tags) { + if (tag[0] == "expiration") { + return parseInt(tag[1]); + } + } + return null; +} diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index e11c5c5c..8014cd2a 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -2,9 +2,9 @@ import { tick } from "svelte"; import Icon from "$comp/Icon.svelte"; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; - import { fetchRelayInformation } from 'nostr-tools/nip11'; import * as toolsnip17 from 'nostr-tools/nip17'; import { SimplePool } from 'nostr-tools/pool'; + import { relaysSupporting } from '$lib/nip11'; import * as libnip17 from '$lib/nip17'; import { sign, getPrivateKey } from '$lib/nostr'; import { t } from "$lib/translations"; @@ -13,11 +13,6 @@ const { user, recipient } = data; const pool = new SimplePool(); - import { PUBLIC_DM_RELAYS } from '$env/static/public'; - const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); - const DM_FETCH_LIMIT = 256; - - let relayLists = new Map(); let text = $state(""); let messageRumours = $state([]); @@ -36,89 +31,21 @@ } } - const zeroPad = (num: number): string => { - return num < 10 ? '0' + num.toString() : num.toString(); - } - - const timeString = (date: Date): string => { - const hour = zeroPad(date.getHours()); - const minute = zeroPad(date.getMinutes()); - const second = zeroPad(date.getSeconds()); - return `${hour}:${minute}:${second}`; - } - - const getPreferredRelays = async (pubkey: string): string[] => { - if (relayLists.has(pubkey)) - return relayLists.get(pubkey); - - const events = await pool.querySync( - DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } - ); - - let relays = []; + const dateMap = (events: object[]): Map<number, object> => { + let eventsMap = new Map(); for (const event of events) { - for (const tag of event.tags) { - if (tag.length >= 2 && tag[0] == "relay") { - relays.push(tag[1]); - } - } - } - - relayLists.set(pubkey, relays); - return relays; - } - - // Accepts a list of relays, and returns a list of the relays that support all the provided NIPs. - const relaysSupporting = async (relays: string[], nips: number[]) => { - let supportingRelays = []; - - for (const relay of relays) { - const relayInfo = await fetchRelayInformation(relay); - if (nips.every(relay => relayInfo.supported_nips.includes(relay))) { - supportingRelays.push(relay); - } + const created = Math.floor(event.created_at / 86400); + appendMultimap(eventsMap, created, event); } - - return supportingRelays; - } - - const expired = (event: object) => { - for (const tag of event.tags) { - if (tag[0] == "expiration") { - return (Date.now() / 1000) >= Number(tag[1]); - } + for (const [date, dateEvents] of eventsMap) { + dateEvents.sort((ev1, ev2) => ev1.created_at - ev2.created_at); } - return false; - } - - const expirationClose = (expiration: number) => { - return expiration <= (Date.now() / 1000 + 2 * 86400); + return eventsMap; } - const loadEvents = async (relays: string[]) => { - const wrapped = await pool.querySync( - relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } - ); - - // intentionally decrypting sequentially to avoid having a bunch of popups - let decryptedRumours = new Map(); - for (event of wrapped) { - if (!expired(event)) { - const rumour = await decryptMessage(event); - const created = Math.floor(rumour.created_at / 86400); - for (const tag of event.tags) { - if (tag[0] == "expiration") { - rumour.expiration = parseInt(tag[1]); - } - } - appendMultimap(decryptedRumours, created, rumour); - } else console.log("relay contains expired event", event.id); - } - for (const [date, event] of decryptedRumours) { - event.sort((ev1, ev2) => ev1.created_at - ev2.created_at); - } - - messageRumours = decryptedRumours; + const loadEvents = async () => { + const rumours = await libnip17.getMessageRumours(user); + messageRumours = dateMap(rumours); dates = Array.from(messageRumours.keys()); dates.sort((d1, d2) => d1 - d2); @@ -128,59 +55,25 @@ messages.scrollTop = messages.scrollHeight; } - getPreferredRelays(user.pubkey).then((relays) => { - if (!relays || relays.length == 0) { - relayWarningShown = true; - return; - } - relayWarningShown = false; - loadEvents(relays); - }); - getPreferredRelays(recipient.pubkey).then((relays) => { - canSend = relays && relays.length > 0; - relaysSupporting(relays, [40]) - .then((supporting) => canSendExpiring = supporting.length > 0); - }); - - const sendMessage = async (message: string) => { - const expiry = expiryEnabled ? expiryDays : null; - let event1, event2; - if (await window.nostr.getPublicKey() === user.pubkey) { - event1 = await libnip17.createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, recipient.pubkey, expiry); - event2 = await libnip17.createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, user.pubkey, expiry); - } else { - const sk = await getPrivateKey(user); - event1 = libnip17.createNIP17MessageSK( - message, sk, recipient.pubkey, recipient.pubkey, expiry); - event2 = libnip17.createNIP17MessageSK( - message, sk, recipient.pubkey, user.pubkey, expiry); - } - - let recipientRelays = await getPreferredRelays(recipient.pubkey); - if (expiryEnabled) { - recipientRelays = await relaysSupporting(recipientRelays, [40]); - } - const p1 = Promise.any(pool.publish(recipientRelays, event1)); + const zeroPad = (num: number): string => { + return num < 10 ? '0' + num.toString() : num.toString(); + } - let senderRelays = await getPreferredRelays(user.pubkey); - if (expiryEnabled) { - senderRelays = await relaysSupporting(senderRelays, [40]); - } - const p2 = Promise.any(pool.publish(senderRelays, event2)); + const timeString = (date: Date): string => { + const hour = zeroPad(date.getHours()); + const minute = zeroPad(date.getMinutes()); + const second = zeroPad(date.getSeconds()); + return `${hour}:${minute}:${second}`; + } - await Promise.all([p1, p2]); - getPreferredRelays(user.pubkey).then(loadEvents); + const expirationClose = (expiration: number) => { + return expiration <= (Date.now() / 1000 + 2 * 86400); } - const decryptMessage = async (event: object): object => { - if (await window.nostr.getPublicKey() === user.pubkey) { - return libnip17.decryptNIP17MessageNIP07(event); - } else { - const sk = await getPrivateKey(user); - return toolsnip17.unwrapEvent(event, sk); - } + const sendMessage = async (message: string) => { + const expiry = expiryEnabled ? expiryDays : null; + await libnip17.send(message, user, recipient, expiry); + loadEvents(); } const dmName = (pubkey: string): string => { @@ -192,6 +85,21 @@ return pubkey; } } + + libnip17.getPreferredRelays(user.pubkey).then((relays) => { + if (!relays || relays.length == 0) { + relayWarningShown = true; + return; + } + relayWarningShown = false; + loadEvents(); + }); + + libnip17.getPreferredRelays(recipient.pubkey).then((relays) => { + canSend = relays && relays.length > 0; + relaysSupporting(relays, [40]) + .then((supporting) => canSendExpiring = supporting.length > 0); + }); </script> <style> |
