mirror of
https://github.com/RPCS3/discord-bot.git
synced 2025-04-13 10:20:29 +00:00
336 lines
16 KiB
C#
336 lines
16 KiB
C#
using CompatApiClient.Utils;
|
|
using CompatBot.Commands.AutoCompleteProviders;
|
|
using CompatBot.Database;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace CompatBot.Commands;
|
|
|
|
[Command("warning"), RequiresBotModRole, AllowDMUsage]
|
|
[Description("Command used to manage warnings")]
|
|
internal static partial class Warnings
|
|
{
|
|
[Command("give")]
|
|
[Description("Issue a new warning to a user")]
|
|
public static async ValueTask Warn(
|
|
SlashCommandContext ctx,
|
|
[Description("User to warn")]
|
|
DiscordUser user,
|
|
[Description("Warning explanation")]
|
|
string reason
|
|
)
|
|
{
|
|
await ctx.DeferResponseAsync(ephemeral: true).ConfigureAwait(false);
|
|
var (saved, suppress, recent, total) = await AddAsync(user.Id, ctx.User, reason).ConfigureAwait(false);
|
|
if (!saved)
|
|
{
|
|
await ctx.RespondAsync($"{Config.Reactions.Failure} Couldn't save the warning, please try again", ephemeral: true).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (!suppress)
|
|
{
|
|
var userMsgContent = $"""
|
|
User warning saved, {user.Mention} has {recent} recent warning{StringUtils.GetSuffix(recent)} ({total} total)
|
|
Warned for: {reason} by {ctx.User.Mention}
|
|
""";
|
|
var userMsg = new DiscordMessageBuilder()
|
|
.WithContent(userMsgContent)
|
|
.AddMention(new UserMention(user));
|
|
await ctx.Channel.SendMessageAsync(userMsg).ConfigureAwait(false);
|
|
}
|
|
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
|
}
|
|
|
|
[Command("update")]
|
|
[Description("Change warning details")]
|
|
public static async ValueTask Edit(
|
|
SlashCommandContext ctx,
|
|
[Description("Warning ID to edit"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
|
int id,
|
|
[Description("Updated warning explanation")]
|
|
string reason,
|
|
[Description("User to filter autocomplete results")]
|
|
DiscordUser? user = null
|
|
)
|
|
{
|
|
await using var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false);
|
|
var warnings = await wdb.Warning.Where(w => id.Equals(w.Id)).ToListAsync().ConfigureAwait(false);
|
|
if (warnings.Count is 0)
|
|
{
|
|
await ctx.RespondAsync($"{Config.Reactions.Failure} Warning not found", ephemeral: true).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var warningToEdit = warnings.First();
|
|
if (warningToEdit.IssuerId != ctx.User.Id)
|
|
{
|
|
await ctx.RespondAsync($"{Config.Reactions.Denied} This warning wasn't issued by you", ephemeral: true).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
warningToEdit.Reason = reason;
|
|
await wdb.SaveChangesAsync().ConfigureAwait(false);
|
|
await ctx.RespondAsync("Warning successfully updated", ephemeral: true).ConfigureAwait(false);
|
|
}
|
|
|
|
[Command("remove")]
|
|
[Description("Removes specified warnings")]
|
|
public static async ValueTask Remove(
|
|
SlashCommandContext ctx,
|
|
[Description("Warning ID to remove"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
|
int id,
|
|
[Description("Reason for warning removal")]
|
|
string reason,
|
|
[Description("User to filter autocomplete results")]
|
|
DiscordUser? user = null
|
|
)
|
|
{
|
|
await ctx.DeferResponseAsync(ephemeral: true).ConfigureAwait(false);
|
|
var removedCount = 0;
|
|
var warnedUserId = 0ul;
|
|
await using (var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false))
|
|
{
|
|
var warningsToRemove = await wdb.Warning.Where(w => w.Id == id).ToListAsync().ConfigureAwait(false);
|
|
if (warningsToRemove.Count > 0)
|
|
{
|
|
foreach (var w in warningsToRemove)
|
|
{
|
|
w.Retracted = true;
|
|
w.RetractedBy = ctx.User.Id;
|
|
w.RetractionReason = reason;
|
|
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
|
|
}
|
|
removedCount = await wdb.SaveChangesAsync().ConfigureAwait(false);
|
|
warnedUserId = warningsToRemove[0].DiscordId;
|
|
}
|
|
}
|
|
if (removedCount is 0)
|
|
await ctx.RespondAsync($"{Config.Reactions.Failure} Failed to remove warning", ephemeral: true).ConfigureAwait(false);
|
|
else
|
|
{
|
|
user ??= await ctx.Client.GetUserAsync(warnedUserId).ConfigureAwait(false);
|
|
await ctx.Channel.SendMessageAsync($"Warning successfully removed for {user.Mention} by {ctx.User.Mention}").ConfigureAwait(false);
|
|
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize(), false).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Command("clear")]
|
|
[Description("Removes **all** warnings for a user")]
|
|
public static async ValueTask Clear(
|
|
SlashCommandContext ctx,
|
|
[Description("User to clear warnings for")]
|
|
DiscordUser user,
|
|
[Description("Reason for clear warning removal")]
|
|
string reason
|
|
)
|
|
{
|
|
try
|
|
{
|
|
await ctx.DeferResponseAsync(ephemeral: true).ConfigureAwait(false);
|
|
int removed;
|
|
await using (var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false))
|
|
{
|
|
var warningsToRemove = await wdb.Warning.Where(w => w.DiscordId == user.Id && !w.Retracted)
|
|
.ToListAsync().ConfigureAwait(false);
|
|
foreach (var w in warningsToRemove)
|
|
{
|
|
w.Retracted = true;
|
|
w.RetractedBy = ctx.User.Id;
|
|
w.RetractionReason = reason;
|
|
w.RetractionTimestamp = DateTime.UtcNow.Ticks;
|
|
}
|
|
removed = await wdb.SaveChangesAsync().ConfigureAwait(false);
|
|
}
|
|
await ctx.Channel.SendMessageAsync($"{removed} warning{StringUtils.GetSuffix(removed)} successfully removed!").ConfigureAwait(false);
|
|
await ListUserWarningsAsync(ctx.Client, ctx.Interaction, user.Id, user.Username.Sanitize()).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Error(e);
|
|
}
|
|
}
|
|
|
|
[Command("revert")]
|
|
[Description("Bring back warning that's been removed before")]
|
|
public static async ValueTask Revert(
|
|
SlashCommandContext ctx,
|
|
[Description("Warning ID to change"), SlashAutoCompleteProvider<WarningAutoCompleteProvider>]
|
|
int id,
|
|
[Description("User to filter autocomplete results")]
|
|
DiscordUser? user = null
|
|
)
|
|
{
|
|
await using var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false);
|
|
var warn = await wdb.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
|
|
if (warn is { Retracted: true })
|
|
{
|
|
warn.Retracted = false;
|
|
warn.RetractedBy = null;
|
|
warn.RetractionReason = null;
|
|
warn.RetractionTimestamp = null;
|
|
await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
|
|
await ctx.RespondAsync($"{Config.Reactions.Success} Reissued the warning", ephemeral: true).ConfigureAwait(false);
|
|
}
|
|
else
|
|
await ctx.RespondAsync($"{Config.Reactions.Failure} Warning is not retracted", ephemeral: true).ConfigureAwait(false);
|
|
}
|
|
|
|
internal static async ValueTask<(bool saved, bool suppress, int recentCount, int totalCount)>
|
|
AddAsync(ulong userId, DiscordUser issuer, string reason, string? fullReason = null)
|
|
{
|
|
try
|
|
{
|
|
await using var wdb = await BotDb.OpenWriteAsync().ConfigureAwait(false);
|
|
await wdb.Warning.AddAsync(
|
|
new()
|
|
{
|
|
DiscordId = userId,
|
|
IssuerId = issuer.Id,
|
|
Reason = reason,
|
|
FullReason = fullReason ?? "",
|
|
Timestamp = DateTime.UtcNow.Ticks
|
|
}
|
|
).ConfigureAwait(false);
|
|
await wdb.SaveChangesAsync().ConfigureAwait(false);
|
|
|
|
var threshold = DateTime.UtcNow.AddMinutes(-15).Ticks;
|
|
var totalCount = wdb.Warning.Count(w => w.DiscordId == userId && !w.Retracted);
|
|
var recentCount = wdb.Warning.Count(w => w.DiscordId == userId && !w.Retracted && w.Timestamp > threshold);
|
|
if (recentCount < 4)
|
|
return (true, false, recentCount, totalCount);
|
|
|
|
Config.Log.Debug("Suicide behavior detected, not spamming with warning responses");
|
|
return (true, true, recentCount, totalCount);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Error(e, "Couldn't save the warning");
|
|
return default;
|
|
}
|
|
}
|
|
|
|
//note: be sure to pass a sanitized userName
|
|
//note2: itneraction must be deferred
|
|
internal static async ValueTask ListUserWarningsAsync(DiscordClient client, DiscordInteraction interaction, ulong userId, string userName, bool skipIfOne = true, bool useFollowup = false)
|
|
{
|
|
try
|
|
{
|
|
var isMod = await interaction.User.IsWhitelistedAsync(client, interaction.Guild).ConfigureAwait(false);
|
|
if (interaction.User.Id != userId && !isMod)
|
|
{
|
|
Config.Log.Error($"Somehow {interaction.User.Username} ({interaction.User.Id}) triggered warning list for {userId}");
|
|
return;
|
|
}
|
|
|
|
const bool ephemeral = true;
|
|
int count, removed;
|
|
bool isKot, isDoggo;
|
|
await using var db = await BotDb.OpenReadAsync().ConfigureAwait(false);
|
|
count = await db.Warning.CountAsync(w => w.DiscordId == userId && !w.Retracted).ConfigureAwait(false);
|
|
removed = await db.Warning.CountAsync(w => w.DiscordId == userId && w.Retracted).ConfigureAwait(false);
|
|
isKot = db.Kot.Any(k => k.UserId == userId);
|
|
isDoggo = db.Doggo.Any(d => d.UserId == userId);
|
|
var response = new DiscordInteractionResponseBuilder().AsEphemeral(ephemeral);
|
|
if (count is 0)
|
|
{
|
|
if (isKot && isDoggo)
|
|
{
|
|
if (new Random().NextDouble() < 0.5)
|
|
isKot = false;
|
|
else
|
|
isDoggo = false;
|
|
}
|
|
var msg = (removed, ephemeral, isKot, isDoggo) switch
|
|
{
|
|
(0, _, true, false) => $"{userName} has no warnings, is an upstanding kot, and a paw bean of this community",
|
|
(0, _, false, true) => $"{userName} has no warnings, is a good boy, and a wiggling tail of this community",
|
|
(0, _, _, _) => $"{userName} has no warnings, is an upstanding citizen, and a pillar of this community",
|
|
(_, true, _, _) => $"{userName} has no warnings ({removed} retracted warning{(removed == 1 ? "" : "s")})",
|
|
(_, _, true, false) => $"{userName} has no warnings, but are they a good kot?",
|
|
(_, _, false, true) => $"{userName} has no warnings, but are they a good boy?",
|
|
_ => $"{userName} has no warnings",
|
|
};
|
|
;
|
|
await interaction.EditOriginalResponseAsync(new(response.WithContent(msg))).ConfigureAwait(false);
|
|
if (!ephemeral || removed is 0)
|
|
return;
|
|
}
|
|
|
|
if (count is 1 && skipIfOne)
|
|
{
|
|
await interaction.EditOriginalResponseAsync(new(response.WithContent("No additional warnings on record"))).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
const int maxWarningsInPublicChannel = 3;
|
|
var showCount = Math.Min(maxWarningsInPublicChannel, count);
|
|
var table = new AsciiTable(
|
|
new AsciiColumn("ID", alignToRight: true),
|
|
new AsciiColumn("±", disabled: !ephemeral || !isMod),
|
|
new AsciiColumn("By", maxWidth: 15),
|
|
new AsciiColumn("On date (UTC)"),
|
|
new AsciiColumn("Reason"),
|
|
new AsciiColumn("Context", disabled: !ephemeral || !isMod, maxWidth: 4096)
|
|
);
|
|
IQueryable<Warning> query = db.Warning.Where(w => w.DiscordId == userId).OrderByDescending(w => w.Id);
|
|
if (!ephemeral || !isMod)
|
|
query = query.Where(w => !w.Retracted);
|
|
if (!ephemeral && !isMod)
|
|
query = query.Take(maxWarningsInPublicChannel);
|
|
foreach (var warning in await query.ToListAsync().ConfigureAwait(false))
|
|
{
|
|
if (warning.Retracted)
|
|
{
|
|
if (isMod && ephemeral)
|
|
{
|
|
var retractedByName = warning.RetractedBy.HasValue
|
|
? await client.GetUserNameAsync(interaction.Channel, warning.RetractedBy.Value, ephemeral, "unknown mod").ConfigureAwait(false)
|
|
: "";
|
|
var retractionTimestamp = warning.RetractionTimestamp.HasValue
|
|
? new DateTime(warning.RetractionTimestamp.Value, DateTimeKind.Utc).ToString("u")
|
|
: "";
|
|
table.Add(warning.Id.ToString(), "-", retractedByName, retractionTimestamp, warning.RetractionReason ?? "", "");
|
|
|
|
var issuerName = warning.IssuerId is 0
|
|
? ""
|
|
: await client.GetUserNameAsync(interaction.Channel, warning.IssuerId, ephemeral, "unknown mod").ConfigureAwait(false);
|
|
var timestamp = warning.Timestamp.HasValue
|
|
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
|
|
: "";
|
|
table.Add(warning.Id.ToString().StrikeThrough(), "+", issuerName.StrikeThrough(), timestamp.StrikeThrough(), warning.Reason.StrikeThrough(), warning.FullReason.StrikeThrough());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var issuerName = warning.IssuerId is 0
|
|
? ""
|
|
: await client.GetUserNameAsync(interaction.Channel, warning.IssuerId, ephemeral, "unknown mod").ConfigureAwait(false);
|
|
var timestamp = warning.Timestamp.HasValue
|
|
? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u")
|
|
: "";
|
|
table.Add(warning.Id.ToString(), "+", issuerName, timestamp, warning.Reason, warning.FullReason);
|
|
}
|
|
}
|
|
|
|
var result = new StringBuilder("Warning list for ").Append(Formatter.Sanitize(userName));
|
|
if (!ephemeral && !isMod && count > maxWarningsInPublicChannel)
|
|
result.Append($" (last {showCount} of {count}, full list in DMs)");
|
|
result.AppendLine(":").Append(table);
|
|
var pages = AutosplitResponseHelper.AutosplitMessage(result.ToString());
|
|
await interaction.EditOriginalResponseAsync(new(response.WithContent(pages[0]))).ConfigureAwait(false);
|
|
if (useFollowup)
|
|
{
|
|
foreach (var page in pages.Skip(1).Take(EmbedPager.MaxFollowupMessages))
|
|
{
|
|
var followupMsg = new DiscordFollowupMessageBuilder()
|
|
.AsEphemeral(ephemeral)
|
|
.WithContent(page);
|
|
await interaction.CreateFollowupMessageAsync(followupMsg).ConfigureAwait(false);
|
|
}
|
|
} }
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e);
|
|
}
|
|
}
|
|
} |