diff options
| -rw-r--r-- | src/components/AppHeader.svelte | 2 | ||||
| -rw-r--r-- | src/lib/nostr.js | 62 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+layout.svelte | 10 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/messages/+page.server.js | 16 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/messages/+page.svelte | 106 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/message/+page.svelte | 41 |
6 files changed, 190 insertions, 47 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index dd1beb68..e5747542 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -100,7 +100,7 @@ <ul class="space-y-5 w-48"> {#each menuButtons as { href, icon, key }} <li> - <a {href}> + <a {href} data-sveltekit-prefetch="off"> <button class="flex justify-center items-center font-semibold hover:opacity-80" ><Icon {icon} style="mr-2 w-6 md:w-8" /> {$t(key)} </button> diff --git a/src/lib/nostr.js b/src/lib/nostr.js index 048738bc..aff75c1b 100644 --- a/src/lib/nostr.js +++ b/src/lib/nostr.js @@ -16,7 +16,6 @@ export let generate = async (user) => { if (!get(pin)?.length === 6) return; - mnemonic = generateMnemonic(); seed = mnemonicToSeedSync(mnemonic); entropy = mnemonicToEntropy(mnemonic); @@ -37,9 +36,60 @@ export let generate = async (user) => { user.salt = Buffer.from(salt).toString('hex'); }; -export let sign = async ({ event, user }) => { - let { cipher, username, salt } = user; +export let encrypt = async ({ message, recipient, user }) => { + let sharedPoint = Buffer.from( + ecc.pointMultiply( + Uint8Array.from(Buffer.from('02' + recipient, 'hex')), + Buffer.from(await getPrivateKey(user), 'hex') + ) + ); + + let iv = crypto.getRandomValues(new Uint8Array(16)); + let cipher = await crypto.subtle.encrypt( + { name: 'AES-CBC', iv }, + await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [ + 'encrypt', + 'decrypt' + ]), + new TextEncoder().encode(message) + ); + + return Buffer.from(cipher).toString('base64') + '?iv=' + Buffer.from(iv).toString('base64'); +}; + +export let decrypt = async ({ event, user }) => { + try { + let { content, pubkey } = event; + if (pubkey === user.pubkey) pubkey = event.tags[0][1]; + pubkey = Uint8Array.from(Buffer.from('02' + pubkey, 'hex')); + + let sharedPoint = Buffer.from( + ecc.pointMultiply(pubkey, Buffer.from(await getPrivateKey(user), 'hex')) + ); + + let [cipher, iv] = content.split('?iv='); + cipher = Uint8Array.from(Buffer.from(cipher, 'base64')); + iv = Uint8Array.from(Buffer.from(iv, 'base64')); + let key = await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [ + 'encrypt', + 'decrypt' + ]); + + let message = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, cipher); + + return Buffer.from(message).toString('utf8'); + } catch (e) { + console.log(e); + } +}; + +function typedArrayToBuffer(array) { + return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset); +} + +let getPrivateKey = async (user) => { + let { cipher, username, salt } = user; let password = get(pw); if (!password) { @@ -49,7 +99,6 @@ export let sign = async ({ event, user }) => { try { await post('/password', { password }); } catch (e) { - console.log(e); passwordPrompt.set(true); } @@ -70,8 +119,11 @@ export let sign = async ({ event, user }) => { seed = mnemonicToSeedSync(mnemonic); key = bip32.fromSeed(seed); child = key.derivePath("m/44'/1237'/0'/0/0"); - privkey = child.privateKey; + return child.privateKey; +}; +export let sign = async ({ event, user }) => { + let privkey = await getPrivateKey(user); event.id = await calculateId(event); event.sig = await signId(privkey, event.id); }; diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index 2d259256..98842016 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -83,6 +83,16 @@ </div> {/if} {/if} + <div class="w-full flex"> + <a href={`/${subject.pubkey}/messages`} class="mx-auto"> + <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> + <div class="mx-auto flex"> + <Icon icon="support" style="mr-1" /> + <div>Message</div> + </div> + </button> + </a> + </div> {#if !subject.anon} <div class="w-full flex"> <a href={`/${subject.username}/receive`} class="mx-auto"> diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/(profile)/messages/+page.server.js new file mode 100644 index 00000000..2efddf77 --- /dev/null +++ b/src/routes/(app)/[username]/(profile)/messages/+page.server.js @@ -0,0 +1,16 @@ +import { get } from '$lib/utils'; + +export async function load({ params, parent, url }) { + let { user, subject } = await parent(); + + let messages = []; + + try { + messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/messages`); + console.log('MESSAGES', messages); + } catch (e) { + console.log(`failed to fetch nostr messages`, e); + } + + return { messages }; +} diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte new file mode 100644 index 00000000..3f8f5a72 --- /dev/null +++ b/src/routes/(app)/[username]/(profile)/messages/+page.svelte @@ -0,0 +1,106 @@ +<script> + import { t } from '$lib/translations'; + import { format, parseISO } from 'date-fns'; + import { scale } from 'svelte/transition'; + import { enhance } from '$app/forms'; + import { Icon } from '$comp'; + import { back, failure } from '$lib/utils'; + import { sign, send, encrypt, decrypt } from '$lib/nostr'; + + export let data; + export let form; + + const scrollToBottom = (node) => { + const scroll = () => + node.scroll({ + top: node.scrollHeight, + behavior: 'smooth' + }); + scroll(); + + return { update: scroll }; + }; + + let { messages, subject, user } = data; + + let sent, submitting, message; + let submit = async () => { + submitting = true; + + let event = { + pubkey: user.pubkey, + created_at: Math.floor(Date.now() / 1000), + kind: 4, + tags: [['p', subject.pubkey]] + }; + + event.message = message; + messages.push(event); + messages = messages; + + try { + event.content = await encrypt({ message, recipient: subject.pubkey, user }); + await sign({ event, user }); + await send(event); + + sent = true; + } catch (e) { + failure('Failed to send message'); + messages.pop(); + messages = messages; + } + + submitting = false; + }; +</script> + +<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5"> + <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> + {$t('transactions.sendMessage')} + </h1> + + <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" use:scrollToBottom={messages}> + {#each messages as event} + {@const ours = event.pubkey === user.pubkey} + {@const theirs = !ours} + <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs> + {#if event.message} + {event.message} + {:else} + {#await decrypt({ event, user })} + - + {:then message} + {(event.message = message) && ''} + {event.message} + {/await} + {/if} + </div> + <div class="text-sm text-gray-400 ml-2 mb-6"> + {format(new Date(event.created_at * 1000), 'MMM d')}, + {format(new Date(event.created_at * 1000), 'h:mm aa')} + </div> + {/each} + </div> + + <form method="POST" class="space-y-5" on:submit|preventDefault={submit}> + <input type="hidden" name="requester_id" value={user.id} /> + <input type="hidden" name="recipient" value={subject.username} /> + + <input + name="message" + placeholder={$t('transactions.sendMessage')} + class="w-full p-4 border rounded-xl" + bind:value={message} + /> + </form> +</div> + +<style> + .ours { + @apply bg-gray-100 rounded-br-none ml-auto; + } + + .theirs { + @apply bg-gray-200 rounded-bl-none; + } +</style> diff --git a/src/routes/(app)/[username]/message/+page.svelte b/src/routes/(app)/[username]/message/+page.svelte deleted file mode 100644 index c8a57b79..00000000 --- a/src/routes/(app)/[username]/message/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ -<script> - import { t } from '$lib/translations'; - import { enhance } from '$app/forms'; - import { Icon } from '$comp'; - import { back } from '$lib/utils'; - - export let data; - export let form; - - let { subject, user } = data; -</script> - -<button class="ml-5 md:ml-20 mt-10 hover:opacity-80" on:click={back}> - <Icon icon="arrow-left" style="w-10" /> -</button> - -<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t('transactions.sendMessage')} - </h1> - - <form action="?/create" method="POST" use:enhance class="space-y-5"> - <input type="hidden" name="requester_id" value={user.id} /> - <input type="hidden" name="recipient" value={subject.username} /> - - <textarea - name="memo" - placeholder={$t('transactions.sendMessage')} - class="w-full p-4 border rounded-xl h-48" - /> - - <div class="w-full flex"> - <button - type="submit" - class="bg-black text-white border rounded-full px-6 py-2 font-bold mx-auto" - > - {$t('transactions.submit')} - </button> - </div> - </form> -</div> |
