mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
## Summary - Supersedes #1913 with a KLIPY-hosted URL implementation. - Adds KLIPY GIF search and trending results to desktop message and forum composers. - Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL and media metadata (no imeta tag, since relays only accept hash-backed local `/media/` entries). - Aligns the Emoji/GIF picker with Buzz's standard segmented control, theme surfaces, and motion behavior. ## Relay-to-provider boundary - The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never reaches the desktop; the key stays server-side behind a redacted `Debug` impl. - The dedicated GIF `reqwest` client sets `redirect::Policy::none()`. Because the API key rides in the request path, following a provider `3xx` could replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns as a non-success status that the handlers map to a generic `502`; the `Location` target is never read or forwarded. - Admission reuses the established NIP-98, membership, replay, and per-pubkey rate-limit gates, with an upstream response-size cap and allowlisting so KLIPY error bodies never cross the relay boundary. ## Accessibility - Under `prefers-reduced-motion: reduce`, the picker grid renders a static provider poster (a normalized `jpg` asset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted. `no-preference` keeps the animated preview. - Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an empty `Attachment ` label. Ordinary hashed uploads keep their existing hash-derived names. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
180 lines
4.0 KiB
JavaScript
180 lines
4.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
klipyGifAttachment,
|
|
klipyGifFilename,
|
|
normalizeKlipyGifs,
|
|
relayKlipyCapability,
|
|
} from "./api.ts";
|
|
|
|
const GIF_ASSET = {
|
|
url: "https://static.klipy.com/example.gif",
|
|
width: 640,
|
|
height: 360,
|
|
size: 42,
|
|
};
|
|
|
|
test("normalizeKlipyGifs selects a compact preview and medium GIF", () => {
|
|
const [gif] = normalizeKlipyGifs([
|
|
{
|
|
id: 7,
|
|
title: " Ship it ",
|
|
slug: "ship-it",
|
|
type: "gif",
|
|
file: {
|
|
md: { gif: GIF_ASSET },
|
|
sm: {
|
|
webp: {
|
|
url: "https://static.klipy.com/preview.webp",
|
|
width: 220,
|
|
height: 124,
|
|
size: 12,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
|
|
assert.equal(gif.title, "Ship it");
|
|
assert.equal(gif.original.url, GIF_ASSET.url);
|
|
assert.equal(gif.preview.url, "https://static.klipy.com/preview.webp");
|
|
assert.equal(gif.poster, null);
|
|
});
|
|
|
|
test("normalizeKlipyGifs carries a static jpg poster when present", () => {
|
|
const [gif] = normalizeKlipyGifs([
|
|
{
|
|
id: 8,
|
|
title: "Static poster",
|
|
slug: "static-poster",
|
|
type: "gif",
|
|
file: {
|
|
md: { gif: GIF_ASSET },
|
|
sm: {
|
|
jpg: {
|
|
url: "https://static.klipy.com/poster.jpg",
|
|
width: 220,
|
|
height: 124,
|
|
size: 8,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
|
|
assert.equal(gif.poster?.url, "https://static.klipy.com/poster.jpg");
|
|
});
|
|
|
|
test("normalizeKlipyGifs omits ads and malformed file records", () => {
|
|
const gifs = normalizeKlipyGifs([
|
|
{ id: 1, slug: "ad", type: "ad" },
|
|
{ id: 2, slug: "missing", type: "gif", file: {} },
|
|
]);
|
|
|
|
assert.deepEqual(gifs, []);
|
|
});
|
|
|
|
test("relayKlipyCapability requires safe search and share endpoints", () => {
|
|
assert.deepEqual(
|
|
relayKlipyCapability({
|
|
gif: {
|
|
provider: "klipy",
|
|
search: "/gifs/search",
|
|
share: "/gifs/share",
|
|
},
|
|
supported_extensions: ["nip-er", "buzz-gif"],
|
|
}),
|
|
{ searchPath: "/gifs/search", sharePath: "/gifs/share" },
|
|
);
|
|
assert.equal(relayKlipyCapability({}), null);
|
|
assert.equal(
|
|
relayKlipyCapability({
|
|
gif: {
|
|
provider: "another",
|
|
search: "/gifs/search",
|
|
share: "/gifs/share",
|
|
},
|
|
supported_extensions: ["buzz-gif"],
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
relayKlipyCapability({
|
|
gif: { provider: "klipy", search: "/gifs/search" },
|
|
supported_extensions: ["buzz-gif"],
|
|
}),
|
|
null,
|
|
);
|
|
for (const path of [
|
|
"https://attacker.example/search",
|
|
"//attacker.example/search",
|
|
"/\\attacker.example/search",
|
|
"/%5c%5cattacker.example/search",
|
|
"/gifs/../admin",
|
|
"/gifs/%2e%2e/admin",
|
|
"/gifs/search?redirect=https://attacker.example",
|
|
"/gifs/search#fragment",
|
|
]) {
|
|
assert.equal(
|
|
relayKlipyCapability({
|
|
gif: {
|
|
provider: "klipy",
|
|
search: path,
|
|
share: "/gifs/share",
|
|
},
|
|
supported_extensions: ["buzz-gif"],
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
relayKlipyCapability({
|
|
gif: {
|
|
provider: "klipy",
|
|
search: "/gifs/search",
|
|
share: path,
|
|
},
|
|
supported_extensions: ["buzz-gif"],
|
|
}),
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("klipyGifFilename sanitizes provider slugs", () => {
|
|
const gif = {
|
|
id: 1,
|
|
original: GIF_ASSET,
|
|
poster: null,
|
|
preview: GIF_ASSET,
|
|
slug: " That's a wrap! ",
|
|
title: "That's a wrap",
|
|
};
|
|
|
|
const filename = klipyGifFilename(gif);
|
|
|
|
assert.equal(filename, "that-s-a-wrap.gif");
|
|
});
|
|
|
|
test("klipyGifAttachment references KLIPY media without an uploaded copy", () => {
|
|
const attachment = klipyGifAttachment({
|
|
id: 1,
|
|
original: GIF_ASSET,
|
|
poster: null,
|
|
preview: GIF_ASSET,
|
|
slug: "ship-it",
|
|
title: "Ship it",
|
|
});
|
|
|
|
assert.deepEqual(attachment, {
|
|
dim: "640x360",
|
|
displayLabel: "Ship it",
|
|
filename: "ship-it.gif",
|
|
sha256: "",
|
|
size: 42,
|
|
type: "image/gif",
|
|
uploaded: 0,
|
|
url: GIF_ASSET.url,
|
|
});
|
|
});
|