summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-07-06 17:12:30 +0200
committerRory& <root@rory.gay>2026-07-08 01:20:38 +0200
commit091b0e3536eeb33130573a1bcf7956a901dafd0e (patch)
tree3cd0ef432bb3862ab403ab217b2bd277e9a85757
parentClean up (diff)
downloadserver-ts-091b0e3536eeb33130573a1bcf7956a901dafd0e.tar.xz
Migrate trending gifs to providers
-rw-r--r--assets/openapi.json61
-rw-r--r--assets/schemas.json55
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs16
-rw-r--r--extra/admin-api/Tests/Spacebar.Tests/Tests/GifTests.cs31
-rw-r--r--src/api/routes/gifs/trending.ts39
-rw-r--r--src/schemas/responses/Tenor.ts7
-rw-r--r--src/util/util/integrations/gifProviders/IGifProvider.ts4
-rw-r--r--src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts26
8 files changed, 141 insertions, 98 deletions
diff --git a/assets/openapi.json b/assets/openapi.json

index 8941e40c..2622eb82 100644 --- a/assets/openapi.json +++ b/assets/openapi.json
@@ -2649,40 +2649,10 @@ "type": "object", "properties": { "categories": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "searchterm": { - "type": "string" - }, - "path": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "image", - "name", - "path", - "searchterm" - ] - } - } - }, - "additionalProperties": false, - "required": [ - "tags" - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/GifTrendingCategory" + } }, "gifs": { "type": "array", @@ -13730,6 +13700,21 @@ } ] }, + "GifTrendingCategory": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "src": { + "type": "string" + } + }, + "required": [ + "name", + "src" + ] + }, "BackupCode": { "type": "object", "properties": { @@ -23580,6 +23565,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 0c883c93..b195be35 100644 --- a/assets/schemas.json +++ b/assets/schemas.json
@@ -2824,40 +2824,10 @@ "type": "object", "properties": { "categories": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "searchterm": { - "type": "string" - }, - "path": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "image", - "name", - "path", - "searchterm" - ] - } - } - }, - "additionalProperties": false, - "required": [ - "tags" - ] + "type": "array", + "items": { + "$ref": "#/definitions/GifTrendingCategory" + } }, "gifs": { "type": "array", @@ -14667,6 +14637,23 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "GifTrendingCategory": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "src": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "name", + "src" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "BackupCode": { "type": "object", "properties": { diff --git a/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs b/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs
index 4cf3b51e..dca2f08d 100644 --- a/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs +++ b/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs
@@ -26,4 +26,20 @@ public class GifItem { [JsonPropertyName("height")] public int Height { get; set; } +} + +public class TrendingGifsResult { + [JsonPropertyName("categories")] + public List<CategoryEntry> Categories { get; set; } + + [JsonPropertyName("gifs")] + public List<GifItem> Gifs { get; set; } + + public class CategoryEntry { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("src")] + public string Source { get; set; } + } } \ No newline at end of file diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/GifTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/GifTests.cs
index 8c4eff33..e122a030 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Tests/GifTests.cs +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/GifTests.cs
@@ -28,10 +28,13 @@ public class GifTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : Client = await _userAbstraction.GetSharedUser(); } + // wish this could just be a single string.... MEmberData requires returning an array spanning the argument count... + public static IEnumerable<string[]> GifProviders() => [["tenor"]]; + public static IEnumerable<object[]> GifSearchTestMatrix() { foreach (var query in (string[])["meow", "meowmeow"]) - foreach (var provider in (string[])["tenor"]) - yield return [query, provider]; + foreach (var provider in GifProviders()) + yield return [query, provider[0]]; } [Theory, MemberData(nameof(GifSearchTestMatrix))] @@ -50,4 +53,28 @@ public class GifTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : Assert.NotEqual(0, gif.Height); }); } + + [Theory, MemberData(nameof(GifProviders))] + public async Task GetTrendingCategories(string provider) { + var resp = await Assert.HttpSuccess(await Client.ApiHttpClient.GetAsync($"gifs/trending?provider={provider}", TestContext.Current.CancellationToken)); + var respContent = await resp.Content.ReadFromJsonAsync<TrendingGifsResult>(cancellationToken: TestContext.Current.CancellationToken); + + Assert.True(respContent!.Categories.Count > 0, "respContent.Categories.Count > 0"); + Assert.True(respContent.Gifs.Count > 0, "respContent.Gifs.Count > 0"); + + Assert.All(respContent.Categories, cat => { + Assert.StringNotNullOrWhitespace(cat.Name); + Assert.StringNotNullOrWhitespace(cat.Source); + }); + + Assert.All(respContent.Gifs, gif => { + Assert.StringNotNullOrWhitespace(gif.Id); + Assert.StringNotNullOrWhitespace(gif.GifSource); + Assert.StringNotNullOrWhitespace(gif.Preview); + Assert.StringNotNullOrWhitespace(gif.Source); + Assert.StringNotNullOrWhitespace(gif.Url); + Assert.NotEqual(0, gif.Width); + Assert.NotEqual(0, gif.Height); + }); + } } \ No newline at end of file diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts
index 1393d6b5..9839d327 100644 --- a/src/api/routes/gifs/trending.ts +++ b/src/api/routes/gifs/trending.ts
@@ -19,7 +19,9 @@ import { route } from "@spacebar/api/util/handlers/route"; import { getGifApiKey, parseGifResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { TenorCategoriesResults, TenorTrendingResults } from "@spacebar/schemas"; +import { TenorCategoriesResults, TenorTrendingResponse, TenorTrendingResults } from "@spacebar/schemas"; +import { GifProviderManager } from "@spacebar/util/util/integrations/gifProviders/GifProviderManager"; +import trendingGifs from "@spacebar/api/routes/gifs/trending-gifs"; const router = Router({ mergeParams: true }); @@ -31,6 +33,10 @@ router.get( type: "string", description: "Locale", }, + provider: { + type: "string", + description: "Provider to use", + }, }, responses: { 200: { @@ -39,34 +45,17 @@ router.get( }, }), async (req: Request, res: Response) => { - // TODO: Custom providers - // TODO: return gifs as mp4 - // const { media_format, locale } = req.query; - const { locale } = req.query; + const provider = GifProviderManager.getProvider((req.query.provider as string) ?? "tenor"); - const apiKey = getGifApiKey(); - - const [responseSource, trendGifSource] = await Promise.all([ - fetch(`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, { - method: "get", - headers: { "Content-Type": "application/json" }, - }), - fetch(`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, { - method: "get", - headers: { "Content-Type": "application/json" }, - }), + const [trendingCategories, trendingGifs] = await Promise.all([ + provider.getTrendingCategories(req.query as typeof provider.getTrendingCategories.arguments), + provider.getTrendingGifs(req.query as typeof provider.getTrendingCategories.arguments), ]); - const { tags } = (await responseSource.json()) as TenorCategoriesResults; - const { results } = (await trendGifSource.json()) as TenorTrendingResults; - res.json({ - categories: tags.map((x) => ({ - name: x.searchterm, - src: x.image, - })), - gifs: [parseGifResult(results[0])], - }).status(200); + categories: trendingCategories, + gifs: trendingGifs, + } satisfies TenorTrendingResponse).status(200); }, ); diff --git a/src/schemas/responses/Tenor.ts b/src/schemas/responses/Tenor.ts
index 61d7c2bc..ac44899c 100644 --- a/src/schemas/responses/Tenor.ts +++ b/src/schemas/responses/Tenor.ts
@@ -82,8 +82,13 @@ export interface GifResponse { preview: string; } +export interface GifTrendingCategory { + name: string; + src: string; +} + export interface TenorTrendingResponse { - categories: TenorCategoriesResults; + categories: GifTrendingCategory[]; gifs: GifResponse[]; } diff --git a/src/util/util/integrations/gifProviders/IGifProvider.ts b/src/util/util/integrations/gifProviders/IGifProvider.ts
index e9726c72..024f33f5 100644 --- a/src/util/util/integrations/gifProviders/IGifProvider.ts +++ b/src/util/util/integrations/gifProviders/IGifProvider.ts
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { GifsResponse } from "@spacebar/schemas"; +import { GifsResponse, GifTrendingCategory } from "@spacebar/schemas"; export interface IGifProvider { id: string; @@ -24,4 +24,6 @@ export interface IGifProvider { init(): Promise<void>; search(query: { q: string; limit?: number; media_format: string; locale: string }): Promise<GifsResponse>; + getTrendingCategories(query: { media_format: string; locale: string }): Promise<GifTrendingCategory[]>; + getTrendingGifs(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
index 2863843b..e771feec 100644 --- a/src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts +++ b/src/util/util/integrations/gifProviders/providers/TenorGifProvider.ts
@@ -17,7 +17,7 @@ */ import type { IGifProvider } from "../IGifProvider"; -import { TenorGif, GifsResponse } from "@spacebar/schemas"; +import { TenorGif, GifsResponse, TenorCategoriesResults, GifTrendingCategory } from "@spacebar/schemas"; import { Config, getGifApiKey, parseGifResult } from "@spacebar/util"; export default class TenorGifProvider implements IGifProvider { @@ -45,6 +45,30 @@ export default class TenorGifProvider implements IGifProvider { return results.map(this.convertGifResult); } + async getTrendingCategories(query: { media_format: string; locale: string }): Promise<GifTrendingCategory[]> { + const response = await fetch(`https://g.tenor.com/v1/categories?locale=${query.locale}&key=${this.#apiKey}`, { + method: "get", + headers: { "Content-Type": "application/json" }, + }); + + const { tags } = (await response.json()) as TenorCategoriesResults; + + return tags.map((x) => ({ + name: x.searchterm, + src: x.image, + })) satisfies GifTrendingCategory[]; + } + + async getTrendingGifs(query: { media_format: string; locale: string }): Promise<GifsResponse> { + const response = await fetch(`https://g.tenor.com/v1/trending?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(this.convertGifResult); + } + private convertGifResult(result: TenorGif) { return { id: result.id,