From abc6e16d04445fad79c13178595159a4728adc88 Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 6 Jul 2026 00:32:23 +0200 Subject: [PATCH] Basic gif integration manager, port tenor search to that --- assets/openapi.json | Bin 993483 -> 993751 bytes assets/schemas.json | Bin 446000 -> 445980 bytes src/api/Server.ts | 2 + src/api/routes/gifs/search.ts | 29 ++++----- src/api/routes/gifs/trending-gifs.ts | 4 +- src/schemas/responses/Tenor.ts | 10 +-- src/util/config/types/GifConfiguration.ts | 3 + src/util/util/Gifs.ts | 1 + .../gifProviders/GifProviderManager.ts | 51 +++++++++++++++ .../integrations/gifProviders/IGifProvider.ts | 27 ++++++++ .../providers/TenorGifProvider.ts | 60 ++++++++++++++++++ 11 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 src/util/util/integrations/gifProviders/GifProviderManager.ts create mode 100644 src/util/util/integrations/gifProviders/IGifProvider.ts create mode 100644 src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts diff --git a/assets/openapi.json b/assets/openapi.json index 2a928a2bc248b7bec592b98dd4bd5dc315b14db2..8941e40c1ac653800f99802f3e907a8b5ca26ce9 100644 GIT binary patch delta 194 zcmX?o(dPPPn}#ins@cr$nQ4<5ucu7kP|T(=jgQ%E`}=H09q#E0<%~C{|0`##2eD+Q zFUV$*Yu{VJ2*gZ4%nZaVK+FonY(UHo#2i4(3B+7L%)NbY1y6zq$nfcmkGQL*fB3_h zy`8I%$A@|P1zT=Qwt}Mkvdom}jF(vCrWY71Czvb bgLYn*>3rQhHtkm@@&GaK_Nx>5zD)xF)r3vx delta 188 zcmcb9+2-^`n}#ins@WVNsd@QD?wM(m8Ly{IZ(w1LozCyg7B@X0nk{zwk{re&F1U*6 z2gNy+re82+iJRY%ER9N QViFG!^KO4JiSPY10O)y2?f?J) diff --git a/assets/schemas.json b/assets/schemas.json index b72d9cc13044bfd0efe59ef6b25d992f88834e36..0c883c932004744b9e34e116aad4400772b347e7 100644 GIT binary patch delta 89 zcmdlmM|#d2>4q(gUe??Ftr?GVGP`G{O%_}uHhqFJoAC4pI*eh{?Ri+(r!R43%mv9! j-#D2m7R(Sl&luN!$Bhw)nShuXh*^M`b^9GRwvcuJkbNMQ delta 128 zcmbO;M|#5?>4q(gUe+8Tsd@QD?wM)R7qT%w^}!Zy8Dn8k6rLK73q^z-ekj@vVB z7*}wxg4InIbY=X8(9#gfWI0_ykeP4#gL8~x2qmJ^0|c3T+E2JK0x=U1GXpUT5VLMS J;l>uy4geLtEKmRd diff --git a/src/api/Server.ts b/src/api/Server.ts index 2bf143504..74718dad7 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -29,6 +29,7 @@ import { BcryptWorkerPool } from "../util/util/workers/bcrypt/BcryptWorkerPool"; import { Authentication, CORS, ImageProxy, BodyParser, ErrorHandler, initRateLimits, initTranslation } from "./middlewares"; import { initInstance } from "./util/handlers/Instance"; import { route, addPendingPoll } from "./util"; +import { GifProviderManager } from "@spacebar/util/util/integrations/gifProviders/GifProviderManager"; const ASSETS_FOLDER = path.join(__dirname, "..", "..", "assets"); const PUBLIC_ASSETS_FOLDER = path.join(ASSETS_FOLDER, "public"); @@ -64,6 +65,7 @@ export class SpacebarServer extends Server { await initInstance(); WebAuthn.init(); // await BcryptWorkerPool.Init(8); // TODO: make configurable + await GifProviderManager.init(); const logRequests = process.env["LOG_REQUESTS"] != undefined; if (logRequests) { diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts index 13c246590..20f778890 100644 --- a/src/api/routes/gifs/search.ts +++ b/src/api/routes/gifs/search.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2026 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -17,9 +17,9 @@ */ import { route } from "@spacebar/api/util/handlers/route"; -import { getGifApiKey, parseGifResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { TenorGif, TenorMediaTypes } from "@spacebar/schemas"; +import { GifMediaTypes } from "@spacebar/schemas"; +import { GifProviderManager } from "@spacebar/util/util/integrations/gifProviders/GifProviderManager"; const router = Router({ mergeParams: true }); @@ -35,33 +35,30 @@ router.get( media_format: { type: "string", description: "Media format", - values: Object.keys(TenorMediaTypes).filter((key) => isNaN(Number(key))), + values: Object.keys(GifMediaTypes).filter((key) => isNaN(Number(key))), }, locale: { type: "string", description: "Locale", }, + provider: { + type: "string", + description: "Provider to use", + }, }, responses: { 200: { - body: "TenorGifsResponse", + body: "GifsResponse", }, }, }), async (req: Request, res: Response) => { - // TODO: Custom providers - const { q, media_format, locale } = req.query; + const { provider } = req.query; - const apiKey = getGifApiKey(); + const impl = GifProviderManager.getProvider(provider as string); + const result = await impl.search(req.query as typeof impl.search.arguments); - const response = await fetch(`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, { - method: "get", - headers: { "Content-Type": "application/json" }, - }); - - const { results } = (await response.json()) as { results: TenorGif[] }; - - res.json(results.map(parseGifResult)).status(200); + res.json(result).status(200); }, ); diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts index 14d3d4dc1..a79dfc541 100644 --- a/src/api/routes/gifs/trending-gifs.ts +++ b/src/api/routes/gifs/trending-gifs.ts @@ -19,7 +19,7 @@ import { route } from "@spacebar/api/util/handlers/route"; import { getGifApiKey, parseGifResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { TenorGif, TenorMediaTypes } from "@spacebar/schemas"; +import { TenorGif, GifMediaTypes } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -30,7 +30,7 @@ router.get( media_format: { type: "string", description: "Media format", - values: Object.keys(TenorMediaTypes).filter((key) => isNaN(Number(key))), + values: Object.keys(GifMediaTypes).filter((key) => isNaN(Number(key))), }, locale: { type: "string", diff --git a/src/schemas/responses/Tenor.ts b/src/schemas/responses/Tenor.ts index cd11b6ef6..61d7c2bc9 100644 --- a/src/schemas/responses/Tenor.ts +++ b/src/schemas/responses/Tenor.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -export enum TenorMediaTypes { +export enum GifMediaTypes { gif, mediumgif, tinygif, @@ -41,7 +41,7 @@ export type TenorGif = { created: number; hasaudio: boolean; id: string; - media: { [type in keyof typeof TenorMediaTypes]: TenorMedia }[]; + media: { [type in keyof typeof GifMediaTypes]: TenorMedia }[]; tags: string[]; title: string; itemurl: string; @@ -71,7 +71,7 @@ export type TenorSearchResults = { results: TenorGif[]; }; -export interface TenorGifResponse { +export interface GifResponse { id: string; title: string; url: string; @@ -84,7 +84,7 @@ export interface TenorGifResponse { export interface TenorTrendingResponse { categories: TenorCategoriesResults; - gifs: TenorGifResponse[]; + gifs: GifResponse[]; } -export type TenorGifsResponse = TenorGifResponse[]; +export type GifsResponse = GifResponse[]; diff --git a/src/util/config/types/GifConfiguration.ts b/src/util/config/types/GifConfiguration.ts index 04afb3bce..ffb0d095d 100644 --- a/src/util/config/types/GifConfiguration.ts +++ b/src/util/config/types/GifConfiguration.ts @@ -17,7 +17,10 @@ */ export class GifConfiguration { + /// @deprecated enabled: boolean = true; + // @deprecated provider = "tenor" as const; // more coming soon + // @deprecated apiKey?: string = "LIVDSRZULELA"; } diff --git a/src/util/util/Gifs.ts b/src/util/util/Gifs.ts index a24ada632..22b851686 100644 --- a/src/util/util/Gifs.ts +++ b/src/util/util/Gifs.ts @@ -2,6 +2,7 @@ import { HTTPError } from "lambert-server/HTTPError"; import { Config } from "./Config"; import { TenorGif } from "@spacebar/schemas"; +// @deprecated export function parseGifResult(result: TenorGif) { return { id: result.id, diff --git a/src/util/util/integrations/gifProviders/GifProviderManager.ts b/src/util/util/integrations/gifProviders/GifProviderManager.ts new file mode 100644 index 000000000..8bbfccf52 --- /dev/null +++ b/src/util/util/integrations/gifProviders/GifProviderManager.ts @@ -0,0 +1,51 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import fs from "node:fs/promises"; +import path from "node:path"; +import type { IGifProvider } from "./IGifProvider"; + +export class GifProviderManager { + private static _providers: Map = new Map(); + public static async init() { + console.log("[GifProviderManager] Initialising providers..."); + const providerImports = await Promise.all( + (await fs.readdir(path.join(__dirname, "providers"))) /**/ + .filter((p) => p.endsWith(".js")) + .map((f) => import(path.join(__dirname, "providers", f))), + ); + + console.log("Import tasks:", providerImports); + for (const providerImport of providerImports) { + const provider = new providerImport.default.default() as IGifProvider; + console.log(`[GifProviderManager] Got provider with id ${provider.id}, calling init...`); + await provider.init(); + console.log(`[GifProviderManager] Initialized '${provider.id}':`, provider, " - Available:", provider.available); + if (provider.available) this._providers.set(provider.id, provider); + console.log(`[GifProviderManager] Initialized`, this._providers.size, "/", providerImports.length, "GIF providers..."); + } + + console.log("[GifProviderManager] Ready with", this._providers.size, "available providers!"); + } + + public static getProvider(id: string): IGifProvider { + if (this._providers.has(id)) return this._providers.get(id)!; + + throw new Error(`Unknown GIF provider, or it is not enabled: ${id}, known GIF providers: ${this._providers.keys().toArray().join(", ")}`); + } +} diff --git a/src/util/util/integrations/gifProviders/IGifProvider.ts b/src/util/util/integrations/gifProviders/IGifProvider.ts new file mode 100644 index 000000000..e9726c72b --- /dev/null +++ b/src/util/util/integrations/gifProviders/IGifProvider.ts @@ -0,0 +1,27 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { GifsResponse } from "@spacebar/schemas"; + +export interface IGifProvider { + id: string; + available: boolean; + + init(): Promise; + search(query: { q: string; limit?: number; media_format: string; locale: string }): Promise; +} diff --git a/src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts b/src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts new file mode 100644 index 000000000..ca5635989 --- /dev/null +++ b/src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts @@ -0,0 +1,60 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import type { IGifProvider } from "../IGifProvider"; +import { TenorGif, GifsResponse } from "@spacebar/schemas"; +import { Config, getGifApiKey, parseGifResult } from "@spacebar/util"; + +export default class TenorGifProvider implements IGifProvider { + id = "tenor"; + available = true; + #apiKey: string; + + async init(): Promise { + if (!(Config.get().gif.enabled && Config.get().gif.apiKey)) { + this.available = false; + return; + } + + this.#apiKey = Config.get().gif.apiKey!; + console.log("[TenorGifProvider] Hellorld!"); + } + + async search(query: { q: string; limit?: number; media_format: string; locale: string }): Promise { + const response = await fetch(`https://g.tenor.com/v1/search?q=${query.q}&media_format=${query.media_format}&locale=${query.locale}&key=${this.#apiKey}`, { + method: "get", + headers: { "Content-Type": "application/json" }, + }); + + const { results } = (await response.json()) as { results: TenorGif[] }; + return results.map(parseGifResult); + } + + private convertGifResult(result: TenorGif) { + return { + id: result.id, + title: result.title, + url: result.itemurl, + src: result.media[0].mp4.url, + gif_src: result.media[0].gif.url, + width: result.media[0].mp4.dims[0], + height: result.media[0].mp4.dims[1], + preview: result.media[0].mp4.preview, + }; + } +}