Files
FsMcp/FsMcp/FoodsharingApiClient.cs

117 lines
3.9 KiB
C#

using System.Net.Http.Json;
namespace FsMcp;
internal sealed class FoodsharingApiClient
{
private const string RequestThrottleMsEnvVar = "REQUEST_THROTTLE_MS";
private readonly SemaphoreSlim _loginLock = new(1, 1);
public HttpClient HttpClient { get; } = new(new RequestThrottleHandler(TimeSpan.FromMilliseconds(GetRequestThrottleMs())));
private static int GetRequestThrottleMs()
{
string? configuredValue = Environment.GetEnvironmentVariable(RequestThrottleMsEnvVar);
if (int.TryParse(configuredValue, out int throttleMs) && throttleMs >= 0)
{
return throttleMs;
}
return 500;
}
public async Task EnsureLoginAsync()
{
if (HttpClient.DefaultRequestHeaders.Contains("X-CSRF-Token"))
{
return;
}
await _loginLock.WaitAsync();
try
{
if (HttpClient.DefaultRequestHeaders.Contains("X-CSRF-Token"))
{
return;
}
string username = Environment.GetEnvironmentVariable("USERNAME") ?? throw new InvalidOperationException("USERNAME environment variable is not set.");
string password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new InvalidOperationException("PASSWORD environment variable is not set.");
var payload = new
{
email = username,
password,
remember_me = true
};
using var response = await HttpClient.PostAsJsonAsync(Endpoints.UserLogin, payload);
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Login failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
}
if (!response.Headers.TryGetValues("Set-Cookie", out var setCookieValues))
{
throw new InvalidOperationException("Set-Cookie headers are missing from login response.");
}
string setCookieHeader = string.Join(";", setCookieValues);
string? csrfToken = setCookieHeader
.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(cookie => cookie.Trim())
.FirstOrDefault(cookie => cookie.StartsWith("FS_CSRF_TOKEN="))
?.Split('=', 2)[1];
if (string.IsNullOrWhiteSpace(csrfToken))
{
throw new InvalidOperationException("CSRF token not found in Set-Cookie header.");
}
HttpClient.DefaultRequestHeaders.Remove("X-CSRF-Token");
HttpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
}
finally
{
_loginLock.Release();
}
}
}
internal sealed class RequestThrottleHandler : DelegatingHandler
{
private readonly TimeSpan _minimumInterval;
private readonly SemaphoreSlim _requestGate = new(1, 1);
private DateTimeOffset _lastRequestStartedAt = DateTimeOffset.MinValue;
public RequestThrottleHandler(TimeSpan minimumInterval)
: base(new HttpClientHandler())
{
_minimumInterval = minimumInterval;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await _requestGate.WaitAsync(cancellationToken);
try
{
var waitFor = _lastRequestStartedAt + _minimumInterval - DateTimeOffset.UtcNow;
if (waitFor > TimeSpan.Zero)
{
await Task.Delay(waitFor, cancellationToken);
}
_lastRequestStartedAt = DateTimeOffset.UtcNow;
}
finally
{
_requestGate.Release();
}
return await base.SendAsync(request, cancellationToken);
}
}