Анимации, профиль, медиа

This commit is contained in:
Халимов Рустам
2026-04-02 02:37:03 +03:00
parent 4ae7dd60ce
commit 9df7d7aaf1
24 changed files with 1365 additions and 1527 deletions
@@ -95,7 +95,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
if (filterType == "media")
{
return mediaType == "image" || mediaType == "video";
return (mediaType == "image" || mediaType == "video") && !isGif;
}
return true;
@@ -0,0 +1,71 @@
using Knot.Shared.Kernel;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Shared.Kernel.Constants;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace Knot.Modules.Klipy.Application.Klipy.Commands;
public record MarkGifSharedCommand(string GifId, string Query = "") : ICommand;
internal sealed class MarkGifSharedCommandHandler : ICommandHandler<MarkGifSharedCommand>
{
private readonly IKlipySettings _settings;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IUserContext _userContext;
public MarkGifSharedCommandHandler(IKlipySettings settings, IHttpClientFactory httpClientFactory, IUserContext userContext)
{
_settings = settings;
_httpClientFactory = httpClientFactory;
_userContext = userContext;
}
public async Task<Result> Handle(MarkGifSharedCommand request, CancellationToken cancellationToken)
{
var conf = _settings.Current;
if (!conf.Enabled || string.IsNullOrEmpty(conf.ApiKey))
return Result.Failure(new Error(Errors.KlipyNotConfigured, "Klipy is not configured"));
var customerId = _userContext.IsAuthenticated ? _userContext.UserId.ToString().ToLowerInvariant() : Knot.Shared.Kernel.Constants.Klipy.DefaultCustomerId;
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Correct URL according to docs: POST https://api.klipy.com/api/v1/{app_key}/gifs/share/{gif_id}
var baseUrl = "https://api.klipy.com/api/v1";
var url = $"{baseUrl}/{conf.ApiKey}/gifs/share/{request.GifId}";
// According to docs, q and customer_id must be in the JSON body
var body = new
{
customer_id = customerId,
q = request.Query ?? "" // Must be empty string if not from search
};
using var content = JsonContent.Create(body);
var response = await client.PostAsync(url, content, cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var urlCo = url.Replace("api.klipy.com", "api.klipy.co");
using var contentCo = JsonContent.Create(body);
response = await client.PostAsync(urlCo, contentCo, cancellationToken);
}
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
return Result.Failure(new Error(Errors.KlipyApiError, $"Klipy Share API Error: {response.StatusCode} {errorBody}"));
}
}
return Result.Success();
}
}
@@ -0,0 +1,72 @@
using Knot.Shared.Kernel;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Shared.Kernel.Constants;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record GetGifCategoriesQuery(string CountryCode = "RU") : IQuery<JsonElement?>;
internal sealed class GetGifCategoriesQueryHandler : IQueryHandler<GetGifCategoriesQuery, JsonElement?>
{
private readonly IKlipySettings _settings;
private readonly IMemoryCache _cache;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IUserContext _userContext;
public GetGifCategoriesQueryHandler(IKlipySettings settings, IMemoryCache cache, IHttpClientFactory httpClientFactory, IUserContext userContext)
{
_settings = settings;
_cache = cache;
_httpClientFactory = httpClientFactory;
_userContext = userContext;
}
public async Task<Result<JsonElement?>> Handle(GetGifCategoriesQuery request, CancellationToken cancellationToken)
{
var conf = _settings.Current;
if (!conf.Enabled || string.IsNullOrEmpty(conf.ApiKey))
return Result.Failure<JsonElement?>(new Error(Errors.KlipyNotConfigured, "Klipy is not configured"));
var locale = string.IsNullOrEmpty(request.CountryCode) ? "ru_RU" : (request.CountryCode.Length == 2 ? $"{request.CountryCode.ToLowerInvariant()}_{request.CountryCode.ToUpperInvariant()}" : request.CountryCode);
var customerId = _userContext.IsAuthenticated ? _userContext.UserId.ToString().ToLowerInvariant() : Knot.Shared.Kernel.Constants.Klipy.DefaultCustomerId;
var cacheKeyCat = $"klipy_categories_{conf.ApiKey}_{locale}";
if (_cache.TryGetValue(cacheKeyCat, out JsonElement cachedResult))
return Result.Success<JsonElement?>(cachedResult);
var client = _httpClientFactory.CreateClient();
// Correct URL according to docs: GET https://api.klipy.com/api/v1/{app_key}/gifs/categories?locale={locale}&customer_id={customer_id}
var baseUrl = "https://api.klipy.com/api/v1";
var url = $"{baseUrl}/{conf.ApiKey}/gifs/categories?locale={locale}&customer_id={customerId}";
var response = await client.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var urlCo = url.Replace("api.klipy.com", "api.klipy.co");
response = await client.GetAsync(urlCo, cancellationToken);
}
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
return Result.Failure<JsonElement?>(new Error(Errors.KlipyApiError, $"Klipy Categories API Error: {response.StatusCode} {errorBody}"));
}
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: cancellationToken);
_cache.Set(cacheKeyCat, result, TimeSpan.FromMinutes(Knot.Shared.Kernel.Constants.Klipy.CategoriesCacheMinutes));
return Result.Success<JsonElement?>(result);
}
}
@@ -0,0 +1,70 @@
using Knot.Shared.Kernel;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Shared.Kernel.Constants;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record GetRecentGifsQuery(int Page = 1) : IQuery<JsonElement?>;
internal sealed class GetRecentGifsQueryHandler : IQueryHandler<GetRecentGifsQuery, JsonElement?>
{
private readonly IKlipySettings _settings;
private readonly IMemoryCache _cache;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IUserContext _userContext;
public GetRecentGifsQueryHandler(IKlipySettings settings, IMemoryCache cache, IHttpClientFactory httpClientFactory, IUserContext userContext)
{
_settings = settings;
_cache = cache;
_httpClientFactory = httpClientFactory;
_userContext = userContext;
}
public async Task<Result<JsonElement?>> Handle(GetRecentGifsQuery request, CancellationToken cancellationToken)
{
var conf = _settings.Current;
if (!conf.Enabled || string.IsNullOrEmpty(conf.ApiKey))
return Result.Failure<JsonElement?>(new Error(Errors.KlipyNotConfigured, "Klipy is not configured"));
if (!_userContext.IsAuthenticated)
return Result.Failure<JsonElement?>(new Error("AuthError", "User not authenticated"));
var customerId = _userContext.UserId.ToString().ToLowerInvariant();
// Disable cache for history to ensure it updates immediately
var client = _httpClientFactory.CreateClient();
// Correct URL according to docs: GET https://api.klipy.com/api/v1/{app_key}/gifs/recent/{customer_id}?page={page}&per_page={per_page}
var baseUrl = "https://api.klipy.com/api/v1";
var url = $"{baseUrl}/{conf.ApiKey}/gifs/recent/{customerId}?page={request.Page}&per_page=24";
var response = await client.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var urlCo = url.Replace("api.klipy.com", "api.klipy.co");
response = await client.GetAsync(urlCo, cancellationToken);
}
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
return Result.Failure<JsonElement?>(new Error(Errors.KlipyApiError, $"Klipy Recent API Error: {response.StatusCode} {errorBody}"));
}
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: cancellationToken);
return Result.Success<JsonElement?>(result);
}
}
@@ -13,7 +13,7 @@ using MediatR;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record GetTrendingGifsQuery : IQuery<JsonElement?>;
public record GetTrendingGifsQuery(int Page = 1) : IQuery<JsonElement?>;
internal sealed class GetTrendingGifsQueryHandler : IQueryHandler<GetTrendingGifsQuery, JsonElement?>
{
@@ -34,21 +34,21 @@ internal sealed class GetTrendingGifsQueryHandler : IQueryHandler<GetTrendingGif
if (!conf.Enabled || string.IsNullOrEmpty(conf.ApiKey))
return Result.Failure<JsonElement?>(new Error(Errors.KlipyNotConfigured, "Klipy is not configured"));
var cacheKeyTrending = $"klipy_trending_{conf.ApiKey}";
var cacheKeyTrending = $"klipy_trending_{conf.ApiKey}_{request.Page}";
if (_cache.TryGetValue(cacheKeyTrending, out JsonElement cachedResult))
return Result.Success<JsonElement?>(cachedResult);
var customerId = Knot.Shared.Kernel.Constants.Klipy.DefaultCustomerId;
var client = _httpClientFactory.CreateClient();
var urlCo = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCo, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceTrending, "", customerId);
var urlCo = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCo, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceTrending, request.Page, 30, "", customerId);
var response = await client.GetAsync(urlCo, cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var urlCom = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCom, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceTrending, "", customerId);
var urlCom = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCom, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceTrending, request.Page, 30, "", customerId);
response = await client.GetAsync(urlCom, cancellationToken);
}
@@ -13,7 +13,7 @@ using MediatR;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record SearchGifsQuery(string Query) : IQuery<JsonElement?>;
public record SearchGifsQuery(string Query, int Page = 1) : IQuery<JsonElement?>;
internal sealed class SearchGifsQueryHandler : IQueryHandler<SearchGifsQuery, JsonElement?>
{
@@ -37,7 +37,7 @@ internal sealed class SearchGifsQueryHandler : IQueryHandler<SearchGifsQuery, Js
if (string.IsNullOrWhiteSpace(request.Query))
return Result.Failure<JsonElement?>(new Error(Errors.InvalidQuery, "Invalid query parameter"));
var cacheKey = $"klipy_search_{conf.ApiKey}_{request.Query.ToLowerInvariant()}";
var cacheKey = $"klipy_search_{conf.ApiKey}_{request.Query.ToLowerInvariant()}_{request.Page}";
if (_cache.TryGetValue(cacheKey, out JsonElement cachedResult))
return Result.Success<JsonElement?>(cachedResult);
@@ -45,14 +45,14 @@ internal sealed class SearchGifsQueryHandler : IQueryHandler<SearchGifsQuery, Js
var client = _httpClientFactory.CreateClient();
var queryParam = $"q={Uri.EscapeDataString(request.Query)}";
var urlCo = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCo, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceSearch, queryParam, customerId);
var urlCo = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCo, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceSearch, request.Page, 30, queryParam, customerId);
var response = await client.GetAsync(urlCo, cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var urlCom = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCom, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceSearch, queryParam, customerId);
var urlCom = string.Format(Knot.Shared.Kernel.Constants.Klipy.ApiUrlCom, conf.ApiKey, Knot.Shared.Kernel.Constants.Klipy.ResourceSearch, request.Page, 30, queryParam, customerId);
response = await client.GetAsync(urlCom, cancellationToken);
}
@@ -18,16 +18,34 @@ public static class KlipyEndpoints
{
var group = app.MapGroup(Routes.ApiKlipy).RequireAuthorization();
group.MapGet("/trending", async (ISender sender, CancellationToken ct) =>
group.MapGet("/trending", async ([FromQuery] int page, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetTrendingGifsQuery(), ct);
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetTrendingGifsQuery(page > 0 ? page : 1), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
group.MapGet("/search", async ([FromQuery] string q, ISender sender, CancellationToken ct) =>
group.MapGet("/search", async ([FromQuery] string q, [FromQuery] int page, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.SearchGifsQuery(q), ct);
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.SearchGifsQuery(q, page > 0 ? page : 1), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
group.MapGet("/recent", async ([FromQuery] int page, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetRecentGifsQuery(page > 0 ? page : 1), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
group.MapGet("/categories", async ([FromQuery] string? countryCode, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetGifCategoriesQuery(countryCode ?? "RU"), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
group.MapPost("/{gifId}/share", async (string gifId, [FromQuery] string? q, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Commands.MarkGifSharedCommand(gifId, q ?? ""), ct);
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
});
}
}
@@ -2,14 +2,27 @@ namespace Knot.Shared.Kernel.Constants;
public static class Klipy
{
public const string ApiUrlCo = "https://api.klipy.co/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}";
public const string ApiUrlCom = "https://api.klipy.com/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}";
public const string ResourceTrending = "gifs/trending";
public const string ResourceSearch = "gifs/search";
public const string ResourceRecent = "gifs/recent";
public const string ResourceCategories = "gifs/categories";
public const string ResourceShare = "gifs/{0}/share";
public const string ApiUrlCo = "https://api.klipy.co/api/v1/{0}/{1}?page={2}&per_page={3}&{4}&customer_id={5}";
public const string ApiUrlCom = "https://api.klipy.com/api/v1/{0}/{1}?page={2}&per_page={3}&{4}&customer_id={5}";
// Categories use country_code
public const string ApiUrlCategoriesCo = "https://api.klipy.co/api/v1/{0}/{1}?country_code={2}&customer_id={3}";
public const string ApiUrlCategoriesCom = "https://api.klipy.com/api/v1/{0}/{1}?country_code={2}&customer_id={3}";
// Share is POST
public const string ApiUrlShareCo = "https://api.klipy.co/api/v1/{0}/gifs/{1}/share?q={2}&customer_id={3}";
public const string ApiUrlShareCom = "https://api.klipy.com/api/v1/{0}/gifs/{1}/share?q={2}&customer_id={3}";
public const int TrendingCacheMinutes = 60;
public const int SearchCacheMinutes = 15;
public const int CategoriesCacheMinutes = 1440; // 24 hours
public const int RecentCacheMinutes = 5; // Frequent changes
public const string DefaultCustomerId = "anonymous";
}