Merge upstream main latest changes.

This commit is contained in:
Arne Pedersen
2022-12-27 21:05:55 +01:00
11 changed files with 234 additions and 873 deletions

View File

@@ -2,7 +2,7 @@
Access the web app: [https://sendstr.com](https://sendstr.com) Access the web app: [https://sendstr.com](https://sendstr.com)
Sendstr is an e2e encrypted shared clipboard web app powered by Nostr. Sendstr is an e2e encrypted bi-directional clipboard web app powered by Nostr.
The main motivation to build Sendstr was to provide a quick and easy way to transfer text and files (coming soon) between devices. Sendstr defaults to a self-hosted Nostr relay but can easily be configured to point elsewhere. The main motivation to build Sendstr was to provide a quick and easy way to transfer text and files (coming soon) between devices. Sendstr defaults to a self-hosted Nostr relay but can easily be configured to point elsewhere.

38
global.d.ts vendored
View File

@@ -3,44 +3,6 @@ declare module "remark-html" {
export default html export default html
} }
declare module "nostr-tools" {
export const generatePrivateKey: () => string
export const getPublicKey: (priv: string) => string
export const relayPool: () => {
setPrivateKey: (priv: string) => void
addRelay: (url: string, { read: boolean, write: boolean }) => void
publish: ({pubkey, created_at, kind, tags, content}: {
pubkey: string,
created_at: number,
kind: number,
tags: [[string,string]],
content: string,
}) => Promise<void>,
sub: ({
cb,
filter,
}: {
cb: (event: {
content: string
created_at: number
id: string
kind: number
pubkey: string
message: string
sig: string
tags: [[string, string]]
}) => void
filter: Record<string, string[]>[]
}) => {
unsub: () => void
}
}
}
declare module "nostr-tools/nip04" {
export const decrypt: (priv: string, pub: string, message: string) => string
export const encrypt: (priv: string, pub: string, message: string) => string
}
declare module "toastify-js" { declare module "toastify-js" {
const Toastify: ({ const Toastify: ({
text, text,

948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
"next": "12.1.6", "next": "12.1.6",
"next-pwa": "^5.5.4", "next-pwa": "^5.5.4",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nostr-tools": "^0.23.3", "nostr-tools": "^1.0.1",
"qrcode.react": "^3.0.2", "qrcode.react": "^3.0.2",
"react": "17.0.2", "react": "17.0.2",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",

View File

@@ -18,5 +18,5 @@
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"theme_color": "#3C3744", "theme_color": "#3C3744",
"description": "Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load." "description": "Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load."
} }

View File

@@ -1,5 +1,6 @@
import { getRelays } from "./localStorage" import { getRelays } from "./localStorage"
import { NostrEventType, NostrKeysType, NostrPoolType } from "../types" import { NostrEventType, NostrKeysType, NostrPoolType } from "../types"
import { Relay, Sub } from "nostr-tools"
export const getLatestEvent = (events: Record<string, NostrEventType>) => export const getLatestEvent = (events: Record<string, NostrEventType>) =>
Object.entries(events).reduce((acc, x) => { Object.entries(events).reduce((acc, x) => {
@@ -20,30 +21,31 @@ export const subscribe = async (
peerKey: string, peerKey: string,
cb: (event: NostrEventType) => void, cb: (event: NostrEventType) => void,
) => { ) => {
const { decrypt } = await import("nostr-tools/nip04") const { nip04, relayInit } = await import("nostr-tools")
const { relayPool } = await import("nostr-tools")
const pool = relayPool()
pool.setPrivateKey(keys.priv)
const relays = getRelays() const relays = getRelays()
relays.forEach((relay) => relay.enabled && pool.addRelay(relay.url, { read: true, write: true })) .filter((x) => x.enabled)
const sub = pool.sub({ .map((x) => relayInit(x.url))
cb: (event: NostrEventType) => { await Promise.allSettled(relays.map((x) => x.connect()))
try { relays.forEach((relay) =>
relay.on("error", () => console.error(`Failed to connect to relay ${relay.url}`)),
)
const subs = relays.map((relay) =>
relay.sub([{ "#p": [keys.pub, peerKey] }, { authors: [keys.pub, peerKey] }]),
)
subs.forEach((x) =>
x.on("event", async (event: NostrEventType) => {
const p = event.tags.find(([tag]) => tag === "p") || ["p", ""] const p = event.tags.find(([tag]) => tag === "p") || ["p", ""]
const pubkey = event.pubkey === keys.pub ? p[1] : event.pubkey const pubkey = event.pubkey === keys.pub ? p[1] : event.pubkey
const message = decrypt(keys.priv, pubkey, event.content) const message = await nip04.decrypt(keys.priv, pubkey, event.content)
cb({ ...event, message }) cb({ ...event, message })
} catch (e) { }),
console.warn(e) )
} return { subs, relays }
},
filter: [{ "#p": [keys.pub, peerKey] }, { authors: [keys.pub, peerKey] }],
})
return { sub, pool }
} }
type SendEncryptedMessage = { type SendEncryptedMessage = {
pool: NostrPoolType | null relays: Relay[]
subs: Sub[]
priv: string priv: string
pub: string pub: string
peerKey: string peerKey: string
@@ -51,18 +53,28 @@ type SendEncryptedMessage = {
} }
export const sendEncryptedMessage = async ({ export const sendEncryptedMessage = async ({
pool, relays,
priv, priv,
pub, pub,
peerKey, peerKey,
message, message,
}: SendEncryptedMessage) => { }: SendEncryptedMessage) => {
const { encrypt } = await import("nostr-tools/nip04") const { nip04, getEventHash, signEvent } = await import("nostr-tools")
return pool?.publish({ relays.map((relay) => {
nip04
.encrypt(priv, peerKey, message)
.then((content) => {
const event = {
pubkey: pub, pubkey: pub,
created_at: Math.round(Date.now() / 1000), created_at: Math.round(Date.now() / 1000),
kind: 4, kind: 4,
tags: [["p", peerKey]], tags: [["p", peerKey]],
content: encrypt(priv, peerKey, message), content,
}
const id = getEventHash(event)
const sig = signEvent(event, priv)
relay.publish({ ...event, id, sig })
})
.catch((e) => console.error(`Failed to send message to relay ${relay.url}`, e))
}) })
} }

View File

@@ -3,7 +3,6 @@ import Head from "next/head"
import { Header } from "../components/header" import { Header } from "../components/header"
import { Button } from "../components/button" import { Button } from "../components/button"
import { Card } from "../components/card"
import { SendView } from "../views/send" import { SendView } from "../views/send"
import { ReceiveView } from "../views/receive" import { ReceiveView } from "../views/receive"
import { NostrKeysType } from "../types" import { NostrKeysType } from "../types"
@@ -61,16 +60,16 @@ export default function Home({ keys }: HomeProps) {
<meta name="viewport" content="initial-scale=1.0, width=device-width" /> <meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta <meta
name="description" name="description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app
built on top of Nostr. No login needed, new throwaway encryption keys are generated on built on top of Nostr. No login needed, new throwaway encryption keys are generated on
page load, and the default relay deletes messages after 1 hour." page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://sendstr.com/" /> <meta property="og:url" content="https://sendstr.com/" />
<meta property="og:title" content="Senstr" /> <meta property="og:title" content="Sendstr" />
<meta <meta
property="og:description" property="og:description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour." content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="og:image" content="/favicon-16x16.png" /> <meta property="og:image" content="/favicon-16x16.png" />
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
@@ -78,7 +77,7 @@ export default function Home({ keys }: HomeProps) {
<meta property="twitter:title" content="Sendstr" /> <meta property="twitter:title" content="Sendstr" />
<meta <meta
property="twitter:description" property="twitter:description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour." content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="twitter:image" content="/favicon-16x16.png" /> <meta property="twitter:image" content="/favicon-16x16.png" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />

View File

@@ -32,16 +32,16 @@ export default function Settings() {
<meta name="viewport" content="initial-scale=1.0, width=device-width" /> <meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta <meta
name="description" name="description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app
built on top of Nostr. No login needed, new throwaway encryption keys are generated on built on top of Nostr. No login needed, new throwaway encryption keys are generated on
page load, and the default relay deletes messages after 1 hour." page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://sendstr.com/settings" /> <meta property="og:url" content="https://sendstr.com/settings" />
<meta property="og:title" content="Senstr - Settings" /> <meta property="og:title" content="Sendstr - Settings" />
<meta <meta
property="og:description" property="og:description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour." content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="og:image" content="/favicon-16x16.png" /> <meta property="og:image" content="/favicon-16x16.png" />
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
@@ -49,7 +49,7 @@ export default function Settings() {
<meta property="twitter:title" content="Sendstr - Settings" /> <meta property="twitter:title" content="Sendstr - Settings" />
<meta <meta
property="twitter:description" property="twitter:description"
content="Sendstr is an open source end-to-end encrypted shared clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour." content="Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of Nostr. No login needed, new throwaway encryption keys are generated on page load, and the default relay deletes messages after 1 hour."
/> />
<meta property="twitter:image" content="/favicon-16x16.png" /> <meta property="twitter:image" content="/favicon-16x16.png" />
</Head> </Head>

View File

@@ -1,3 +1,5 @@
import { Relay, Sub } from "nostr-tools"
export type NostrSubType = ({ export type NostrSubType = ({
cb, cb,
filter, filter,
@@ -34,17 +36,15 @@ export type NostrPublishType = ({
export type NostrPoolType = { export type NostrPoolType = {
setPrivateKey: (priv: string) => void setPrivateKey: (priv: string) => void
addRelay: (url: string, { read, write }: { read: boolean; write: boolean }) => void addRelay: (url: string, { read, write }: { read: boolean; write: boolean }) => void
sub: NostrSubType subs: NostrSubType
publish: NostrPublishType publish: NostrPublishType
} }
export type NostrType = { export type NostrType = {
priv: string priv: string
pub: string pub: string
pool: NostrPoolType | null subs: Sub[],
sub: { relays: Relay[]
unsub: () => void
} | null
} }
export type NostrEventType = { export type NostrEventType = {

View File

@@ -45,10 +45,10 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
const sub = await subscribe(keys, peerKey, processEvent) const { subs, relays } = await subscribe(keys, peerKey, processEvent)
nostr.current = { ...sub, ...keys } nostr.current = { subs, relays, ...keys }
return () => { return () => {
nostr?.current?.sub?.unsub() nostr?.current?.subs.forEach(sub => sub.unsub())
} }
})() })()
}, [peerKey]) }, [peerKey])
@@ -56,7 +56,7 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
const sendMessage = useRef( const sendMessage = useRef(
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
debounce(async (peerKey: string, message: string) => { debounce(async (peerKey: string, message: string) => {
if (nostr?.current?.pool) { if (nostr?.current?.relays) {
await sendEncryptedMessage({ ...nostr.current, peerKey, message }) await sendEncryptedMessage({ ...nostr.current, peerKey, message })
} }
}, 500), }, 500),

View File

@@ -43,7 +43,7 @@ const PeerInput = ({ peerKey, onChange, setShowScan }: PeerInputProps) => {
<section className="mx-auto max-w-[40rem] p-4"> <section className="mx-auto max-w-[40rem] p-4">
<label>Peer pubkey:</label> <label>Peer pubkey:</label>
<div className="relative"> <div className="relative">
<Input value={peerKey} onChange={(e) => onChange(e?.currentTarget?.value || "")} /> <Input value={peerKey} onChange={(e) => onChange(e?.currentTarget?.value.trim() || "")} />
<div className="absolute right-0 top-0 h-full flex items-center"> <div className="absolute right-0 top-0 h-full flex items-center">
<IconButton className="w-10 h-10 mr-2" onClick={() => setShowScan(true)}> <IconButton className="w-10 h-10 mr-2" onClick={() => setShowScan(true)}>
<div className="m-2"> <div className="m-2">
@@ -79,10 +79,10 @@ export const SendView = ({ keys }: SendViewProps) => {
if (isMobile) { if (isMobile) {
setShowScan(true) setShowScan(true)
} }
const sub = await subscribe(keys, peerKey, processEvent) const { subs, relays } = await subscribe(keys, peerKey, processEvent)
nostr.current = { ...sub, ...keys } nostr.current = { subs, relays, ...keys }
return () => { return () => {
nostr?.current?.sub?.unsub() nostr?.current?.subs.forEach(sub => sub.unsub())
} }
})() })()
}, []) }, [])
@@ -99,7 +99,7 @@ export const SendView = ({ keys }: SendViewProps) => {
const sendMessage = useRef( const sendMessage = useRef(
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
debounce(async (peerKey: string, message: string) => { debounce(async (peerKey: string, message: string) => {
if (nostr.current?.pool) { if (nostr.current?.relays) {
await sendEncryptedMessage({ ...nostr.current, peerKey, message }) await sendEncryptedMessage({ ...nostr.current, peerKey, message })
} }
}, 500), }, 500),