2024-08-13 13:08:50 +02:00
|
|
|
using System.Net;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Threading.RateLimiting;
|
|
|
|
|
using Humanizer;
|
|
|
|
|
using NodaTime;
|
2024-09-02 15:59:16 +02:00
|
|
|
using NodaTime.Serialization.SystemTextJson;
|
|
|
|
|
using NodaTime.Text;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Polly;
|
|
|
|
|
|
|
|
|
|
namespace Catalogger.Backend.Services;
|
|
|
|
|
|
|
|
|
|
public class PluralkitApiService(ILogger logger)
|
|
|
|
|
{
|
|
|
|
|
private const string UserAgent = "Catalogger.NET (https://codeberg.org/starshine/catalogger)";
|
|
|
|
|
private const string ApiBaseUrl = "https://api.pluralkit.me/v2";
|
|
|
|
|
private readonly HttpClient _client = new();
|
|
|
|
|
private readonly ILogger _logger = logger.ForContext<PluralkitApiService>();
|
|
|
|
|
|
|
|
|
|
private readonly ResiliencePipeline _pipeline = new ResiliencePipelineBuilder()
|
|
|
|
|
.AddRateLimiter(new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions()
|
|
|
|
|
{
|
|
|
|
|
Window = 1.Seconds(),
|
|
|
|
|
PermitLimit = 2,
|
|
|
|
|
QueueLimit = 64,
|
|
|
|
|
}))
|
|
|
|
|
.AddTimeout(20.Seconds())
|
|
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
private async Task<T?> DoRequestAsync<T>(string path, bool allowNotFound = false,
|
|
|
|
|
CancellationToken ct = default) where T : class
|
|
|
|
|
{
|
|
|
|
|
var req = new HttpRequestMessage(HttpMethod.Get, $"{ApiBaseUrl}{path}");
|
|
|
|
|
req.Headers.Add("User-Agent", UserAgent);
|
|
|
|
|
|
|
|
|
|
_logger.Debug("Requesting {Path} from PluralKit API", path);
|
|
|
|
|
|
2024-08-20 21:03:03 +02:00
|
|
|
var resp = await _pipeline.ExecuteAsync(async ct2 => await _client.SendAsync(req, ct2), ct);
|
2024-08-13 13:08:50 +02:00
|
|
|
if (resp.StatusCode == HttpStatusCode.NotFound && allowNotFound)
|
|
|
|
|
{
|
|
|
|
|
_logger.Debug("PluralKit API path {Path} returned 404 but 404 response is valid", path);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!resp.IsSuccessStatusCode)
|
|
|
|
|
{
|
|
|
|
|
_logger.Error("Received non-200 status code {StatusCode} from PluralKit API path {Path}", resp.StatusCode,
|
|
|
|
|
req);
|
|
|
|
|
throw new CataloggerError("Non-200 status code from PluralKit API");
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-02 15:59:16 +02:00
|
|
|
var jsonOptions = new JsonSerializerOptions
|
|
|
|
|
{ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }
|
|
|
|
|
.ConfigureForNodaTime(new NodaJsonSettings
|
|
|
|
|
{
|
|
|
|
|
InstantConverter = new NodaPatternConverter<Instant>(InstantPattern.ExtendedIso)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return await resp.Content.ReadFromJsonAsync<T>(jsonOptions, ct) ??
|
2024-08-13 13:08:50 +02:00
|
|
|
throw new CataloggerError("JSON response from PluralKit API was null");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<PkMessage?> GetPluralKitMessageAsync(ulong id, CancellationToken ct = default) =>
|
|
|
|
|
await DoRequestAsync<PkMessage>($"/messages/{id}", allowNotFound: true, ct);
|
|
|
|
|
|
2024-08-20 21:03:03 +02:00
|
|
|
public async Task<PkSystem?> GetPluralKitSystemAsync(ulong id, CancellationToken ct = default) =>
|
|
|
|
|
await DoRequestAsync<PkSystem>($"/systems/{id}", allowNotFound: true, ct);
|
2024-08-13 13:08:50 +02:00
|
|
|
|
|
|
|
|
public record PkMessage(
|
|
|
|
|
ulong Id,
|
|
|
|
|
ulong Original,
|
|
|
|
|
ulong Sender,
|
|
|
|
|
ulong Channel,
|
|
|
|
|
ulong Guild,
|
|
|
|
|
PkSystem? System,
|
|
|
|
|
PkMember? Member);
|
|
|
|
|
|
|
|
|
|
public record PkSystem(string Id, Guid Uuid, string? Name, string? Tag, Instant? Created);
|
|
|
|
|
|
|
|
|
|
public record PkMember(string Id, Guid Uuid, string Name, string? DisplayName);
|
|
|
|
|
}
|