mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
feat: add abuse handling
This commit is contained in:
parent
4bf60a47d7
commit
2dfb851246
17 changed files with 405 additions and 16 deletions
|
|
@ -101,6 +101,14 @@ public class ApplicationCommandProxiedMessage
|
|||
|
||||
public async Task PingMessageAuthor(InteractionContext ctx)
|
||||
{
|
||||
// if the command message was sent by a user account with bot usage disallowed, ignore it
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(ctx.User.Id);
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage)
|
||||
{
|
||||
await ctx.Defer();
|
||||
return;
|
||||
}
|
||||
|
||||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
var msg = await ctx.Repository.GetFullMessage(messageId);
|
||||
if (msg == null)
|
||||
|
|
|
|||
|
|
@ -113,6 +113,48 @@ public partial class CommandTree
|
|||
$"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private async Task HandleAdminAbuseLogCommand(Context ctx)
|
||||
{
|
||||
ctx.AssertBotAdmin();
|
||||
|
||||
if (ctx.Match("n", "new", "create"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogCreate(ctx));
|
||||
else
|
||||
{
|
||||
AbuseLog? abuseLog = null!;
|
||||
var account = await ctx.MatchUser();
|
||||
if (account != null)
|
||||
{
|
||||
abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument()));
|
||||
}
|
||||
|
||||
if (abuseLog == null)
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext())
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogShow(ctx, abuseLog));
|
||||
else if (ctx.Match("au", "adduser"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogAddUser(ctx, abuseLog));
|
||||
else if (ctx.Match("ru", "removeuser"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog));
|
||||
else if (ctx.Match("desc", "description"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDescription(ctx, abuseLog));
|
||||
else if (ctx.Match("deny", "deny-bot-usage"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog));
|
||||
else if (ctx.Match("yeet", "remove", "delete"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDelete(ctx, abuseLog));
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAdminCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("usid", "updatesystemid"))
|
||||
|
|
@ -133,6 +175,10 @@ public partial class CommandTree
|
|||
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
|
||||
else if (ctx.Match("sr", "systemrecover"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
|
||||
else if (ctx.Match("sd", "systemdelete"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
|
||||
else if (ctx.Match("al", "abuselog"))
|
||||
await HandleAdminAbuseLogCommand(ctx);
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} Unknown command.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ public static class ContextEntityArgumentsExt
|
|||
{
|
||||
var text = ctx.PeekArgument();
|
||||
if (text.TryParseMention(out var id))
|
||||
return await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
|
||||
{
|
||||
var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
|
||||
if (user != null) ctx.PopArgument();
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ public class Admin
|
|||
_cache = cache;
|
||||
}
|
||||
|
||||
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
|
||||
{
|
||||
async Task<(ulong Id, User? User)> Inner(ulong id)
|
||||
{
|
||||
var user = await _cache.GetOrFetchUser(_rest, id);
|
||||
return (id, user);
|
||||
}
|
||||
|
||||
return Task.WhenAll(ids.Select(Inner));
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateEmbed(Context ctx, PKSystem system)
|
||||
{
|
||||
string UntilLimit(int count, int limit)
|
||||
|
|
@ -44,17 +55,6 @@ public class Admin
|
|||
return "";
|
||||
}
|
||||
|
||||
Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
|
||||
{
|
||||
async Task<(ulong Id, User? User)> Inner(ulong id)
|
||||
{
|
||||
var user = await _cache.GetOrFetchUser(_rest, id);
|
||||
return (id, user);
|
||||
}
|
||||
|
||||
return Task.WhenAll(ids.Select(Inner));
|
||||
}
|
||||
|
||||
var config = await ctx.Repository.GetSystemConfig(system.Id);
|
||||
|
||||
// Fetch/render info for all accounts simultaneously
|
||||
|
|
@ -78,6 +78,37 @@ public class Admin
|
|||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateAbuseLogEmbed(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
// Fetch/render info for all accounts simultaneously
|
||||
var accounts = await ctx.Repository.GetAbuseLogAccounts(abuseLog.Id);
|
||||
var systems = await Task.WhenAll(accounts.Select(x => ctx.Repository.GetSystemByAccount(x)));
|
||||
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)");
|
||||
|
||||
List<string> flagstr = new();
|
||||
if (abuseLog.DenyBotUsage)
|
||||
flagstr.Add("- bot usage denied");
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title($"Abuse log: {abuseLog.Uuid.ToString()}")
|
||||
.Color(DiscordUtils.Red)
|
||||
.Footer(new Embed.EmbedFooter($"Created on {abuseLog.Created.FormatZoned(ctx.Zone)}"));
|
||||
|
||||
if (systems.Any(x => x != null))
|
||||
{
|
||||
var sysList = string.Join(", ", systems.Select(x => $"`{x.DisplayHid()}`"));
|
||||
eb.Field(new Embed.Field($"{Emojis.Warn} Accounts have registered system(s)", sysList));
|
||||
}
|
||||
|
||||
eb.Field(new Embed.Field("Accounts", string.Join("\n", users).Truncate(1000), true));
|
||||
eb.Field(new Embed.Field("Flags", flagstr.Any() ? string.Join("\n", flagstr) : "(none)", true));
|
||||
|
||||
if (abuseLog.Description != null)
|
||||
eb.Field(new Embed.Field("Description", abuseLog.Description.Truncate(1000)));
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task UpdateSystemId(Context ctx)
|
||||
{
|
||||
ctx.AssertBotAdmin();
|
||||
|
|
@ -342,4 +373,127 @@ public class Admin
|
|||
Color = DiscordUtils.Green,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SystemDelete(Context ctx)
|
||||
{
|
||||
ctx.AssertBotAdmin();
|
||||
|
||||
var target = await ctx.MatchSystem();
|
||||
if (target == null)
|
||||
throw new PKError("Unknown system.");
|
||||
|
||||
await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`",
|
||||
await CreateEmbed(ctx, target));
|
||||
if (!await ctx.ConfirmWithReply(target.Uuid.ToString()))
|
||||
throw new PKError("System deletion cancelled.");
|
||||
|
||||
await ctx.BusyIndicator(async () =>
|
||||
await ctx.Repository.DeleteSystem(target.Id));
|
||||
await ctx.Reply($"{Emojis.Success} System deletion succesful.");
|
||||
}
|
||||
|
||||
public async Task AbuseLogCreate(Context ctx)
|
||||
{
|
||||
var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage");
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
|
||||
|
||||
string? desc = null!;
|
||||
if (ctx.HasNext(false))
|
||||
desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
|
||||
var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage);
|
||||
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
|
||||
|
||||
await ctx.Reply(
|
||||
$"Created new abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply(
|
||||
$"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} "
|
||||
+ $"for accounts associated with abuse log `{abuseLog.Uuid}`.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = ctx.MatchToggle(true);
|
||||
if (abuseLog.DenyBotUsage != value)
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value });
|
||||
|
||||
await ctx.Reply(
|
||||
$"Bot usage is now **{(value ? "denied" : "allowed")}** "
|
||||
+ $"for accounts associated with abuse log `{abuseLog.Uuid}`.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description"))
|
||||
{
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null });
|
||||
await ctx.Reply($"{Emojis.Success} Abuse log description cleared.");
|
||||
}
|
||||
else if (ctx.HasNext())
|
||||
{
|
||||
var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc });
|
||||
await ctx.Reply($"{Emojis.Success} Abuse log description updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for abuse log `{abuseLog.Uuid}`");
|
||||
await ctx.Reply(abuseLog.Description, eb.Build());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
|
||||
|
||||
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
|
||||
await ctx.Reply(
|
||||
$"Added user {account.NameAndMention()} to the abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention).");
|
||||
|
||||
await ctx.Repository.UpdateAccount(account.Id, new()
|
||||
{
|
||||
AbuseLog = null,
|
||||
});
|
||||
|
||||
await ctx.Reply(
|
||||
$"Removed user {account.NameAndMention()} from the abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false))
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Error} Deletion cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Repository.DeleteAbuseLog(abuseLog.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry.");
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +114,11 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (!HasCommandPrefix(content, _config.ClientId, out var cmdStart) || cmdStart == content.Length)
|
||||
return false;
|
||||
|
||||
// if the command message was sent by a user account with bot usage disallowed, ignore it
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(evt.Author.Id);
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage)
|
||||
return false;
|
||||
|
||||
// Trim leading whitespace from command without actually modifying the string
|
||||
// This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string
|
||||
var trimStartLengthDiff =
|
||||
|
|
@ -162,6 +167,9 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||
ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel, channel.Id != rootChannel ? channel.Id : default);
|
||||
|
||||
if (ctx.DenyBotUsage)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
MessageContext ctx;
|
||||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||
ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId);
|
||||
if (ctx.DenyBotUsage)
|
||||
return;
|
||||
|
||||
var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id);
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
// Proxied messages only exist in guild text channels, so skip checking if we're elsewhere
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel)) return;
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(evt.Member!.User!.Id);
|
||||
|
||||
switch (evt.Emoji.Name.Split("\U0000fe0f", 2)[0])
|
||||
{
|
||||
|
|
@ -113,6 +114,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
case "\u23F0": // Alarm clock
|
||||
case "\u2757": // Exclamation mark
|
||||
{
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage) break;
|
||||
var msg = await _repo.GetFullMessage(evt.MessageId);
|
||||
if (msg != null)
|
||||
await HandlePingReaction(evt, msg);
|
||||
|
|
|
|||
|
|
@ -76,12 +76,22 @@ public class InteractionContext
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Defer()
|
||||
{
|
||||
await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource,
|
||||
new InteractionApplicationCommandCallbackData
|
||||
{
|
||||
Components = Array.Empty<MessageComponent>(),
|
||||
Flags = Message.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Ignore()
|
||||
{
|
||||
await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage,
|
||||
new InteractionApplicationCommandCallbackData
|
||||
{
|
||||
Components = Event.Message.Components
|
||||
Components = Event.Message?.Components ?? Array.Empty<MessageComponent>()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue