diff --git a/src/routes/router.ts b/src/routes/router.ts
index 951866f..00649ff 100644
--- a/src/routes/router.ts
+++ b/src/routes/router.ts
@@ -159,14 +159,14 @@ export default class EveRouter extends LitElement {
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral);
}
-
+
.window {
overflow: auto;
grid-column: 2;
grid-row: 2;
position: relative;
}
-
+
.window::after {
content: '';
position: absolute;
@@ -179,7 +179,7 @@ export default class EveRouter extends LitElement {
background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.03));
transition: opacity 0.3s ease;
}
-
+
.window:hover::after {
opacity: 1;
}
@@ -235,7 +235,7 @@ export default class EveRouter extends LitElement {
transform: perspective(1200px) translateX(50vw);
filter: blur(50px);
}
-
+
.window-content.transitioning::after {
opacity: 1;
}
@@ -500,7 +500,7 @@ export default class EveRouter extends LitElement {
() => html``,
)}
- (array: T[]): T[] {
+ return [...new Set(array)];
+}
diff --git a/src/utils/dm.ts b/src/utils/dm.ts
new file mode 100644
index 0000000..db01e40
--- /dev/null
+++ b/src/utils/dm.ts
@@ -0,0 +1,65 @@
+import { inviteResponseKind, ndk } from '@/ndk';
+import { bytesToHex } from '@noble/hashes/utils';
+import { NDKEvent, NDKKind, NDKPrivateKeySigner, type NDKTag, NDKUser } from '@nostr-dev-kit/ndk';
+import type { NPub } from '@nostr/tools/nip19';
+import * as nostrTools from '@nostr/tools/pure';
+import { base64 } from '@scure/base';
+import { arrayToUnique } from './arrayToUnique';
+export async function generateDM(npub: NPub, content: string, additionalTags: NDKTag[] = []) {
+ if (!ndk.signer) {
+ throw new Error('NDK signer not initialized');
+ }
+
+ const recipient = new NDKUser({
+ npub,
+ });
+
+ const randomPrivateKey = nostrTools.generateSecretKey();
+ const randomSigner = new NDKPrivateKeySigner(randomPrivateKey);
+
+ const dmEvent = new NDKEvent(ndk);
+ dmEvent.kind = NDKKind.PrivateDirectMessage;
+ dmEvent.content = content;
+ dmEvent.tags.push(['p', recipient.pubkey], ...additionalTags);
+ await dmEvent.sign(ndk.signer);
+
+ const sealEvent = new NDKEvent(ndk);
+ sealEvent.kind = NDKKind.GiftWrapSeal;
+ sealEvent.content = await randomSigner.encrypt(recipient, JSON.stringify(dmEvent), 'nip44');
+ await sealEvent.sign(randomSigner);
+
+ const giftWrapEvent = new NDKEvent(ndk);
+ giftWrapEvent.kind = NDKKind.GiftWrap;
+ giftWrapEvent.tags.push(['p', recipient.pubkey], ...additionalTags);
+ giftWrapEvent.content = await randomSigner.encrypt(recipient, JSON.stringify(sealEvent), 'nip44');
+ await giftWrapEvent.sign(randomSigner);
+
+ return giftWrapEvent;
+}
+
+export async function decryptInviteResponseDM(event: NDKEvent): Promise<{
+ privateKey: string;
+ existingMembers: string[];
+ ccnName: string;
+}> {
+ if (event.kind !== NDKKind.GiftWrap) {
+ throw new Error('Event is not a gift wrap');
+ }
+
+ const seal = JSON.parse(await ndk.signer?.decrypt(event.author, event.content, 'nip44')!);
+ if (seal.kind !== NDKKind.GiftWrapSeal) {
+ throw new Error('Seal is not a gift wrap seal');
+ }
+
+ const inviteResponse = JSON.parse(await ndk.signer?.decrypt(event.author, seal.content, 'nip44')!);
+ if (inviteResponse.kind !== inviteResponseKind) {
+ throw new Error('Invite response is not of correct type');
+ }
+
+ const privateKey = base64.decode(inviteResponse.content);
+ return {
+ privateKey: bytesToHex(privateKey),
+ existingMembers: arrayToUnique(inviteResponse.tags.filter((tag) => tag[0] === 'p').map((tag) => tag[1])),
+ ccnName: inviteResponse.tags.find((tag) => tag[0] === 'name')?.[1],
+ };
+}
diff --git a/src/utils/getCCNList.ts b/src/utils/getCCNList.ts
new file mode 100644
index 0000000..a044b15
--- /dev/null
+++ b/src/utils/getCCNList.ts
@@ -0,0 +1,33 @@
+import { nip19 } from '@nostr/tools';
+import type { NPub } from '@nostr/tools/nip19';
+
+export function getCCNList(): Promise<{ name: string; pubkey: string }[]> {
+ return new Promise((resolve, reject) => {
+ const ws = new WebSocket('ws://localhost:6942');
+ ws.onopen = () => {
+ ws.send(JSON.stringify(['CCN', 'LIST']));
+ };
+ ws.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ if (data[0] !== 'OK' || data[1] !== 'CCN LIST' || data[2] !== true) return;
+ resolve(JSON.parse(data[3]));
+ };
+ ws.onerror = (event) => {
+ reject(event);
+ };
+ });
+}
+
+export function getActiveCCNNpub() {
+ const selectedCCN = localStorage.getItem('selectedCCN');
+ if (!selectedCCN) return null;
+ const { pubkey } = JSON.parse(selectedCCN);
+ return nip19.npubEncode(pubkey);
+}
+
+export async function getCCNName(npub: NPub) {
+ const ccnList = await getCCNList();
+ const npubToPubkey = nip19.decode<'npub'>(npub);
+ const ccn = ccnList.find((ccn) => ccn.pubkey === npubToPubkey.data);
+ return ccn?.name;
+}
diff --git a/src/utils/invite.ts b/src/utils/invite.ts
new file mode 100644
index 0000000..3ffeac6
--- /dev/null
+++ b/src/utils/invite.ts
@@ -0,0 +1,19 @@
+import { bytesToHex, hexToBytes } from '@noble/ciphers/utils';
+import { nip19 } from '@nostr/tools';
+import type { NPub } from '@nostr/tools/nip19';
+import { bech32m } from '@scure/base';
+
+export function generateInvite(ccn: NPub, invite: string) {
+ const npubHex = nip19.decode<'npub'>(ccn).data;
+ const combinedBytes = bech32m.toWords(new Uint8Array([...hexToBytes(npubHex), ...hexToBytes(invite)]));
+ return bech32m.encode('eveinvite', combinedBytes, false);
+}
+
+export function readInvite(invite: `${string}1${string}`) {
+ const decoded = bech32m.decode(invite, false);
+ if (decoded.prefix !== 'eveinvite') return false;
+ const hexBytes = bech32m.fromWords(decoded.words);
+ const npub = nip19.npubEncode(bytesToHex(hexBytes.slice(0, 32)));
+ const inviteCode = bytesToHex(hexBytes.slice(32));
+ return { npub, invite: inviteCode };
+}
diff --git a/src/utils/sha512.ts b/src/utils/sha512.ts
new file mode 100644
index 0000000..4d0a144
--- /dev/null
+++ b/src/utils/sha512.ts
@@ -0,0 +1,6 @@
+import { bytesToHex } from '@noble/ciphers/utils';
+import { sha512 } from '@noble/hashes/sha512';
+
+export function sha512Hash(input: string) {
+ return bytesToHex(sha512.create().update(input).digest());
+}
diff --git a/src/utils/unsafeRelaySet.ts b/src/utils/unsafeRelaySet.ts
new file mode 100644
index 0000000..c860721
--- /dev/null
+++ b/src/utils/unsafeRelaySet.ts
@@ -0,0 +1,25 @@
+import { ndk } from '@/ndk';
+import { NDKRelay, NDKRelaySet } from '@nostr-dev-kit/ndk';
+
+/**
+ * This is an unsafe relay set that is used to publish events publicly
+ * This should NEVER be used except for the initial setup
+ */
+export const unsafeRelaySet = new NDKRelaySet(
+ new Set([
+ new NDKRelay('wss://nos.lol/', undefined, ndk),
+ new NDKRelay('wss://nostr.einundzwanzig.space/', undefined, ndk),
+ new NDKRelay('wss://nostr.massmux.com/', undefined, ndk),
+ new NDKRelay('wss://nostr.mom/', undefined, ndk),
+ new NDKRelay('wss://purplerelay.com/', undefined, ndk),
+ new NDKRelay('wss://relay.damus.io/', undefined, ndk),
+ new NDKRelay('wss://relay.goodmorningbitcoin.com/', undefined, ndk),
+ new NDKRelay('wss://relay.lexingtonbitcoin.org/', undefined, ndk),
+ new NDKRelay('wss://relay.nostr.band/', undefined, ndk),
+ new NDKRelay('wss://relay.primal.net/', undefined, ndk),
+ new NDKRelay('wss://relay.snort.social/', undefined, ndk),
+ new NDKRelay('wss://strfry.iris.to/', undefined, ndk),
+ new NDKRelay('wss://cache2.primal.net/v1', undefined, ndk),
+ ]),
+ ndk,
+);