aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdrien Hopkins <[email protected]>2025-12-05 09:51:00 -0500
committerAdrien Hopkins <[email protected]>2025-12-05 10:19:23 -0500
commit4374655730c3762ea56e9c732a7c0b7ab872ae1f (patch)
tree779b77b60ff693010b8c35c7d3e608a1240e0ecc /src
parenteb6ef86f3d5200967e1d80f6832796348ae7fce5 (diff)
downloadcoinos-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.ts15
-rw-r--r--src/lib/nip17.ts105
-rw-r--r--src/lib/nip40.ts16
-rw-r--r--src/routes/(app)/dm/[recipient]/+page.svelte174
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>