diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index eeabda93..61ae35b1 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -25,6 +25,7 @@ namespace PluralKit.Bot private readonly DiscordClient _shard; private readonly DiscordMessage _message; private readonly Parameters _parameters; + private readonly MessageContext _messageContext; private readonly IDataStore _data; private readonly PKSystem _senderSystem; @@ -33,7 +34,7 @@ namespace PluralKit.Bot private Command _currentCommand; public Context(ILifetimeScope provider, DiscordClient shard, DiscordMessage message, int commandParseOffset, - PKSystem senderSystem) + PKSystem senderSystem, MessageContext messageContext) { _rest = provider.Resolve(); _client = provider.Resolve(); @@ -41,6 +42,7 @@ namespace PluralKit.Bot _shard = shard; _data = provider.Resolve(); _senderSystem = senderSystem; + _messageContext = messageContext; _metrics = provider.Resolve(); _provider = provider; _parameters = new Parameters(message.Content.Substring(commandParseOffset)); @@ -52,6 +54,7 @@ namespace PluralKit.Bot public DiscordGuild Guild => _message.Channel.Guild; public DiscordClient Shard => _shard; public DiscordShardedClient Client => _client; + public MessageContext MessageContext => _messageContext; public DiscordRestClient Rest => _rest; diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 714478f4..0f73ad09 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -1,7 +1,8 @@ using System; -using System.Linq; using System.Threading.Tasks; +using Dapper; + using DSharpPlus.Entities; using PluralKit.Core; @@ -10,11 +11,11 @@ namespace PluralKit.Bot { public class Autoproxy { - private IDataStore _data; + private readonly DbConnectionFactory _db; - public Autoproxy(IDataStore data) + public Autoproxy(DbConnectionFactory db) { - _data = data; + _db = db; } public async Task AutoproxyRoot(Context ctx) @@ -39,48 +40,33 @@ namespace PluralKit.Bot private async Task AutoproxyOff(Context ctx) { - var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); - if (settings.AutoproxyMode == AutoproxyMode.Off) - { + if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Off) await ctx.Reply($"{Emojis.Note} Autoproxy is already off in this server."); - } else { - settings.AutoproxyMode = AutoproxyMode.Off; - settings.AutoproxyMember = null; - await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await UpdateAutoproxy(ctx, AutoproxyMode.Off, null); await ctx.Reply($"{Emojis.Success} Autoproxy turned off in this server."); } } private async Task AutoproxyLatch(Context ctx) { - var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); - if (settings.AutoproxyMode == AutoproxyMode.Latch) - { + if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Latch) await ctx.Reply($"{Emojis.Note} Autoproxy is already set to latch mode in this server. If you want to disable autoproxying, use `pk;autoproxy off`."); - } else { - settings.AutoproxyMode = AutoproxyMode.Latch; - settings.AutoproxyMember = null; - await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await UpdateAutoproxy(ctx, AutoproxyMode.Latch, null); await ctx.Reply($"{Emojis.Success} Autoproxy set to latch mode in this server. Messages will now be autoproxied using the *last-proxied member* in this server."); } } private async Task AutoproxyFront(Context ctx) { - var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); - if (settings.AutoproxyMode == AutoproxyMode.Front) - { + if (ctx.MessageContext.AutoproxyMode == AutoproxyMode.Front) await ctx.Reply($"{Emojis.Note} Autoproxy is already set to front mode in this server. If you want to disable autoproxying, use `pk;autoproxy off`."); - } else { - settings.AutoproxyMode = AutoproxyMode.Front; - settings.AutoproxyMember = null; - await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + await UpdateAutoproxy(ctx, AutoproxyMode.Front, null); await ctx.Reply($"{Emojis.Success} Autoproxy set to front mode in this server. Messages will now be autoproxied using the *current first fronter*, if any."); } } @@ -88,51 +74,59 @@ namespace PluralKit.Bot private async Task AutoproxyMember(Context ctx, PKMember member) { ctx.CheckOwnMember(member); - - var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); - settings.AutoproxyMode = AutoproxyMode.Member; - settings.AutoproxyMember = member.Id; - await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, settings); + + await UpdateAutoproxy(ctx, AutoproxyMode.Member, member.Id); await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.Name}** in this server."); } private async Task CreateAutoproxyStatusEmbed(Context ctx) { - var settings = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); - var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy ** - Autoproxies as a specific member"; var eb = new DiscordEmbedBuilder().WithTitle($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); + + 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.Member => await _db.Execute(c => c.QueryMember(ctx.MessageContext.AutoproxyMember.Value)), + _ => null + }; - switch (settings.AutoproxyMode) { + switch (ctx.MessageContext.AutoproxyMode) { case AutoproxyMode.Off: eb.WithDescription($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); break; - case AutoproxyMode.Front: { - var lastSwitch = await _data.GetLatestSwitch(ctx.System); - if (lastSwitch == null) - eb.WithDescription("Autoproxy is currently set to **front mode** in this server, but you have no registered switches. Use the `pk;switch` command to log one."); + case AutoproxyMode.Front: + { + if (fronters.Count == 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 { - var firstMember = await _data.GetSwitchMembers(lastSwitch).FirstOrDefaultAsync(); - eb.WithDescription(firstMember == null - ? "Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered." - : $"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{firstMember.Name.EscapeMarkdown()}** (`{firstMember.Hid}`). To disable, type `pk;autoproxy off`."); + if (relevantMember == null) + throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately."); + eb.WithDescription($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.Name.EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`."); } break; } // AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up - case AutoproxyMode.Member when settings.AutoproxyMember != null: { - var member = await _data.GetMemberById(settings.AutoproxyMember.Value); - eb.WithDescription($"Autoproxy is active for member **{member.Name}** (`{member.Hid}`) in this server. To disable, type `pk;autoproxy off`."); + case AutoproxyMode.Member when relevantMember != null: { + eb.WithDescription($"Autoproxy is active for member **{relevantMember.Name}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`."); break; } case AutoproxyMode.Latch: - eb.WithDescription($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`."); + eb.WithDescription("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`."); break; + default: throw new ArgumentOutOfRangeException(); } return eb.Build(); } + + private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, int? autoproxyMember) => + _db.Execute(c => + c.ExecuteAsync( + "update system_guild set autoproxy_mode = @autoproxyMode, autoproxy_member = @autoproxyMember where guild = @guild and system = @system", + new {autoproxyMode, autoproxyMember, guild = ctx.Guild.Id, system = ctx.System.Id})); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 74df6381..c425381c 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -1,6 +1,10 @@ +#nullable enable +using System; using System.Linq; using System.Threading.Tasks; +using Dapper; + using DSharpPlus; using DSharpPlus.Entities; @@ -10,157 +14,145 @@ namespace PluralKit.Bot { public class MemberAvatar { - private IDataStore _data; + private readonly DbConnectionFactory _db; - public MemberAvatar(IDataStore data) + public MemberAvatar(DbConnectionFactory db) { - _data = data; - } - - public async Task Avatar(Context ctx, PKMember target) - { - var guildData = ctx.Guild != null ? await _data.GetMemberGuildSettings(target, ctx.Guild.Id) : null; - - if (ctx.Match("clear", "remove", "reset") || ctx.MatchFlag("c", "clear")) - { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - target.AvatarUrl = null; - await _data.SaveMember(target); - - if (guildData?.AvatarUrl != null) - await ctx.Reply($"{Emojis.Success} Member avatar cleared. Note that this member has a server-specific avatar set here, type `pk;member {target.Hid} serveravatar clear` if you wish to clear that too."); - else - await ctx.Reply($"{Emojis.Success} Member avatar cleared."); - return; - } - - if (ctx.RemainderOrNull() == null && ctx.Message.Attachments.Count == 0) - { - if ((target.AvatarUrl?.Trim() ?? "").Length > 0) - { - var eb = new DiscordEmbedBuilder() - .WithTitle($"{target.Name.SanitizeMentions()}'s avatar") - .WithImageUrl(target.AvatarUrl); - if (target.System == ctx.System?.Id) - eb.WithDescription($"To clear, use `pk;member {target.Hid} avatar -clear`."); - await ctx.Reply(embed: eb.Build()); - } - else - { - if (target.System == ctx.System?.Id) - throw new PKSyntaxError($"This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); - throw new PKError($"This member does not have an avatar set."); - } - - return; - } - var user = await ctx.MatchUser(); - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - else if (user != null) - { - if (user.AvatarHash == null) throw Errors.UserHasNoAvatar; - target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); - - await _data.SaveMember(target); - - var embed = new DiscordEmbedBuilder().WithImageUrl(target.AvatarUrl).Build(); - await ctx.Reply( - $"{Emojis.Success} Member avatar changed to {user.Username}'s avatar! {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's avatar will need to be re-set.", embed: embed); - } - else if (ctx.RemainderOrNull() is string url) - { - if (url.Length > Limits.MaxUriLength) throw Errors.InvalidUrl(url); - await AvatarUtils.VerifyAvatarOrThrow(url); - target.AvatarUrl = url; - await _data.SaveMember(target); - - var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); - await ctx.Reply($"{Emojis.Success} Member avatar changed.", embed: embed); - } - else if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment attachment) - { - await AvatarUtils.VerifyAvatarOrThrow(attachment.Url); - target.AvatarUrl = attachment.Url; - await _data.SaveMember(target); - - await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working."); - } - // No-arguments no-attachment case covered by conditional at the very top + _db = db; } - public async Task ServerAvatar(Context ctx, PKMember target) + private async Task AvatarClear(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs) { - ctx.CheckGuildContext(); - var guildData = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); - - if (ctx.Match("clear", "remove", "reset") || ctx.MatchFlag("c", "clear")) + ctx.CheckSystem().CheckOwnMember(target); + await UpdateAvatar(location, ctx, target, null); + if (location == AvatarLocation.Server) { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - guildData.AvatarUrl = null; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); - if (target.AvatarUrl != null) await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member will now use the global avatar in this server (**{ctx.Guild.Name}**)."); else await ctx.Reply($"{Emojis.Success} Member server avatar cleared. This member now has no avatar."); - - return; } - - if (ctx.RemainderOrNull() == null && ctx.Message.Attachments.Count == 0) + else { - if ((guildData.AvatarUrl?.Trim() ?? "").Length > 0) + if (mgs?.AvatarUrl != null) + await ctx.Reply($"{Emojis.Success} Member avatar cleared. Note that this member has a server-specific avatar set here, type `pk;member {target.Hid} serveravatar clear` if you wish to clear that too."); + else + await ctx.Reply($"{Emojis.Success} Member avatar cleared."); + } + } + + private async Task AvatarShow(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? guildData) + { + var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar"; + var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar"; + + var currentValue = location == AvatarLocation.Member ? target.AvatarUrl : guildData?.AvatarUrl; + if (string.IsNullOrEmpty(currentValue)) + { + if (location == AvatarLocation.Member) { - var eb = new DiscordEmbedBuilder() - .WithTitle($"{target.Name.SanitizeMentions()}'s server avatar (for {ctx.Guild.Name})") - .WithImageUrl(guildData.AvatarUrl); if (target.System == ctx.System?.Id) - eb.WithDescription($"To clear, use `pk;member {target.Hid} serveravatar clear`."); - await ctx.Reply(embed: eb.Build()); + throw new PKSyntaxError("This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + throw new PKError("This member does not have an avatar set."); } - else + + if (location == AvatarLocation.Server) throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Hid} avatar` to see their global avatar."); - - return; } - var user = await ctx.MatchUser(); - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - if (user != null) - { - if (user.AvatarHash == null) throw Errors.UserHasNoAvatar; - guildData.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); + var eb = new DiscordEmbedBuilder() + .WithTitle($"{target.Name.SanitizeMentions()}'s {field}") + .WithImageUrl(currentValue); + if (target.System == ctx.System?.Id) + eb.WithDescription($"To clear, use `pk;member {target.Hid} {cmd} clear`."); + await ctx.Reply(embed: eb.Build()); + } + + private async Task AvatarFromUser(AvatarLocation location, Context ctx, PKMember target, DiscordUser user) + { + if (user.AvatarHash == null) throw Errors.UserHasNoAvatar; - var embed = new DiscordEmbedBuilder().WithImageUrl(guildData.AvatarUrl).Build(); - await ctx.Reply( - $"{Emojis.Success} Member server avatar changed to {user.Username}'s avatar! This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**). {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's server avatar will need to be re-set.", embed: embed); - } - else if (ctx.RemainderOrNull() is string url) - { - if (url.Length > Limits.MaxUriLength) throw Errors.InvalidUrl(url); - await AvatarUtils.VerifyAvatarOrThrow(url); - guildData.AvatarUrl = url; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); + var url = user.GetAvatarUrl(ImageFormat.Png, 256); + await UpdateAvatar(location, ctx, target, url); + + var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); + if (location == AvatarLocation.Server) + await ctx.Reply($"{Emojis.Success} Member server avatar changed to {user.Username}'s avatar! This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**). {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's server avatar will need to be re-set.", embed: embed); + else if (location == AvatarLocation.Member) + await ctx.Reply($"{Emojis.Success} Member avatar changed to {user.Username}'s avatar! {Emojis.Warn} Please note that if {user.Username} changes their avatar, the member's avatar will need to be re-set.", embed: embed); + } - var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); + private async Task AvatarFromArg(AvatarLocation location, Context ctx, PKMember target, string url) + { + if (url.Length > Limits.MaxUriLength) throw Errors.InvalidUrl(url); + await AvatarUtils.VerifyAvatarOrThrow(url); + + await UpdateAvatar(location, ctx, target, url); + + var embed = new DiscordEmbedBuilder().WithImageUrl(url).Build(); + if (location == AvatarLocation.Server) await ctx.Reply($"{Emojis.Success} Member server avatar changed. This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).", embed: embed); - } - else if (ctx.Message.Attachments.FirstOrDefault() is DiscordAttachment attachment) - { - await AvatarUtils.VerifyAvatarOrThrow(attachment.Url); - guildData.AvatarUrl = attachment.Url; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildData); + } + private async Task AvatarFromAttachment(AvatarLocation location, Context ctx, PKMember target, DiscordAttachment attachment) + { + await AvatarUtils.VerifyAvatarOrThrow(attachment.Url); + await UpdateAvatar(location, ctx, target, attachment.Url); + if (location == AvatarLocation.Server) await ctx.Reply($"{Emojis.Success} Member server avatar changed to attached image. This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**). Please note that if you delete the message containing the attachment, the avatar will stop working."); - } - // No-arguments no-attachment case covered by conditional at the very top + else if (location == AvatarLocation.Member) + await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working."); + } + + public async Task ServerAvatar(Context ctx, PKMember target) + { + ctx.CheckGuildContext(); + var guildData = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id)); + await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData); + } + + public async Task Avatar(Context ctx, PKMember target) + { + var guildData = ctx.Guild != null ? + await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id)) + : null; + + await AvatarCommandTree(AvatarLocation.Member, ctx, target, guildData); + } + + private async Task AvatarCommandTree(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? guildData) + { + if (ctx.Match("clear", "remove", "reset") || ctx.MatchFlag("c", "clear")) + await AvatarClear(location, ctx, target, guildData); + else if (ctx.RemainderOrNull() == null && ctx.Message.Attachments.Count == 0) + await AvatarShow(location, ctx, target, guildData); + else if (await ctx.MatchUser() is {} user) + await AvatarFromUser(location, ctx, target, user); + else if (ctx.RemainderOrNull() is {} url) + await AvatarFromArg(location, ctx, target, url); + else if (ctx.Message.Attachments.FirstOrDefault() is {} attachment) + await AvatarFromAttachment(location, ctx, target, attachment); + else throw new Exception("Unexpected condition when parsing avatar command"); + } + + private Task UpdateAvatar(AvatarLocation location, Context ctx, PKMember target, string? avatar) => + location switch + { + AvatarLocation.Server => _db.Execute(c => + c.ExecuteAsync( + "update member_guild set avatar_url = @Avatar where member = @Member and guild = @Guild", + new {Avatar = avatar, Guild = ctx.Guild.Id, Member = target.Id})), + AvatarLocation.Member => _db.Execute(c => + c.ExecuteAsync( + "update members set avatar_url = @Avatar where id = @Member", + new {Avatar = avatar, Member = target.Id})), + _ => throw new ArgumentOutOfRangeException($"Unknown avatar location {location}") + }; + + private enum AvatarLocation + { + Member, + Server } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index e471a2f9..251d5494 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -1,10 +1,9 @@ -using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using DSharpPlus.Entities; +using Dapper; -using NodaTime; +using DSharpPlus.Entities; using PluralKit.Core; @@ -12,11 +11,13 @@ namespace PluralKit.Bot { public class MemberEdit { - private IDataStore _data; + private readonly IDataStore _data; + private readonly DbConnectionFactory _db; - public MemberEdit(IDataStore data) + public MemberEdit(IDataStore data, DbConnectionFactory db) { _data = data; + _db = db; } public async Task Name(Context ctx, PKMember target) { @@ -46,7 +47,7 @@ namespace PluralKit.Bot if (ctx.Guild != null) { - var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + var memberGuildConfig = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName.SanitizeMentions()}) in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name here."); } @@ -224,7 +225,7 @@ namespace PluralKit.Bot { MemberGuildSettings memberGuildConfig = null; if (ctx.Guild != null) - memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + memberGuildConfig = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id)); var eb = new DiscordEmbedBuilder().WithTitle($"Member names") .WithFooter($"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name."); @@ -257,7 +258,7 @@ namespace PluralKit.Bot var successStr = text; if (ctx.Guild != null) { - var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + var memberGuildConfig = await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name, \"{memberGuildConfig.DisplayName.SanitizeMentions()}\", here."; } @@ -296,14 +297,14 @@ namespace PluralKit.Bot public async Task ServerName(Context ctx, PKMember target) { ctx.CheckGuildContext(); - var guildSettings = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); if (MatchClear(ctx)) { CheckEditMemberPermission(ctx, target); - - guildSettings.DisplayName = null; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildSettings); + + await _db.Execute(c => + c.ExecuteAsync("update member_guild set display_name = null where member = @member and guild = @guild", + new {member = target.Id, guild = ctx.Guild.Id})); if (target.DisplayName != null) await ctx.Reply($"{Emojis.Success} Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."); @@ -323,8 +324,10 @@ namespace PluralKit.Bot CheckEditMemberPermission(ctx, target); var newServerName = ctx.RemainderOrNull(); - guildSettings.DisplayName = newServerName; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildSettings); + + await _db.Execute(c => + c.ExecuteAsync("update member_guild set display_name = @newServerName where member = @member and guild = @guild", + new {member = target.Id, guild = ctx.Guild.Id, newServerName})); await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."); } diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 8998631a..ac6b40b7 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -2,6 +2,8 @@ using System; using System.Linq; using System.Threading.Tasks; +using Dapper; + using DSharpPlus; using DSharpPlus.Entities; @@ -16,12 +18,14 @@ namespace PluralKit.Bot public class SystemEdit { private IDataStore _data; + private DbConnectionFactory _db; private EmbedService _embeds; - public SystemEdit(IDataStore data, EmbedService embeds) + public SystemEdit(IDataStore data, EmbedService embeds, DbConnectionFactory db) { _data = data; _embeds = embeds; + _db = db; } public async Task Name(Context ctx) @@ -181,7 +185,7 @@ namespace PluralKit.Bot public async Task SystemProxy(Context ctx) { ctx.CheckSystem().CheckGuildContext(); - var gs = await _data.GetSystemGuildSettings(ctx.System, ctx.Guild.Id); + var gs = await _db.Execute(c => c.QueryOrInsertSystemGuildConfig(ctx.Guild.Id, ctx.System.Id)); bool newValue; if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; @@ -196,8 +200,9 @@ namespace PluralKit.Bot return; } - gs.ProxyEnabled = newValue; - await _data.SetSystemGuildSettings(ctx.System, ctx.Guild.Id, gs); + await _db.Execute(c => + c.ExecuteAsync("update system_guild set proxy_enabled = @newValue where system = @system and guild = @guild", + new {newValue, system = ctx.System.Id, guild = ctx.Guild.Id})); if (newValue) await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **enabled** for your system."); diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index a152b384..59919cf5 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -95,7 +95,7 @@ namespace PluralKit.Bot try { var system = ctx.SystemId != null ? await _data.GetSystemById(ctx.SystemId.Value) : null; - await _tree.ExecuteCommand(new Context(_services, evt.Client, evt.Message, argPos, system)); + await _tree.ExecuteCommand(new Context(_services, evt.Client, evt.Message, argPos, system, ctx)); } catch (PKError) { diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index ce9c1bae..96b32cb4 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -16,12 +16,14 @@ namespace PluralKit.Bot { public class EmbedService { private IDataStore _data; + private DbConnectionFactory _db; private DiscordShardedClient _client; - public EmbedService(DiscordShardedClient client, IDataStore data) + public EmbedService(DiscordShardedClient client, IDataStore data, DbConnectionFactory db) { _client = client; _data = data; + _db = db; } public async Task CreateSystemEmbed(DiscordClient client, PKSystem system, LookupContext ctx) { @@ -93,7 +95,7 @@ namespace PluralKit.Bot { color = DiscordUtils.Gray; } - var guildSettings = guild != null ? await _data.GetMemberGuildSettings(member, guild.Id) : null; + var guildSettings = guild != null ? await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(guild.Id, member.Id)) : null; var guildDisplayName = guildSettings?.DisplayName; var avatar = guildSettings?.AvatarUrl ?? member.AvatarUrl; diff --git a/PluralKit.Core/Models/MemberGuildSettings.cs b/PluralKit.Core/Models/MemberGuildSettings.cs new file mode 100644 index 00000000..90003f13 --- /dev/null +++ b/PluralKit.Core/Models/MemberGuildSettings.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace PluralKit.Core +{ + public class MemberGuildSettings + { + public int Member { get; } + public ulong Guild { get; } + public string? DisplayName { get; } + public string? AvatarUrl { get; } + + public MemberGuildSettings() { } + + public MemberGuildSettings(int member, ulong guild) + { + Member = member; + Guild = guild; + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/ModelQueryExt.cs b/PluralKit.Core/Models/ModelQueryExt.cs index f8dca6a7..3cac2599 100644 --- a/PluralKit.Core/Models/ModelQueryExt.cs +++ b/PluralKit.Core/Models/ModelQueryExt.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Data; using System.Threading.Tasks; @@ -8,7 +9,21 @@ namespace PluralKit.Core { public static class ModelQueryExt { + public static Task QueryMember(this IDbConnection conn, int id) => + conn.QueryFirstOrDefaultAsync("select * from members where id = @id", new {id}); + public static Task QueryOrInsertGuildConfig(this IDbConnection conn, ulong guild) => - conn.QueryFirstOrDefaultAsync("insert into servers (id) values (@Guild) on conflict do nothing returning *", new {Guild = guild}); + conn.QueryFirstAsync("insert into servers (id) values (@Guild) on conflict do nothing returning *", new {Guild = guild}); + + public static Task QueryOrInsertSystemGuildConfig(this IDbConnection conn, ulong guild, int 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 IDbConnection conn, ulong guild, int 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}); } } \ No newline at end of file diff --git a/PluralKit.Core/Models/SystemGuildSettings.cs b/PluralKit.Core/Models/SystemGuildSettings.cs new file mode 100644 index 00000000..770b50b2 --- /dev/null +++ b/PluralKit.Core/Models/SystemGuildSettings.cs @@ -0,0 +1,11 @@ +namespace PluralKit.Core +{ + public class SystemGuildSettings + { + public ulong Guild { get; } + public bool ProxyEnabled { get; } = true; + + public AutoproxyMode AutoproxyMode { get; } = AutoproxyMode.Off; + public int? AutoproxyMember { get; } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Services/IDataStore.cs b/PluralKit.Core/Services/IDataStore.cs index f38e5b70..6c90336b 100644 --- a/PluralKit.Core/Services/IDataStore.cs +++ b/PluralKit.Core/Services/IDataStore.cs @@ -48,23 +48,7 @@ namespace PluralKit.Core { public int Member; public Instant Timestamp; } - - public class SystemGuildSettings - { - public ulong Guild { get; set; } - public bool ProxyEnabled { get; set; } = true; - public AutoproxyMode AutoproxyMode { get; set; } = AutoproxyMode.Off; - public int? AutoproxyMember { get; set; } - } - - public class MemberGuildSettings - { - public int Member { get; set; } - public ulong Guild { get; set; } - public string DisplayName { get; set; } - public string AvatarUrl { get; set; } - } public interface IDataStore { /// @@ -112,17 +96,7 @@ namespace PluralKit.Core { /// /// The system to check in. Task> GetConflictingProxies(PKSystem system, ProxyTag tag); - - /// - /// Gets a specific system's guild-specific settings for a given guild. - /// - Task GetSystemGuildSettings(PKSystem system, ulong guild); - /// - /// Saves a specific system's guild-specific settings. - /// - Task SetSystemGuildSettings(PKSystem system, ulong guild, SystemGuildSettings settings); - /// /// Creates a system, auto-generating its corresponding IDs. /// @@ -210,16 +184,6 @@ namespace PluralKit.Core { /// Task DeleteMember(PKMember member); - /// - /// Gets a specific member's guild-specific settings for a given guild. - /// - Task GetMemberGuildSettings(PKMember member, ulong guild); - - /// - /// Saves a specific member's guild-specific settings. - /// - Task SetMemberGuildSettings(PKMember member, ulong guild, MemberGuildSettings settings); - /// /// Gets a message and its information by its ID. /// diff --git a/PluralKit.Core/Services/PostgresDataStore.cs b/PluralKit.Core/Services/PostgresDataStore.cs index 8eb39d54..80775cef 100644 --- a/PluralKit.Core/Services/PostgresDataStore.cs +++ b/PluralKit.Core/Services/PostgresDataStore.cs @@ -35,28 +35,7 @@ namespace PluralKit.Core { Suffix = tag.Suffix }); } - - public async Task GetSystemGuildSettings(PKSystem system, ulong guild) - { - using (var conn = await _conn.Obtain()) - return await conn.QuerySingleOrDefaultAsync( - "select * from system_guild where system = @System and guild = @Guild", - new {System = system.Id, Guild = guild}) ?? new SystemGuildSettings(); - } - public async Task SetSystemGuildSettings(PKSystem system, ulong guild, SystemGuildSettings settings) - { - using (var conn = await _conn.Obtain()) - await conn.ExecuteAsync("insert into system_guild (system, guild, proxy_enabled, autoproxy_mode, autoproxy_member) values (@System, @Guild, @ProxyEnabled, @AutoproxyMode, @AutoproxyMember) on conflict (system, guild) do update set proxy_enabled = @ProxyEnabled, autoproxy_mode = @AutoproxyMode, autoproxy_member = @AutoproxyMember", new - { - System = system.Id, - Guild = guild, - settings.ProxyEnabled, - settings.AutoproxyMode, - settings.AutoproxyMember - }); - _logger.Information("Updated system guild settings {@SystemGuildSettings}", settings); - } - + public async Task CreateSystem(string systemName = null) { PKSystem system; using (var conn = await _conn.Obtain()) @@ -189,23 +168,6 @@ namespace PluralKit.Core { _logger.Information("Deleted member {@Member}", member); } - public async Task GetMemberGuildSettings(PKMember member, ulong guild) - { - using var conn = await _conn.Obtain(); - return await conn.QuerySingleOrDefaultAsync( - "select * from member_guild where member = @Member and guild = @Guild", new { Member = member.Id, Guild = guild}) - ?? new MemberGuildSettings { Guild = guild, Member = member.Id }; - } - - public async Task SetMemberGuildSettings(PKMember member, ulong guild, MemberGuildSettings settings) - { - using var conn = await _conn.Obtain(); - await conn.ExecuteAsync( - "insert into member_guild (member, guild, display_name, avatar_url) values (@Member, @Guild, @DisplayName, @AvatarUrl) on conflict (member, guild) do update set display_name = @DisplayName, avatar_url = @AvatarUrl", - settings); - _logger.Information("Updated member guild settings {@MemberGuildSettings}", settings); - } - public async Task GetSystemMemberCount(PKSystem system, bool includePrivate) { var query = "select count(*) from members where system = @Id"; diff --git a/PluralKit.Core/Utils/DatabaseUtils.cs b/PluralKit.Core/Utils/DatabaseUtils.cs index b7d90705..4ef869eb 100644 --- a/PluralKit.Core/Utils/DatabaseUtils.cs +++ b/PluralKit.Core/Utils/DatabaseUtils.cs @@ -311,6 +311,11 @@ namespace PluralKit.Core public static class DatabaseExt { + public static async Task Execute(this DbConnectionFactory db, Func func) + { + await using var conn = await db.Obtain(); + await func(conn); + } public static async Task Execute(this DbConnectionFactory db, Func> func) { await using var conn = await db.Obtain();