diff --git a/src/SocialAgent.Data/Repositories/ISocialDataRepository.cs b/src/SocialAgent.Data/Repositories/ISocialDataRepository.cs index 986e62a..2e75b21 100644 --- a/src/SocialAgent.Data/Repositories/ISocialDataRepository.cs +++ b/src/SocialAgent.Data/Repositories/ISocialDataRepository.cs @@ -21,4 +21,8 @@ public interface ISocialDataRepository // Poll State Task GetPollStateAsync(string providerId, CancellationToken ct = default); Task UpsertPollStateAsync(PollState state, CancellationToken ct = default); + + // Retention + Task PurgeOldPostsAsync(DateTimeOffset olderThan, CancellationToken ct = default); + Task PurgeOldNotificationsAsync(DateTimeOffset olderThan, CancellationToken ct = default); } diff --git a/src/SocialAgent.Data/Repositories/SocialDataRepository.cs b/src/SocialAgent.Data/Repositories/SocialDataRepository.cs index af8ec48..5a86a69 100644 --- a/src/SocialAgent.Data/Repositories/SocialDataRepository.cs +++ b/src/SocialAgent.Data/Repositories/SocialDataRepository.cs @@ -138,4 +138,18 @@ public async Task UpsertPollStateAsync(PollState state, CancellationToken ct = d } await db.SaveChangesAsync(ct); } + + public async Task PurgeOldPostsAsync(DateTimeOffset olderThan, CancellationToken ct = default) + { + return await db.Posts + .Where(p => p.CreatedAt < olderThan) + .ExecuteDeleteAsync(ct); + } + + public async Task PurgeOldNotificationsAsync(DateTimeOffset olderThan, CancellationToken ct = default) + { + return await db.Notifications + .Where(n => n.CreatedAt < olderThan) + .ExecuteDeleteAsync(ct); + } } diff --git a/src/SocialAgent.Host/Program.cs b/src/SocialAgent.Host/Program.cs index 0235674..81b8974 100644 --- a/src/SocialAgent.Host/Program.cs +++ b/src/SocialAgent.Host/Program.cs @@ -45,6 +45,7 @@ // Background services builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); // Authentication (API key required in non-Development environments) builder.Services.AddApiKeyAuthentication(builder.Configuration); diff --git a/src/SocialAgent.Host/Services/DataRetentionService.cs b/src/SocialAgent.Host/Services/DataRetentionService.cs new file mode 100644 index 0000000..3e957e4 --- /dev/null +++ b/src/SocialAgent.Host/Services/DataRetentionService.cs @@ -0,0 +1,54 @@ +using SocialAgent.Data.Repositories; + +namespace SocialAgent.Host.Services; + +public class DataRetentionService( + IServiceScopeFactory scopeFactory, + ILogger logger, + IConfiguration configuration) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var retentionDays = configuration.GetValue("SocialAgent:RetentionDays", 30); + var intervalHours = configuration.GetValue("SocialAgent:RetentionCheckIntervalHours", 24); + + logger.LogInformation( + "Data retention service starting: {RetentionDays} day retention, checking every {Interval}h", + retentionDays, intervalHours); + + // Delay startup to let migrations complete + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await PurgeOldDataAsync(retentionDays, stoppingToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogError(ex, "Error during data retention purge"); + } + + await Task.Delay(TimeSpan.FromHours(intervalHours), stoppingToken); + } + } + + private async Task PurgeOldDataAsync(int retentionDays, CancellationToken ct) + { + var cutoff = DateTimeOffset.UtcNow.AddDays(-retentionDays); + + using var scope = scopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + + var postsDeleted = await repository.PurgeOldPostsAsync(cutoff, ct); + var notificationsDeleted = await repository.PurgeOldNotificationsAsync(cutoff, ct); + + if (postsDeleted > 0 || notificationsDeleted > 0) + { + logger.LogInformation( + "Data retention purge: deleted {Posts} posts and {Notifications} notifications older than {Days} days", + postsDeleted, notificationsDeleted, retentionDays); + } + } +} diff --git a/src/SocialAgent.Host/appsettings.json b/src/SocialAgent.Host/appsettings.json index 2e181ab..4b69de6 100644 --- a/src/SocialAgent.Host/appsettings.json +++ b/src/SocialAgent.Host/appsettings.json @@ -16,6 +16,8 @@ }, "SocialAgent": { "PollingIntervalMinutes": 5, + "RetentionDays": 30, + "RetentionCheckIntervalHours": 24, "DatabaseProvider": "Sqlite", "Providers": { "Mastodon": {