72 lines
3.8 KiB
C#

using GuestOps.Web;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders(); builder.Logging.AddConsole();
builder.Logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning);
builder.Services.AddSingleton<IStore, MongoStore>();
var keyPath = builder.Configuration["Keys:Path"] ?? throw new InvalidOperationException("Keys:Path is required.");
builder.Services.AddDataProtection().SetApplicationName("GuestOps-Web").PersistKeysToFileSystem(new DirectoryInfo(keyPath));
builder.Services.AddHttpClient<GoogleMailbox>(c => c.Timeout = TimeSpan.FromSeconds(25)).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false });
builder.Services.AddTransient<ReplyDelivery>();
builder.Services.AddHostedService<DeliveryWorker>();
builder.Services.AddHostedService<MailboxWorker>();
await builder.Build().RunAsync();
sealed class MailboxWorker(IStore store, IServiceScopeFactory factory, ILogger<MailboxWorker> log) : BackgroundService
{
readonly string owner = Guid.NewGuid().ToString("N");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await store.Initialize();
while (!stoppingToken.IsCancellationRequested)
{
try
{
foreach (var mailbox in await store.Mailboxes())
{
if (!await store.TryLease(mailbox.Id, owner)) continue;
// Per-page deadline is shorter than the lease. Import is idempotent.
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); deadline.CancelAfter(TimeSpan.FromMinutes(2));
try { using var scope = factory.CreateScope(); await scope.ServiceProvider.GetRequiredService<GoogleMailbox>().Sync(mailbox, deadline.Token); }
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; }
catch (Exception ex)
{
log.LogWarning("Mailbox synchronization paused for {Id} ({Type})", mailbox.Id, ex.GetType().Name);
mailbox.SyncError = "Synchronization failed. Retry later or reconnect your Google mailbox.";
await store.SaveSync(mailbox);
}
finally { await store.ReleaseLease(mailbox.Id, owner); }
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { log.LogWarning("Worker cycle paused ({Type})", ex.GetType().Name); }
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
}
}
}
sealed class DeliveryWorker(IStore store, IServiceScopeFactory factory, ILogger<DeliveryWorker> log) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
foreach (var message in await store.Deliveries())
{
using var scope = factory.CreateScope();
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); deadline.CancelAfter(TimeSpan.FromMinutes(2));
await scope.ServiceProvider.GetRequiredService<ReplyDelivery>().Process(message, deadline.Token);
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { log.LogWarning("Reply delivery cycle paused ({Type})", ex.GetType().Name); }
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}