bot: add member renaming command

This commit is contained in:
Ske 2019-04-29 19:43:09 +02:00
parent ff78639ac0
commit 9a5a5ce34f
7 changed files with 108 additions and 45 deletions

View File

@ -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 <name>")]
[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 <member> rename <newname>")]
[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<PKMember> ReadContextParameterAsync(string value)
{
var res = await new PKMemberTypeReader().ReadAsync(Context, value, _services);

View File

@ -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 <name>")]
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 <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 <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 <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<PKSystem>() ?? 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<PKSystem>() ?? 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}`";

9
PluralKit/Bot/Errors.cs Normal file
View File

@ -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.");
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
namespace PluralKit.Bot {
class MustHaveSystem : PreconditionAttribute
{
public override async Task<PreconditionResult> 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<PreconditionResult> 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<PKMember>() == null) return PreconditionResult.FromError(Errors.MissingMemberError);
if (c.GetContextEntity<PKMember>().System != c.SenderSystem.Id) return PreconditionResult.FromError(Errors.NotOwnMemberError);
return PreconditionResult.FromSuccess();
}
}
}

View File

@ -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<T> : ModuleBase<PKCommandContext> 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<string>("contextValue", (pb) => pb.WithDefault(""));
cb.AddPrecondition(new MustNotHaveContextPrecondition());
cb.AddParameter<T>("contextValue", (pb) => pb.WithDefault(""));
cb.AddParameter<string>("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<PreconditionResult> 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<object>() == 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)
{
}
}
}

View File

@ -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")]

View File

@ -72,7 +72,8 @@ namespace PluralKit {
}
public async Task<PKMember> GetByName(PKSystem system, string name) {
return await conn.QuerySingleOrDefaultAsync<PKMember>("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<PKMember>("select * from members where lower(name) = @Name and system = @SystemID", new { Name = name, SystemID = system.Id });
}
public async Task<ICollection<PKMember>> 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);
}
}