import { Router } from 'express'; import { getKlipyApiKey, hasKlipyApiKey } from '../config/variables'; const router = Router(); const KLIPY_API_BASE_URL = 'https://api.klipy.com/api/v1'; const REQUEST_TIMEOUT_MS = 8000; const DEFAULT_PAGE = 1; const DEFAULT_PER_PAGE = 24; const MAX_PER_PAGE = 50; interface NormalizedMediaMeta { url: string; width?: number; height?: number; } interface NormalizedKlipyGif { id: string; slug: string; title?: string; url: string; previewUrl: string; width: number; height: number; } interface KlipyGifVariants { md?: unknown; sm?: unknown; xs?: unknown; hd?: unknown; } interface KlipyGifItem { type?: unknown; slug?: unknown; id?: unknown; title?: unknown; file?: KlipyGifVariants; } interface KlipyApiResponse { data?: { data?: unknown; has_next?: unknown; }; } interface ResolvedGifMedia { previewMeta: NormalizedMediaMeta | null; sourceMeta: NormalizedMediaMeta; } function pickFirst(...values: (T | null | undefined)[]): T | undefined { for (const value of values) { if (value != null) return value; } return undefined; } function sanitizeString(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); return trimmed || undefined; } function toPositiveNumber(value: unknown): number | undefined { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; } function clampPositiveInt(value: unknown, fallback: number, max = Number.MAX_SAFE_INTEGER): number { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 1) return fallback; return Math.min(Math.floor(parsed), max); } function normalizeMediaMeta(candidate: unknown): NormalizedMediaMeta | null { if (!candidate) return null; if (typeof candidate === 'string') { return { url: candidate }; } if (typeof candidate === 'object' && candidate !== null) { const url = sanitizeString((candidate as { url?: unknown }).url); if (!url) return null; return { url, width: toPositiveNumber((candidate as { width?: unknown }).width), height: toPositiveNumber((candidate as { height?: unknown }).height) }; } return null; } function pickGifMeta(sizeVariant: unknown): NormalizedMediaMeta | null { const candidate = sizeVariant as { gif?: unknown; webp?: unknown; } | undefined; return normalizeMediaMeta(candidate?.gif) ?? normalizeMediaMeta(candidate?.webp); } function extractKlipyResponseData(payload: unknown): { items: unknown[]; hasNext: boolean } { if (typeof payload !== 'object' || payload === null) { return { items: [], hasNext: false }; } const response = payload as KlipyApiResponse; const items = Array.isArray(response.data?.data) ? response.data.data : []; return { items, hasNext: response.data?.has_next === true }; } function resolveGifMedia(file?: KlipyGifVariants): ResolvedGifMedia | null { const previewVariant = pickFirst(file?.md, file?.sm, file?.xs, file?.hd); const sourceVariant = pickFirst(file?.hd, file?.md, file?.sm, file?.xs); const previewMeta = pickGifMeta(previewVariant); const sourceMeta = pickGifMeta(sourceVariant) ?? previewMeta; if (!sourceMeta?.url) return null; return { previewMeta, sourceMeta }; } function resolveGifSlug(gifItem: KlipyGifItem): string | undefined { return sanitizeString(gifItem.slug) ?? sanitizeString(gifItem.id); } function normalizeGifItem(item: unknown): NormalizedKlipyGif | null { if (!item || typeof item !== 'object') return null; const gifItem = item as KlipyGifItem; const resolvedMedia = resolveGifMedia(gifItem.file); const slug = resolveGifSlug(gifItem); if (gifItem.type === 'ad') return null; if (!slug || !resolvedMedia) return null; const { previewMeta, sourceMeta } = resolvedMedia; return { id: slug, slug, title: sanitizeString(gifItem.title), url: sourceMeta.url, previewUrl: previewMeta?.url ?? sourceMeta.url, width: sourceMeta.width ?? previewMeta?.width ?? 0, height: sourceMeta.height ?? previewMeta?.height ?? 0 }; } function extractErrorMessage(payload: unknown): string | null { if (!payload) return null; if (typeof payload === 'string') return payload.slice(0, 240); if (typeof payload === 'object' && payload !== null) { const data = payload as { error?: unknown; message?: unknown }; if (typeof data.error === 'string') return data.error; if (typeof data.message === 'string') return data.message; } return null; } router.get('/klipy/config', (_req, res) => { res.json({ enabled: hasKlipyApiKey() }); }); router.get('/klipy/gifs', async (req, res) => { if (!hasKlipyApiKey()) { return res.status(503).json({ error: 'KLIPY is not configured on this server.' }); } try { const query = sanitizeString(req.query.q) ?? ''; const page = clampPositiveInt(req.query.page, DEFAULT_PAGE); const perPage = clampPositiveInt(req.query.per_page, DEFAULT_PER_PAGE, MAX_PER_PAGE); const customerId = sanitizeString(req.query.customer_id); const locale = sanitizeString(req.query.locale); const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }); if (query) params.set('q', query); if (customerId) params.set('customer_id', customerId); if (locale) params.set('locale', locale); const endpoint = query ? 'search' : 'trending'; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); const response = await fetch( `${KLIPY_API_BASE_URL}/${encodeURIComponent(getKlipyApiKey())}/gifs/${endpoint}?${params.toString()}`, { headers: { accept: 'application/json' }, signal: controller.signal } ); clearTimeout(timeout); const text = await response.text(); let payload: unknown = null; if (text) { try { payload = JSON.parse(text) as unknown; } catch { payload = text; } } if (!response.ok) { return res.status(response.status).json({ error: extractErrorMessage(payload) || 'Failed to fetch GIFs from KLIPY.' }); } const { items: rawItems, hasNext } = extractKlipyResponseData(payload); const results = rawItems .map((item: unknown) => normalizeGifItem(item)) .filter((item: NormalizedKlipyGif | null): item is NormalizedKlipyGif => !!item); res.json({ enabled: true, results, hasNext }); } catch (error) { if ((error as { name?: string })?.name === 'AbortError') { return res.status(504).json({ error: 'KLIPY request timed out.' }); } console.error('KLIPY GIF route error:', error); res.status(502).json({ error: 'Failed to fetch GIFs from KLIPY.' }); } }); export default router;