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)
|
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 messageId = ctx.Event.Data!.TargetId!.Value;
|
||||||
var msg = await ctx.Repository.GetFullMessage(messageId);
|
var msg = await ctx.Repository.GetFullMessage(messageId);
|
||||||
if (msg == null)
|
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>.");
|
$"{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)
|
private async Task HandleAdminCommand(Context ctx)
|
||||||
{
|
{
|
||||||
if (ctx.Match("usid", "updatesystemid"))
|
if (ctx.Match("usid", "updatesystemid"))
|
||||||
|
|
@ -133,6 +175,10 @@ public partial class CommandTree
|
||||||
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
|
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
|
||||||
else if (ctx.Match("sr", "systemrecover"))
|
else if (ctx.Match("sr", "systemrecover"))
|
||||||
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
|
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
|
else
|
||||||
await ctx.Reply($"{Emojis.Error} Unknown command.");
|
await ctx.Reply($"{Emojis.Error} Unknown command.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ public static class ContextEntityArgumentsExt
|
||||||
{
|
{
|
||||||
var text = ctx.PeekArgument();
|
var text = ctx.PeekArgument();
|
||||||
if (text.TryParseMention(out var id))
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,17 @@ public class Admin
|
||||||
_cache = cache;
|
_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)
|
public async Task<Embed> CreateEmbed(Context ctx, PKSystem system)
|
||||||
{
|
{
|
||||||
string UntilLimit(int count, int limit)
|
string UntilLimit(int count, int limit)
|
||||||
|
|
@ -44,17 +55,6 @@ public class Admin
|
||||||
return "";
|
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);
|
var config = await ctx.Repository.GetSystemConfig(system.Id);
|
||||||
|
|
||||||
// Fetch/render info for all accounts simultaneously
|
// Fetch/render info for all accounts simultaneously
|
||||||
|
|
@ -78,6 +78,37 @@ public class Admin
|
||||||
return eb.Build();
|
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)
|
public async Task UpdateSystemId(Context ctx)
|
||||||
{
|
{
|
||||||
ctx.AssertBotAdmin();
|
ctx.AssertBotAdmin();
|
||||||
|
|
@ -342,4 +373,127 @@ public class Admin
|
||||||
Color = DiscordUtils.Green,
|
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)
|
if (!HasCommandPrefix(content, _config.ClientId, out var cmdStart) || cmdStart == content.Length)
|
||||||
return false;
|
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
|
// 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
|
// This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string
|
||||||
var trimStartLengthDiff =
|
var trimStartLengthDiff =
|
||||||
|
|
@ -162,6 +167,9 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
||||||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||||
ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel, channel.Id != rootChannel ? channel.Id : default);
|
ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel, channel.Id != rootChannel ? channel.Id : default);
|
||||||
|
|
||||||
|
if (ctx.DenyBotUsage)
|
||||||
|
return false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions);
|
return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions);
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
||||||
MessageContext ctx;
|
MessageContext ctx;
|
||||||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||||
ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId);
|
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 equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
|
||||||
var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id);
|
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
|
// Proxied messages only exist in guild text channels, so skip checking if we're elsewhere
|
||||||
if (!DiscordUtils.IsValidGuildChannel(channel)) return;
|
if (!DiscordUtils.IsValidGuildChannel(channel)) return;
|
||||||
|
var abuse_log = await _repo.GetAbuseLogByAccount(evt.Member!.User!.Id);
|
||||||
|
|
||||||
switch (evt.Emoji.Name.Split("\U0000fe0f", 2)[0])
|
switch (evt.Emoji.Name.Split("\U0000fe0f", 2)[0])
|
||||||
{
|
{
|
||||||
|
|
@ -113,6 +114,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
||||||
case "\u23F0": // Alarm clock
|
case "\u23F0": // Alarm clock
|
||||||
case "\u2757": // Exclamation mark
|
case "\u2757": // Exclamation mark
|
||||||
{
|
{
|
||||||
|
if (abuse_log != null && abuse_log.DenyBotUsage) break;
|
||||||
var msg = await _repo.GetFullMessage(evt.MessageId);
|
var msg = await _repo.GetFullMessage(evt.MessageId);
|
||||||
if (msg != null)
|
if (msg != null)
|
||||||
await HandlePingReaction(evt, msg);
|
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()
|
public async Task Ignore()
|
||||||
{
|
{
|
||||||
await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage,
|
await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage,
|
||||||
new InteractionApplicationCommandCallbackData
|
new InteractionApplicationCommandCallbackData
|
||||||
{
|
{
|
||||||
Components = Event.Message.Components
|
Components = Event.Message?.Components ?? Array.Empty<MessageComponent>()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,12 @@ internal partial class Database: IDatabase
|
||||||
SqlMapper.AddTypeHandler(new NumericIdHandler<MemberId, int>(i => new MemberId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdHandler<MemberId, int>(i => new MemberId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdHandler<SwitchId, int>(i => new SwitchId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdHandler<SwitchId, int>(i => new SwitchId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdHandler<GroupId, int>(i => new GroupId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdHandler<GroupId, int>(i => new GroupId(i)));
|
||||||
|
SqlMapper.AddTypeHandler(new NumericIdHandler<AbuseLogId, int>(i => new AbuseLogId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SystemId, int>(i => new SystemId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SystemId, int>(i => new SystemId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<MemberId, int>(i => new MemberId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<MemberId, int>(i => new MemberId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SwitchId, int>(i => new SwitchId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SwitchId, int>(i => new SwitchId(i)));
|
||||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<GroupId, int>(i => new GroupId(i)));
|
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<GroupId, int>(i => new GroupId(i)));
|
||||||
|
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<AbuseLogId, int>(i => new AbuseLogId(i)));
|
||||||
|
|
||||||
// Register our custom types to Npgsql
|
// Register our custom types to Npgsql
|
||||||
// Without these it'll still *work* but break at the first launch + probably cause other small issues
|
// Without these it'll still *work* but break at the first launch + probably cause other small issues
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,5 @@ public class MessageContext
|
||||||
public int? LatchTimeout { get; }
|
public int? LatchTimeout { get; }
|
||||||
public bool CaseSensitiveProxyTags { get; }
|
public bool CaseSensitiveProxyTags { get; }
|
||||||
public bool ProxyErrorMessageEnabled { get; }
|
public bool ProxyErrorMessageEnabled { get; }
|
||||||
|
public bool DenyBotUsage { get; }
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
allow_autoproxy bool,
|
allow_autoproxy bool,
|
||||||
latch_timeout integer,
|
latch_timeout integer,
|
||||||
case_sensitive_proxy_tags bool,
|
case_sensitive_proxy_tags bool,
|
||||||
proxy_error_message_enabled bool
|
proxy_error_message_enabled bool,
|
||||||
|
deny_bot_usage bool
|
||||||
)
|
)
|
||||||
as $$
|
as $$
|
||||||
-- CTEs to query "static" (accessible only through args) data
|
-- CTEs to query "static" (accessible only through args) data
|
||||||
|
|
@ -28,6 +29,7 @@ as $$
|
||||||
left join systems on systems.id = accounts.system
|
left join systems on systems.id = accounts.system
|
||||||
left join system_config on system_config.system = accounts.system
|
left join system_config on system_config.system = accounts.system
|
||||||
left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id
|
left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id
|
||||||
|
left join abuse_logs on abuse_logs.id = accounts.abuse_log
|
||||||
where accounts.uid = account_id),
|
where accounts.uid = account_id),
|
||||||
guild as (select * from servers where id = guild_id)
|
guild as (select * from servers where id = guild_id)
|
||||||
select
|
select
|
||||||
|
|
@ -50,14 +52,17 @@ as $$
|
||||||
system.account_autoproxy as allow_autoproxy,
|
system.account_autoproxy as allow_autoproxy,
|
||||||
system.latch_timeout as latch_timeout,
|
system.latch_timeout as latch_timeout,
|
||||||
system.case_sensitive_proxy_tags as case_sensitive_proxy_tags,
|
system.case_sensitive_proxy_tags as case_sensitive_proxy_tags,
|
||||||
system.proxy_error_message_enabled as proxy_error_message_enabled
|
system.proxy_error_message_enabled as proxy_error_message_enabled,
|
||||||
|
coalesce(abuse_logs.deny_bot_usage, false) as deny_bot_usage
|
||||||
-- We need a "from" clause, so we just use some bogus data that's always present
|
-- We need a "from" clause, so we just use some bogus data that's always present
|
||||||
-- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data
|
-- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data
|
||||||
from (select 1) as _placeholder
|
from (select 1) as _placeholder
|
||||||
left join system on true
|
left join system on true
|
||||||
left join guild on true
|
left join guild on true
|
||||||
|
left join accounts on true
|
||||||
left join system_last_switch on system_last_switch.system = system.id
|
left join system_last_switch on system_last_switch.system = system.id
|
||||||
left join system_guild on system_guild.system = system.id and system_guild.guild = guild_id
|
left join system_guild on system_guild.system = system.id and system_guild.guild = guild_id
|
||||||
|
left join abuse_logs on abuse_logs.id = accounts.abuse_log
|
||||||
$$ language sql stable rows 1;
|
$$ language sql stable rows 1;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
23
PluralKit.Core/Database/Migrations/44.sql
Normal file
23
PluralKit.Core/Database/Migrations/44.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- database version 44
|
||||||
|
-- add abuse handling measures
|
||||||
|
|
||||||
|
create table abuse_logs (
|
||||||
|
id serial primary key,
|
||||||
|
uuid uuid default gen_random_uuid(),
|
||||||
|
description text,
|
||||||
|
deny_bot_usage bool not null default false,
|
||||||
|
created timestamp not null default (current_timestamp at time zone 'utc')
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table accounts add column abuse_log integer default null references abuse_logs (id) on delete set null;
|
||||||
|
create index abuse_logs_uuid_idx on abuse_logs (uuid);
|
||||||
|
|
||||||
|
-- we now need to handle a row in "accounts" table being created with no
|
||||||
|
-- system (rather than just system being set to null after insert)
|
||||||
|
--
|
||||||
|
-- set default null and drop the sequence (from the column being created
|
||||||
|
-- as type SERIAL)
|
||||||
|
alter table accounts alter column system set default null;
|
||||||
|
drop sequence accounts_system_seq;
|
||||||
|
|
||||||
|
update info set schema_version = 44;
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
using SqlKata;
|
||||||
|
|
||||||
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
public partial class ModelRepository
|
||||||
|
{
|
||||||
|
public Task<AbuseLog?> GetAbuseLogByGuid(Guid id)
|
||||||
|
{
|
||||||
|
var query = new Query("abuse_logs").Where("uuid", id);
|
||||||
|
return _db.QueryFirst<AbuseLog?>(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AbuseLog?> GetAbuseLogByAccount(ulong accountId)
|
||||||
|
{
|
||||||
|
var query = new Query("accounts")
|
||||||
|
.Select("abuse_logs.*")
|
||||||
|
.LeftJoin("abuse_logs", "abuse_logs.id", "accounts.abuse_log")
|
||||||
|
.Where("uid", accountId)
|
||||||
|
.WhereNotNull("abuse_log");
|
||||||
|
|
||||||
|
return _db.QueryFirst<AbuseLog?>(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<ulong>> GetAbuseLogAccounts(AbuseLogId id)
|
||||||
|
{
|
||||||
|
var query = new Query("accounts").Select("uid").Where("abuse_log", id);
|
||||||
|
return _db.Query<ulong>(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AbuseLog> CreateAbuseLog(string? desc = null, bool? denyBotUsage = null, IPKConnection? conn = null)
|
||||||
|
{
|
||||||
|
var query = new Query("abuse_logs").AsInsert(new
|
||||||
|
{
|
||||||
|
description = desc,
|
||||||
|
deny_bot_usage = denyBotUsage,
|
||||||
|
});
|
||||||
|
|
||||||
|
var abuseLog = await _db.QueryFirst<AbuseLog>(conn, query, "returning *");
|
||||||
|
_logger.Information("Created {AbuseLogId}", abuseLog.Id);
|
||||||
|
return abuseLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAbuseLogAccount(AbuseLogId abuseLog, ulong accountId, IPKConnection? conn = null)
|
||||||
|
{
|
||||||
|
var query = new Query("accounts").AsInsert(new
|
||||||
|
{
|
||||||
|
abuse_log = abuseLog,
|
||||||
|
uid = accountId,
|
||||||
|
});
|
||||||
|
await _db.ExecuteQuery(conn, query, "on conflict (uid) do update set abuse_log = @p0");
|
||||||
|
|
||||||
|
_logger.Information("Linked account {UserId} to {AbuseLogId}", accountId, abuseLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AbuseLog> UpdateAbuseLog(AbuseLogId id, AbuseLogPatch patch, IPKConnection? conn = null)
|
||||||
|
{
|
||||||
|
_logger.Information("Updated {AbuseLogId}: {@AbuseLogPatch}", id, patch);
|
||||||
|
var query = patch.Apply(new Query("abuse_logs").Where("id", id));
|
||||||
|
return await _db.QueryFirst<AbuseLog>(conn, query, "returning *");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAbuseLog(AbuseLogId id)
|
||||||
|
{
|
||||||
|
var query = new Query("abuse_logs").AsDelete().Where("id", id);
|
||||||
|
await _db.ExecuteQuery(query);
|
||||||
|
_logger.Information("Deleted {AbuseLogId}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ namespace PluralKit.Core;
|
||||||
internal class DatabaseMigrator
|
internal class DatabaseMigrator
|
||||||
{
|
{
|
||||||
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
|
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
|
||||||
private const int TargetSchemaVersion = 43;
|
private const int TargetSchemaVersion = 44;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public DatabaseMigrator(ILogger logger)
|
public DatabaseMigrator(ILogger logger)
|
||||||
|
|
|
||||||
36
PluralKit.Core/Models/AbuseLog.cs
Normal file
36
PluralKit.Core/Models/AbuseLog.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
public readonly struct AbuseLogId: INumericId<AbuseLogId, int>
|
||||||
|
{
|
||||||
|
public int Value { get; }
|
||||||
|
|
||||||
|
public AbuseLogId(int value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(AbuseLogId other) => Value == other.Value;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => obj is AbuseLogId other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => Value;
|
||||||
|
|
||||||
|
public static bool operator ==(AbuseLogId left, AbuseLogId right) => left.Equals(right);
|
||||||
|
|
||||||
|
public static bool operator !=(AbuseLogId left, AbuseLogId right) => !left.Equals(right);
|
||||||
|
|
||||||
|
public int CompareTo(AbuseLogId other) => Value.CompareTo(other.Value);
|
||||||
|
|
||||||
|
public override string ToString() => $"AbuseLog #{Value}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AbuseLog
|
||||||
|
{
|
||||||
|
public AbuseLogId Id { get; private set; }
|
||||||
|
public Guid Uuid { get; private set; }
|
||||||
|
public string Description { get; private set; }
|
||||||
|
public bool DenyBotUsage { get; private set; }
|
||||||
|
public Instant Created { get; private set; }
|
||||||
|
}
|
||||||
16
PluralKit.Core/Models/Patch/AbuseLogPatch.cs
Normal file
16
PluralKit.Core/Models/Patch/AbuseLogPatch.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
using SqlKata;
|
||||||
|
|
||||||
|
namespace PluralKit.Core;
|
||||||
|
|
||||||
|
public class AbuseLogPatch: PatchObject
|
||||||
|
{
|
||||||
|
public Partial<string> Description { get; set; }
|
||||||
|
public Partial<bool> DenyBotUsage { get; set; }
|
||||||
|
|
||||||
|
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
|
||||||
|
.With("description", Description)
|
||||||
|
.With("deny_bot_usage", DenyBotUsage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,12 @@ public class AccountPatch: PatchObject
|
||||||
{
|
{
|
||||||
public Partial<ulong> DmChannel { get; set; }
|
public Partial<ulong> DmChannel { get; set; }
|
||||||
public Partial<bool> AllowAutoproxy { get; set; }
|
public Partial<bool> AllowAutoproxy { get; set; }
|
||||||
|
public Partial<AbuseLogId?> AbuseLog { get; set; }
|
||||||
|
|
||||||
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
|
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
|
||||||
.With("dm_channel", DmChannel)
|
.With("dm_channel", DmChannel)
|
||||||
.With("allow_autoproxy", AllowAutoproxy)
|
.With("allow_autoproxy", AllowAutoproxy)
|
||||||
|
.With("abuse_log", AbuseLog)
|
||||||
);
|
);
|
||||||
|
|
||||||
public JObject ToJson()
|
public JObject ToJson()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue