summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-07-06 00:32:23 +0200
committerRory& <root@rory.gay>2026-07-08 01:20:38 +0200
commitee57b28c324e7851168ae104a94d4bfd9252dae0 (patch)
tree46a24596ae66e01b5b2cd6f9387f6499281c613f
parentRemove a log that bothered me (diff)
downloadserver-ts-ee57b28c324e7851168ae104a94d4bfd9252dae0.tar.xz
Basic gif integration manager, port tenor search to that
-rw-r--r--assets/openapi.json18
-rw-r--r--assets/schemas.json8
-rw-r--r--src/api/Server.ts2
-rw-r--r--src/api/routes/gifs/search.ts29
-rw-r--r--src/api/routes/gifs/trending-gifs.ts4
-rw-r--r--src/schemas/responses/Tenor.ts10
-rw-r--r--src/util/config/types/GifConfiguration.ts3
-rw-r--r--src/util/util/Gifs.ts1
-rw-r--r--src/util/util/integrations/gifProviders/GifProviderManager.ts51
-rw-r--r--src/util/util/integrations/gifProviders/IGifProvider.ts27
-rw-r--r--src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts60
11 files changed, 181 insertions, 32 deletions
diff --git a/assets/openapi.json b/assets/openapi.json

index 2a928a2b..8941e40c 100644 --- a/assets/openapi.json +++ b/assets/openapi.json
@@ -2606,7 +2606,7 @@ "$ref": "#/components/schemas/Team" } }, - "TenorGifResponse": { + "GifResponse": { "type": "object", "properties": { "id": { @@ -2687,7 +2687,7 @@ "gifs": { "type": "array", "items": { - "$ref": "#/components/schemas/TenorGifResponse" + "$ref": "#/components/schemas/GifResponse" } } }, @@ -2696,10 +2696,10 @@ "gifs" ] }, - "TenorGifsResponse": { + "GifsResponse": { "type": "array", "items": { - "$ref": "#/components/schemas/TenorGifResponse" + "$ref": "#/components/schemas/GifResponse" } }, "TokenResponse": { @@ -23642,7 +23642,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TenorGifsResponse" + "$ref": "#/components/schemas/GifsResponse" } } } @@ -23673,6 +23673,14 @@ "type": "string" }, "description": "Locale" + }, + { + "name": "provider", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Provider to use" } ], "tags": [ diff --git a/assets/schemas.json b/assets/schemas.json
index b72d9cc1..0c883c93 100644 --- a/assets/schemas.json +++ b/assets/schemas.json
@@ -2779,7 +2779,7 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "TenorGifResponse": { + "GifResponse": { "type": "object", "properties": { "id": { @@ -2862,7 +2862,7 @@ "gifs": { "type": "array", "items": { - "$ref": "#/definitions/TenorGifResponse" + "$ref": "#/definitions/GifResponse" } } }, @@ -2873,10 +2873,10 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "TenorGifsResponse": { + "GifsResponse": { "type": "array", "items": { - "$ref": "#/definitions/TenorGifResponse" + "$ref": "#/definitions/GifResponse" }, "$schema": "http://json-schema.org/draft-07/schema#" }, diff --git a/src/api/Server.ts b/src/api/Server.ts
index 2bf14350..74718dad 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 13c24659..20f77889 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 apiKey = getGifApiKey(); - - 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 { provider } = req.query; - const { results } = (await response.json()) as { results: TenorGif[] }; + const impl = GifProviderManager.getProvider(provider as string); + const result = await impl.search(req.query as typeof impl.search.arguments); - 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 14d3d4dc..a79dfc54 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 cd11b6ef..61d7c2bc 100644 --- a/src/schemas/responses/Tenor.ts +++ b/src/schemas/responses/Tenor.ts
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -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 04afb3bc..ffb0d095 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 a24ada63..22b85168 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 00000000..8bbfccf5 --- /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 <https://www.gnu.org/licenses/>. +*/ + +import fs from "node:fs/promises"; +import path from "node:path"; +import type { IGifProvider } from "./IGifProvider"; + +export class GifProviderManager { + private static _providers: Map<string, IGifProvider> = new Map<string, IGifProvider>(); + 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 00000000..e9726c72 --- /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 <https://www.gnu.org/licenses/>. +*/ + +import { GifsResponse } from "@spacebar/schemas"; + +export interface IGifProvider { + id: string; + available: boolean; + + init(): Promise<void>; + search(query: { q: string; limit?: number; media_format: string; locale: string }): Promise<GifsResponse>; +} 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 00000000..ca563598 --- /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 <https://www.gnu.org/licenses/>. +*/ + +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<void> { + 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<GifsResponse> { + 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, + }; + } +}