117 lines
3.9 KiB
C#
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);
|
|
}
|
|
} |