114 lines
4 KiB
C#
114 lines
4 KiB
C#
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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<PluralkitApiService>();
|
|
|
|
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
}.ConfigureForNodaTime(
|
|
new NodaJsonSettings
|
|
{
|
|
InstantConverter = new NodaPatternConverter<Instant>(InstantPattern.ExtendedIso),
|
|
}
|
|
);
|
|
|
|
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);
|
|
|
|
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<T>(_jsonOptions, ct)
|
|
?? 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);
|
|
|
|
public async Task<PkSystem?> GetPluralKitSystemAsync(
|
|
ulong id,
|
|
CancellationToken ct = default
|
|
) => await DoRequestAsync<PkSystem>($"/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);
|
|
}
|