using System.Net; using System.Text.Json; using System.Threading.RateLimiting; using Humanizer; using NodaTime; using NodaTime.Serialization.SystemTextJson; using NodaTime.Text; 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(); 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 DoRequestAsync(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); var resp = await _pipeline.ExecuteAsync(async ct2 => await _client.SendAsync(req, ct2), ct); 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"); } var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower } .ConfigureForNodaTime(new NodaJsonSettings { InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso) }); return await resp.Content.ReadFromJsonAsync(jsonOptions, ct) ?? throw new CataloggerError("JSON response from PluralKit API was null"); } public async Task GetPluralKitMessageAsync(ulong id, CancellationToken ct = default) => await DoRequestAsync($"/messages/{id}", allowNotFound: true, ct); public async Task GetPluralKitSystemAsync(ulong id, CancellationToken ct = default) => await DoRequestAsync($"/systems/{id}", allowNotFound: true, ct); 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); }