From 091b0e3536eeb33130573a1bcf7956a901dafd0e Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 6 Jul 2026 17:12:30 +0200 Subject: [PATCH] Migrate trending gifs to providers --- assets/openapi.json | Bin 993751 -> 993073 bytes assets/schemas.json | Bin 445980 -> 445223 bytes .../Models/Spacebar.Models.Api/GifResponse.cs | 16 +++++++ .../Tests/Spacebar.Tests/Tests/GifTests.cs | 31 +++++++++++++- src/api/routes/gifs/trending.ts | 39 +++++++----------- src/schemas/responses/Tenor.ts | 7 +++- .../integrations/gifProviders/IGifProvider.ts | 4 +- .../providers/TenorGifProvider.ts | 26 +++++++++++- 8 files changed, 93 insertions(+), 30 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index 8941e40c1ac653800f99802f3e907a8b5ca26ce9..2622eb82c637d69ccfdeb3982b5776143f3f7011 100644 GIT binary patch delta 181 zcmcb9*=FN8n}#in!3EPFL@}lcx@V?^6s6{+WagziCzhn9=T9$O&0;)x!YzsRjxt6d zW&&bnAZ7t#*6kf-Y?prtplg~gSi~kVxq!ue@`Ap&=}U4L-6n7F1aVlWUwFqP&RkrS zJiU;U-DvUx503WKLUteq(VRfc1;pIjQww=mMW!pn@mg*#o5+*R*#5GW2Z(vMzij1u Gw-^9V!br3L delta 356 zcmdmZ&gS}Mn}#in!3EPd1hDu`ljLNden)^wYPx_gui*3vT`URH3-p=OxQbH~i;^=+ zQj2n@Gd8kkPY-y@s5HHyiefE29pa8av&t9onSMBsDjG{^m56;6lqL%@MScEXjYiVqC34onS*P3 zy&t18L@Ys_Ng8bLbS7p_1+c|jN^qA>7c5~Dhqz`zERz)2P@w6S(+eIm*8n}rI$b-D zSr8;VdBPLD>HqtASlcBE8G)Dyh?#+y1&CRXfS41Axqz5^`-EH` nev#=0RopJqCh%BKpU}a>GW}024@bLyGY=5+Zuf8Id$bq;`mB4t diff --git a/assets/schemas.json b/assets/schemas.json index 0c883c932004744b9e34e116aad4400772b347e7..b195be3554b2d6d8eefd1dc7d01c2ba9c31841fa 100644 GIT binary patch delta 120 zcmbO;M|$}*>4q(gt@e`}cKZuuriB!x=A~rjr8_54q(gt@e{2th1YLtI8@l`M@{B=?6@i@-jDvYVq`Q9<{P5z QyO;yp8{zFULfQID0Xv>nNdN!< diff --git a/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs b/extra/admin-api/Models/Spacebar.Models.Api/GifResponse.cs index 4cf3b51e4..dca2f08d2 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 Categories { get; set; } + + [JsonPropertyName("gifs")] + public List 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 8c4eff337..e122a0308 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 GifProviders() => [["tenor"]]; + public static IEnumerable 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(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 1393d6b5b..9839d3279 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 61d7c2bc9..ac44899cf 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 e9726c72b..024f33f5f 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 . */ -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; search(query: { q: string; limit?: number; media_format: string; locale: string }): Promise; + getTrendingCategories(query: { media_format: string; locale: string }): Promise; + getTrendingGifs(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 index 2863843be..e771feecb 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 { + 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 { + 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,