using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Nodes; using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; namespace Boyfriend.Services; /// /// Handles saving, loading, initializing and providing . /// public sealed class GuildDataService : IHostedService { private readonly ConcurrentDictionary _datas = new(); private readonly IDiscordRestGuildAPI _guildApi; private readonly ILogger _logger; // https://github.com/dotnet/aspnetcore/issues/39139 public GuildDataService( IHostApplicationLifetime lifetime, IDiscordRestGuildAPI guildApi, ILogger logger) { _guildApi = guildApi; _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) { _logger.LogInformation("Saving guild data..."); 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 idString = $"{guildId}"; var memberDataPath = $"{guildId}/MemberData"; var settingsPath = $"{guildId}/Settings.json"; var scheduledEventsPath = $"{guildId}/ScheduledEvents.json"; Directory.CreateDirectory(idString); 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; } var memberResult = await _guildApi.GetGuildMemberAsync(guildId, data.Id.ToSnowflake(), ct); if (memberResult.IsSuccess) { data.Roles = memberResult.Entity.Roles.ToList().ConvertAll(r => r.Value); } 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; } public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) { return (await GetData(guildId, ct)).Settings; } public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { return (await GetData(guildId, ct)).GetOrCreateMemberData(userId); } public ICollection GetGuildIds() { return _datas.Keys; } }