Skip to content

Commit ad3b4a2

Browse files
Implement pardoned warnings (#302)
* Implement pardoning warnings Adds `isPardoned` attribute to warnings; adds /pardon and /unpardon; pardoned warnings are listed separately in !warnings output * Exclude pardoned warnings from totals in !warnings * Implement automatic pardoning on ban for compromised account Automatically pardons warnings issued within last 12 hours when ban is for a compromised account * Fix error from copy-pasting * Fix warnings embed truncation Make truncation dynamic based on what will fit in the embed * Accept review suggestion Co-authored-by: Erisa A <[email protected]> * Use 0 as default value for compromisedAccountBanAutoPardonHours * Show warning pardon status in warning autocomplete * Always show pardoned field in warning embeds, add option to show inline * Add warning autocomplete providers for pardoned & unpardoned warnings Shifted logic for getting warnings to suggest into a single function that all 3 warning autocomplete providers use --------- Co-authored-by: Erisa A <[email protected]>
1 parent a2a3284 commit ad3b4a2

File tree

8 files changed

+267
-36
lines changed

8 files changed

+267
-36
lines changed

Commands/BanCmds.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,26 @@ public async Task EditBanCmd(TextCommandContext ctx,
468468
else
469469
ban.ExpireTime = ban.ActionTime + banDuration;
470470
ban.Reason = reason;
471-
471+
472+
// If ban is for a compromised account, add to list so the context message can be more-easily deleted later
473+
// and pardon any automatic warnings issued within the last 12 hours
474+
if (ban.Reason.ToLower().Contains("compromised"))
475+
{
476+
Program.redis.HashSet("compromisedAccountBans", targetUser.Id, JsonConvert.SerializeObject(ban));
477+
478+
var warnings = (await Program.redis.HashGetAllAsync(targetUser.Id.ToString())).Select(x => JsonConvert.DeserializeObject<UserWarning>(x.Value)).ToList();
479+
foreach (var warning in warnings)
480+
{
481+
if (warning.Type == WarningType.Warning
482+
&& (warning.ModUserId == Program.discord.CurrentUser.Id || (await Program.discord.GetUserAsync(warning.ModUserId)).IsBot)
483+
&& (DateTime.Now - warning.WarnTimestamp).TotalHours < Program.cfgjson.CompromisedAccountBanAutoPardonHours)
484+
{
485+
warning.IsPardoned = true;
486+
await Program.redis.HashSetAsync(warning.TargetUserId.ToString(), warning.WarningId, JsonConvert.SerializeObject(warning));
487+
}
488+
}
489+
}
490+
472491
var guild = await Program.discord.GetGuildAsync(ban.ServerId);
473492

474493
var contextMessage = await DiscordHelpers.GetMessageFromReferenceAsync(ban.ContextMessageReference);

Commands/WarningCmds.cs

Lines changed: 180 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -181,38 +181,70 @@ internal partial class WarningsAutocompleteProvider : IAutoCompleteProvider
181181
{
182182
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext ctx)
183183
{
184-
var list = new List<DiscordAutoCompleteChoice>();
185-
186-
var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user");
187-
if (useroption == default)
188-
{
189-
return list;
190-
}
184+
return await GetWarningsForAutocompleteAsync(ctx);
185+
}
186+
}
187+
188+
internal partial class PardonedWarningsAutocompleteProvider : IAutoCompleteProvider
189+
{
190+
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext ctx)
191+
{
192+
return await GetWarningsForAutocompleteAsync(ctx, pardonedOnly: true);
193+
}
194+
}
195+
196+
internal partial class UnpardonedWarningsAutocompleteProvider : IAutoCompleteProvider
197+
{
198+
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext ctx)
199+
{
200+
return await GetWarningsForAutocompleteAsync(ctx, excludePardoned: true);
201+
}
202+
}
203+
204+
private static async Task<List<DiscordAutoCompleteChoice>> GetWarningsForAutocompleteAsync(AutoCompleteContext ctx, bool excludePardoned = false, bool pardonedOnly = false)
205+
{
206+
if (excludePardoned && pardonedOnly)
207+
throw new ArgumentException("Cannot simultaneously exclude pardoned warnings from autocomplete suggestions and only show pardoned warnings.");
208+
209+
var list = new List<DiscordAutoCompleteChoice>();
191210

192-
var user = await ctx.Client.GetUserAsync((ulong)useroption.Value);
211+
var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user");
212+
if (useroption == default)
213+
{
214+
return list;
215+
}
193216

194-
var warnings = (await Program.redis.HashGetAllAsync(user.Id.ToString()))
195-
.Where(x => JsonConvert.DeserializeObject<UserWarning>(x.Value).Type == WarningType.Warning).ToDictionary(
196-
x => x.Name.ToString(),
197-
x => JsonConvert.DeserializeObject<UserWarning>(x.Value)
198-
).OrderByDescending(x => x.Value.WarningId);
217+
var user = await ctx.Client.GetUserAsync((ulong)useroption.Value);
199218

200-
foreach (var warning in warnings)
201-
{
202-
if (list.Count >= 25)
203-
break;
219+
var warnings = (await Program.redis.HashGetAllAsync(user.Id.ToString()))
220+
.Where(x => JsonConvert.DeserializeObject<UserWarning>(x.Value).Type == WarningType.Warning).ToDictionary(
221+
x => x.Name.ToString(),
222+
x => JsonConvert.DeserializeObject<UserWarning>(x.Value)
223+
).OrderByDescending(x => x.Value.WarningId);
204224

205-
string warningString = $"{StringHelpers.Pad(warning.Value.WarningId)} - {StringHelpers.Truncate(warning.Value.WarnReason, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.UtcNow - warning.Value.WarnTimestamp, true)}";
225+
foreach (var warning in warnings)
226+
{
227+
if (list.Count >= 25)
228+
break;
206229

207-
var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused);
208-
if (focusedOption is not null)
209-
if (warning.Value.WarnReason.Contains((string)focusedOption.Value) || warningString.ToLower().Contains(focusedOption.Value.ToString().ToLower()))
210-
list.Add(new DiscordAutoCompleteChoice(warningString, StringHelpers.Pad(warning.Value.WarningId)));
230+
string warningString = $"{StringHelpers.Pad(warning.Value.WarningId)} - {StringHelpers.Truncate(warning.Value.WarnReason, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.UtcNow - warning.Value.WarnTimestamp, true)}";
231+
if (warning.Value.IsPardoned)
232+
{
233+
if (excludePardoned)
234+
continue;
235+
236+
warningString += " (pardoned)";
211237
}
238+
else if (pardonedOnly)
239+
continue;
212240

213-
return list;
214-
//return Task.FromResult((IEnumerable<DiscordAutoCompleteChoice>)list);
241+
var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused);
242+
if (focusedOption is not null)
243+
if (warning.Value.WarnReason.Contains((string)focusedOption.Value) || warningString.ToLower().Contains(focusedOption.Value.ToString().ToLower()))
244+
list.Add(new DiscordAutoCompleteChoice(warningString, StringHelpers.Pad(warning.Value.WarningId)));
215245
}
246+
247+
return list;
216248
}
217249

218250
[Command("warndetails")]
@@ -379,6 +411,130 @@ await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Progr
379411
.AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), userID: user.Id)));
380412
}
381413
}
414+
415+
[Command("pardon")]
416+
[Description("Pardon a warning.")]
417+
[AllowedProcessors(typeof(SlashCommandProcessor))]
418+
[RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)]
419+
public async Task PardonSlashCommand(SlashCommandContext ctx,
420+
[Parameter("user"), Description("The user to pardon a warning for.")] DiscordUser user,
421+
[SlashAutoCompleteProvider(typeof(UnpardonedWarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to pardon.")] string warning,
422+
[Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool showPublic = false)
423+
{
424+
if (warning.Contains(' '))
425+
{
426+
warning = warning.Split(' ')[0];
427+
}
428+
429+
long warnId;
430+
try
431+
{
432+
warnId = Convert.ToInt64(warning);
433+
}
434+
catch
435+
{
436+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Looks like your warning option was invalid! Give it another go?", ephemeral: true);
437+
return;
438+
}
439+
440+
var warningObject = GetWarning(user.Id, warnId);
441+
442+
if (warningObject is null)
443+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again.", ephemeral: true);
444+
else if (warningObject.Type == WarningType.Note)
445+
{
446+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Make sure you've got the right warning ID.", ephemeral: true);
447+
}
448+
else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warningObject.ModUserId != ctx.User.Id && warningObject.ModUserId != ctx.Client.CurrentUser.Id)
449+
{
450+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!", ephemeral: true);
451+
}
452+
else
453+
{
454+
await ctx.DeferResponseAsync(ephemeral: !showPublic);
455+
456+
if (warningObject.IsPardoned)
457+
{
458+
await ctx.FollowupAsync($"{Program.cfgjson.Emoji.Error} That warning has already been pardoned!");
459+
return;
460+
}
461+
462+
warningObject.IsPardoned = true;
463+
await Program.redis.HashSetAsync(warningObject.TargetUserId.ToString(), warningObject.WarningId.ToString(), JsonConvert.SerializeObject(warningObject));
464+
465+
await LogChannelHelper.LogMessageAsync("mod",
466+
new DiscordMessageBuilder()
467+
.WithContent($"{Program.cfgjson.Emoji.Information} Warning pardoned:" +
468+
$"`{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})")
469+
.AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), true, userID: user.Id))
470+
);
471+
472+
await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully pardoned warning `{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})")
473+
.AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), userID: user.Id, showPardonedInline: true)));
474+
}
475+
}
476+
477+
[Command("unpardon")]
478+
[Description("Unpardon a warning.")]
479+
[AllowedProcessors(typeof(SlashCommandProcessor))]
480+
[RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)]
481+
public async Task UnpardonSlashCommand(SlashCommandContext ctx,
482+
[Parameter("user"), Description("The user to unpardon a warning for.")] DiscordUser user,
483+
[SlashAutoCompleteProvider(typeof(PardonedWarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to unpardon.")] string warning,
484+
[Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool showPublic = false)
485+
{
486+
if (warning.Contains(' '))
487+
{
488+
warning = warning.Split(' ')[0];
489+
}
490+
491+
long warnId;
492+
try
493+
{
494+
warnId = Convert.ToInt64(warning);
495+
}
496+
catch
497+
{
498+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Looks like your warning option was invalid! Give it another go?", ephemeral: true);
499+
return;
500+
}
501+
502+
var warningObject = GetWarning(user.Id, warnId);
503+
504+
if (warningObject is null)
505+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again.", ephemeral: true);
506+
else if (warningObject.Type == WarningType.Note)
507+
{
508+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Make sure you've got the right warning ID.", ephemeral: true);
509+
}
510+
else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warningObject.ModUserId != ctx.User.Id && warningObject.ModUserId != ctx.Client.CurrentUser.Id)
511+
{
512+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!", ephemeral: true);
513+
}
514+
else
515+
{
516+
await ctx.DeferResponseAsync(ephemeral: !showPublic);
517+
518+
if (!warningObject.IsPardoned)
519+
{
520+
await ctx.FollowupAsync($"{Program.cfgjson.Emoji.Error} That warning isn't pardoned!");
521+
return;
522+
}
523+
524+
warningObject.IsPardoned = false;
525+
await Program.redis.HashSetAsync(warningObject.TargetUserId.ToString(), warningObject.WarningId.ToString(), JsonConvert.SerializeObject(warningObject));
526+
527+
await LogChannelHelper.LogMessageAsync("mod",
528+
new DiscordMessageBuilder()
529+
.WithContent($"{Program.cfgjson.Emoji.Information} Warning unpardoned:" +
530+
$"`{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})")
531+
.AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), true, userID: user.Id))
532+
);
533+
534+
await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully unpardoned warning `{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})")
535+
.AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), userID: user.Id, showPardonedInline: true)));
536+
}
537+
}
382538

383539
[
384540
Command("warntextcmd"),

Helpers/BanHelpers.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,24 @@ public static async Task<bool> BanFromServerAsync(ulong targetUserId, string rea
9494
MostRecentBan = newBan;
9595

9696
// If ban is for a compromised account, add to list so the context message can be more-easily deleted later
97+
// and pardon any automatic warnings issued within the last 12 hours
9798
if (compromisedAccount)
99+
{
98100
Program.redis.HashSet("compromisedAccountBans", targetUserId, JsonConvert.SerializeObject(newBan));
101+
102+
var warnings = (await Program.redis.HashGetAllAsync(targetUserId.ToString())).Select(x => JsonConvert.DeserializeObject<UserWarning>(x.Value)).ToList();
103+
foreach (var warning in warnings)
104+
{
105+
if (warning.Type == WarningType.Warning
106+
&& (warning.ModUserId == Program.discord.CurrentUser.Id || (await Program.discord.GetUserAsync(warning.ModUserId)).IsBot)
107+
&& (DateTime.Now - warning.WarnTimestamp).TotalHours < Program.cfgjson.CompromisedAccountBanAutoPardonHours
108+
&& !warning.IsPardoned)
109+
{
110+
warning.IsPardoned = true;
111+
await Program.redis.HashSetAsync(warning.TargetUserId.ToString(), warning.WarningId, JsonConvert.SerializeObject(warning));
112+
}
113+
}
114+
}
99115

100116
try
101117
{

Helpers/MuteHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public static (int MuteHours, int WarnsSinceThreshold) GetHoursToMuteFor(Diction
8181
{
8282
UserWarning entryWarning = entry.Value;
8383
TimeSpan span = DateTime.UtcNow - entryWarning.WarnTimestamp;
84-
if (span <= timeToCheck)
84+
if (span <= timeToCheck && !entryWarning.IsPardoned)
8585
warnsSinceThreshold += 1;
8686
}
8787

0 commit comments

Comments
 (0)