diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 61ae35b1..2502e317 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -276,7 +276,7 @@ namespace PluralKit.Bot public LookupContext LookupContextFor(PKSystem target) => System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner; - public LookupContext LookupContextFor(int systemId) => + public LookupContext LookupContextFor(SystemId systemId) => System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner; public Context CheckSystemPrivacy(PKSystem target, PrivacyLevel level) diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 3f871829..1c875ec1 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -87,7 +87,7 @@ namespace PluralKit.Bot var fronters = ctx.MessageContext.LastSwitchMembers; var relevantMember = ctx.MessageContext.AutoproxyMode switch { - AutoproxyMode.Front => fronters.Count > 0 ? await _db.Execute(c => c.QueryMember(fronters[0])) : null, + AutoproxyMode.Front => fronters.Length > 0 ? await _db.Execute(c => c.QueryMember(fronters[0])) : null, AutoproxyMode.Member => await _db.Execute(c => c.QueryMember(ctx.MessageContext.AutoproxyMember.Value)), _ => null }; @@ -97,7 +97,7 @@ namespace PluralKit.Bot break; case AutoproxyMode.Front: { - if (fronters.Count == 0) + if (fronters.Length == 0) eb.WithDescription("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch."); else { @@ -123,7 +123,7 @@ namespace PluralKit.Bot return eb.Build(); } - private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, int? autoproxyMember) => + private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember) => _db.Execute(c => c.ExecuteAsync( "update system_guild set autoproxy_mode = @autoproxyMode, autoproxy_member = @autoproxyMember where guild = @guild and system = @system", diff --git a/PluralKit.Bot/Proxy/ProxyMatcher.cs b/PluralKit.Bot/Proxy/ProxyMatcher.cs index d4f81081..5dcfbdc9 100644 --- a/PluralKit.Bot/Proxy/ProxyMatcher.cs +++ b/PluralKit.Bot/Proxy/ProxyMatcher.cs @@ -48,7 +48,7 @@ namespace PluralKit.Bot AutoproxyMode.Member when ctx.AutoproxyMember != null => members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), - AutoproxyMode.Front when ctx.LastSwitchMembers.Count > 0 => + AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx.LastMessage) => diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 85aac202..b010df62 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Data; using System.IO; +using System.Linq; using System.Threading.Tasks; using App.Metrics; @@ -36,13 +38,14 @@ namespace PluralKit.Core public static void InitStatic() { + DefaultTypeMap.MatchNamesWithUnderscores = true; + // Dapper by default tries to pass ulongs to Npgsql, which rejects them since PostgreSQL technically // doesn't support unsigned types on its own. // Instead we add a custom mapper to encode them as signed integers instead, converting them back and forth. SqlMapper.RemoveTypeMap(typeof(ulong)); SqlMapper.AddTypeHandler(new UlongEncodeAsLongHandler()); SqlMapper.AddTypeHandler(new UlongArrayHandler()); - DefaultTypeMap.MatchNamesWithUnderscores = true; NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); // With the thing we add above, Npgsql already handles NodaTime integration @@ -51,6 +54,14 @@ namespace PluralKit.Core SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); + // Add ID types to Dapper + SqlMapper.AddTypeHandler(new NumericIdHandler(i => new SystemId(i))); + SqlMapper.AddTypeHandler(new NumericIdHandler(i => new MemberId(i))); + SqlMapper.AddTypeHandler(new NumericIdHandler(i => new SwitchId(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))); + // Register our custom types to Npgsql // Without these it'll still *work* but break at the first launch + probably cause other small issues NpgsqlConnection.GlobalTypeMapper.MapComposite("proxy_tag"); @@ -153,5 +164,37 @@ namespace PluralKit.Core public override ulong[] Parse(object value) => Array.ConvertAll((long[]) value, i => (ulong) i); } + + private class NumericIdHandler: SqlMapper.TypeHandler + where T: INumericId + where TInner: IEquatable, IComparable + { + private readonly Func _factory; + + public NumericIdHandler(Func factory) + { + _factory = factory; + } + + public override void SetValue(IDbDataParameter parameter, T value) => parameter.Value = value.Value; + + public override T Parse(object value) => _factory((TInner) value); + } + + private class NumericIdArrayHandler: SqlMapper.TypeHandler + where T: INumericId + where TInner: IEquatable, IComparable + { + private readonly Func _factory; + + public NumericIdArrayHandler(Func factory) + { + _factory = factory; + } + + public override void SetValue(IDbDataParameter parameter, T[] value) => parameter.Value = Array.ConvertAll(value, v => v.Value); + + public override T[] Parse(object value) => Array.ConvertAll((TInner[]) value, v => _factory(v)); + } } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 369479f7..4de64d2c 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -10,18 +10,18 @@ namespace PluralKit.Core /// public class MessageContext { - public int? SystemId { get; } + public SystemId? SystemId { get; } public ulong? LogChannel { get; } public bool InBlacklist { get; } public bool InLogBlacklist { get; } public bool LogCleanupEnabled { get; } public bool ProxyEnabled { get; } public AutoproxyMode AutoproxyMode { get; } - public int? AutoproxyMember { get; } + public MemberId? AutoproxyMember { get; } public ulong? LastMessage { get; } - public int? LastMessageMember { get; } - public int LastSwitch { get; } - public IReadOnlyList LastSwitchMembers { get; } = new int[0]; + public MemberId? LastMessageMember { get; } + public SwitchId LastSwitch { get; } + public MemberId[] LastSwitchMembers { get; } = new MemberId[0]; public Instant LastSwitchTimestamp { get; } public string? SystemTag { get; } public string? SystemAvatar { get; } diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index d57480af..96becdde 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -8,7 +8,7 @@ namespace PluralKit.Core /// public class ProxyMember { - public int Id { get; } + public MemberId Id { get; } public IReadOnlyCollection ProxyTags { get; } = new ProxyTag[0]; public bool KeepProxy { get; } diff --git a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs index 2f156472..b62637af5 100644 --- a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs +++ b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs @@ -9,10 +9,10 @@ namespace PluralKit.Core { public static class DatabaseViewsExt { - public static Task> QueryCurrentFronters(this IPKConnection conn, int system) => + public static Task> QueryCurrentFronters(this IPKConnection conn, SystemId system) => conn.QueryAsync("select * from system_fronters where system = @system", new {system}); - public static Task> QueryMemberList(this IPKConnection conn, int system, PrivacyLevel? privacyFilter = null, string? filter = null, bool includeDescriptionInNameFilter = false) + public static Task> QueryMemberList(this IPKConnection conn, SystemId system, PrivacyLevel? privacyFilter = null, string? filter = null, bool includeDescriptionInNameFilter = false) { StringBuilder query = new StringBuilder("select * from member_list where system = @system"); diff --git a/PluralKit.Core/Database/Views/SystemFronter.cs b/PluralKit.Core/Database/Views/SystemFronter.cs index c727dc22..4983b720 100644 --- a/PluralKit.Core/Database/Views/SystemFronter.cs +++ b/PluralKit.Core/Database/Views/SystemFronter.cs @@ -4,10 +4,10 @@ namespace PluralKit.Core { public class SystemFronter { - public int SystemId { get; } - public int SwitchId { get; } + public SystemId SystemId { get; } + public SwitchId SwitchId { get; } public Instant SwitchTimestamp { get; } - public int MemberId { get; } + public MemberId MemberId { get; } public string MemberHid { get; } public string MemberName { get; } } diff --git a/PluralKit.Core/Models/INumericId.cs b/PluralKit.Core/Models/INumericId.cs new file mode 100644 index 00000000..25d1eac5 --- /dev/null +++ b/PluralKit.Core/Models/INumericId.cs @@ -0,0 +1,11 @@ +using System; + +namespace PluralKit.Core +{ + public interface INumericId: IEquatable, IComparable + where T: INumericId + where TInner: IEquatable, IComparable + { + public TInner Value { get; } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/MemberGuildSettings.cs b/PluralKit.Core/Models/MemberGuildSettings.cs index 2729f0f2..7a3102cb 100644 --- a/PluralKit.Core/Models/MemberGuildSettings.cs +++ b/PluralKit.Core/Models/MemberGuildSettings.cs @@ -3,7 +3,7 @@ namespace PluralKit.Core { public class MemberGuildSettings { - public int Member { get; } + public MemberId Member { get; } public ulong Guild { get; } public string? DisplayName { get; } public string? AvatarUrl { get; } diff --git a/PluralKit.Core/Models/MemberId.cs b/PluralKit.Core/Models/MemberId.cs new file mode 100644 index 00000000..2fa90ba6 --- /dev/null +++ b/PluralKit.Core/Models/MemberId.cs @@ -0,0 +1,24 @@ +namespace PluralKit.Core +{ + public readonly struct MemberId: INumericId + { + public int Value { get; } + + public MemberId(int value) + { + Value = value; + } + + public bool Equals(MemberId other) => Value == other.Value; + + public override bool Equals(object obj) => obj is MemberId other && Equals(other); + + public override int GetHashCode() => Value; + + public static bool operator ==(MemberId left, MemberId right) => left.Equals(right); + + public static bool operator !=(MemberId left, MemberId right) => !left.Equals(right); + + public int CompareTo(MemberId other) => Value.CompareTo(other.Value); + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/ModelQueryExt.cs b/PluralKit.Core/Models/ModelQueryExt.cs index e91f41c4..e4d7fa5f 100644 --- a/PluralKit.Core/Models/ModelQueryExt.cs +++ b/PluralKit.Core/Models/ModelQueryExt.cs @@ -7,22 +7,22 @@ namespace PluralKit.Core { public static class ModelQueryExt { - public static Task QuerySystem(this IPKConnection conn, int id) => + public static Task QuerySystem(this IPKConnection conn, SystemId id) => conn.QueryFirstOrDefaultAsync("select * from systems where id = @id", new {id}); - public static Task QueryMember(this IPKConnection conn, int id) => + public static Task QueryMember(this IPKConnection conn, MemberId id) => conn.QueryFirstOrDefaultAsync("select * from members where id = @id", new {id}); public static Task QueryOrInsertGuildConfig(this IPKConnection conn, ulong guild) => conn.QueryFirstAsync("insert into servers (id) values (@guild) on conflict (id) do update set id = @guild returning *", new {guild}); - public static Task QueryOrInsertSystemGuildConfig(this IPKConnection conn, ulong guild, int system) => + public static Task QueryOrInsertSystemGuildConfig(this IPKConnection conn, ulong guild, SystemId system) => conn.QueryFirstAsync( "insert into member_guild (guild, member) values (@guild, @member) on conflict (guild, member) do update set guild = @guild, member = @member returning *", new {guild, system}); public static Task QueryOrInsertMemberGuildConfig( - this IPKConnection conn, ulong guild, int member) => + this IPKConnection conn, ulong guild, MemberId member) => conn.QueryFirstAsync( "insert into member_guild (guild, member) values (@guild, @member) on conflict (guild, member) do update set guild = @guild, member = @member returning *", new {guild, member}); diff --git a/PluralKit.Core/Models/PKMember.cs b/PluralKit.Core/Models/PKMember.cs index 73b5d551..c13c9c05 100644 --- a/PluralKit.Core/Models/PKMember.cs +++ b/PluralKit.Core/Models/PKMember.cs @@ -8,9 +8,9 @@ using NodaTime.Text; namespace PluralKit.Core { public class PKMember { - public int Id { get; } + public MemberId Id { get; } public string Hid { get; set; } - public int System { get; set; } + public SystemId System { get; set; } public string Color { get; set; } public string AvatarUrl { get; set; } public string Name { get; set; } diff --git a/PluralKit.Core/Models/PKSwitch.cs b/PluralKit.Core/Models/PKSwitch.cs index b8057a3c..0ac4be40 100644 --- a/PluralKit.Core/Models/PKSwitch.cs +++ b/PluralKit.Core/Models/PKSwitch.cs @@ -3,8 +3,8 @@ namespace PluralKit.Core { public class PKSwitch { - public int Id { get; } - public int System { get; set; } + public SwitchId Id { get; } + public SystemId System { get; set; } public Instant Timestamp { get; } } } \ No newline at end of file diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index c47234ce..73d457a4 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -8,7 +8,7 @@ namespace PluralKit.Core { public class PKSystem { // Additions here should be mirrored in SystemStore::Save - [Key] public int Id { get; } + [Key] public SystemId Id { get; } public string Hid { get; } public string Name { get; set; } public string Description { get; set; } diff --git a/PluralKit.Core/Models/SwitchId.cs b/PluralKit.Core/Models/SwitchId.cs new file mode 100644 index 00000000..11a775a0 --- /dev/null +++ b/PluralKit.Core/Models/SwitchId.cs @@ -0,0 +1,24 @@ +namespace PluralKit.Core +{ + public readonly struct SwitchId: INumericId + { + public int Value { get; } + + public SwitchId(int value) + { + Value = value; + } + + public bool Equals(SwitchId other) => Value == other.Value; + + public override bool Equals(object obj) => obj is SwitchId other && Equals(other); + + public override int GetHashCode() => Value; + + public static bool operator ==(SwitchId left, SwitchId right) => left.Equals(right); + + public static bool operator !=(SwitchId left, SwitchId right) => !left.Equals(right); + + public int CompareTo(SwitchId other) => Value.CompareTo(other.Value); + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/SystemGuildSettings.cs b/PluralKit.Core/Models/SystemGuildSettings.cs index 770b50b2..88afed36 100644 --- a/PluralKit.Core/Models/SystemGuildSettings.cs +++ b/PluralKit.Core/Models/SystemGuildSettings.cs @@ -2,10 +2,10 @@ { public class SystemGuildSettings { - public ulong Guild { get; } + public SystemId Guild { get; } public bool ProxyEnabled { get; } = true; public AutoproxyMode AutoproxyMode { get; } = AutoproxyMode.Off; - public int? AutoproxyMember { get; } + public MemberId? AutoproxyMember { get; } } } \ No newline at end of file diff --git a/PluralKit.Core/Models/SystemId.cs b/PluralKit.Core/Models/SystemId.cs new file mode 100644 index 00000000..42c1336a --- /dev/null +++ b/PluralKit.Core/Models/SystemId.cs @@ -0,0 +1,24 @@ +namespace PluralKit.Core +{ + public readonly struct SystemId: INumericId + { + public int Value { get; } + + public SystemId(int value) + { + Value = value; + } + + public bool Equals(SystemId other) => Value == other.Value; + + public override bool Equals(object obj) => obj is SystemId other && Equals(other); + + public override int GetHashCode() => Value; + + public static bool operator ==(SystemId left, SystemId right) => left.Equals(right); + + public static bool operator !=(SystemId left, SystemId right) => !left.Equals(right); + + public int CompareTo(SystemId other) => Value.CompareTo(other.Value); + } +} \ No newline at end of file diff --git a/PluralKit.Core/Services/IDataStore.cs b/PluralKit.Core/Services/IDataStore.cs index d3d3b141..f6493465 100644 --- a/PluralKit.Core/Services/IDataStore.cs +++ b/PluralKit.Core/Services/IDataStore.cs @@ -45,7 +45,7 @@ namespace PluralKit.Core { public struct SwitchMembersListEntry { - public int Member; + public MemberId Member; public Instant Timestamp; } @@ -131,7 +131,7 @@ namespace PluralKit.Core { /// Gets a system by its internal member ID. /// /// The with the given internal ID, or null if no member was found. - Task GetMemberById(int memberId); + Task GetMemberById(MemberId memberId); /// /// Gets a member by its user-facing human ID. @@ -195,7 +195,7 @@ namespace PluralKit.Core { /// The ID of the original trigger message containing the proxy tags. /// The member (and by extension system) that was proxied. /// - Task AddMessage(IPKConnection conn, ulong senderAccount, ulong guildId, ulong channelId, ulong postedMessageId, ulong triggerMessageId, int proxiedMemberId); + Task AddMessage(IPKConnection conn, ulong senderAccount, ulong guildId, ulong channelId, ulong postedMessageId, ulong triggerMessageId, MemberId proxiedMemberId); /// /// Deletes a message from the data store. diff --git a/PluralKit.Core/Services/PostgresDataStore.cs b/PluralKit.Core/Services/PostgresDataStore.cs index a9c57dc6..4579f012 100644 --- a/PluralKit.Core/Services/PostgresDataStore.cs +++ b/PluralKit.Core/Services/PostgresDataStore.cs @@ -125,7 +125,7 @@ namespace PluralKit.Core { return member; } - public async Task GetMemberById(int id) { + public async Task GetMemberById(MemberId id) { using (var conn = await _conn.Obtain()) return await conn.QuerySingleOrDefaultAsync("select * from members where id = @Id", new { Id = id }); } @@ -177,7 +177,7 @@ namespace PluralKit.Core { return await conn.ExecuteScalarAsync("select count(id) from members"); } - public async Task AddMessage(IPKConnection conn, ulong senderId, ulong guildId, ulong channelId, ulong postedMessageId, ulong triggerMessageId, int proxiedMemberId) { + public async Task AddMessage(IPKConnection conn, ulong senderId, ulong guildId, ulong channelId, ulong postedMessageId, ulong triggerMessageId, MemberId proxiedMemberId) { // "on conflict do nothing" in the (pretty rare) case of duplicate events coming in from Discord, which would lead to a DB error before await conn.ExecuteAsync("insert into messages(mid, guild, channel, member, sender, original_mid) values(@MessageId, @GuildId, @ChannelId, @MemberId, @SenderId, @OriginalMid) on conflict do nothing", new { MessageId = postedMessageId, @@ -334,7 +334,7 @@ namespace PluralKit.Core { // query DB for all members involved in any of the switches above and collect into a dictionary for future use // this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary // key used in GetPerMemberSwitchDuration below - Dictionary memberObjects; + Dictionary memberObjects; using (var conn = await _conn.Obtain()) { memberObjects = ( @@ -351,7 +351,7 @@ namespace PluralKit.Core { select new SwitchListEntry { TimespanStart = g.Key, - Members = g.Where(x => x.Member != 0).Select(x => memberObjects[x.Member]).ToList() + Members = g.Where(x => x.Member != default(MemberId)).Select(x => memberObjects[x.Member]).ToList() }; // Loop through every switch that overlaps the range and add it to the output list diff --git a/PluralKit.Core/Utils/BulkImporter.cs b/PluralKit.Core/Utils/BulkImporter.cs index cd7b848f..4defb17a 100644 --- a/PluralKit.Core/Utils/BulkImporter.cs +++ b/PluralKit.Core/Utils/BulkImporter.cs @@ -16,14 +16,14 @@ namespace PluralKit.Core { public class BulkImporter: IAsyncDisposable { - private readonly int _systemId; + private readonly SystemId _systemId; private readonly IPKConnection _conn; private readonly IPKTransaction _tx; - private readonly Dictionary _knownMembers = new Dictionary(); + private readonly Dictionary _knownMembers = new Dictionary(); private readonly Dictionary _existingMembersByHid = new Dictionary(); private readonly Dictionary _existingMembersByName = new Dictionary(); - private BulkImporter(int systemId, IPKConnection conn, IPKTransaction tx) + private BulkImporter(SystemId systemId, IPKConnection conn, IPKTransaction tx) { _systemId = systemId; _conn = conn; @@ -124,7 +124,7 @@ namespace PluralKit.Core // Fetch the existing switches in the database so we can avoid duplicates var existingSwitches = (await _conn.QueryAsync("select * from switches where system = @System", new {System = _systemId})).ToList(); var existingTimestamps = existingSwitches.Select(sw => sw.Timestamp).ToImmutableHashSet(); - var lastSwitchId = existingSwitches.Count != 0 ? existingSwitches.Select(sw => sw.Id).Max() : -1; + var lastSwitchId = existingSwitches.Count != 0 ? existingSwitches.Select(sw => sw.Id).Max() : (SwitchId?) null; // Import switch definitions var importedSwitches = new Dictionary(); @@ -152,7 +152,7 @@ namespace PluralKit.Core // IDs are sequential, so any ID in this system, with a switch ID > the last max, will be one we just added var justAddedSwitches = await _conn.QueryAsync( "select * from switches where system = @System and id > @LastSwitchId", - new {System = _systemId, LastSwitchId = lastSwitchId}); + new {System = _systemId, LastSwitchId = lastSwitchId?.Value ?? -1}); // Lastly, import the switch members await using (var importer = _conn.BeginBinaryImport("copy switch_members (switch, member) from stdin (format binary)"))