From 04fbce01c09da96ef913220b7c6b8bee99915bec Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Sun, 9 Aug 2026 17:10:21 +0000 Subject: [PATCH] fix: complete already removed temporary bans --- Jobs/TemporaryBansJob.cs | 33 +++++++++++++++++-- Morpheus.Tests/TemporaryBansJobTests.cs | 42 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Jobs/TemporaryBansJob.cs b/Jobs/TemporaryBansJob.cs index e7486ea..0ba92b8 100644 --- a/Jobs/TemporaryBansJob.cs +++ b/Jobs/TemporaryBansJob.cs @@ -1,4 +1,6 @@ +using System.Net; using Discord; +using Discord.Net; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; using Morpheus.Database; @@ -36,9 +38,12 @@ public async Task Execute(IJobExecutionContext context) continue; } - await guild.RemoveBanAsync(ban.UserId); - ban.UnbannedAt = DateTime.UtcNow; - Log($"Unbanned user {ban.UserId} from guild {ban.GuildId} (temp ban {ban.Id})."); + bool wasAlreadyUnbanned = await CompleteUnbanAsync( + ban, + () => guild.RemoveBanAsync(ban.UserId)); + Log(wasAlreadyUnbanned + ? $"User {ban.UserId} was already unbanned from guild {ban.GuildId} (temp ban {ban.Id})." + : $"Unbanned user {ban.UserId} from guild {ban.GuildId} (temp ban {ban.Id})."); } catch (Exception ex) { @@ -49,6 +54,28 @@ public async Task Execute(IJobExecutionContext context) await db.SaveChangesAsync(); } + + internal static async Task CompleteUnbanAsync( + TemporaryBan ban, + Func removeBanAsync) + { + bool wasAlreadyUnbanned = false; + try + { + await removeBanAsync(); + } + catch (HttpException ex) when ( + ex.HttpCode == HttpStatusCode.NotFound && + ex.DiscordCode == DiscordErrorCode.UnknownBan) + { + // A moderator may have removed the ban before this job ran. Discord's + // Unknown Ban response means the desired state is already reached. + wasAlreadyUnbanned = true; + } + + ban.UnbannedAt = DateTime.UtcNow; + return wasAlreadyUnbanned; + } } diff --git a/Morpheus.Tests/TemporaryBansJobTests.cs b/Morpheus.Tests/TemporaryBansJobTests.cs index 4fd6089..af611e2 100644 --- a/Morpheus.Tests/TemporaryBansJobTests.cs +++ b/Morpheus.Tests/TemporaryBansJobTests.cs @@ -1,3 +1,6 @@ +using System.Net; +using Discord; +using Discord.Net; using Discord.WebSocket; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -10,6 +13,45 @@ namespace Morpheus.Tests; public class TemporaryBansJobTests { + [Fact] + public async Task CompleteUnbanAsync_WhenBanIsAlreadyMissing_MarksBanCompleted() + { + TemporaryBan ban = new(); + HttpException notFound = new( + HttpStatusCode.NotFound, + null!, + DiscordErrorCode.UnknownBan, + "Unknown Ban", + []); + + bool wasAlreadyUnbanned = await TemporaryBansJob.CompleteUnbanAsync( + ban, + () => Task.FromException(notFound)); + + Assert.True(wasAlreadyUnbanned); + Assert.NotNull(ban.UnbannedAt); + } + + [Fact] + public async Task CompleteUnbanAsync_WhenDifferentDiscordResourceIsMissing_LeavesBanPending() + { + TemporaryBan ban = new(); + HttpException unknownGuild = new( + HttpStatusCode.NotFound, + null!, + DiscordErrorCode.UnknownGuild, + "Unknown Guild", + []); + + HttpException thrown = await Assert.ThrowsAsync(() => + TemporaryBansJob.CompleteUnbanAsync( + ban, + () => Task.FromException(unknownGuild))); + + Assert.Same(unknownGuild, thrown); + Assert.Null(ban.UnbannedAt); + } + [Fact] public async Task Execute_WhenGuildIsUnavailable_LeavesBanPendingForRetry() {