diff --git a/PluralKit.Bot/ApplicationCommands/Message.cs b/PluralKit.Bot/ApplicationCommands/Message.cs index 1fc43989..f0f239b9 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -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) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index aa45ab6c..2fe3793c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -113,6 +113,48 @@ public partial class CommandTree $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); } + private async Task HandleAdminAbuseLogCommand(Context ctx) + { + ctx.AssertBotAdmin(); + + if (ctx.Match("n", "new", "create")) + await ctx.Execute(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, a => a.AbuseLogShow(ctx, abuseLog)); + else if (ctx.Match("au", "adduser")) + await ctx.Execute(Admin, a => a.AbuseLogAddUser(ctx, abuseLog)); + else if (ctx.Match("ru", "removeuser")) + await ctx.Execute(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog)); + else if (ctx.Match("desc", "description")) + await ctx.Execute(Admin, a => a.AbuseLogDescription(ctx, abuseLog)); + else if (ctx.Match("deny", "deny-bot-usage")) + await ctx.Execute(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog)); + else if (ctx.Match("yeet", "remove", "delete")) + await ctx.Execute(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, a => a.SystemGroupLimit(ctx)); else if (ctx.Match("sr", "systemrecover")) await ctx.Execute(Admin, a => a.SystemRecover(ctx)); + else if (ctx.Match("sd", "systemdelete")) + await ctx.Execute(Admin, a => a.SystemDelete(ctx)); + else if (ctx.Match("al", "abuselog")) + await HandleAdminAbuseLogCommand(ctx); else await ctx.Reply($"{Emojis.Error} Unknown command."); } diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index c3bb1cb2..533e374f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -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; } diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 9e47feb6..442e2205 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -27,6 +27,17 @@ public class Admin _cache = cache; } + private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable 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 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 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 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 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."); + } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index e0387505..9a1ccde4 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -114,6 +114,11 @@ public class MessageCreated: IEventHandler 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 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); diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index 2ba523f2..468a8654 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -69,6 +69,8 @@ public class MessageEdited: IEventHandler 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); diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 633d8f01..5caa7d6f 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -85,6 +85,7 @@ public class ReactionAdded: IEventHandler // 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 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); diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs index a02792c1..444d39f4 100644 --- a/PluralKit.Bot/Utils/InteractionContext.cs +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -76,12 +76,22 @@ public class InteractionContext }); } + public async Task Defer() + { + await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource, + new InteractionApplicationCommandCallbackData + { + Components = Array.Empty(), + 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() }); } diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index d7625dd9..ec3a510a 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -83,10 +83,12 @@ internal partial class Database: IDatabase SqlMapper.AddTypeHandler(new NumericIdHandler(i => new MemberId(i))); SqlMapper.AddTypeHandler(new NumericIdHandler(i => new SwitchId(i))); SqlMapper.AddTypeHandler(new NumericIdHandler(i => new GroupId(i))); + SqlMapper.AddTypeHandler(new NumericIdHandler(i => new AbuseLogId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new SystemId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new MemberId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new SwitchId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new GroupId(i))); + SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new AbuseLogId(i))); // Register our custom types to Npgsql // Without these it'll still *work* but break at the first launch + probably cause other small issues diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 8d424dce..7ac4245e 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -31,4 +31,5 @@ public class MessageContext public int? LatchTimeout { get; } public bool CaseSensitiveProxyTags { get; } public bool ProxyErrorMessageEnabled { get; } + public bool DenyBotUsage { get; } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 7d7e03c5..910366aa 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -17,7 +17,8 @@ allow_autoproxy bool, latch_timeout integer, case_sensitive_proxy_tags bool, - proxy_error_message_enabled bool + proxy_error_message_enabled bool, + deny_bot_usage bool ) as $$ -- CTEs to query "static" (accessible only through args) data @@ -28,6 +29,7 @@ as $$ left join systems on systems.id = 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 abuse_logs on abuse_logs.id = accounts.abuse_log where accounts.uid = account_id), guild as (select * from servers where id = guild_id) select @@ -50,14 +52,17 @@ as $$ system.account_autoproxy as allow_autoproxy, system.latch_timeout as latch_timeout, 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 -- 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 left join system 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_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; diff --git a/PluralKit.Core/Database/Migrations/44.sql b/PluralKit.Core/Database/Migrations/44.sql new file mode 100644 index 00000000..a93597ab --- /dev/null +++ b/PluralKit.Core/Database/Migrations/44.sql @@ -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; diff --git a/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs b/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs new file mode 100644 index 00000000..b2276178 --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs @@ -0,0 +1,70 @@ +using Dapper; + +using SqlKata; + +namespace PluralKit.Core; + +public partial class ModelRepository +{ + public Task GetAbuseLogByGuid(Guid id) + { + var query = new Query("abuse_logs").Where("uuid", id); + return _db.QueryFirst(query); + } + + public Task 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(query); + } + + public Task> GetAbuseLogAccounts(AbuseLogId id) + { + var query = new Query("accounts").Select("uid").Where("abuse_log", id); + return _db.Query(query); + } + + public async Task 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(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 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(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); + } +} \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 432e99c3..f42f057e 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -9,7 +9,7 @@ namespace PluralKit.Core; internal class DatabaseMigrator { 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; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Models/AbuseLog.cs b/PluralKit.Core/Models/AbuseLog.cs new file mode 100644 index 00000000..ffc11798 --- /dev/null +++ b/PluralKit.Core/Models/AbuseLog.cs @@ -0,0 +1,36 @@ +using NodaTime; + +namespace PluralKit.Core; + +public readonly struct AbuseLogId: INumericId +{ + 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; } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/AbuseLogPatch.cs b/PluralKit.Core/Models/Patch/AbuseLogPatch.cs new file mode 100644 index 00000000..050523dc --- /dev/null +++ b/PluralKit.Core/Models/Patch/AbuseLogPatch.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; + +using SqlKata; + +namespace PluralKit.Core; + +public class AbuseLogPatch: PatchObject +{ + public Partial Description { get; set; } + public Partial DenyBotUsage { get; set; } + + public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper + .With("description", Description) + .With("deny_bot_usage", DenyBotUsage) + ); +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/AccountPatch.cs b/PluralKit.Core/Models/Patch/AccountPatch.cs index daba9b9c..60f036c7 100644 --- a/PluralKit.Core/Models/Patch/AccountPatch.cs +++ b/PluralKit.Core/Models/Patch/AccountPatch.cs @@ -8,10 +8,12 @@ public class AccountPatch: PatchObject { public Partial DmChannel { get; set; } public Partial AllowAutoproxy { get; set; } + public Partial AbuseLog { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("dm_channel", DmChannel) .With("allow_autoproxy", AllowAutoproxy) + .With("abuse_log", AbuseLog) ); public JObject ToJson()