diff --git a/e2e/specs/admin.spec.ts b/e2e/specs/admin.spec.ts index 783d446e..44309ba9 100644 --- a/e2e/specs/admin.spec.ts +++ b/e2e/specs/admin.spec.ts @@ -291,6 +291,8 @@ test("US-094 invite people to a space", async ({seed, as}) => { await expect(page.getByRole("alert")).toContainText("Members have successfully been added!") + // Adding members opened over the invite, so saving goes back to it. + await invite.getByRole("button", {name: "Done"}).click() await page.locator(".secondary-nav").getByRole("link", {name: "Directory"}).click() await expect(memberCard(page, "Nadia Newcomer")).toBeVisible() diff --git a/e2e/specs/community.spec.ts b/e2e/specs/community.spec.ts index 8e040f38..2f6ba8dc 100644 --- a/e2e/specs/community.spec.ts +++ b/e2e/specs/community.spec.ts @@ -782,20 +782,18 @@ test("US-054 curate the library", async ({seed, as}) => { await expect(page.getByRole("heading", {name: "Reading List"})).toBeVisible() await expect(page.getByText("#reading")).toBeVisible() - // Several links go in at once, each as a card of its own. + // Each link goes in on its own and becomes a card of its own. const nevent = nip19.neventEncode({id: message.id, kind: MESSAGE, relays: [url]}) - await page.getByRole("button", {name: "Add a link"}).click() + for (const link of ["https://handbook.test/style-guide", `nostr:${nevent}`]) { + await page.getByRole("button", {name: "Add a link"}).click() - const links = dialog(page, "Add a Link") + const links = dialog(page, "Add a Link") - await links - .getByPlaceholder("URL or nevent...") - .fill(`https://handbook.test/style-guide\nnostr:${nevent}`) - // A title and a description are about one link, so a batch is offered neither. - await expect(links.getByPlaceholder("Optional title")).toHaveCount(0) - - await links.getByRole("button", {name: "Add 2 links"}).click() + await links.getByLabel("Link", {exact: true}).fill(link) + await links.getByRole("button", {name: "Add link"}).click() + await expect(links).toHaveCount(0) + } await expect(page.locator('a[href="https://handbook.test/style-guide"]')).toBeVisible() await expect(page.getByText("the deploy broke again")).toBeVisible() @@ -810,7 +808,7 @@ test("US-054 curate the library", async ({seed, as}) => { const linkEdit = dialog(page, "Edit Link") - await linkEdit.getByPlaceholder("Optional title").fill("House style guide") + await linkEdit.getByLabel("Title", {exact: true}).fill("House style guide") await linkEdit.getByRole("button", {name: "Save changes"}).click() await expect(page.getByRole("alert")).toContainText("Link updated!") @@ -835,7 +833,7 @@ test("US-054 curate the library", async ({seed, as}) => { const fromPoll = dialog(page, "Add a Link") - await fromPoll.getByPlaceholder("Optional title").fill("Standup poll") + await fromPoll.getByLabel("Title", {exact: true}).fill("Standup poll") await fromPoll.getByRole("button", {name: "Add link"}).click() await expect(page.getByRole("alert")).toContainText("Link added!") diff --git a/package.json b/package.json index eee21583..3c595a7c 100644 --- a/package.json +++ b/package.json @@ -98,16 +98,16 @@ "@types/throttle-debounce": "^5.0.2", "@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/sveltekit": "^1.1.0", - "@welshman/app": "^0.12.2", - "@welshman/content": "^0.12.2", - "@welshman/domain": "^0.12.2", - "@welshman/editor": "^0.12.2", - "@welshman/feeds": "^0.12.2", - "@welshman/lib": "^0.12.2", - "@welshman/net": "^0.12.2", - "@welshman/signer": "^0.12.2", - "@welshman/store": "^0.12.2", - "@welshman/util": "^0.12.2", + "@welshman/app": "^0.12.3", + "@welshman/content": "^0.12.3", + "@welshman/domain": "^0.12.3", + "@welshman/editor": "^0.12.3", + "@welshman/feeds": "^0.12.3", + "@welshman/lib": "^0.12.3", + "@welshman/net": "^0.12.3", + "@welshman/signer": "^0.12.3", + "@welshman/store": "^0.12.3", + "@welshman/util": "^0.12.3", "cheerio": "^1.2.0", "compressorjs-next": "^1.1.2", "dompurify": "^3.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f00f49c..05238271 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/server/preview.js b/server/preview.js index 35e71ad8..73cf3270 100644 --- a/server/preview.js +++ b/server/preview.js @@ -3,7 +3,13 @@ import {Hono} from "hono" import {load} from "cheerio" import {ParsedType, parse, renderAsText, summarize} from "@welshman/content" import {MINUTE, ellipsize, first, identity, int, ms, sleep, tryCatch} from "@welshman/lib" -import {displayRelayUrl, getIdFilters, normalizeRelayUrl} from "@welshman/util" +import { + displayRelayUrl, + getIdFilters, + isIPAddress, + isShareableRelayUrl, + normalizeRelayUrl, +} from "@welshman/util" import { Article, Classified, @@ -13,6 +19,7 @@ import { ZapGoal, displayPubkey, } from "@welshman/domain" +import {LOCAL_RELAY_URL, MockAdapter} from "@welshman/net" import {Domain, Network, Profiles, Relays, Rooms, createApp, makeRoomKey} from "@welshman/app" const PLATFORM_NAME = process.env.VITE_PLATFORM_NAME @@ -21,7 +28,29 @@ const fromCsv = value => (value || "").split(",").filter(identity) const defaultRelays = fromCsv(process.env.VITE_DEFAULT_RELAYS).map(normalizeRelayUrl) +// Anyone can put any host in a url or an event's tags, so the server only reaches public relays. +const isPublicRelayUrl = url => isShareableRelayUrl(url) && !isIPAddress(url) + +const toRelayUrl = value => { + const url = tryCatch(() => normalizeRelayUrl(value)) + + return url && isPublicRelayUrl(url) ? url : undefined +} + +// Every socket goes through here, so a relay that isn't public refuses rather than being dialed. +const refuseRelay = url => { + const adapter = new MockAdapter(url, ([verb, id]) => { + if (verb === "REQ") { + setTimeout(() => adapter.receive(["CLOSED", id, "blocked: not a public relay"])) + } + }) + + return adapter +} + const app = createApp({ + getAdapter: url => + url === LOCAL_RELAY_URL || isPublicRelayUrl(url) ? undefined : refuseRelay(url), config: { getDefaultRelays: () => defaultRelays, getIndexerRelays: () => fromCsv(process.env.VITE_INDEXER_RELAYS).map(normalizeRelayUrl), @@ -200,14 +229,17 @@ const remember = async (key, describe) => { return cached.metadata } - const metadata = await describe() + // A relay that stalls leaves the page to go out without a preview rather than hold it open. + const metadata = await Promise.race([describe(), sleep(ms(4))]) - if (cache.size > 500) { - cache.clear() + if (metadata) { + if (cache.size > 500) { + cache.clear() + } + + cache.set(key, {metadata, expiresAt: Date.now() + ms(int(5, MINUTE))}) } - cache.set(key, {metadata, expiresAt: Date.now() + ms(int(5, MINUTE))}) - return metadata } @@ -234,7 +266,7 @@ export const createPreview = template => { // A space relay answers nothing without auth, so most of these fall back to its nip-11 profile. const renderSpace = describe => render(async (url, params) => { - const relayUrl = tryCatch(() => normalizeRelayUrl(params.relay)) + const relayUrl = toRelayUrl(params.relay) if (relayUrl) { const space = await loadSpace(relayUrl) @@ -248,7 +280,7 @@ export const createPreview = template => { preview.get( "/join", render(async url => { - const relayUrl = tryCatch(() => normalizeRelayUrl(url.searchParams.get("r"))) + const relayUrl = toRelayUrl(url.searchParams.get("r")) if (relayUrl) { const space = await loadSpace(relayUrl) diff --git a/server/test/preview.test.mjs b/server/test/preview.test.mjs index 70a7f718..1444b94d 100644 --- a/server/test/preview.test.mjs +++ b/server/test/preview.test.mjs @@ -68,6 +68,12 @@ describe("preview metadata", () => { assert.equal(image, "https://app.flotilla.social/maskable-icon-512x512.png") }) + test("won't reach a relay on a private or local host", async () => { + for (const path of ["/join?r=localhost:8080", "/spaces/127.0.0.1", "/spaces/printer.local"]) { + assert.equal((await request(path)).title, "Flotilla") + } + }) + test("leaves the platform's own pages alone", async () => { for (const path of ["/", "/settings/profile", "/spaces/create", "/people/not-an-npub"]) { assert.equal((await request(path)).title, "Flotilla") diff --git a/src/app/components/CallControlBar.svelte b/src/app/components/CallControlBar.svelte index 78cde20f..6d973b68 100644 --- a/src/app/components/CallControlBar.svelte +++ b/src/app/components/CallControlBar.svelte @@ -175,7 +175,7 @@ diff --git a/src/app/components/CommentCompose.svelte b/src/app/components/CommentCompose.svelte index 752d0527..1998db19 100644 --- a/src/app/components/CommentCompose.svelte +++ b/src/app/components/CommentCompose.svelte @@ -1,7 +1,7 @@ @@ -176,88 +136,76 @@ Link {/snippet} {#snippet info()} -

- A URL or a nostr link (note, nevent, naddr, npub, or nprofile). Drop files here to upload - them. Paste several links, one per line, to file them all at once. -

+ {#if value.trim() && !pinReference} + Please enter a valid URL or nostr link. + {:else} + A URL or a nostr link, or upload a file. + {/if} {/snippet} {#snippet input()} -