// Copyright (C) 2021-present sam (starshines.gay) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using System.Net; using System.Threading.RateLimiting; using Humanizer; using NodaTime; 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"); } return await resp.Content.ReadFromJsonAsync(JsonUtils.ApiJsonOptions, 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); }