From b53a0046fd6b608e70a81eac2eb754a55474323c Mon Sep 17 00:00:00 2001 From: SylveonDeko Date: Mon, 13 Nov 2023 00:04:42 -0500 Subject: [PATCH] update and quarantine checks, yay! --- src/Mewdeko/Common/Configs/BotConfig.cs | 31 +++- src/Mewdeko/Mewdeko.csproj | 1 + src/Mewdeko/Modules/OwnerOnly/OwnerOnly.cs | 170 +++++++++++++++-- .../OwnerOnly/Services/OwnerOnlyService.cs | 172 +++++++++++++++++- src/Mewdeko/Services/Impl/RedisCache.cs | 29 ++- src/Mewdeko/data/aliases.yml | 3 + .../data/strings/commands/commands.en-US.yml | 4 + 7 files changed, 381 insertions(+), 29 deletions(-) diff --git a/src/Mewdeko/Common/Configs/BotConfig.cs b/src/Mewdeko/Common/Configs/BotConfig.cs index 9c25cd172..8bccf3530 100644 --- a/src/Mewdeko/Common/Configs/BotConfig.cs +++ b/src/Mewdeko/Common/Configs/BotConfig.cs @@ -33,6 +33,7 @@ public BotConfig() "Your name is Mewdeko. You are a discord bot. Your profile picture is of the character Hanekawa Tsubasa in Black Hanekawa form. You were created by sylveondeko"; ChatGptMaxTokens = 1000; ChatGptTemperature = 0.9; + QuarantineNotification = true; } [Comment(@"DO NOT CHANGE")] @@ -53,17 +54,21 @@ and copy the hex code fo your selected color (marked as #)")] Allowed values: Simple, Normal, None")] public ConsoleOutputType ConsoleOutputType { get; set; } - // [Comment(@"For what kind of updates will the bot check. - // Allowed values: Release, Commit, None")] - // public UpdateCheckType CheckForUpdates { get; set; } + [Comment(@"For what kind of updates will the bot check. + Allowed values: Release, Commit, None")] + public UpdateCheckType CheckForUpdates { get; set; } - // [Comment(@"How often will the bot check for updates, in hours")] - // public int CheckUpdateInterval { get; set; } + [Comment(@"How often will the bot check for updates, in hours")] + public int CheckUpdateInterval { get; set; } + + [Comment("Set which branch to check for updates")] + public string UpdateBranch { get; set; } [Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")] public bool ForwardMessages { get; set; } - [Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml), + [Comment( + @"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml), or all owners? (this might cause the bot to lag if there's a lot of owners specified)")] public bool ForwardToAllOwners { get; set; } @@ -127,7 +132,8 @@ This setting can be changed via .rots command. [Comment("The model to use for chatgpt")] public string ChatGptModel { get; set; } - [Comment(@"The authorization redirect url for the auth command. This MUST be added to your valid redirect urls in the discord developer portal.")] + [Comment( + @"The authorization redirect url for the auth command. This MUST be added to your valid redirect urls in the discord developer portal.")] public string RedirectUrl { get; set; } [Comment("Used to set the error emote used across the bot.")] @@ -142,6 +148,10 @@ This setting can be changed via .rots command. [Comment("Used to set the support server invite on public Mewdeko")] public string SupportServer { get; set; } + [Comment( + "Notify the owner of the bot when the bot gets quarantined. Only dms first owner if ForwardMessages is enabled.")] + public bool QuarantineNotification { get; set; } + public string Prefixed(string text) => Prefix + text; } @@ -182,4 +192,11 @@ public enum ConsoleOutputType Normal = 0, Simple = 1, None = 2 +} + +public enum UpdateCheckType +{ + Release = 0, + Commit = 1, + None = 2 } \ No newline at end of file diff --git a/src/Mewdeko/Mewdeko.csproj b/src/Mewdeko/Mewdeko.csproj index b2b2ff306..434cf1e96 100644 --- a/src/Mewdeko/Mewdeko.csproj +++ b/src/Mewdeko/Mewdeko.csproj @@ -93,6 +93,7 @@ + diff --git a/src/Mewdeko/Modules/OwnerOnly/OwnerOnly.cs b/src/Mewdeko/Modules/OwnerOnly/OwnerOnly.cs index 04fd6b934..3be59276a 100644 --- a/src/Mewdeko/Modules/OwnerOnly/OwnerOnly.cs +++ b/src/Mewdeko/Modules/OwnerOnly/OwnerOnly.cs @@ -16,23 +16,25 @@ using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Microsoft.EntityFrameworkCore; +using Octokit; using Serilog; namespace Mewdeko.Modules.OwnerOnly; [OwnerOnly] -public class OwnerOnly(DiscordSocketClient client, - Mewdeko bot, - IBotStrings strings, - InteractiveService serv, - ICoordinator coord, - IEnumerable settingServices, - DbService db, - IDataCache cache, - CommandService commandService, - IServiceProvider services, - GuildSettingsService guildSettings, - CommandHandler commandHandler) +public class OwnerOnly( + DiscordSocketClient client, + Mewdeko bot, + IBotStrings strings, + InteractiveService serv, + ICoordinator coord, + IEnumerable settingServices, + DbService db, + IDataCache cache, + CommandService commandService, + IServiceProvider services, + GuildSettingsService guildSettings, + CommandHandler commandHandler) : MewdekoModuleBase { public enum SettableUserStatus @@ -53,6 +55,150 @@ public async Task ClearUsedTokens() } } + [Cmd, Aliases] + public async Task Update() + { + var shell = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd" : "/bin/bash"; + + var buttons = new ComponentBuilder() + .WithButton("Stable", "stable") + .WithButton("Nightly", "nightly", ButtonStyle.Danger); + + var eb = new EmbedBuilder() + .WithOkColor() + .WithDescription("Which version would you like to check updates against?"); + + var msg = await ctx.Channel.SendMessageAsync(embed: eb.Build(), components: buttons.Build()); + var result = await GetButtonInputAsync(ctx.Channel.Id, msg.Id, ctx.User.Id); + + if (result is null) + { + await msg.ModifyAsync(x => x.Embed = new EmbedBuilder() + .WithErrorColor() + .WithDescription("Timed out.") + .Build()); + return; + } + + var branch = result switch + { + "stable" => "main", + "nightly" => "psqldeko", + _ => "main" + }; + + var process = Process.Start(new ProcessStartInfo + { + FileName = shell, + Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "/c \"git rev-parse --abbrev-ref HEAD\"" + : "-c \"git rev-parse --abbrev-ref HEAD\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }); + await process.WaitForExitAsync(); + var text = await process.StandardOutput.ReadToEndAsync(); + if (text.Replace("\n", "") != branch) + { + if (await PromptUserConfirmAsync( + "Switching branches can cause issues like database incompatibility," + + " or in the case of going from stable to nightly, " + + "major bugs, ***Are you sure you want to continue?***", + ctx.User.Id)) + { + await ctx.Channel.SendConfirmAsync("Switching branches and updating, please wait..."); + var typing = ctx.Channel.EnterTypingState(); + var sw = Stopwatch.StartNew(); + var process2 = Process.Start(new ProcessStartInfo + { + FileName = shell, + Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? $"/c \"git checkout {branch} && git pull\"" + : $"-c \"git checkout {branch} && git pull\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }); + await process2.WaitForExitAsync(); + Log.Information("Update Logs: {UpdateLogs}", await process2.StandardOutput.ReadToEndAsync()); + sw.Stop(); + var eb2 = new EmbedBuilder() + .WithDescription("Update complete.") + .WithOkColor() + .WithFooter("Time taken: " + sw.Elapsed.ToString("g")); + await ctx.Channel.SendMessageAsync(embed: eb2.Build()); + typing.Dispose(); + } + else + { + await ctx.Channel.SendErrorAsync("Cancelled."); + } + } + else + { + var getCommit = Process.Start(new ProcessStartInfo + { + FileName = shell, + Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "/c \"git rev-parse HEAD\"" + : "-c \"git rev-parse HEAD\"", + RedirectStandardOutput = true, + CreateNoWindow = true + }); + await getCommit.WaitForExitAsync(); + var commit = await getCommit.StandardOutput.ReadToEndAsync(); + var github = new GitHubClient(new ProductHeaderValue("Mewdeko")); + var repo = await github.Repository.Branch.Get("sylveondeko", "Mewdeko", branch); + if (repo is null) + { + await ctx.Channel.SendErrorAsync( + "Failed to get repo info. Please create an issue on the repo or join the support server."); + return; + } + + var commitSha = repo.Commit.Sha; + if (commitSha != commit.Replace("\n", "")) + { + if (await PromptUserConfirmAsync( + "Are you sure you want to update?", + ctx.User.Id)) + { + await ctx.Channel.SendConfirmAsync("Updating, please wait..."); + var typing = ctx.Channel.EnterTypingState(); + var sw = Stopwatch.StartNew(); + var process2 = Process.Start(new ProcessStartInfo + { + FileName = shell, + Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? $"/c git pull" + : $"-c git pull", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }); + await process2.WaitForExitAsync(); + Log.Information("Update Logs: {UpdateLogs}", await process2.StandardOutput.ReadToEndAsync()); + sw.Stop(); + var eb2 = new EmbedBuilder() + .WithDescription("Update complete.") + .WithOkColor() + .WithFooter("Time taken: " + sw.Elapsed.ToString("g")); + await ctx.Channel.SendMessageAsync(embed: eb2.Build()); + typing.Dispose(); + } + else + { + await ctx.Channel.SendErrorAsync("Cancelled."); + } + } + else + { + await ctx.Channel.SendErrorAsync("Already up to date."); + } + } + } + [Cmd, Aliases] public async Task Sudo(IGuildUser user, [Remainder] string args) { diff --git a/src/Mewdeko/Modules/OwnerOnly/Services/OwnerOnlyService.cs b/src/Mewdeko/Modules/OwnerOnly/Services/OwnerOnlyService.cs index e29859d58..ddff5ef11 100644 --- a/src/Mewdeko/Modules/OwnerOnly/Services/OwnerOnlyService.cs +++ b/src/Mewdeko/Modules/OwnerOnly/Services/OwnerOnlyService.cs @@ -3,11 +3,13 @@ using System.Net.Http; using System.Text; using System.Threading; +using Mewdeko.Common.Configs; using Mewdeko.Common.ModuleBehaviors; using Mewdeko.Services.Settings; using Mewdeko.Services.strings; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Octokit; using OpenAI_API; using OpenAI_API.Chat; using OpenAI_API.Models; @@ -68,7 +70,7 @@ public OwnerOnlyService(DiscordSocketClient client, CommandHandler cmdHandler, D .WithProviders(phProviders) .Build(); - _ = Task.Run(async () => await RotatingStatuses()); + _ = Task.Run(RotatingStatuses); } var sub = redis.GetSubscriber(); @@ -106,6 +108,174 @@ public OwnerOnlyService(DiscordSocketClient client, CommandHandler cmdHandler, D // ignored } }, CommandFlags.FireAndForget); + + _ = CheckUpdateTimer(); + handler.GuildMemberUpdated += QuarantineCheck; + } + + private async Task QuarantineCheck(Cacheable args, SocketGuildUser arsg2) + { + if (!args.HasValue) + return; + + if (args.Id != client.CurrentUser.Id) + return; + + var value = args.Value; + + if (value.Roles is null) + return; + + + if (!bss.Data.QuarantineNotification) + return; + + if (!Equals(value.Roles, arsg2.Roles)) + { + var quarantineRole = value.Guild.Roles.FirstOrDefault(x => x.Name == "Quarantine"); + if (quarantineRole is null) + return; + + if (value.Roles.All(x => x.Id != quarantineRole.Id) && arsg2.Roles.Any(x => x.Id == quarantineRole.Id)) + { + if (bss.Data.ForwardToAllOwners) + { + foreach (var i in creds.OwnerIds) + { + var user = await client.Rest.GetUserAsync(i); + if (user is null) continue; + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync( + $"Quarantined in {value.Guild.Name} [{value.Guild.Id}]"); + } + } + else + { + var user = await client.Rest.GetUserAsync(creds.OwnerIds[0]); + if (user is not null) + { + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync( + $"Quarantined in {value.Guild.Name} [{value.Guild.Id}]"); + } + } + } + } + } + + private async Task CheckUpdateTimer() + { + using var timer = new PeriodicTimer(TimeSpan.FromHours(bss.Data.CheckUpdateInterval)); + do + { + var github = new GitHubClient(new ProductHeaderValue("Mewdeko")); + var redis = cache.Redis.GetDatabase(); + switch (bss.Data.CheckForUpdates) + { + case UpdateCheckType.Release: + var latestRelease = await github.Repository.Release.GetLatest("SylveonDeko", "Mewdeko"); + var eb = new EmbedBuilder() + .WithAuthor($"New Release found: {latestRelease.TagName}", + "https://seeklogo.com/images/G/github-logo-5F384D0265-seeklogo.com.png", + latestRelease.HtmlUrl) + .WithDescription( + $"- If on Windows, you can download the new release [here]({latestRelease.Assets[0].BrowserDownloadUrl})\n" + + $"- If running source just run the `{bss.Data.Prefix}update command and the bot will do the rest for you.`") + .WithOkColor(); + var list = await redis.StringGetAsync($"{creds.RedisKey()}_ReleaseList"); + if (!list.HasValue) + { + await redis.StringSetAsync($"{creds.RedisKey()}_ReleaseList", + JsonConvert.SerializeObject(latestRelease)); + Log.Information("Setting latest release to {ReleaseTag}", latestRelease.TagName); + } + else + { + var release = JsonConvert.DeserializeObject(list); + if (release.TagName != latestRelease.TagName) + { + Log.Information("New release found: {ReleaseTag}", latestRelease.TagName); + await redis.StringSetAsync($"{creds.RedisKey()}_ReleaseList", + JsonConvert.SerializeObject(latestRelease)); + if (bss.Data.ForwardToAllOwners) + { + foreach (var i in creds.OwnerIds) + { + var user = await client.Rest.GetUserAsync(i); + if (user is null) continue; + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync(embed: eb.Build()); + } + } + else + { + var user = await client.Rest.GetUserAsync(creds.OwnerIds[0]); + if (user is not null) + { + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync(embed: eb.Build()); + } + } + } + } + + break; + case UpdateCheckType.Commit: + var latestCommit = + await github.Repository.Commit.Get("SylveonDeko", "Mewdeko", bss.Data.UpdateBranch); + if (latestCommit is null) + { + Log.Warning( + "Failed to get latest commit, make sure you have the correct branch set in bot.yml"); + break; + } + + var redisCommit = await redis.StringGetAsync($"{creds.RedisKey()}_CommitList"); + if (!redisCommit.HasValue) + { + await redis.StringSetAsync($"{creds.RedisKey()}_CommitList", + latestCommit.Sha); + Log.Information("Setting latest commit to {CommitSha}", latestCommit.Sha); + } + else + { + if (redisCommit.ToString() != latestCommit.Sha) + { + Log.Information("New commit found: {CommitSha}", latestCommit.Sha); + await redis.StringSetAsync($"{creds.RedisKey()}_CommitList", + JsonConvert.SerializeObject(latestCommit)); + if (bss.Data.ForwardToAllOwners) + { + foreach (var i in creds.OwnerIds) + { + var user = await client.Rest.GetUserAsync(i); + if (user is null) continue; + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync( + $"New commit found: {latestCommit.Sha}\n{latestCommit.HtmlUrl}"); + } + } + else + { + var user = await client.Rest.GetUserAsync(creds.OwnerIds[0]); + if (user is not null) + { + var channel = await user.CreateDMChannelAsync(); + await channel.SendMessageAsync( + $"New commit found: {latestCommit.Sha}\n{latestCommit.HtmlUrl}"); + } + } + } + } + + break; + case UpdateCheckType.None: + break; + default: + Log.Error("Invalid UpdateCheckType {UpdateCheckType}", bss.Data.CheckForUpdates); + break; + } + } while (await timer.WaitForNextTickAsync()); } private async Task OnMessageReceived(SocketMessage args) diff --git a/src/Mewdeko/Services/Impl/RedisCache.cs b/src/Mewdeko/Services/Impl/RedisCache.cs index a0c121a6f..9eb0ac8c9 100644 --- a/src/Mewdeko/Services/Impl/RedisCache.cs +++ b/src/Mewdeko/Services/Impl/RedisCache.cs @@ -43,6 +43,7 @@ private async Task LoadRedis(ConfigurationOptions options, IBotCredentials creds public IImageCache LocalImages { get; set; } public ILocalDataCache LocalData { get; set; } + // things here so far don't need the bot id // because it's a good thing if different bots // which are hosted on the same PC @@ -75,7 +76,9 @@ public async Task> GetStatusRoleCache() { var db = Redis.GetDatabase(); var result = await db.StringGetAsync($"{redisKey}_statusroles"); - return result.HasValue ? JsonConvert.DeserializeObject>(result) : new List(); + return result.HasValue + ? JsonConvert.DeserializeObject>(result) + : new List(); } public async Task AddProcessingUser(ulong id) @@ -193,7 +196,8 @@ public Task AddIgnoredUsers(ulong guildId, ulong userId, string ignored) public Task TryAddHighlightStaggerUser(ulong userId) { var db = Redis.GetDatabase(); - return Task.FromResult(db.StringSet($"{redisKey}_hstagger_{userId}", 0, TimeSpan.FromMinutes(2), when: When.NotExists, flags: CommandFlags.FireAndForget)); + return Task.FromResult(db.StringSet($"{redisKey}_hstagger_{userId}", 0, TimeSpan.FromMinutes(2), + when: When.NotExists, flags: CommandFlags.FireAndForget)); } public string GetIgnoredUsers(ulong guildId, ulong userId) @@ -335,7 +339,8 @@ public bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time) public Task TryAddHighlightStagger(ulong guildId, ulong userId) { var db = Redis.GetDatabase(); - return Task.FromResult(db.StringSet($"{redisKey}_hstagger_{guildId}_{userId}", 0, TimeSpan.FromMinutes(3), when: When.NotExists, flags: CommandFlags.FireAndForget)); + return Task.FromResult(db.StringSet($"{redisKey}_hstagger_{guildId}_{userId}", 0, TimeSpan.FromMinutes(3), + when: When.NotExists, flags: CommandFlags.FireAndForget)); } public Task GetHighlightStagger(ulong guildId, ulong userId) @@ -374,7 +379,8 @@ public void SetEconomy(string data) public async Task SetGuildSettingBool(ulong guildId, string setting, bool value) { var db = Redis.GetDatabase(); - await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), flags: CommandFlags.FireAndForget).ConfigureAwait(false); + await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), + flags: CommandFlags.FireAndForget).ConfigureAwait(false); } public async Task GetGuildSettingBool(ulong guildId, string setting) @@ -387,7 +393,8 @@ public async Task GetGuildSettingBool(ulong guildId, string setting) public async Task SetGuildSettingInt(ulong guildId, string setting, int value) { var db = Redis.GetDatabase(); - await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), flags: CommandFlags.FireAndForget).ConfigureAwait(false); + await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), + flags: CommandFlags.FireAndForget).ConfigureAwait(false); } public async Task GetGuildSettingInt(ulong guildId, string setting) @@ -404,7 +411,8 @@ public async Task SetShip(ulong user1, ulong user2, int score) { User1 = user1, User2 = user2, Score = score }; - await db.StringSetAsync($"{redisKey}_shipcache:{user1}:{user2}", JsonConvert.SerializeObject(toCache), expiry: TimeSpan.FromHours(12)); + await db.StringSetAsync($"{redisKey}_shipcache:{user1}:{user2}", JsonConvert.SerializeObject(toCache), + expiry: TimeSpan.FromHours(12)); } public async Task GetShip(ulong user1, ulong user2) @@ -417,7 +425,8 @@ public async Task SetShip(ulong user1, ulong user2, int score) public async Task SetGuildSettingString(ulong guildId, string setting, string value) { var db = Redis.GetDatabase(); - await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), flags: CommandFlags.FireAndForget).ConfigureAwait(false); + await db.StringSetAsync($"{redisKey}_{setting}_{guildId}", JsonConvert.SerializeObject(value), + flags: CommandFlags.FireAndForget).ConfigureAwait(false); } public async Task GetGuildSettingString(ulong guildId, string setting) @@ -460,13 +469,15 @@ public void SetLastCurrencyDecay() { var db = Redis.GetDatabase(); - db.StringSet($"{redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow), flags: CommandFlags.FireAndForget); + db.StringSet($"{redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow), + flags: CommandFlags.FireAndForget); } public Task SetStreamDataAsync(string url, string data) { var db = Redis.GetDatabase(); - return db.StringSetAsync($"{redisKey}_stream_{url}", data, TimeSpan.FromHours(6), flags: CommandFlags.FireAndForget); + return db.StringSetAsync($"{redisKey}_stream_{url}", data, TimeSpan.FromHours(6), + flags: CommandFlags.FireAndForget); } public bool TryGetStreamData(string url, out string dataStr) diff --git a/src/Mewdeko/data/aliases.yml b/src/Mewdeko/data/aliases.yml index edcdb8264..57d5929bb 100644 --- a/src/Mewdeko/data/aliases.yml +++ b/src/Mewdeko/data/aliases.yml @@ -10,6 +10,9 @@ gpingrole: gdmmmessage: - gdmmmessage - giveawaydmmessage +update: + - update + - upd gbanner: - gbanner - giveawaybanner diff --git a/src/Mewdeko/data/strings/commands/commands.en-US.yml b/src/Mewdeko/data/strings/commands/commands.en-US.yml index 8aa5e8d66..e92b543a6 100644 --- a/src/Mewdeko/data/strings/commands/commands.en-US.yml +++ b/src/Mewdeko/data/strings/commands/commands.en-US.yml @@ -13,6 +13,10 @@ gpingrole: - "roleid" - "@role" desc: "Sets the role to ping when a giveaway starts" +update: + args: + - "" + desc: "Updates the bot" gdmmessage: args: - "message"