diff --git a/PluralKit/Bot/Commands/MemberCommands.cs b/PluralKit/Bot/Commands/MemberCommands.cs index e6246ede..380b45f1 100644 --- a/PluralKit/Bot/Commands/MemberCommands.cs +++ b/PluralKit/Bot/Commands/MemberCommands.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Discord.Commands; @@ -13,31 +14,57 @@ namespace PluralKit.Bot.Commands [Command("new")] [Remarks("member new ")] + [MustHaveSystem] public async Task NewMember([Remainder] string memberName) { - if (Context.SenderSystem == null) Context.RaiseNoSystemError(); - if (ContextEntity != null) RaiseNoContextError(); - // Warn if member name will be unproxyable (with/without tag) - var maxLength = Context.SenderSystem.Tag != null ? 32 - Context.SenderSystem.Tag.Length - 1 : 32; - if (memberName.Length > maxLength) { - var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Member name too long ({memberName.Length} > {maxLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later)"); + if (memberName.Length > Context.SenderSystem.MaxMemberNameLength) { + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Member name too long ({memberName.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later)"); if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled."); } // Warn if there's already a member by this name var existingMember = await Members.GetByName(Context.SenderSystem, memberName); if (existingMember != null) { - var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name}\" (`{existingMember.Hid}`). Do you want to create another member with the same name?"); + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?"); if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled."); } // Create the member var member = await Members.Create(Context.SenderSystem, memberName); + // Send confirmation and space hint await Context.Channel.SendMessageAsync($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Type `pk;help member` for a list of commands to edit this member."); if (memberName.Contains(" ")) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); } + [Command("rename")] + [Alias("name", "changename", "setname")] + [Remarks("member rename ")] + [MustPassOwnMember] + public async Task RenameMember([Remainder] string newName) { + // TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean? + + // Warn if member name will be unproxyable (with/without tag) + if (newName.Length > Context.SenderSystem.MaxMemberNameLength) { + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} New member name too long ({newName.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to change it anyway?"); + if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled."); + } + + // Warn if there's already a member by this name + var existingMember = await Members.GetByName(Context.SenderSystem, newName); + if (existingMember != null) { + var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?"); + if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled."); + } + + // Rename the mebmer + ContextEntity.Name = newName; + await Members.Save(ContextEntity); + + await Context.Channel.SendMessageAsync($"{Emojis.Success} Member renamed."); + if (newName.Contains(" ")) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); + } + public override async Task ReadContextParameterAsync(string value) { var res = await new PKMemberTypeReader().ReadAsync(Context, value, _services); diff --git a/PluralKit/Bot/Commands/SystemCommands.cs b/PluralKit/Bot/Commands/SystemCommands.cs index 664640d4..e60f6dbf 100644 --- a/PluralKit/Bot/Commands/SystemCommands.cs +++ b/PluralKit/Bot/Commands/SystemCommands.cs @@ -20,7 +20,7 @@ namespace PluralKit.Bot.Commands [Command] public async Task Query(PKSystem system = null) { if (system == null) system = Context.SenderSystem; - if (system == null) Context.RaiseNoSystemError(); + if (system == null) throw Errors.NotOwnSystemError; await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system)); } @@ -29,8 +29,8 @@ namespace PluralKit.Bot.Commands [Remarks("system new ")] public async Task New([Remainder] string systemName = null) { - if (ContextEntity != null) RaiseNoContextError(); - if (Context.SenderSystem != null) throw new PKError("You already have a system registered with PluralKit. To view it, type `pk;system`. If you'd like to delete your system and start anew, type `pk;system delete`, or if you'd like to unlink this account from it, type `pk;unlink`."); + if (ContextEntity != null) throw Errors.NotOwnSystemError; + if (Context.SenderSystem != null) throw Errors.NoSystemError; var system = await Systems.Create(systemName); await Systems.Link(system, Context.User.Id); @@ -39,9 +39,8 @@ namespace PluralKit.Bot.Commands [Command("name")] [Remarks("system name ")] + [MustHaveSystem] public async Task Name([Remainder] string newSystemName = null) { - if (ContextEntity != null) RaiseNoContextError(); - if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newSystemName != null && newSystemName.Length > 250) throw new PKError($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)"); Context.SenderSystem.Name = newSystemName; @@ -51,9 +50,8 @@ namespace PluralKit.Bot.Commands [Command("description")] [Remarks("system description ")] + [MustHaveSystem] public async Task Description([Remainder] string newDescription = null) { - if (ContextEntity != null) RaiseNoContextError(); - if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newDescription != null && newDescription.Length > 1000) throw new PKError($"Your chosen description is too long. ({newDescription.Length} > 250 characters)"); Context.SenderSystem.Description = newDescription; @@ -63,9 +61,8 @@ namespace PluralKit.Bot.Commands [Command("tag")] [Remarks("system tag ")] + [MustHaveSystem] public async Task Tag([Remainder] string newTag = null) { - if (ContextEntity != null) RaiseNoContextError(); - if (Context.SenderSystem == null) Context.RaiseNoSystemError(); if (newTag.Length > 30) throw new PKError($"Your chosen description is too long. ({newTag.Length} > 30 characters)"); Context.SenderSystem.Tag = newTag; @@ -83,10 +80,8 @@ namespace PluralKit.Bot.Commands [Command("delete")] [Remarks("system delete")] + [MustHaveSystem] public async Task Delete() { - if (ContextEntity != null) RaiseNoContextError(); - if (Context.SenderSystem == null) Context.RaiseNoSystemError(); - var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{Context.SenderSystem.Hid}`).\n**Note: this action is permanent.**"); var reply = await Context.AwaitMessage(Context.Channel, Context.User, timeout: TimeSpan.FromMinutes(1)); if (reply.Content != Context.SenderSystem.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{Context.SenderSystem.Hid}`) *verbatim*."); @@ -103,7 +98,7 @@ namespace PluralKit.Bot.Commands [Remarks("system [system] list")] public async Task MemberShortList() { var system = Context.GetContextEntity() ?? Context.SenderSystem; - if (system == null) Context.RaiseNoSystemError(); + if (system == null) throw Errors.NoSystemError; var members = await Members.GetBySystem(system); var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; @@ -120,7 +115,7 @@ namespace PluralKit.Bot.Commands [Remarks("system [system] list full")] public async Task MemberLongList() { var system = Context.GetContextEntity() ?? Context.SenderSystem; - if (system == null) Context.RaiseNoSystemError(); + if (system == null) throw Errors.NoSystemError; var members = await Members.GetBySystem(system); var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; diff --git a/PluralKit/Bot/Errors.cs b/PluralKit/Bot/Errors.cs new file mode 100644 index 00000000..38396e67 --- /dev/null +++ b/PluralKit/Bot/Errors.cs @@ -0,0 +1,9 @@ +namespace PluralKit.Bot { + public static class Errors { + public static PKError NotOwnSystemError => new PKError($"You can only run this command on your own system."); + public static PKError NotOwnMemberError => new PKError($"You can only run this command on your own member."); + public static PKError NoSystemError => new PKError("You do not have a system registered with PluralKit. To create one, type `pk;system new`."); + public static PKError ExistinSystemError => new PKError("You already have a system registered with PluralKit. To view it, type `pk;system`. If you'd like to delete your system and start anew, type `pk;system delete`, or if you'd like to unlink this account from it, type `pk;unlink`."); + public static PKError MissingMemberError => new PKSyntaxError("You need to specify a member to run this command on."); + } +} \ No newline at end of file diff --git a/PluralKit/Bot/Preconditions.cs b/PluralKit/Bot/Preconditions.cs new file mode 100644 index 00000000..f08cb674 --- /dev/null +++ b/PluralKit/Bot/Preconditions.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace PluralKit.Bot { + class MustHaveSystem : PreconditionAttribute + { + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var c = context as PKCommandContext; + if (c == null) return PreconditionResult.FromError("Must be called on a PKCommandContext (should never happen!)"); + if (c.SenderSystem == null) return PreconditionResult.FromError(Errors.NoSystemError); + return PreconditionResult.FromSuccess(); + } + } + + class MustPassOwnMember : PreconditionAttribute + { + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + // OK when: + // - Sender has a system + // - Sender passes a member as a context parameter + // - Sender owns said member + + var c = context as PKCommandContext; + if (c == null) + if (c.SenderSystem == null) return PreconditionResult.FromError(Errors.NoSystemError); + if (c.GetContextEntity() == null) return PreconditionResult.FromError(Errors.MissingMemberError); + if (c.GetContextEntity().System != c.SenderSystem.Id) return PreconditionResult.FromError(Errors.NotOwnMemberError); + return PreconditionResult.FromSuccess(); + } + } +} \ No newline at end of file diff --git a/PluralKit/Bot/Utils.cs b/PluralKit/Bot/Utils.cs index 71f0c6c8..79823af1 100644 --- a/PluralKit/Bot/Utils.cs +++ b/PluralKit/Bot/Utils.cs @@ -114,11 +114,6 @@ namespace PluralKit.Bot public void SetContextEntity(object entity) { _entity = entity; } - - public void RaiseNoSystemError() - { - throw new PKError($"You do not have a system registered with PluralKit. To create one, type `pk;system new`. If you already have a system registered on another account, type `pk;link {User.Mention}` from that account to link it here."); - } } public abstract class ContextParameterModuleBase : ModuleBase where T: class @@ -138,42 +133,42 @@ namespace PluralKit.Bot // context, with the context argument removed so it delegates to the subcommand executor builder.AddCommand("", async (ctx, param, services, info) => { var pkCtx = ctx as PKCommandContext; - var res = await ReadContextParameterAsync(param[0] as string); - pkCtx.SetContextEntity(res); + pkCtx.SetContextEntity(param[0] as T); await commandService.ExecuteAsync(pkCtx, Prefix + " " + param[1] as string, services); }, (cb) => { cb.WithPriority(-9999); - cb.AddPrecondition(new ContextParameterFallbackPreconditionAttribute()); - cb.AddParameter("contextValue", (pb) => pb.WithDefault("")); + cb.AddPrecondition(new MustNotHaveContextPrecondition()); + cb.AddParameter("contextValue", (pb) => pb.WithDefault("")); cb.AddParameter("rest", (pb) => pb.WithDefault("").WithIsRemainder(true)); }); } - - public void RaiseNoContextError() { - throw new PKError($"You can only run this command on your own {ContextNoun}."); - } } - public class ContextParameterFallbackPreconditionAttribute : PreconditionAttribute + public class MustNotHaveContextPrecondition : PreconditionAttribute { - public ContextParameterFallbackPreconditionAttribute() + public MustNotHaveContextPrecondition() { } public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { - if (context.GetType().Name != "ContextualContext`1") { - return PreconditionResult.FromSuccess(); - } else { - return PreconditionResult.FromError(""); - } + if ((context as PKCommandContext)?.GetContextEntity() == null) return PreconditionResult.FromSuccess(); + return PreconditionResult.FromError("(should not be seen)"); } } - class PKError : Exception + + public class PKError : Exception { public PKError(string message) : base(message) { } } + + public class PKSyntaxError : PKError + { + public PKSyntaxError(string message) : base(message) + { + } + } } \ No newline at end of file diff --git a/PluralKit/Models.cs b/PluralKit/Models.cs index 0afe078e..3f17c5aa 100644 --- a/PluralKit/Models.cs +++ b/PluralKit/Models.cs @@ -16,6 +16,8 @@ namespace PluralKit public string Token { get; set; } public DateTime Created { get; set; } public string UiTz { get; set; } + + public int MaxMemberNameLength => Tag != null ? 32 - Tag.Length - 1 : 32; } [Table("members")] diff --git a/PluralKit/Stores.cs b/PluralKit/Stores.cs index bab94dad..e81032ce 100644 --- a/PluralKit/Stores.cs +++ b/PluralKit/Stores.cs @@ -72,7 +72,8 @@ namespace PluralKit { } public async Task GetByName(PKSystem system, string name) { - return await conn.QuerySingleOrDefaultAsync("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id }); + // QueryFirst, since members can (in rare cases) share names + return await conn.QueryFirstOrDefaultAsync("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id }); } public async Task> GetUnproxyableMembers(PKSystem system) { @@ -88,11 +89,11 @@ namespace PluralKit { } public async Task Save(PKMember member) { - await conn.UpdateAsync(member); + await conn.ExecuteAsync("update members set name = @Name, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, prefix = @Prefix, suffix = @Suffix where id = @Id", member); } public async Task Delete(PKMember member) { - await conn.DeleteAsync(member); + await conn.ExecuteAsync("delete from members where id = @Id", member); } }