Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions Jobs/HoneypotRenameJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -52,7 +55,7 @@ public async Task Execute(IJobExecutionContext context)
// Get all guilds that have honeypot enabled
List<Guild> guilds = await dB.Guilds
.Where(g => g.HoneypotChannelId != 0 && g.SendHoneypotMessages)
.ToListAsync();
.ToListAsync(cancellationToken);

if (!guilds.Any())
{
Expand All @@ -64,6 +67,8 @@ public async Task Execute(IJobExecutionContext context)

foreach (var guild in guilds)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
SocketGuild? discordGuild = discordClient.GetGuild(guild.DiscordId);
Expand Down Expand Up @@ -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);
}
}
}
56 changes: 56 additions & 0 deletions Morpheus.Tests/HoneypotRenameJobTests.cs
Original file line number Diff line number Diff line change
@@ -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<DB> options = new DbContextOptionsBuilder<DB>()
.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<OperationCanceledException>(() => job.Execute(context));
}

private static IJobExecutionContext CreateContext(CancellationToken cancellationToken)
{
JobExecutionContextProxy.CurrentCancellationToken = cancellationToken;
return DispatchProxy.Create<IJobExecutionContext, JobExecutionContextProxy>();
}

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);
}
}
}