diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs index c49e77b..c9d9bb2 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupCommands.cs @@ -2,7 +2,7 @@ namespace EllieBot.Modules.Administration; -public partial class Administration +public partial class Administration { [Group] public partial class CleanupCommands : CleanupModuleBase @@ -39,5 +39,27 @@ public partial class Administration await Response().Text("This guild's bot data will be saved.").SendAsync(); } + + [Cmd] + [OwnerOnly] + public async Task LeaveUnkeptServers() + { + var keptGuildCount = await _svc.GetKeptGuildCount(); + + var response = await PromptUserConfirmAsync(new EmbedBuilder() + .WithDescription($""" + Do you want the bot to leave all unkept servers? + + There are currently {keptGuildCount} kept servers. + + **This is a highly destructive and irreversible action.** + """)); + + if (!response) + return; + + await _svc.LeaveUnkeptServers(); + await ctx.OkAsync(); + } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs index 7bf7128..be01a0b 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/CleanupService.cs @@ -2,16 +2,21 @@ using LinqToDB.Data; using LinqToDB.EntityFrameworkCore; using LinqToDB.Mapping; +using LinqToDB.Tools; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; +using System.Security.Cryptography; namespace EllieBot.Modules.Administration.DangerousCommands; public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService { + private TypedKey _cleanupReportKey = new("cleanup:report"); + private TypedKey _cleanupTriggerKey = new("cleanup:trigger"); + + private TypedKey _keepTriggerKey = new("keep:trigger"); + private readonly IPubSub _pubSub; - private TypedKey _keepReportKey = new("cleanup:report"); - private TypedKey _keepTriggerKey = new("cleanup:trigger"); private readonly DiscordSocketClient _client; private ConcurrentDictionary guildIds = new(); private readonly IBotCredsProvider _creds; @@ -29,11 +34,82 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService _db = db; } + public async Task OnReadyAsync() + { + await _pubSub.Sub(_cleanupTriggerKey, OnCleanupTrigger); + await _pubSub.Sub(_keepTriggerKey, InternalTriggerKeep); + + _client.JoinedGuild += ClientOnJoinedGuild; + + if (_client.ShardId == 0) + await _pubSub.Sub(_cleanupReportKey, OnKeepReport); + } + + private bool keepTriggered = false; + + private async ValueTask InternalTriggerKeep(bool arg) + { + if (keepTriggered) + return; + + keepTriggered = true; + try + { + await Task.Delay(10 + (10 * _client.ShardId)); + + var allGuildIds = _client.Guilds.Select(x => x.Id); + + var table = await GetKeptGuildsTable(); + + var dontDeleteList = await table + .Where(x => allGuildIds.Contains(x.GuildId)) + .Select(x => x.GuildId) + .ToListAsyncLinqToDB(); + + var dontDelete = dontDeleteList.ToHashSet(); + + guildIds = new(); + foreach (var guildId in allGuildIds) + { + if (dontDelete.Contains(guildId)) + continue; + + // 1 leave per 20 seconds per shard + await Task.Delay(RandomNumberGenerator.GetInt32(18_000, 22_000)); + + SocketGuild? guild = null; + try + { + guild = _client.GetGuild(guildId); + + if (guild is null) + { + Log.Warning("Unable to find guild {GuildId}", guildId); + continue; + } + + await guild.LeaveAsync(); + } + catch (Exception ex) + { + Log.Warning("Unable to leave guild {GuildName} [{GuildId}]: {ErrorMessage}", + guild?.Name, + guildId, + ex.Message); + } + } + } + finally + { + keepTriggered = false; + } + } + public async Task DeleteMissingGuildDataAsync() { guildIds = new(); var totalShards = _creds.GetCreds().TotalShards; - await _pubSub.Pub(_keepTriggerKey, true); + await _pubSub.Pub(_cleanupTriggerKey, true); var counter = 0; while (guildIds.Keys.Count < totalShards) { @@ -133,10 +209,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService public async Task KeepGuild(ulong guildId) { - await using var db = _db.GetDbContext(); - await using var ctx = db.CreateLinqToDBContext(); - - var table = ctx.CreateTable(tableOptions: TableOptions.CheckExistence); + var table = await GetKeptGuildsTable(); if (await table.AnyAsyncLinqToDB(x => x.GuildId == guildId)) return false; @@ -149,30 +222,37 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService return true; } + public async Task GetKeptGuildCount() + { + var table = await GetKeptGuildsTable(); + return await table.CountAsync(); + } + + private async Task> GetKeptGuildsTable() + { + await using var db = _db.GetDbContext(); + await using var ctx = db.CreateLinqToDBContext(); + var table = ctx.CreateTable(tableOptions: TableOptions.CheckExistence); + return table; + } + + public async Task LeaveUnkeptServers() + => await _pubSub.Pub(_keepTriggerKey, true); + private ValueTask OnKeepReport(KeepReport report) { guildIds[report.ShardId] = report.GuildIds; return default; } - public async Task OnReadyAsync() - { - await _pubSub.Sub(_keepTriggerKey, OnKeepTrigger); - - _client.JoinedGuild += ClientOnJoinedGuild; - - if (_client.ShardId == 0) - await _pubSub.Sub(_keepReportKey, OnKeepReport); - } - private async Task ClientOnJoinedGuild(SocketGuild arg) { await KeepGuild(arg.Id); } - private ValueTask OnKeepTrigger(bool arg) + private ValueTask OnCleanupTrigger(bool arg) { - _pubSub.Pub(_keepReportKey, + _pubSub.Pub(_cleanupReportKey, new KeepReport() { ShardId = _client.ShardId, diff --git a/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs b/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs index 5d4a720..e48bba4 100644 --- a/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs +++ b/src/EllieBot/Modules/Administration/DangerousCommands/_common/ICleanupService.cs @@ -4,4 +4,6 @@ public interface ICleanupService { Task DeleteMissingGuildDataAsync(); Task KeepGuild(ulong guildId); + Task GetKeptGuildCount(); + Task LeaveUnkeptServers(); } \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index be045d0..66b7401 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -1423,4 +1423,6 @@ coins: afk: - afk keep: - - keep \ No newline at end of file + - keep +leaveunkeptservers: + - leaveunkeptservers \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 8016e89..912ca40 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -4554,5 +4554,12 @@ keep: The current serve, won't be deleted from Ellie's database during the purge. ex: - '' + params: + - { } +leaveunkeptservers: + desc: |- + Leaves all servers whose owners didn't run .keep + ex: + - '' params: - { } \ No newline at end of file