using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Data; using Remora.Rest.Core; namespace Octobot.Services; /// /// Handles saving, loading, initializing and providing . /// public sealed class GuildDataService : IHostedService { private readonly ConcurrentDictionary _datas = new(); private readonly ILogger _logger; // https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService( IHostApplicationLifetime lifetime, ILogger logger) { _logger = logger; lifetime.ApplicationStopping.Register(ApplicationStopping); } public Task StartAsync(CancellationToken ct) { return Task.CompletedTask; } public Task StopAsync(CancellationToken ct) { return Task.CompletedTask; } private void ApplicationStopping() { SaveAsync(CancellationToken.None).GetAwaiter().GetResult(); } public async Task SaveAsync(CancellationToken ct) { var tasks = new List(); var datas = _datas.Values.ToArray(); foreach (var data in datas) { await using var settingsStream = File.Create(data.SettingsPath); tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct)); await using var eventsStream = File.Create(data.ScheduledEventsPath); tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct)); var memberDatas = data.MemberData.Values.ToArray(); foreach (var memberData in memberDatas) { await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json"); tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct)); } } await Task.WhenAll(tasks); } public async Task GetData(Snowflake guildId, CancellationToken ct = default) { return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); } private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) { var path = $"GuildData/{guildId}"; var memberDataPath = $"{path}/MemberData"; var settingsPath = $"{path}/Settings.json"; var scheduledEventsPath = $"{path}/ScheduledEvents.json"; MigrateGuildData(guildId, path); Directory.CreateDirectory(path); if (!File.Exists(settingsPath)) { await File.WriteAllTextAsync(settingsPath, "{}", ct); } if (!File.Exists(scheduledEventsPath)) { await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); } await using var settingsStream = File.OpenRead(settingsPath); var jsonSettings = JsonNode.Parse(settingsStream); await using var eventsStream = File.OpenRead(scheduledEventsPath); var events = await JsonSerializer.DeserializeAsync>( eventsStream, cancellationToken: ct); var memberData = new Dictionary(); foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) { await using var dataStream = dataFileInfo.OpenRead(); var data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); if (data is null) { continue; } memberData.Add(data.Id, data); } var finalData = new GuildData( jsonSettings ?? new JsonObject(), settingsPath, events ?? new Dictionary(), scheduledEventsPath, memberData, memberDataPath); _datas.TryAdd(guildId, finalData); return finalData; } private void MigrateGuildData(Snowflake guildId, string newPath) { var oldPath = $"{guildId}"; if (Directory.Exists(oldPath)) { Directory.CreateDirectory($"{newPath}/.."); Directory.Move(oldPath, newPath); _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, newPath); } } public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { return (await GetData(guildId, ct)).Settings; } public ICollection GetGuildIds() { return _datas.Keys; } public bool UnloadGuildData(Snowflake id) { return _datas.TryRemove(id, out _); } }