Правка архитектуры по бэку
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Host.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ConfigController : ControllerBase
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
|
||||
public ConfigController(ISettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetPublicConfig()
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
return Ok(new
|
||||
{
|
||||
conf.EnableCalls,
|
||||
conf.EnableKlipy,
|
||||
conf.MaxFileSizeMb,
|
||||
conf.MaxGroupMembers,
|
||||
conf.EnableConfederation,
|
||||
conf.EnableRegistration
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Host.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class FederationController : ControllerBase
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public FederationController(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
[HttpPost("handshake")]
|
||||
public IActionResult Handshake([FromBody] HandshakeRequest request)
|
||||
{
|
||||
var conf = _settingsService.Current;
|
||||
if (!conf.EnableConfederation)
|
||||
return StatusCode(503, new { error = "Confederation is disabled on this node." });
|
||||
|
||||
if (string.IsNullOrEmpty(request.Domain) || string.IsNullOrEmpty(request.PublicKey))
|
||||
return BadRequest(new { error = "Domain and PublicKey are required." });
|
||||
|
||||
// Discovery: Checks allowed list
|
||||
var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new List<string>();
|
||||
if (!allowedList.Contains(request.Domain.ToLower()))
|
||||
return Forbid("Domain not in whitelist.");
|
||||
|
||||
// Here we would store the peer's public key in the DB to encrypt future traffic.
|
||||
// For demonstration, we simply generate a response with our own public key.
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||
|
||||
return Ok(new HandshakeResponse
|
||||
{
|
||||
Domain = Environment.GetEnvironmentVariable("DOMAIN") ?? "knot.local",
|
||||
PublicKey = selfPublicKey,
|
||||
Status = "Accepted"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class HandshakeRequest
|
||||
{
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string PublicKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class HandshakeResponse
|
||||
{
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string PublicKey { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Host.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class KlipyController : ControllerBase
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public KlipyController(ISettingsService settings, IMemoryCache cache, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_settings = settings;
|
||||
_cache = cache;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[HttpGet("trending")]
|
||||
public async Task<IActionResult> GetTrending()
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey))
|
||||
return BadRequest(new { error = "Klipy is disabled or not configured." });
|
||||
|
||||
var cacheKeyTrending = $"klipy_trending_{conf.KlipyApiKey}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKeyTrending, out JsonElement cachedResult))
|
||||
{
|
||||
return Ok(cachedResult);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? "anonymous" : conf.KlipyCustomerId;
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var url = $"https://api.klipy.co/api/v1/{conf.KlipyApiKey}/gifs/trending?page=1&per_page=30&customer_id={customerId}";
|
||||
|
||||
var response = await client.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// fallback to api.klipy.com if .co failed with 404 or something, though maybe not needed
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) {
|
||||
url = $"https://api.klipy.com/api/v1/{conf.KlipyApiKey}/gifs/trending?page=1&per_page=30&customer_id={customerId}";
|
||||
response = await client.GetAsync(url);
|
||||
errorContent = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return StatusCode(500, new { error = "Failed to fetch from Klipy", details = errorContent, url });
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
_cache.Set(cacheKeyTrending, result, TimeSpan.FromMinutes(60)); // Cache trending for 60 minutes
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = "Failed to fetch from Klipy", details = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> Search([FromQuery] string q)
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey))
|
||||
return BadRequest(new { error = "Klipy is disabled or not configured." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return BadRequest(new { error = "Query is empty." });
|
||||
|
||||
var cacheKey = $"klipy_search_{conf.KlipyApiKey}_{q.ToLowerInvariant()}";
|
||||
if (_cache.TryGetValue(cacheKey, out JsonElement cachedResult))
|
||||
{
|
||||
return Ok(cachedResult);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? "anonymous" : conf.KlipyCustomerId;
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var url = $"https://api.klipy.co/api/v1/{conf.KlipyApiKey}/gifs/search?page=1&per_page=30&q={Uri.EscapeDataString(q)}&customer_id={customerId}";
|
||||
|
||||
var response = await client.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) {
|
||||
url = $"https://api.klipy.com/api/v1/{conf.KlipyApiKey}/gifs/search?page=1&per_page=30&q={Uri.EscapeDataString(q)}&customer_id={customerId}";
|
||||
response = await client.GetAsync(url);
|
||||
errorContent = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return StatusCode(500, new { error = "Failed to fetch from Klipy", details = errorContent, url });
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(15)); // Cache searches for 15 minutes
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = "Failed to fetch from Klipy", details = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/server-net/src/Host/Endpoints/ConfigEndpoints.cs
Normal file
25
apps/server-net/src/Host/Endpoints/ConfigEndpoints.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Carter;
|
||||
using Host.Services;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация эндпоинтов для конфигурации приложения.
|
||||
/// </summary>
|
||||
public sealed class ConfigEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(Routes.ApiConfig); // Без RequiresAuthorization, т.к. контроллер раньше был AllowAnonymous для GET
|
||||
|
||||
group.MapGet("/", (IConfigService configService) =>
|
||||
{
|
||||
var config = configService.GetPublicConfig();
|
||||
return Results.Ok(config);
|
||||
});
|
||||
}
|
||||
}
|
||||
46
apps/server-net/src/Host/Endpoints/FederationEndpoints.cs
Normal file
46
apps/server-net/src/Host/Endpoints/FederationEndpoints.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Carter;
|
||||
using Host.Services;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using System;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация эндпоинтов федерации.
|
||||
/// </summary>
|
||||
public sealed class FederationEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(Routes.ApiFederation); // Без RequireAuthorization для handshake
|
||||
|
||||
group.MapPost("/handshake", async ([FromBody] HandshakeRequest request, IFederationService federationService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await federationService.HandshakeAsync(request);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.StatusCode(503); // Service Unavailable
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.StatusCode(500);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
60
apps/server-net/src/Host/Endpoints/KlipyEndpoints.cs
Normal file
60
apps/server-net/src/Host/Endpoints/KlipyEndpoints.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Host.Services;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация эндпоинтов для сервиса Klipy.
|
||||
/// </summary>
|
||||
public sealed class KlipyEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(Routes.ApiKlipy).RequireAuthorization();
|
||||
|
||||
group.MapGet("/trending", async (IKlipyService klipyService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await klipyService.GetTrendingAsync();
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.StatusCode(500); // Можно возвращать JSON с сообщением, но для продакшена лучше централизованный Exception handling
|
||||
}
|
||||
});
|
||||
|
||||
group.MapGet("/search", async ([FromQuery] string q, IKlipyService klipyService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await klipyService.SearchAsync(q);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.StatusCode(500);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
24
apps/server-net/src/Host/Endpoints/WebRtcEndpoints.cs
Normal file
24
apps/server-net/src/Host/Endpoints/WebRtcEndpoints.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Carter;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Host.Services;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация эндпоинтов WebRTC
|
||||
/// </summary>
|
||||
public sealed class WebRtcEndpoints : ICarterModule
|
||||
{
|
||||
public void AddRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(Knot.Shared.Kernel.Constants.Routes.ApiWebRtc)
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapGet("/ice-servers", (IWebRtcService service) => service.GetIceServers())
|
||||
.WithName("GetIceServers")
|
||||
.WithOpenApi();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
|
||||
@@ -62,10 +62,18 @@ builder.Services.AddCors(options =>
|
||||
? originsFromConfig.Split(',')
|
||||
: (!string.IsNullOrEmpty(domain) ? new[] { $"https://{domain}" } : new[] { "*" });
|
||||
|
||||
policy.WithOrigins(origins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
var corsBuilder = policy.WithOrigins(origins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
|
||||
if (origins.Contains("*"))
|
||||
{
|
||||
// The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time.
|
||||
}
|
||||
else
|
||||
{
|
||||
corsBuilder.AllowCredentials();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
18
apps/server-net/src/Host/Services/ConfigService.cs
Normal file
18
apps/server-net/src/Host/Services/ConfigService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
public sealed class ConfigService : IConfigService
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
|
||||
public ConfigService(ISettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IPublicConfig GetPublicConfig()
|
||||
{
|
||||
return new PublicConfig(_settings.Current);
|
||||
}
|
||||
}
|
||||
44
apps/server-net/src/Host/Services/FederationService.cs
Normal file
44
apps/server-net/src/Host/Services/FederationService.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
public sealed class FederationService : IFederationService
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
|
||||
public FederationService(ISettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public Task<HandshakeResponse> HandshakeAsync(HandshakeRequest request)
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableConfederation)
|
||||
throw new InvalidOperationException(Errors.DisabledByAdmin);
|
||||
|
||||
if (string.IsNullOrEmpty(request.Domain) || string.IsNullOrEmpty(request.PublicKey))
|
||||
throw new ArgumentException("Укажите Domain и PublicKey.");
|
||||
|
||||
var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new System.Collections.Generic.List<string>();
|
||||
if (!allowedList.Contains(request.Domain.ToLowerInvariant()))
|
||||
throw new UnauthorizedAccessException("Домен не входит в белый список.");
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||
|
||||
var response = new HandshakeResponse
|
||||
{
|
||||
Domain = Environment.GetEnvironmentVariable("DOMAIN") ?? "knot.local",
|
||||
PublicKey = selfPublicKey,
|
||||
Status = "Accepted"
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
11
apps/server-net/src/Host/Services/IConfigService.cs
Normal file
11
apps/server-net/src/Host/Services/IConfigService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис конфигурации приложения.
|
||||
/// </summary>
|
||||
public interface IConfigService
|
||||
{
|
||||
IPublicConfig GetPublicConfig();
|
||||
}
|
||||
24
apps/server-net/src/Host/Services/IFederationService.cs
Normal file
24
apps/server-net/src/Host/Services/IFederationService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис федерации (связь с другими узлами).
|
||||
/// </summary>
|
||||
public interface IFederationService
|
||||
{
|
||||
Task<HandshakeResponse> HandshakeAsync(HandshakeRequest request);
|
||||
}
|
||||
|
||||
public class HandshakeRequest
|
||||
{
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string PublicKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class HandshakeResponse
|
||||
{
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string PublicKey { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
14
apps/server-net/src/Host/Services/IKlipyService.cs
Normal file
14
apps/server-net/src/Host/Services/IKlipyService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис интеграции с Klipy GIF.
|
||||
/// </summary>
|
||||
public interface IKlipyService
|
||||
{
|
||||
Task<JsonElement?> GetTrendingAsync();
|
||||
|
||||
Task<JsonElement?> SearchAsync(string query);
|
||||
}
|
||||
14
apps/server-net/src/Host/Services/IPublicConfig.cs
Normal file
14
apps/server-net/src/Host/Services/IPublicConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс публичной конфигурации приложения.
|
||||
/// </summary>
|
||||
public interface IPublicConfig
|
||||
{
|
||||
bool EnableCalls { get; }
|
||||
bool EnableKlipy { get; }
|
||||
int MaxFileSizeMb { get; }
|
||||
int MaxGroupMembers { get; }
|
||||
bool EnableConfederation { get; }
|
||||
bool EnableRegistration { get; }
|
||||
}
|
||||
17
apps/server-net/src/Host/Services/IWebRtcService.cs
Normal file
17
apps/server-net/src/Host/Services/IWebRtcService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис для работы с WebRTC конфигурацией.
|
||||
/// </summary>
|
||||
public interface IWebRtcService
|
||||
{
|
||||
/// <summary>
|
||||
/// Получить список ICE-серверов.
|
||||
/// </summary>
|
||||
IResult GetIceServers();
|
||||
}
|
||||
102
apps/server-net/src/Host/Services/KlipyService.cs
Normal file
102
apps/server-net/src/Host/Services/KlipyService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
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.Tasks;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
public sealed class KlipyService : IKlipyService
|
||||
{
|
||||
private readonly ISettingsService _settings;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public KlipyService(ISettingsService settings, IMemoryCache cache, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_settings = settings;
|
||||
_cache = cache;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<JsonElement?> GetTrendingAsync()
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey))
|
||||
throw new InvalidOperationException(Errors.KlipyNotConfigured);
|
||||
|
||||
var cacheKeyTrending = $"klipy_trending_{conf.KlipyApiKey}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKeyTrending, out JsonElement cachedResult))
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? Klipy.DefaultCustomerId : conf.KlipyCustomerId;
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
|
||||
var urlCo = string.Format(Klipy.ApiUrlCo, conf.KlipyApiKey, Klipy.ResourceTrending, "", customerId);
|
||||
|
||||
var response = await client.GetAsync(urlCo);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
var urlCom = string.Format(Klipy.ApiUrlCom, conf.KlipyApiKey, Klipy.ResourceTrending, "", customerId);
|
||||
response = await client.GetAsync(urlCom);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException(Errors.KlipyApiError);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
_cache.Set(cacheKeyTrending, result, TimeSpan.FromMinutes(Klipy.TrendingCacheMinutes));
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<JsonElement?> SearchAsync(string query)
|
||||
{
|
||||
var conf = _settings.Current;
|
||||
if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey))
|
||||
throw new InvalidOperationException(Errors.KlipyNotConfigured);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
throw new ArgumentException(Errors.InvalidQuery, nameof(query));
|
||||
|
||||
var cacheKey = $"klipy_search_{conf.KlipyApiKey}_{query.ToLowerInvariant()}";
|
||||
|
||||
if (_cache.TryGetValue(cacheKey, out JsonElement cachedResult))
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? Klipy.DefaultCustomerId : conf.KlipyCustomerId;
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
|
||||
var queryParam = $"q={Uri.EscapeDataString(query)}";
|
||||
var urlCo = string.Format(Klipy.ApiUrlCo, conf.KlipyApiKey, Klipy.ResourceSearch, queryParam, customerId);
|
||||
|
||||
var response = await client.GetAsync(urlCo);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
var urlCom = string.Format(Klipy.ApiUrlCom, conf.KlipyApiKey, Klipy.ResourceSearch, queryParam, customerId);
|
||||
response = await client.GetAsync(urlCom);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException(Errors.KlipySearchError);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
|
||||
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(Klipy.SearchCacheMinutes));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
16
apps/server-net/src/Host/Services/PublicConfig.cs
Normal file
16
apps/server-net/src/Host/Services/PublicConfig.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
|
||||
namespace Host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация публичной конфигурации приложения.
|
||||
/// </summary>
|
||||
public sealed class PublicConfig(SystemSettingsDto settings) : IPublicConfig
|
||||
{
|
||||
public bool EnableCalls => settings.EnableCalls;
|
||||
public bool EnableKlipy => settings.EnableKlipy;
|
||||
public int MaxFileSizeMb => settings.MaxFileSizeMb;
|
||||
public int MaxGroupMembers => settings.MaxGroupMembers;
|
||||
public bool EnableConfederation => settings.EnableConfederation;
|
||||
public bool EnableRegistration => settings.EnableRegistration;
|
||||
}
|
||||
@@ -1,54 +1,53 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Host.Controllers;
|
||||
namespace Host.Services;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/webrtc")]
|
||||
public sealed class WebRtcController : ControllerBase
|
||||
/// <summary>
|
||||
/// Реализация сервиса управления WebRTC настройками.
|
||||
/// </summary>
|
||||
public sealed class WebRtcService : IWebRtcService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public WebRtcController(IConfiguration configuration, ISettingsService settingsService)
|
||||
public WebRtcService(
|
||||
IConfiguration configuration,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
[HttpGet("ice-servers")]
|
||||
public IActionResult GetIceServers()
|
||||
public IResult GetIceServers()
|
||||
{
|
||||
var settings = _settingsService.Current;
|
||||
if (!settings.EnableCalls)
|
||||
{
|
||||
return StatusCode(503, new { error = "Calls module is disabled by administrator." });
|
||||
return Results.StatusCode(503); // TODO: Возвращать структуру ошибки с пояснением на русском языке (Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin)
|
||||
}
|
||||
|
||||
var turnUrl = !string.IsNullOrEmpty(settings.TurnHost)
|
||||
? $"turn:{settings.TurnHost}:{settings.TurnPort}"
|
||||
var turnUrl = !string.IsNullOrEmpty(settings.TurnHost)
|
||||
? $"turn:{settings.TurnHost}:{settings.TurnPort}"
|
||||
: _configuration["WebRtc:TurnUrl"];
|
||||
|
||||
|
||||
var turnUsername = !string.IsNullOrEmpty(settings.TurnUser)
|
||||
? settings.TurnUser
|
||||
|
||||
var turnUsername = !string.IsNullOrEmpty(settings.TurnUser)
|
||||
? settings.TurnUser
|
||||
: _configuration["WebRtc:TurnUsername"];
|
||||
|
||||
var turnSecret = !string.IsNullOrEmpty(settings.TurnSecret)
|
||||
? settings.TurnSecret
|
||||
var turnSecret = !string.IsNullOrEmpty(settings.TurnSecret)
|
||||
? settings.TurnSecret
|
||||
: _configuration["WebRtc:TurnPassword"];
|
||||
|
||||
var iceServers = new List<object>();
|
||||
|
||||
if (!string.IsNullOrEmpty(turnUrl))
|
||||
{
|
||||
// Use the VPS server as STUN
|
||||
var stunUrl = turnUrl.Replace("turn:", "stun:");
|
||||
iceServers.Add(new { urls = new[] { stunUrl } });
|
||||
|
||||
// Allow TURN even without username/password
|
||||
|
||||
if (!string.IsNullOrEmpty(turnUsername))
|
||||
{
|
||||
iceServers.Add(new
|
||||
@@ -67,6 +66,6 @@ public sealed class WebRtcController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { iceServers });
|
||||
return Results.Ok(new { iceServers });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Knot.Shared.Kernel.Constants;
|
||||
|
||||
public static class Errors
|
||||
{
|
||||
public const string Unauthorized = "Не авторизован.";
|
||||
public const string Forbidden = "Доступ запрещен.";
|
||||
public const string NotFound = "Сущность не найдена.";
|
||||
public const string ValidationError = "Ошибка валидации.";
|
||||
public const string InternalServerError = "Внутренняя ошибка сервера.";
|
||||
public const string DisabledByAdmin = "Модуль отключен администратором.";
|
||||
public const string KlipyNotConfigured = "Ключ Klipy не настроен или модуль отключен.";
|
||||
public const string KlipyApiError = "Ошибка запроса к Klipy API.";
|
||||
public const string KlipySearchError = "Ошибка запроса к Klipy API при поиске.";
|
||||
public const string InvalidQuery = "Пустой запрос недопустим.";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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 int TrendingCacheMinutes = 60;
|
||||
public const int SearchCacheMinutes = 15;
|
||||
|
||||
public const string DefaultCustomerId = "anonymous";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Knot.Shared.Kernel.Constants;
|
||||
|
||||
public enum Policies
|
||||
{
|
||||
AdminOnly
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Knot.Shared.Kernel.Constants;
|
||||
|
||||
public enum Roles
|
||||
{
|
||||
Admin,
|
||||
User
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Knot.Shared.Kernel.Constants;
|
||||
|
||||
public static class Routes
|
||||
{
|
||||
public const string ApiAdmin = "/api/admin";
|
||||
public const string ApiAuth = "/api/auth";
|
||||
public const string ApiChats = "/api/chats";
|
||||
public const string ApiConfig = "/api/config";
|
||||
public const string ApiFederation = "/api/federation";
|
||||
public const string ApiFiles = "/api/files";
|
||||
public const string ApiFriends = "/api/friends";
|
||||
public const string ApiKlipy = "/api/klipy";
|
||||
public const string ApiMessages = "/api/messages";
|
||||
public const string ApiStories = "/api/stories";
|
||||
public const string ApiTelegramImport = "/api/telegram/import";
|
||||
public const string ApiUsers = "/api/users";
|
||||
public const string ApiWebRtc = "/api/webrtc";
|
||||
}
|
||||
Reference in New Issue
Block a user