diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 0c441ccdc2..753262a4f6 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -8,7 +8,7 @@ import { parseStarterPackUri, } from '#/lib/strings/starter-pack' import {messages} from '#/locale/locales/en/messages' -import {klipyStaticUrl} from '#/state/queries/klipy' +import {klipyUrlToBskyGifUrl} from '#/state/queries/klipy' import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor' import {cleanError} from '../../src/lib/strings/errors' import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' @@ -858,7 +858,7 @@ describe('parseEmbedPlayerFromUrl', () => { source: 'klipy', isGif: true, hideDetails: true, - playerUri: 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif', + playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif', dimensions: { width: 300, height: 200, @@ -1073,13 +1073,30 @@ describe('tenorUrlToBskyGifUrl', () => { ) }) -describe('klipyStaticUrl', () => { - it('returns the URL as-is for valid KLIPY static URLs', () => { - const input = 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif' - expect(klipyStaticUrl(input)).toEqual(input) +describe('klipyUrlToBskyGifUrl', () => { + const inputs = [ + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ] + + it.each(inputs)( + 'returns url with k.gifs.bsky.app as hostname for input url', + input => { + const out = klipyUrlToBskyGifUrl(input) + expect(out.startsWith('https://k.gifs.bsky.app/')).toEqual(true) + }, + ) + + it('preserves the path and query params when rewriting', () => { + const out = klipyUrlToBskyGifUrl( + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ) + expect(out).toEqual( + 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + ) }) it('returns empty string for invalid URLs', () => { - expect(klipyStaticUrl('not-a-url')).toEqual('') + expect(klipyUrlToBskyGifUrl('not-a-url')).toEqual('') }) }) diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index a884e4ec46..56873e6363 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -59,7 +59,10 @@ export const ExternalEmbed = ({ } }, [link.uri, playHaptic]) - if (embedPlayerParams?.source === 'tenor') { + if ( + embedPlayerParams?.source === 'tenor' || + embedPlayerParams?.source === 'klipy' + ) { const parsedAlt = parseAltFromGIFDescription(link.description) return ( diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index bbd4c11305..d8a5999797 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -683,8 +683,12 @@ export function parseKlipyGif(urlp: URL): return {success: false} } - // Use the base URL without dimension params as the player URI + // Use the base URL without dimension params as the player URI, + // routed through the bsky KLIPY proxy (k.gifs.bsky.app). Mirrors + // Tenor's t.gifs.bsky.app rewrite, but on a separate hostname so + // the two upstreams can be routed independently. const playerUrl = new URL(urlp.href) + playerUrl.hostname = 'k.gifs.bsky.app' playerUrl.searchParams.delete('hh') playerUrl.searchParams.delete('ww') diff --git a/src/state/queries/klipy.ts b/src/state/queries/klipy.ts index a4a5f8e753..588cc593c4 100644 --- a/src/state/queries/klipy.ts +++ b/src/state/queries/klipy.ts @@ -140,7 +140,7 @@ function createKlipyApi( }, }) if (!res.ok) { - throw new Error('Failed to fetch KLIPY API') + throw new Error(`Failed to fetch KLIPY API (status ${res.status})`) } const body: {next: string; results: Gif[]} = await res.json() return { @@ -151,18 +151,21 @@ function createKlipyApi( } /** - * Returns the static URL for a KLIPY GIF preview image. - * KLIPY images are served directly from their CDN (static.klipy.com), - * unlike Tenor which routes through t.gifs.bsky.app. + * Rewrites a KLIPY static CDN URL through the bsky proxy + * (k.gifs.bsky.app). Mirrors `tenorUrlToBskyGifUrl`, but uses a + * separate hostname from Tenor's t.gifs.bsky.app so the two + * upstreams can be routed independently. */ -export function klipyStaticUrl(gifUrl: string) { +export function klipyUrlToBskyGifUrl(klipyUrl: string) { + let url try { - new URL(gifUrl) - return gifUrl + url = new URL(klipyUrl) } catch (e) { - logger.debug('invalid url passed to klipyStaticUrl()') + logger.debug('invalid url passed to klipyUrlToBskyGifUrl()') return '' } + url.hostname = 'k.gifs.bsky.app' + return url.href } type KlipyAutocompleteResponse = { diff --git a/src/state/queries/tenor.ts b/src/state/queries/tenor.ts index 3adc9139c8..97ce426e72 100644 --- a/src/state/queries/tenor.ts +++ b/src/state/queries/tenor.ts @@ -105,14 +105,19 @@ export function tenorUrlToBskyGifUrl(tenorUrl: string) { /** * Returns the appropriate URL for a GIF preview image. - * Rewrites Tenor URLs through the bsky proxy (t.gifs.bsky.app); - * KLIPY URLs pass through directly to their CDN. + * Tenor URLs (media.tenor.com) are routed through t.gifs.bsky.app; + * KLIPY URLs (static.klipy.com) are routed through k.gifs.bsky.app. */ export function gifPreviewUrl(gifUrl: string) { try { const url = new URL(gifUrl) if (url.hostname === 'media.tenor.com') { - return tenorUrlToBskyGifUrl(gifUrl) + url.hostname = 't.gifs.bsky.app' + return url.href + } + if (url.hostname === 'static.klipy.com') { + url.hostname = 'k.gifs.bsky.app' + return url.href } return gifUrl } catch (e) {