diff --git a/assets/openapi.json b/assets/openapi.json index 8941e40c1..2622eb82c 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 0c883c932..b195be355 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ 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,