From 69f1e83945e3e7fc1a2f92ba6a94abae26169fd9 Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Thu, 20 Aug 2026 05:06:25 +0000 Subject: [PATCH] fix: honor honeypot rename cancellation --- Jobs/HoneypotRenameJob.cs | 17 +++++-- Morpheus.Tests/HoneypotRenameJobTests.cs | 56 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 Morpheus.Tests/HoneypotRenameJobTests.cs diff --git a/Jobs/HoneypotRenameJob.cs b/Jobs/HoneypotRenameJob.cs index 064f2db..81f6e72 100644 --- a/Jobs/HoneypotRenameJob.cs +++ b/Jobs/HoneypotRenameJob.cs @@ -42,6 +42,9 @@ public static string GetHoneypotChannelName(string prefix = "honeypot", int suff public async Task Execute(IJobExecutionContext context) { + CancellationToken cancellationToken = context.CancellationToken; + cancellationToken.ThrowIfCancellationRequested(); + if (discordClient.CurrentUser == null) { Log("Discord client not ready; skipping honeypot rename run.", LogSeverity.Warning); @@ -52,7 +55,7 @@ public async Task Execute(IJobExecutionContext context) // Get all guilds that have honeypot enabled List guilds = await dB.Guilds .Where(g => g.HoneypotChannelId != 0 && g.SendHoneypotMessages) - .ToListAsync(); + .ToListAsync(cancellationToken); if (!guilds.Any()) { @@ -64,6 +67,8 @@ public async Task Execute(IJobExecutionContext context) foreach (var guild in guilds) { + cancellationToken.ThrowIfCancellationRequested(); + try { SocketGuild? discordGuild = discordClient.GetGuild(guild.DiscordId); @@ -95,16 +100,22 @@ public async Task Execute(IJobExecutionContext context) continue; } - await channel.ModifyAsync(props => props.Name = newName); + await channel.ModifyAsync( + props => props.Name = newName, + new RequestOptions { CancelToken = cancellationToken }); Log($"Renamed honeypot channel {guild.HoneypotChannelId} in guild {guild.Name} to '{newName}'."); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Log($"Failed to rename honeypot channel for guild {guild.Name} ({guild.DiscordId}): {ex.Message}", LogSeverity.Warning); } // Small delay to avoid hitting global rate limits if running at scale - await Task.Delay(700); + await Task.Delay(700, cancellationToken); } } } diff --git a/Morpheus.Tests/HoneypotRenameJobTests.cs b/Morpheus.Tests/HoneypotRenameJobTests.cs new file mode 100644 index 0000000..8846f14 --- /dev/null +++ b/Morpheus.Tests/HoneypotRenameJobTests.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using Discord.WebSocket; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Morpheus.Database; +using Morpheus.Jobs; +using Morpheus.Services; +using Quartz; + +namespace Morpheus.Tests; + +public class HoneypotRenameJobTests +{ + [Fact] + public async Task Execute_WhenCanceledBeforeLoadingGuilds_PropagatesCancellation() + { + await using SqliteConnection connection = new("Data Source=:memory:"); + await connection.OpenAsync(); + DbContextOptions options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using DB db = new(options); + await db.Database.EnsureCreatedAsync(); + + using DiscordSocketClient discordClient = new(); + HoneypotRenameJob job = new(new LogsService(new LogQueue()), db, discordClient); + using CancellationTokenSource cancellation = new(); + await cancellation.CancelAsync(); + + IJobExecutionContext context = CreateContext(cancellation.Token); + + await Assert.ThrowsAnyAsync(() => job.Execute(context)); + } + + private static IJobExecutionContext CreateContext(CancellationToken cancellationToken) + { + JobExecutionContextProxy.CurrentCancellationToken = cancellationToken; + return DispatchProxy.Create(); + } + + private class JobExecutionContextProxy : DispatchProxy + { + public static CancellationToken CurrentCancellationToken { get; set; } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.ReturnType == typeof(CancellationToken)) + return CurrentCancellationToken; + + Type returnType = targetMethod?.ReturnType ?? typeof(void); + return returnType == typeof(void) || !returnType.IsValueType + ? null + : Activator.CreateInstance(returnType); + } + } +}