mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
fix(desktop): preserve sends with invalid link previews
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -192,6 +192,7 @@ export function useComposerLinkPreviews(content: string) {
|
||||
faviconUrl: favicon.url,
|
||||
faviconSha256: favicon.sha256,
|
||||
});
|
||||
if (!tag) return;
|
||||
setReadyTags((current) => ({ ...current, [preview.href]: tag }));
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -70,6 +70,7 @@ test("snapshot tags sanitize control characters and enforce UTF-8 byte limits",
|
||||
faviconSha256: "",
|
||||
});
|
||||
|
||||
assert.ok(tag);
|
||||
for (const value of tag.slice(4, 7)) {
|
||||
assert.ok(
|
||||
Array.from(value).every((char) => {
|
||||
@@ -83,6 +84,30 @@ test("snapshot tags sanitize control characters and enforce UTF-8 byte limits",
|
||||
assert.ok(Buffer.byteLength(tag[6], "utf8") <= 1000);
|
||||
});
|
||||
|
||||
test("snapshot tags omit canonical URLs rejected by native validation", () => {
|
||||
for (const canonicalUrl of [
|
||||
`${URL}#`,
|
||||
`${URL}#details`,
|
||||
`http://linear.app/acme/issue/ABC-123/example`,
|
||||
`https://user@linear.app/acme/issue/ABC-123/example`,
|
||||
]) {
|
||||
assert.equal(
|
||||
buildLinkPreviewSnapshotTag({
|
||||
canonicalUrl,
|
||||
title: "Example",
|
||||
siteName: "Linear",
|
||||
description: "Description",
|
||||
imageUrl: "",
|
||||
imageSha256: "",
|
||||
faviconUrl: "",
|
||||
faviconSha256: "",
|
||||
}),
|
||||
null,
|
||||
canonicalUrl,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("messages without authored snapshots never create recipient previews", () => {
|
||||
assert.deepEqual(parseLinkPreviewSnapshots([], CONTENT, ORIGIN), []);
|
||||
assert.deepEqual(parseLinkPreviewSnapshots(undefined, CONTENT, ORIGIN), []);
|
||||
|
||||
@@ -9,6 +9,18 @@ const MAX_SNAPSHOTS = 8;
|
||||
const SHA256_RE = /^[0-9a-f]{64}$/;
|
||||
const IMAGE_EXT_RE = /^(?:jpg|png|gif|webp)$/;
|
||||
|
||||
export function isValidLinkPreviewSnapshotCanonicalUrl(value: string): boolean {
|
||||
if (value.includes("#")) return false;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return (
|
||||
url.protocol === "https:" && !url.username && !url.password && !url.hash
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export type LinkPreviewSnapshot = {
|
||||
canonicalUrl: string;
|
||||
title: string;
|
||||
@@ -40,7 +52,7 @@ function sanitizeSnapshotText(value: string, maxBytes: number): string {
|
||||
|
||||
function validText(value: string, max: number): boolean {
|
||||
return (
|
||||
value.length <= max &&
|
||||
new TextEncoder().encode(value).length <= max &&
|
||||
!Array.from(value).some((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return code <= 0x1f || code === 0x7f;
|
||||
@@ -144,7 +156,9 @@ export function parseLinkPreviewSnapshots(
|
||||
|
||||
export function buildLinkPreviewSnapshotTag(
|
||||
snapshot: LinkPreviewSnapshot,
|
||||
): string[] {
|
||||
): string[] | null {
|
||||
if (!isValidLinkPreviewSnapshotCanonicalUrl(snapshot.canonicalUrl))
|
||||
return null;
|
||||
return [
|
||||
"link-preview",
|
||||
"snapshot",
|
||||
|
||||
@@ -62,6 +62,10 @@ import type {
|
||||
RuntimeFileConfigSubset,
|
||||
} from "@/shared/api/tauri";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import {
|
||||
isValidLinkPreviewSnapshotCanonicalUrl,
|
||||
parseLinkPreviewSnapshots,
|
||||
} from "@/shared/lib/linkPreviewSnapshot";
|
||||
|
||||
type TestIdentity = {
|
||||
privateKey: string;
|
||||
@@ -8976,6 +8980,31 @@ async function handleSendChannelMessage(
|
||||
// real command and echoed on the stored event. Preserve them in the mock so
|
||||
// E2E recipient rendering exercises the same authored-snapshot path.
|
||||
const linkPreviewTags = args.linkPreviewTags ?? [];
|
||||
if (linkPreviewTags.length > 0) {
|
||||
const suppression = linkPreviewTags.some(
|
||||
(tag) =>
|
||||
tag.length === 2 && tag[0] === "link-preview" && tag[1] === "none",
|
||||
);
|
||||
const snapshots = linkPreviewTags.filter(
|
||||
(tag) => tag[0] === "link-preview" && tag[1] === "snapshot",
|
||||
);
|
||||
const validSnapshots = parseLinkPreviewSnapshots(
|
||||
snapshots,
|
||||
args.content,
|
||||
new URL(getRelayHttpUrl(config)).origin,
|
||||
);
|
||||
if (
|
||||
(suppression && linkPreviewTags.length !== 1) ||
|
||||
(!suppression &&
|
||||
(snapshots.length !== linkPreviewTags.length ||
|
||||
validSnapshots.length !== snapshots.length ||
|
||||
snapshots.some(
|
||||
(tag) => !isValidLinkPreviewSnapshotCanonicalUrl(tag[3] ?? ""),
|
||||
)))
|
||||
) {
|
||||
throw new Error("invalid link-preview snapshot tag");
|
||||
}
|
||||
}
|
||||
// Both kinds end up on the stored event's tag set, just like the real relay.
|
||||
const extraTags = [
|
||||
...mediaTags,
|
||||
|
||||
@@ -532,14 +532,31 @@ for (const [pasteShape, wrapUrl] of [
|
||||
});
|
||||
}
|
||||
|
||||
test("completed link preview sends once and leaves the composer cleared", async ({
|
||||
test("completed link previews send when one URL has an unsnapshotable fragment", async ({
|
||||
page,
|
||||
}) => {
|
||||
const previewUrl = "https://github.com/block/buzz/pull/3246?send=complete";
|
||||
const previewUrls = [
|
||||
"https://twitter.com/tellaho",
|
||||
"https://github.com/block/buzz/pull/3246",
|
||||
"https://x.com/tellaho/status/1884289176381841506#",
|
||||
];
|
||||
const pastedText = previewUrls.join("\n");
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await page.getByTestId("message-input").fill(previewUrl);
|
||||
await waitForReadyComposerSnapshots(page);
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.focus();
|
||||
await input.evaluate((element, text) => {
|
||||
const clipboardData = new DataTransfer();
|
||||
clipboardData.setData("text/plain", text);
|
||||
element.dispatchEvent(
|
||||
new ClipboardEvent("paste", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData,
|
||||
}),
|
||||
);
|
||||
}, pastedText);
|
||||
await waitForReadyComposerSnapshots(page, 2);
|
||||
|
||||
const send = page.getByTestId("send-message");
|
||||
await expect(send).toBeEnabled();
|
||||
@@ -558,8 +575,8 @@ test("completed link preview sends once and leaves the composer cleared", async
|
||||
expect(
|
||||
(
|
||||
calls[0]?.payload as { linkPreviewTags?: string[][] | null } | undefined
|
||||
)?.linkPreviewTags?.[0]?.slice(0, 4),
|
||||
).toEqual(["link-preview", "snapshot", "1", previewUrl]);
|
||||
)?.linkPreviewTags?.map((tag) => tag[3]),
|
||||
).toEqual(previewUrls.slice(0, 2));
|
||||
});
|
||||
|
||||
test("send does not wait for a pending link preview snapshot", async ({
|
||||
|
||||
Reference in New Issue
Block a user