Merge branch 'master' into patch-2
This commit is contained in:
@@ -1,66 +1,67 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("token")]
|
||||
public class APICommands: ModuleBase<PKCommandContext>
|
||||
public class APICommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
|
||||
[Command]
|
||||
[MustHaveSystem]
|
||||
[Remarks("token")]
|
||||
public async Task GetToken()
|
||||
private SystemStore _systems;
|
||||
public APICommands(SystemStore systems)
|
||||
{
|
||||
_systems = systems;
|
||||
}
|
||||
|
||||
public async Task GetToken(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Get or make a token
|
||||
var token = Context.SenderSystem.Token ?? await MakeAndSetNewToken();
|
||||
var token = ctx.System.Token ?? await MakeAndSetNewToken(ctx.System);
|
||||
|
||||
// If we're not already in a DM, reply with a reminder to check
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
|
||||
// DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile)
|
||||
await Context.User.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:");
|
||||
await Context.User.SendMessageAsync(token);
|
||||
await ctx.Author.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:");
|
||||
await ctx.Author.SendMessageAsync(token);
|
||||
}
|
||||
|
||||
private async Task<string> MakeAndSetNewToken()
|
||||
private async Task<string> MakeAndSetNewToken(PKSystem system)
|
||||
{
|
||||
Context.SenderSystem.Token = PluralKit.Utils.GenerateToken();
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
return Context.SenderSystem.Token;
|
||||
system.Token = PluralKit.Utils.GenerateToken();
|
||||
await _systems.Save(system);
|
||||
return system.Token;
|
||||
}
|
||||
|
||||
[Command("refresh")]
|
||||
[MustHaveSystem]
|
||||
[Alias("expire", "invalidate", "update", "new")]
|
||||
[Remarks("token refresh")]
|
||||
public async Task RefreshToken()
|
||||
|
||||
public async Task RefreshToken(Context ctx)
|
||||
{
|
||||
if (Context.SenderSystem.Token == null)
|
||||
ctx.CheckSystem();
|
||||
|
||||
if (ctx.System.Token == null)
|
||||
{
|
||||
// If we don't have a token, call the other method instead
|
||||
// This does pretty much the same thing, except words the messages more appropriately for that :)
|
||||
await GetToken();
|
||||
await GetToken(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a new token from scratch
|
||||
var token = await MakeAndSetNewToken();
|
||||
var token = await MakeAndSetNewToken(ctx.System);
|
||||
|
||||
// If we're not already in a DM, reply with a reminder to check
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
|
||||
// DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile)
|
||||
await Context.User.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:");
|
||||
await Context.User.SendMessageAsync(token);
|
||||
await ctx.Author.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:");
|
||||
await ctx.Author.SendMessageAsync(token);
|
||||
}
|
||||
}
|
||||
}
|
285
PluralKit.Bot/Commands/CommandTree.cs
Normal file
285
PluralKit.Bot/Commands/CommandTree.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class CommandTree
|
||||
{
|
||||
public static Command SystemInfo = new Command("system", "system [system]", "uwu");
|
||||
public static Command SystemNew = new Command("system new", "system new [name]", "uwu");
|
||||
public static Command SystemRename = new Command("system name", "system rename [name]", "uwu");
|
||||
public static Command SystemDesc = new Command("system description", "system description [description]", "uwu");
|
||||
public static Command SystemTag = new Command("system tag", "system tag [tag]", "uwu");
|
||||
public static Command SystemAvatar = new Command("system avatar", "system avatar [url|@mention]", "uwu");
|
||||
public static Command SystemDelete = new Command("system delete", "system delete", "uwu");
|
||||
public static Command SystemTimezone = new Command("system timezone", "system timezone [timezone]", "uwu");
|
||||
public static Command SystemList = new Command("system list", "system list [full]", "uwu");
|
||||
public static Command SystemFronter = new Command("system fronter", "system fronter", "uwu");
|
||||
public static Command SystemFrontHistory = new Command("system fronthistory", "system fronthistory", "uwu");
|
||||
public static Command SystemFrontPercent = new Command("system frontpercent", "system frontpercent [timespan]", "uwu");
|
||||
public static Command MemberInfo = new Command("member", "member <member>", "uwu");
|
||||
public static Command MemberNew = new Command("member new", "member new <name>", "uwu");
|
||||
public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "uwu");
|
||||
public static Command MemberDesc = new Command("member description", "member <member> description [description]", "uwu");
|
||||
public static Command MemberPronouns = new Command("member pronouns", "member <member> pronouns [pronouns]", "uwu");
|
||||
public static Command MemberColor = new Command("member color", "member <member> color [color]", "uwu");
|
||||
public static Command MemberBirthday = new Command("member birthday", "member <member> birthday [birthday]", "uwu");
|
||||
public static Command MemberProxy = new Command("member proxy", "member <member> proxy [example proxy]", "uwu");
|
||||
public static Command MemberDelete = new Command("member delete", "member <member> delete", "uwu");
|
||||
public static Command MemberAvatar = new Command("member avatar", "member <member> avatar [url|@mention]", "uwu");
|
||||
public static Command MemberDisplayName = new Command("member displayname", "member <member> displayname [display name]", "uwu");
|
||||
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "uwu");
|
||||
public static Command SwitchOut = new Command("switch out", "switch out", "uwu");
|
||||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "uwu");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete", "uwu");
|
||||
public static Command Link = new Command("link", "link <account>", "uwu");
|
||||
public static Command Unlink = new Command("unlink", "unlink [account]", "uwu");
|
||||
public static Command TokenGet = new Command("token", "token", "uwu");
|
||||
public static Command TokenRefresh = new Command("token refresh", "token refresh", "uwu");
|
||||
public static Command Import = new Command("import", "import [fileurl]", "uwu");
|
||||
public static Command Export = new Command("export", "export", "uwu");
|
||||
public static Command HelpCommandList = new Command("commands", "commands", "uwu");
|
||||
public static Command HelpProxy = new Command("help proxy", "help proxy", "uwu");
|
||||
public static Command Help = new Command("help", "help", "uwu");
|
||||
public static Command Message = new Command("message", "message <id|link>", "uwu");
|
||||
public static Command Log = new Command("log", "log <channel>", "uwu");
|
||||
public static Command Invite = new Command("invite", "invite", "uwu");
|
||||
public static Command PermCheck = new Command("permcheck", "permcheck <guild>", "uwu");
|
||||
|
||||
private IDiscordClient _client;
|
||||
|
||||
public CommandTree(IDiscordClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public Task ExecuteCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("system", "s"))
|
||||
return HandleSystemCommand(ctx);
|
||||
if (ctx.Match("member", "m"))
|
||||
return HandleMemberCommand(ctx);
|
||||
if (ctx.Match("switch", "sw"))
|
||||
return HandleSwitchCommand(ctx);
|
||||
if (ctx.Match("link"))
|
||||
return ctx.Execute<LinkCommands>(Link, m => m.LinkSystem(ctx));
|
||||
if (ctx.Match("unlink"))
|
||||
return ctx.Execute<LinkCommands>(Unlink, m => m.UnlinkAccount(ctx));
|
||||
if (ctx.Match("token"))
|
||||
if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen"))
|
||||
return ctx.Execute<APICommands>(TokenRefresh, m => m.RefreshToken(ctx));
|
||||
else
|
||||
return ctx.Execute<APICommands>(TokenGet, m => m.GetToken(ctx));
|
||||
if (ctx.Match("import"))
|
||||
return ctx.Execute<ImportExportCommands>(Import, m => m.Import(ctx));
|
||||
if (ctx.Match("export"))
|
||||
return ctx.Execute<ImportExportCommands>(Export, m => m.Export(ctx));
|
||||
if (ctx.Match("help"))
|
||||
if (ctx.Match("commands"))
|
||||
return ctx.Execute<HelpCommands>(HelpCommandList, m => m.CommandList(ctx));
|
||||
else if (ctx.Match("proxy"))
|
||||
return ctx.Execute<HelpCommands>(HelpProxy, m => m.HelpProxy(ctx));
|
||||
else return ctx.Execute<HelpCommands>(Help, m => m.HelpRoot(ctx));
|
||||
if (ctx.Match("commands"))
|
||||
return ctx.Execute<HelpCommands>(HelpCommandList, m => m.CommandList(ctx));
|
||||
if (ctx.Match("message", "msg"))
|
||||
return ctx.Execute<ModCommands>(Message, m => m.GetMessage(ctx));
|
||||
if (ctx.Match("log"))
|
||||
return ctx.Execute<ModCommands>(Log, m => m.SetLogChannel(ctx));
|
||||
if (ctx.Match("invite")) return ctx.Execute<MiscCommands>(Invite, m => m.Invite(ctx));
|
||||
if (ctx.Match("mn")) return ctx.Execute<MiscCommands>(null, m => m.Mn(ctx));
|
||||
if (ctx.Match("fire")) return ctx.Execute<MiscCommands>(null, m => m.Fire(ctx));
|
||||
if (ctx.Match("thunder")) return ctx.Execute<MiscCommands>(null, m => m.Thunder(ctx));
|
||||
if (ctx.Match("freeze")) return ctx.Execute<MiscCommands>(null, m => m.Freeze(ctx));
|
||||
if (ctx.Match("starstorm")) return ctx.Execute<MiscCommands>(null, m => m.Starstorm(ctx));
|
||||
if (ctx.Match("stats")) return ctx.Execute<MiscCommands>(null, m => m.Stats(ctx));
|
||||
if (ctx.Match("permcheck"))
|
||||
return ctx.Execute<MiscCommands>(PermCheck, m => m.PermCheckGuild(ctx));
|
||||
|
||||
ctx.Reply(
|
||||
$"{Emojis.Error} Unknown command `{ctx.PeekArgument().SanitizeMentions()}`. For a list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleSystemCommand(Context ctx)
|
||||
{
|
||||
// If we have no parameters, default to self-target
|
||||
if (!ctx.HasNext())
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
|
||||
|
||||
// First, we match own-system-only commands (ie. no target system parameter)
|
||||
else if (ctx.Match("new", "create", "make", "add", "register", "init"))
|
||||
await ctx.Execute<SystemCommands>(SystemNew, m => m.New(ctx));
|
||||
else if (ctx.Match("name", "rename", "changename"))
|
||||
await ctx.Execute<SystemCommands>(SystemRename, m => m.Name(ctx));
|
||||
else if (ctx.Match("tag"))
|
||||
await ctx.Execute<SystemCommands>(SystemTag, m => m.Tag(ctx));
|
||||
else if (ctx.Match("description", "desc", "bio"))
|
||||
await ctx.Execute<SystemCommands>(SystemDesc, m => m.Description(ctx));
|
||||
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
|
||||
await ctx.Execute<SystemCommands>(SystemAvatar, m => m.SystemAvatar(ctx));
|
||||
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
|
||||
await ctx.Execute<SystemCommands>(SystemDelete, m => m.Delete(ctx));
|
||||
else if (ctx.Match("timezone", "tz"))
|
||||
await ctx.Execute<SystemCommands>(SystemTimezone, m => m.SystemTimezone(ctx));
|
||||
else if (ctx.Match("list", "l", "members"))
|
||||
{
|
||||
if (ctx.Match("f", "full", "big", "details", "long"))
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberLongList(ctx, ctx.System));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, ctx.System));
|
||||
}
|
||||
else if (ctx.Match("f", "front", "fronter", "fronters"))
|
||||
{
|
||||
if (ctx.Match("h", "history"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||
else if (ctx.Match("p", "percent", "%"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, ctx.System));
|
||||
}
|
||||
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
|
||||
else
|
||||
await HandleSystemCommandTargeted(ctx);
|
||||
}
|
||||
|
||||
private async Task HandleSystemCommandTargeted(Context ctx)
|
||||
{
|
||||
// Commands that have a system target (eg. pk;system <system> fronthistory)
|
||||
var target = await ctx.MatchSystem();
|
||||
if (target == null)
|
||||
{
|
||||
var list = CreatePotentialCommandList(SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\nPerhaps you meant to use one of the following commands?\n{list}");
|
||||
}
|
||||
else if (ctx.Match("list", "l", "members"))
|
||||
{
|
||||
if (ctx.Match("f", "full", "big", "details", "long"))
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberLongList(ctx, target));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, target));
|
||||
}
|
||||
else if (ctx.Match("f", "front", "fronter", "fronters"))
|
||||
{
|
||||
if (ctx.Match("h", "history"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
|
||||
else if (ctx.Match("p", "percent", "%"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, target));
|
||||
}
|
||||
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
|
||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
|
||||
else if (ctx.Match("info", "view", "show"))
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
|
||||
else if (!ctx.HasNext())
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent,
|
||||
SystemInfo);
|
||||
}
|
||||
|
||||
private async Task HandleMemberCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("new", "n", "add", "create", "register"))
|
||||
await ctx.Execute<MemberCommands>(MemberNew, m => m.NewMember(ctx));
|
||||
else if (await ctx.MatchMember() is PKMember target)
|
||||
await HandleMemberCommandTargeted(ctx, target);
|
||||
else if (!ctx.HasNext())
|
||||
await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, MemberDesc, MemberPronouns,
|
||||
MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar);
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} {ctx.CreateMemberNotFoundError(ctx.PopArgument())}");
|
||||
}
|
||||
|
||||
private async Task HandleMemberCommandTargeted(Context ctx, PKMember target)
|
||||
{
|
||||
// Commands that have a member target (eg. pk;member <member> delete)
|
||||
if (ctx.Match("rename", "name", "changename", "setname"))
|
||||
await ctx.Execute<MemberCommands>(MemberRename, m => m.RenameMember(ctx, target));
|
||||
else if (ctx.Match("description", "info", "bio", "text", "desc"))
|
||||
await ctx.Execute<MemberCommands>(MemberDesc, m => m.MemberDescription(ctx, target));
|
||||
else if (ctx.Match("pronouns", "pronoun"))
|
||||
await ctx.Execute<MemberCommands>(MemberPronouns, m => m.MemberPronouns(ctx, target));
|
||||
else if (ctx.Match("color", "colour"))
|
||||
await ctx.Execute<MemberCommands>(MemberColor, m => m.MemberColor(ctx, target));
|
||||
else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate"))
|
||||
await ctx.Execute<MemberCommands>(MemberBirthday, m => m.MemberBirthday(ctx, target));
|
||||
else if (ctx.Match("proxy", "tags", "proxytags", "brackets"))
|
||||
await ctx.Execute<MemberCommands>(MemberProxy, m => m.MemberProxy(ctx, target));
|
||||
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
|
||||
await ctx.Execute<MemberCommands>(MemberDelete, m => m.MemberDelete(ctx, target));
|
||||
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
|
||||
await ctx.Execute<MemberCommands>(MemberAvatar, m => m.MemberAvatar(ctx, target));
|
||||
else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname"))
|
||||
await ctx.Execute<MemberCommands>(MemberDisplayName, m => m.MemberDisplayName(ctx, target));
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
|
||||
}
|
||||
|
||||
private async Task HandleSwitchCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("out"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchOut, m => m.SwitchOut(ctx));
|
||||
else if (ctx.Match("move", "shift", "offset"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchMove, m => m.SwitchMove(ctx));
|
||||
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchDelete, m => m.SwitchDelete(ctx));
|
||||
else if (ctx.HasNext()) // there are following arguments
|
||||
await ctx.Execute<SwitchCommands>(Switch, m => m.Switch(ctx));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchDelete, SystemFronter, SystemFrontHistory);
|
||||
}
|
||||
|
||||
private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands)
|
||||
{
|
||||
var commandListStr = CreatePotentialCommandList(potentialCommands);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} Unknown command `pk;{ctx.FullCommand}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)
|
||||
{
|
||||
var commandListStr = CreatePotentialCommandList(potentialCommands);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private static string CreatePotentialCommandList(params Command[] potentialCommands)
|
||||
{
|
||||
return string.Join("\n", potentialCommands.Select(cmd => $"- `pk;{cmd.Usage}`"));
|
||||
}
|
||||
|
||||
private async Task<string> CreateSystemNotFoundError(Context ctx)
|
||||
{
|
||||
var input = ctx.PopArgument();
|
||||
if (input.TryParseMention(out var id))
|
||||
{
|
||||
// Try to resolve the user ID to find the associated account,
|
||||
// so we can print their username.
|
||||
var user = await _client.GetUserAsync(id);
|
||||
|
||||
// Print descriptive errors based on whether we found the user or not.
|
||||
if (user == null)
|
||||
return $"Account with ID `{id}` not found.";
|
||||
return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered.";
|
||||
}
|
||||
|
||||
return $"System with ID `{input}` not found.";
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,56 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class HelpCommands: ModuleBase<PKCommandContext>
|
||||
public class HelpCommands
|
||||
{
|
||||
[Group("help")]
|
||||
public class HelpGroup: ModuleBase<PKCommandContext>
|
||||
public async Task HelpProxy(Context ctx)
|
||||
{
|
||||
[Command("proxy")]
|
||||
[Priority(1)]
|
||||
[Remarks("help proxy")]
|
||||
public async Task HelpProxy()
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying");
|
||||
}
|
||||
|
||||
[Command("member")]
|
||||
[Priority(1)]
|
||||
[Remarks("help member")]
|
||||
public async Task HelpMember()
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
"The member help page has been moved! See the website: https://pluralkit.me/guide#member-management");
|
||||
}
|
||||
|
||||
[Command]
|
||||
[Remarks("help")]
|
||||
public async Task HelpRoot([Remainder] string _ignored = null)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(embed: new EmbedBuilder()
|
||||
.WithTitle("PluralKit")
|
||||
.WithDescription("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.")
|
||||
.AddField("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.")
|
||||
.AddField("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation.")
|
||||
.AddField("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the documentation](https://pluralkit.me/guide#member-management) for more information.")
|
||||
.AddField("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nType **`pk;invite`** to get a link to invite this bot to your own server!")
|
||||
.AddField("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.")
|
||||
.AddField("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78")
|
||||
.WithFooter("By @Ske#6201 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/")
|
||||
.WithColor(Color.Blue)
|
||||
.Build());
|
||||
}
|
||||
await ctx.Reply(
|
||||
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying");
|
||||
}
|
||||
|
||||
[Command("commands")]
|
||||
[Remarks("commands")]
|
||||
public async Task CommandList()
|
||||
|
||||
public async Task HelpMember(Context ctx)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
"The member help page has been moved! See the website: https://pluralkit.me/guide#member-management");
|
||||
}
|
||||
|
||||
public async Task HelpRoot(Context ctx)
|
||||
{
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.WithTitle("PluralKit")
|
||||
.WithDescription("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.")
|
||||
.AddField("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.")
|
||||
.AddField("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation.")
|
||||
.AddField("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the documentation](https://pluralkit.me/guide#member-management) for more information.")
|
||||
.AddField("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nType **`pk;invite`** to get a link to invite this bot to your own server!")
|
||||
.AddField("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.")
|
||||
.AddField("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78")
|
||||
.WithFooter("By @Ske#6201 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/")
|
||||
.WithColor(Color.Blue)
|
||||
.Build());
|
||||
}
|
||||
|
||||
public async Task CommandList(Context ctx)
|
||||
{
|
||||
await ctx.Reply(
|
||||
"The command list has been moved! See the website: https://pluralkit.me/commands");
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.Net;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class ImportExportCommands : ModuleBase<PKCommandContext>
|
||||
public class ImportExportCommands
|
||||
{
|
||||
public DataFileService DataFiles { get; set; }
|
||||
|
||||
[Command("import")]
|
||||
[Remarks("import [fileurl]")]
|
||||
public async Task Import([Remainder] string url = null)
|
||||
private DataFileService _dataFiles;
|
||||
public ImportExportCommands(DataFileService dataFiles)
|
||||
{
|
||||
if (url == null) url = Context.Message.Attachments.FirstOrDefault()?.Url;
|
||||
_dataFiles = dataFiles;
|
||||
}
|
||||
|
||||
public async Task Import(Context ctx)
|
||||
{
|
||||
var url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url;
|
||||
if (url == null) throw Errors.NoImportFilePassed;
|
||||
|
||||
await Context.BusyIndicator(async () =>
|
||||
await ctx.BusyIndicator(async () =>
|
||||
{
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
var response = await client.GetAsync(url);
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await client.GetAsync(url);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Invalid URL throws this, we just error back out
|
||||
throw Errors.InvalidImportFile;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode) throw Errors.InvalidImportFile;
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -63,8 +77,8 @@ namespace PluralKit.Bot.Commands
|
||||
issueStr +=
|
||||
"\n- PluralKit does not support per-member system tags. Since you had multiple members with distinct tags, tags will not be imported. You can set your system tag using the `pk;system tag <tag>` command later.";
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync($"{issueStr}\n\nDo you want to proceed with the import?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
var msg = await ctx.Reply($"{issueStr}\n\nDo you want to proceed with the import?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
}
|
||||
|
||||
data = res.System;
|
||||
@@ -78,37 +92,36 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
if (!data.Valid) throw Errors.InvalidImportFile;
|
||||
|
||||
if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(Context.User.Id))
|
||||
if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id))
|
||||
{
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
}
|
||||
|
||||
// If passed system is null, it'll create a new one
|
||||
// (and that's okay!)
|
||||
var result = await DataFiles.ImportSystem(data, Context.SenderSystem, Context.User.Id);
|
||||
var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.Author.Id);
|
||||
|
||||
if (Context.SenderSystem == null)
|
||||
if (ctx.System != null)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} PluralKit has created a system for you based on the given file. Your system ID is `{result.System.Hid}`. Type `pk;system` for more information.");
|
||||
await ctx.Reply($"{Emojis.Success} PluralKit has created a system for you based on the given file. Your system ID is `{result.System.Hid}`. Type `pk;system` for more information.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Updated {result.ModifiedNames.Count} members, created {result.AddedNames.Count} members. Type `pk;system list` to check!");
|
||||
await ctx.Reply($"{Emojis.Success} Updated {result.ModifiedNames.Count} members, created {result.AddedNames.Count} members. Type `pk;system list` to check!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Command("export")]
|
||||
[Remarks("export")]
|
||||
[MustHaveSystem]
|
||||
public async Task Export()
|
||||
|
||||
public async Task Export(Context ctx)
|
||||
{
|
||||
var json = await Context.BusyIndicator(async () =>
|
||||
ctx.CheckSystem();
|
||||
|
||||
var json = await ctx.BusyIndicator(async () =>
|
||||
{
|
||||
// Make the actual data file
|
||||
var data = await DataFiles.ExportSystem(Context.SenderSystem);
|
||||
var data = await _dataFiles.ExportSystem(ctx.System);
|
||||
return JsonConvert.SerializeObject(data, Formatting.None);
|
||||
});
|
||||
|
||||
@@ -118,16 +131,16 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
try
|
||||
{
|
||||
await Context.User.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!");
|
||||
await ctx.Author.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!");
|
||||
|
||||
// If the original message wasn't posted in DMs, send a public reminder
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
// If user has DMs closed, tell 'em to open them
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} Could not send the data file in your DMs. Do you have DMs closed?");
|
||||
}
|
||||
}
|
||||
|
@@ -1,50 +1,57 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class LinkCommands: ModuleBase<PKCommandContext>
|
||||
public class LinkCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
private SystemStore _systems;
|
||||
|
||||
|
||||
[Command("link")]
|
||||
[Remarks("link <account>")]
|
||||
[MustHaveSystem]
|
||||
public async Task LinkSystem(IUser account)
|
||||
public LinkCommands(SystemStore systems)
|
||||
{
|
||||
var accountIds = await Systems.GetLinkedAccountIds(Context.SenderSystem);
|
||||
_systems = systems;
|
||||
}
|
||||
|
||||
public async Task LinkSystem(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
|
||||
var accountIds = await _systems.GetLinkedAccountIds(ctx.System);
|
||||
if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked;
|
||||
|
||||
var existingAccount = await Systems.GetByAccount(account.Id);
|
||||
var existingAccount = await _systems.GetByAccount(account.Id);
|
||||
if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount);
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
$"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.");
|
||||
if (!await Context.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled;
|
||||
await Systems.Link(Context.SenderSystem, account.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Account linked to system.");
|
||||
var msg = await ctx.Reply($"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.");
|
||||
if (!await ctx.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled;
|
||||
await _systems.Link(ctx.System, account.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Account linked to system.");
|
||||
}
|
||||
|
||||
[Command("unlink")]
|
||||
[Remarks("unlink [account]")]
|
||||
[MustHaveSystem]
|
||||
public async Task UnlinkAccount(IUser account = null)
|
||||
public async Task UnlinkAccount(Context ctx)
|
||||
{
|
||||
if (account == null) account = Context.User;
|
||||
ctx.CheckSystem();
|
||||
|
||||
var accountIds = (await Systems.GetLinkedAccountIds(Context.SenderSystem)).ToList();
|
||||
IUser account;
|
||||
if (!ctx.HasNext())
|
||||
account = ctx.Author;
|
||||
else
|
||||
account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
|
||||
|
||||
var accountIds = (await _systems.GetLinkedAccountIds(ctx.System)).ToList();
|
||||
if (!accountIds.Contains(account.Id)) throw Errors.AccountNotLinked;
|
||||
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
var msg = await ctx.Reply(
|
||||
$"Are you sure you want to unlink {account.Mention} from your system?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled;
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled;
|
||||
|
||||
await Systems.Unlink(Context.SenderSystem, account.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Account unlinked.");
|
||||
await _systems.Unlink(ctx.System, account.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Account unlinked.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,161 +2,170 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("member")]
|
||||
[Alias("m")]
|
||||
public class MemberCommands : ContextParameterModuleBase<PKMember>
|
||||
public class MemberCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
public MemberStore Members { get; set; }
|
||||
public EmbedService Embeds { get; set; }
|
||||
private SystemStore _systems;
|
||||
private MemberStore _members;
|
||||
private EmbedService _embeds;
|
||||
|
||||
public override string Prefix => "member";
|
||||
public override string ContextNoun => "member";
|
||||
private ProxyCacheService _proxyCache;
|
||||
|
||||
[Command("new")]
|
||||
[Alias("n", "add", "create", "register")]
|
||||
[Remarks("member new <name>")]
|
||||
[MustHaveSystem]
|
||||
public async Task NewMember([Remainder] string memberName) {
|
||||
public MemberCommands(SystemStore systems, MemberStore members, EmbedService embeds, ProxyCacheService proxyCache)
|
||||
{
|
||||
_systems = systems;
|
||||
_members = members;
|
||||
_embeds = embeds;
|
||||
_proxyCache = proxyCache;
|
||||
}
|
||||
|
||||
public async Task NewMember(Context ctx) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
|
||||
|
||||
// Hard name length cap
|
||||
if (memberName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(memberName.Length);
|
||||
|
||||
// Warn if member name will be unproxyable (with/without tag)
|
||||
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, or set a member display name)");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
if (memberName.Length > ctx.System.MaxMemberNameLength) {
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} Member name too long ({memberName.Length} > {ctx.System.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later, or set a member display name)");
|
||||
if (!await ctx.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);
|
||||
var existingMember = await _members.GetByName(ctx.System, 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.Sanitize()}\" (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.");
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
}
|
||||
|
||||
// Create the member
|
||||
var member = await Members.Create(Context.SenderSystem, memberName);
|
||||
var member = await _members.Create(ctx.System, memberName);
|
||||
|
||||
// Send confirmation and space hint
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member \"{memberName.Sanitize()}\" (`{member.Hid}`) registered! See the user guide for commands for editing this member: https://pluralkit.me/guide#member-management");
|
||||
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, or just use the member's 5-character ID (which is `{member.Hid}`).");
|
||||
await ctx.Reply($"{Emojis.Success} Member \"{memberName.SanitizeMentions()}\" (`{member.Hid}`) registered! See the user guide for commands for editing this member: https://pluralkit.me/guide#member-management");
|
||||
if (memberName.Contains(" ")) await ctx.Reply($"{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, or just use the member's 5-character ID (which is `{member.Hid}`).");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("rename")]
|
||||
[Alias("name", "changename", "setname")]
|
||||
[Remarks("member <member> rename <newname>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task RenameMember([Remainder] string newName) {
|
||||
|
||||
public async Task RenameMember(Context ctx, PKMember target) {
|
||||
// TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean?
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var newName = ctx.RemainderOrNull();
|
||||
|
||||
// Hard name length cap
|
||||
if (newName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(newName.Length);
|
||||
|
||||
// Warn if member name will be unproxyable (with/without tag), only if member doesn't have a display name
|
||||
if (ContextEntity.DisplayName == null && 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? (You can set a member display name instead)");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
if (target.DisplayName == null && newName.Length > ctx.System.MaxMemberNameLength) {
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} New member name too long ({newName.Length} > {ctx.System.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to change it anyway? (You can set a member display name instead)");
|
||||
if (!await ctx.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);
|
||||
var existingMember = await _members.GetByName(ctx.System, 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.Sanitize()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.SanitizeMentions()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
}
|
||||
|
||||
// Rename the member
|
||||
ContextEntity.Name = newName;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Name = newName;
|
||||
await _members.Save(target);
|
||||
|
||||
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.");
|
||||
if (ContextEntity.DisplayName != null) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member has a display name set (`{ContextEntity.DisplayName}`), and will be proxied using that name instead.");
|
||||
await ctx.Reply($"{Emojis.Success} Member renamed.");
|
||||
if (newName.Contains(" ")) await ctx.Reply($"{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.");
|
||||
if (target.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a display name set ({target.DisplayName.SanitizeMentions()}), and will be proxied using that name instead.");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public async Task MemberDescription(Context ctx, PKMember target) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
[Command("description")]
|
||||
[Alias("info", "bio", "text", "desc")]
|
||||
[Remarks("member <member> description <description>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDescription([Remainder] string description = null) {
|
||||
var description = ctx.RemainderOrNull();
|
||||
if (description.IsLongerThan(Limits.MaxDescriptionLength)) throw Errors.DescriptionTooLongError(description.Length);
|
||||
|
||||
ContextEntity.Description = description;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Description = description;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
public async Task MemberPronouns(Context ctx, PKMember target) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
[Command("pronouns")]
|
||||
[Alias("pronoun")]
|
||||
[Remarks("member <member> pronouns <pronouns>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberPronouns([Remainder] string pronouns = null) {
|
||||
var pronouns = ctx.RemainderOrNull();
|
||||
if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) throw Errors.MemberPronounsTooLongError(pronouns.Length);
|
||||
|
||||
ContextEntity.Pronouns = pronouns;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Pronouns = pronouns;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
[Command("color")]
|
||||
[Alias("colour")]
|
||||
[Remarks("member <member> color <color>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberColor([Remainder] string color = null)
|
||||
public async Task MemberColor(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var color = ctx.RemainderOrNull();
|
||||
if (color != null)
|
||||
{
|
||||
if (color.StartsWith("#")) color = color.Substring(1);
|
||||
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
|
||||
}
|
||||
|
||||
ContextEntity.Color = color;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Color = color;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
[Command("birthday")]
|
||||
[Alias("birthdate", "bday", "cakeday", "bdate")]
|
||||
[Remarks("member <member> birthday <birthday>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberBirthday([Remainder] string birthday = null)
|
||||
public async Task MemberBirthday(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
LocalDate? date = null;
|
||||
var birthday = ctx.RemainderOrNull();
|
||||
if (birthday != null)
|
||||
{
|
||||
date = PluralKit.Utils.ParseDate(birthday, true);
|
||||
if (date == null) throw Errors.BirthdayParseError(birthday);
|
||||
}
|
||||
|
||||
ContextEntity.Birthday = date;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Birthday = date;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {ContextEntity.BirthdayString}")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {target.BirthdayString}")}.");
|
||||
}
|
||||
|
||||
[Command("proxy")]
|
||||
[Alias("proxy", "tags", "proxytags", "brackets")]
|
||||
[Remarks("member <member> proxy <proxy tags>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberProxy([Remainder] string exampleProxy = null)
|
||||
public async Task MemberProxy(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
// Handling the clear case in an if here to keep the body dedented
|
||||
var exampleProxy = ctx.RemainderOrNull();
|
||||
if (exampleProxy == null)
|
||||
{
|
||||
// Just reset and send OK message
|
||||
ContextEntity.Prefix = null;
|
||||
ContextEntity.Suffix = null;
|
||||
await Members.Save(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member proxy tags cleared.");
|
||||
target.Prefix = null;
|
||||
target.Suffix = null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member proxy tags cleared.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,101 +175,110 @@ namespace PluralKit.Bot.Commands
|
||||
if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText;
|
||||
|
||||
// If the prefix/suffix is empty, use "null" instead (for DB)
|
||||
ContextEntity.Prefix = prefixAndSuffix[0].Length > 0 ? prefixAndSuffix[0] : null;
|
||||
ContextEntity.Suffix = prefixAndSuffix[1].Length > 0 ? prefixAndSuffix[1] : null;
|
||||
await Members.Save(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member proxy tags changed to `{ContextEntity.ProxyString.Sanitize()}`. Try proxying now!");
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Alias("remove", "destroy", "erase", "yeet")]
|
||||
[Remarks("member <member> delete")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDelete()
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Warn} Are you sure you want to delete \"{ContextEntity.Name.Sanitize()}\"? If so, reply to this message with the member's ID (`{ContextEntity.Hid}`). __***This cannot be undone!***__");
|
||||
if (!await Context.ConfirmWithReply(ContextEntity.Hid)) throw Errors.MemberDeleteCancelled;
|
||||
await Members.Delete(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member deleted.");
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("member <member> avatar <avatar url>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberAvatarByMention(IUser member)
|
||||
{
|
||||
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
ContextEntity.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await Members.Save(ContextEntity);
|
||||
target.Prefix = prefixAndSuffix[0].Length > 0 ? prefixAndSuffix[0] : null;
|
||||
target.Suffix = prefixAndSuffix[1].Length > 0 ? prefixAndSuffix[1] : null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member proxy tags changed to `{target.ProxyString.SanitizeMentions()}`. Try proxying now!");
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(ContextEntity.AvatarUrl).Build();
|
||||
await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Success} Member avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the webhook's avatar will need to be re-set.", embed: embed);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("member <member> avatar <avatar url>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberAvatar([Remainder] string avatarUrl = null)
|
||||
public async Task MemberDelete(Context ctx, PKMember target)
|
||||
{
|
||||
string url = avatarUrl ?? Context.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await Context.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
|
||||
ContextEntity.AvatarUrl = url;
|
||||
await Members.Save(ContextEntity);
|
||||
|
||||
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.Name.SanitizeMentions()}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
|
||||
await _members.Delete(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member deleted.");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("displayname")]
|
||||
[Alias("nick", "nickname", "displayname")]
|
||||
[Remarks("member <member> displayname <displayname>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDisplayName([Remainder] string newDisplayName = null)
|
||||
{
|
||||
// Refuse if proxy name will be unproxyable (with/without tag)
|
||||
if (newDisplayName != null && newDisplayName.Length > Context.SenderSystem.MaxMemberNameLength)
|
||||
throw Errors.DisplayNameTooLong(newDisplayName, Context.SenderSystem.MaxMemberNameLength);
|
||||
public async Task MemberAvatar(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
ContextEntity.DisplayName = newDisplayName;
|
||||
await Members.Save(ContextEntity);
|
||||
if (await ctx.MatchUser() is IUser user)
|
||||
{
|
||||
if (user.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
|
||||
await _members.Save(target);
|
||||
|
||||
var embed = new EmbedBuilder().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 webhook's avatar will need to be re-set.", embed: embed);
|
||||
|
||||
}
|
||||
else if (ctx.RemainderOrNull() is string url)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(url);
|
||||
target.AvatarUrl = url;
|
||||
await _members.Save(target);
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(url).Build();
|
||||
await ctx.Reply($"{Emojis.Success} Member avatar changed.", embed: embed);
|
||||
}
|
||||
else if (ctx.Message.Attachments.FirstOrDefault() is Attachment attachment)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(attachment.Url);
|
||||
target.AvatarUrl = attachment.Url;
|
||||
await _members.Save(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.");
|
||||
}
|
||||
else
|
||||
{
|
||||
target.AvatarUrl = null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member avatar cleared.");
|
||||
}
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public async Task MemberDisplayName(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var newDisplayName = ctx.RemainderOrNull();
|
||||
// Refuse if proxy name will be unproxyable (with/without tag)
|
||||
if (newDisplayName != null && newDisplayName.Length > ctx.System.MaxMemberNameLength)
|
||||
throw Errors.DisplayNameTooLong(newDisplayName, ctx.System.MaxMemberNameLength);
|
||||
|
||||
target.DisplayName = newDisplayName;
|
||||
await _members.Save(target);
|
||||
|
||||
var successStr = $"{Emojis.Success} ";
|
||||
if (newDisplayName != null)
|
||||
{
|
||||
successStr +=
|
||||
$"Member display name changed. This member will now be proxied using the name `{newDisplayName}`.";
|
||||
$"Member display name changed. This member will now be proxied using the name \"{newDisplayName.SanitizeMentions()}\".";
|
||||
}
|
||||
else
|
||||
{
|
||||
successStr += $"Member display name cleared. ";
|
||||
|
||||
// If we're removing display name and the *real* name will be unproxyable, warn.
|
||||
if (ContextEntity.Name.Length > Context.SenderSystem.MaxMemberNameLength)
|
||||
if (target.Name.Length > ctx.System.MaxMemberNameLength)
|
||||
successStr +=
|
||||
$" {Emojis.Warn} This member's actual name is too long ({ContextEntity.Name.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), and thus cannot be proxied.";
|
||||
$" {Emojis.Warn} This member's actual name is too long ({target.Name.Length} > {ctx.System.MaxMemberNameLength} characters), and thus cannot be proxied.";
|
||||
else
|
||||
successStr += $"This member will now be proxied using their member name `{ContextEntity.Name}.";
|
||||
successStr += $"This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\".";
|
||||
}
|
||||
await Context.Channel.SendMessageAsync(successStr);
|
||||
}
|
||||
|
||||
[Command]
|
||||
[Alias("view", "show", "info")]
|
||||
[Remarks("member <member>")]
|
||||
public async Task ViewMember(PKMember member)
|
||||
{
|
||||
var system = await Systems.GetById(member.System);
|
||||
await Context.Channel.SendMessageAsync(embed: await Embeds.CreateMemberEmbed(system, member));
|
||||
await ctx.Reply(successStr);
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public override async Task<PKMember> ReadContextParameterAsync(string value)
|
||||
public async Task ViewMember(Context ctx, PKMember target)
|
||||
{
|
||||
var res = await new PKMemberTypeReader().ReadAsync(Context, value, _services);
|
||||
return res.IsSuccess ? res.BestMatch as PKMember : null;
|
||||
var system = await _systems.GetById(target.System);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target));
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,23 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using App.Metrics;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Humanizer;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands {
|
||||
public class MiscCommands: ModuleBase<PKCommandContext> {
|
||||
public BotConfig BotConfig { get; set; }
|
||||
public IMetrics Metrics { get; set; }
|
||||
|
||||
[Command("invite")]
|
||||
[Alias("inv")]
|
||||
[Remarks("invite")]
|
||||
public async Task Invite()
|
||||
public class MiscCommands
|
||||
{
|
||||
private BotConfig _botConfig;
|
||||
private IMetrics _metrics;
|
||||
|
||||
public MiscCommands(BotConfig botConfig, IMetrics metrics)
|
||||
{
|
||||
var clientId = BotConfig.ClientId ?? (await Context.Client.GetApplicationInfoAsync()).Id;
|
||||
_botConfig = botConfig;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public async Task Invite(Context ctx)
|
||||
{
|
||||
var clientId = _botConfig.ClientId ?? (await ctx.Client.GetApplicationInfoAsync()).Id;
|
||||
var permissions = new GuildPermissions(
|
||||
addReactions: true,
|
||||
attachFiles: true,
|
||||
@@ -29,38 +33,36 @@ namespace PluralKit.Bot.Commands {
|
||||
);
|
||||
|
||||
var invite = $"https://discordapp.com/oauth2/authorize?client_id={clientId}&scope=bot&permissions={permissions.RawValue}";
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
|
||||
await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
|
||||
}
|
||||
|
||||
public Task Mn(Context ctx) => ctx.Reply("Gotta catch 'em all!");
|
||||
public Task Fire(Context ctx) => ctx.Reply("*A giant lightning bolt promptly erupts into a pillar of fire as it hits your opponent.*");
|
||||
public Task Thunder(Context ctx) => ctx.Reply("*A giant ball of lightning is conjured and fired directly at your opponent, vanquishing them.*");
|
||||
public Task Freeze(Context ctx) => ctx.Reply("*A giant crystal ball of ice is charged and hurled toward your opponent, bursting open and freezing them solid on contact.*");
|
||||
public Task Starstorm(Context ctx) => ctx.Reply("*Vibrant colours burst forth from the sky as meteors rain down upon your opponent.*");
|
||||
|
||||
[Command("mn")] public Task Mn() => Context.Channel.SendMessageAsync("Gotta catch 'em all!");
|
||||
[Command("fire")] public Task Fire() => Context.Channel.SendMessageAsync("*A giant lightning bolt promptly erupts into a pillar of fire as it hits your opponent.*");
|
||||
[Command("thunder")] public Task Thunder() => Context.Channel.SendMessageAsync("*A giant ball of lightning is conjured and fired directly at your opponent, vanquishing them.*");
|
||||
[Command("freeze")] public Task Freeze() => Context.Channel.SendMessageAsync("*A giant crystal ball of ice is charged and hurled toward your opponent, bursting open and freezing them solid on contact.*");
|
||||
[Command("starstorm")] public Task Starstorm() => Context.Channel.SendMessageAsync("*Vibrant colours burst forth from the sky as meteors rain down upon your opponent.*");
|
||||
|
||||
[Command("stats")]
|
||||
public async Task Stats()
|
||||
public async Task Stats(Context ctx)
|
||||
{
|
||||
var messagesReceived = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name).Value;
|
||||
var messagesProxied = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name).Value;
|
||||
var proxySuccessRate = messagesProxied.Items.First(i => i.Item == "success");
|
||||
var messagesReceived = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name).Value;
|
||||
var messagesProxied = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name).Value;
|
||||
|
||||
var commandsRun = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name).Value;
|
||||
var commandsRun = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name).Value;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: new EmbedBuilder()
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.AddField("Messages processed", $"{messagesReceived.OneMinuteRate:F1}/s ({messagesReceived.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.AddField("Messages proxied", $"{messagesProxied.OneMinuteRate:F1}/s ({messagesProxied.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.AddField("Commands executed", $"{commandsRun.OneMinuteRate:F1}/s ({commandsRun.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.AddField("Proxy success rate", $"{proxySuccessRate.Percent/100:P1}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Command("permcheck")]
|
||||
[Summary("permcheck [guild]")]
|
||||
public async Task PermCheckGuild(ulong guildId)
|
||||
|
||||
public async Task PermCheckGuild(Context ctx)
|
||||
{
|
||||
var guildIdStr = ctx.PopArgument() ?? throw new PKSyntaxError("You must pass a server ID.");
|
||||
if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse `{guildIdStr.SanitizeMentions()}` as an ID.");
|
||||
|
||||
// TODO: will this call break for sharding if you try to request a guild on a different bot instance?
|
||||
var guild = Context.Client.GetGuild(guildId) as IGuild;
|
||||
var guild = ctx.Client.GetGuild(guildId) as IGuild;
|
||||
if (guild == null)
|
||||
throw Errors.GuildNotFound(guildId);
|
||||
|
||||
@@ -95,7 +97,7 @@ namespace PluralKit.Bot.Commands {
|
||||
|
||||
// Generate the output embed
|
||||
var eb = new EmbedBuilder()
|
||||
.WithTitle($"Permission check for **{guild.Name}**");
|
||||
.WithTitle($"Permission check for **{guild.Name.SanitizeMentions()}**");
|
||||
|
||||
if (permissionsMissing.Count == 0)
|
||||
{
|
||||
@@ -120,13 +122,7 @@ namespace PluralKit.Bot.Commands {
|
||||
}
|
||||
|
||||
// Send! :)
|
||||
await Context.Channel.SendMessageAsync(embed: eb.Build());
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
|
||||
[Command("permcheck")]
|
||||
[Summary("permcheck [guild]")]
|
||||
[RequireContext(ContextType.Guild, ErrorMessage =
|
||||
"When running this command in DMs, you must pass a guild ID.")]
|
||||
public Task PermCheckGuild() => PermCheckGuild(Context.Guild.Id);
|
||||
}
|
||||
}
|
@@ -1,44 +1,56 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class ModCommands: ModuleBase<PKCommandContext>
|
||||
public class ModCommands
|
||||
{
|
||||
public LogChannelService LogChannels { get; set; }
|
||||
public MessageStore Messages { get; set; }
|
||||
|
||||
public EmbedService Embeds { get; set; }
|
||||
|
||||
[Command("log")]
|
||||
[Remarks("log <channel>")]
|
||||
[RequireUserPermission(GuildPermission.ManageGuild, ErrorMessage = "You must have the Manage Server permission to use this command.")]
|
||||
[RequireContext(ContextType.Guild, ErrorMessage = "This command can not be run in a DM.")]
|
||||
public async Task SetLogChannel(ITextChannel channel = null)
|
||||
private LogChannelService _logChannels;
|
||||
private MessageStore _messages;
|
||||
|
||||
private EmbedService _embeds;
|
||||
|
||||
public ModCommands(LogChannelService logChannels, MessageStore messages, EmbedService embeds)
|
||||
{
|
||||
await LogChannels.SetLogChannel(Context.Guild, channel);
|
||||
_logChannels = logChannels;
|
||||
_messages = messages;
|
||||
_embeds = embeds;
|
||||
}
|
||||
|
||||
public async Task SetLogChannel(Context ctx)
|
||||
{
|
||||
ctx.CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server").CheckGuildContext();
|
||||
|
||||
ITextChannel channel = null;
|
||||
if (ctx.HasNext())
|
||||
channel = ctx.MatchChannel() ?? throw new PKSyntaxError("You must pass a #channel to set.");
|
||||
|
||||
await _logChannels.SetLogChannel(ctx.Guild, channel);
|
||||
|
||||
if (channel != null)
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Proxy logging channel set to #{channel.Name.Sanitize()}.");
|
||||
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name.SanitizeMentions()}.");
|
||||
else
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Proxy logging channel cleared.");
|
||||
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
|
||||
}
|
||||
|
||||
[Command("message")]
|
||||
[Remarks("message <messageid>")]
|
||||
[Alias("msg")]
|
||||
public async Task GetMessage(ulong messageId)
|
||||
|
||||
public async Task GetMessage(Context ctx)
|
||||
{
|
||||
var message = await Messages.Get(messageId);
|
||||
var word = ctx.PopArgument() ?? throw new PKSyntaxError("You must pass a message ID or link.");
|
||||
|
||||
ulong messageId;
|
||||
if (ulong.TryParse(word, out var id))
|
||||
messageId = id;
|
||||
else if (Regex.Match(word, "https://discordapp.com/channels/\\d+/(\\d+)") is Match match && match.Success)
|
||||
messageId = ulong.Parse(match.Groups[1].Value);
|
||||
else throw new PKSyntaxError($"Could not parse `{word}` as a message ID or link.");
|
||||
|
||||
var message = await _messages.Get(messageId);
|
||||
if (message == null) throw Errors.MessageNotFound(messageId);
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await Embeds.CreateMessageInfoEmbed(message));
|
||||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));
|
||||
}
|
||||
|
||||
[Command("message")]
|
||||
[Remarks("message <messageid>")]
|
||||
[Alias("msg")]
|
||||
public async Task GetMessage(IMessage msg) => await GetMessage(msg.Id);
|
||||
}
|
||||
}
|
@@ -2,75 +2,91 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using NodaTime;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("switch")]
|
||||
[Alias("sw")]
|
||||
public class SwitchCommands: ModuleBase<PKCommandContext>
|
||||
public class SwitchCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
public SwitchStore Switches { get; set; }
|
||||
private SwitchStore _switches;
|
||||
|
||||
[Command]
|
||||
[Remarks("switch <member> [member...]")]
|
||||
[MustHaveSystem]
|
||||
public async Task Switch(params PKMember[] members) => await DoSwitchCommand(members);
|
||||
|
||||
[Command("out")]
|
||||
[Alias("none")]
|
||||
[Remarks("switch out")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchOut() => await DoSwitchCommand(new PKMember[] { });
|
||||
|
||||
private async Task DoSwitchCommand(ICollection<PKMember> members)
|
||||
public SwitchCommands(SwitchStore switches)
|
||||
{
|
||||
// Make sure all the members *are actually in the system*
|
||||
// PKMember parameters won't let this happen if they resolve by name
|
||||
// but they can if they resolve with ID
|
||||
if (members.Any(m => m.System != Context.SenderSystem.Id)) throw Errors.SwitchMemberNotInSystem;
|
||||
_switches = switches;
|
||||
}
|
||||
|
||||
public async Task Switch(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
var members = new List<PKMember>();
|
||||
|
||||
// Loop through all the given arguments
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
// and attempt to match a member
|
||||
var member = await ctx.MatchMember();
|
||||
if (member == null)
|
||||
// if we can't, big error. Every member name must be valid.
|
||||
throw new PKError(ctx.CreateMemberNotFoundError(ctx.PopArgument()));
|
||||
|
||||
ctx.CheckOwnMember(member); // Ensure they're in our own system
|
||||
members.Add(member); // Then add to the final output list
|
||||
}
|
||||
|
||||
// Finally, do the actual switch
|
||||
await DoSwitchCommand(ctx, members);
|
||||
}
|
||||
public async Task SwitchOut(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Switch with no members = switch-out
|
||||
await DoSwitchCommand(ctx, new PKMember[] { });
|
||||
}
|
||||
|
||||
private async Task DoSwitchCommand(Context ctx, ICollection<PKMember> members)
|
||||
{
|
||||
// Make sure there are no dupes in the list
|
||||
// We do this by checking if removing duplicate member IDs results in a list of different length
|
||||
if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers;
|
||||
|
||||
// Find the last switch and its members if applicable
|
||||
var lastSwitch = await Switches.GetLatestSwitch(Context.SenderSystem);
|
||||
var lastSwitch = await _switches.GetLatestSwitch(ctx.System);
|
||||
if (lastSwitch != null)
|
||||
{
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastSwitch);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastSwitch);
|
||||
// Make sure the requested switch isn't identical to the last one
|
||||
if (lastSwitchMembers.Select(m => m.Id).SequenceEqual(members.Select(m => m.Id)))
|
||||
throw Errors.SameSwitch(members);
|
||||
}
|
||||
|
||||
await Switches.RegisterSwitch(Context.SenderSystem, members);
|
||||
await _switches.RegisterSwitch(ctx.System, members);
|
||||
|
||||
if (members.Count == 0)
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch-out registered.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch-out registered.");
|
||||
else
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.Name)).Sanitize()}.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.Name)).SanitizeMentions()}.");
|
||||
}
|
||||
|
||||
[Command("move")]
|
||||
[Alias("shift")]
|
||||
[Remarks("switch move <date/time>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchMove([Remainder] string str)
|
||||
|
||||
public async Task SwitchMove(Context ctx)
|
||||
{
|
||||
var tz = TzdbDateTimeZoneSource.Default.ForId(Context.SenderSystem.UiTz ?? "UTC");
|
||||
ctx.CheckSystem();
|
||||
|
||||
var result = PluralKit.Utils.ParseDateTime(str, true, tz);
|
||||
if (result == null) throw Errors.InvalidDateTime(str);
|
||||
var timeToMove = ctx.RemainderOrNull() ?? throw new PKSyntaxError("Must pass a date or time to move the switch to.");
|
||||
var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.System.UiTz ?? "UTC");
|
||||
|
||||
var result = PluralKit.Utils.ParseDateTime(timeToMove, true, tz);
|
||||
if (result == null) throw Errors.InvalidDateTime(timeToMove);
|
||||
|
||||
var time = result.Value;
|
||||
if (time.ToInstant() > SystemClock.Instance.GetCurrentInstant()) throw Errors.SwitchTimeInFuture;
|
||||
|
||||
// Fetch the last two switches for the system to do bounds checking on
|
||||
var lastTwoSwitches = (await Switches.GetSwitches(Context.SenderSystem, 2)).ToArray();
|
||||
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
|
||||
|
||||
// If we don't have a switch to move, don't bother
|
||||
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
|
||||
@@ -84,55 +100,53 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
// Now we can actually do the move, yay!
|
||||
// But, we do a prompt to confirm.
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
|
||||
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(Context.SenderSystem.Zone));
|
||||
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
var newSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(time);
|
||||
var newSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - time.ToInstant());
|
||||
|
||||
// yeet
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr.Sanitize()}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled;
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr.SanitizeMentions()}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled;
|
||||
|
||||
// aaaand *now* we do the move
|
||||
await Switches.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch moved.");
|
||||
await _switches.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
|
||||
await ctx.Reply($"{Emojis.Success} Switch moved.");
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Remarks("switch delete")]
|
||||
[Alias("remove", "erase", "cancel", "yeet")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchDelete()
|
||||
|
||||
public async Task SwitchDelete(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Fetch the last two switches for the system to do bounds checking on
|
||||
var lastTwoSwitches = (await Switches.GetSwitches(Context.SenderSystem, 2)).ToArray();
|
||||
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
|
||||
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
|
||||
IUserMessage msg;
|
||||
if (lastTwoSwitches.Length == 1)
|
||||
{
|
||||
msg = await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.Sanitize()}, {lastSwitchDeltaStr} ago). You have no other switches logged. Is this okay?");
|
||||
msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.SanitizeMentions()}, {lastSwitchDeltaStr} ago). You have no other switches logged. Is this okay?");
|
||||
}
|
||||
else
|
||||
{
|
||||
var secondSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[1]);
|
||||
var secondSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[1]);
|
||||
var secondSwitchMemberStr = string.Join(", ", secondSwitchMembers.Select(m => m.Name));
|
||||
var secondSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
|
||||
msg = await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.Sanitize()}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr.Sanitize()} ({secondSwitchDeltaStr} ago). Is this okay?");
|
||||
msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.SanitizeMentions()}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr.SanitizeMentions()} ({secondSwitchDeltaStr} ago). Is this okay?");
|
||||
}
|
||||
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled;
|
||||
await Switches.DeleteSwitch(lastTwoSwitches[0]);
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled;
|
||||
await _switches.DeleteSwitch(lastTwoSwitches[0]);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch deleted.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch deleted.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,230 +2,206 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Humanizer;
|
||||
using NodaTime;
|
||||
using NodaTime.Text;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("system")]
|
||||
[Alias("s")]
|
||||
public class SystemCommands : ContextParameterModuleBase<PKSystem>
|
||||
public class SystemCommands
|
||||
{
|
||||
public override string Prefix => "system";
|
||||
public override string ContextNoun => "system";
|
||||
private SystemStore _systems;
|
||||
private MemberStore _members;
|
||||
|
||||
public SystemStore Systems {get; set;}
|
||||
public MemberStore Members {get; set;}
|
||||
|
||||
public SwitchStore Switches {get; set;}
|
||||
public EmbedService EmbedService {get; set;}
|
||||
|
||||
private SwitchStore _switches;
|
||||
private EmbedService _embeds;
|
||||
|
||||
[Command]
|
||||
[Remarks("system <name>")]
|
||||
public async Task Query(PKSystem system = null) {
|
||||
if (system == null) system = Context.SenderSystem;
|
||||
private ProxyCacheService _proxyCache;
|
||||
|
||||
public SystemCommands(SystemStore systems, MemberStore members, SwitchStore switches, EmbedService embeds, ProxyCacheService proxyCache)
|
||||
{
|
||||
_systems = systems;
|
||||
_members = members;
|
||||
_switches = switches;
|
||||
_embeds = embeds;
|
||||
_proxyCache = proxyCache;
|
||||
}
|
||||
|
||||
public async Task Query(Context ctx, PKSystem system) {
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system));
|
||||
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system));
|
||||
}
|
||||
|
||||
[Command("new")]
|
||||
[Alias("register", "create", "init", "add", "make")]
|
||||
[Remarks("system new <name>")]
|
||||
public async Task New([Remainder] string systemName = null)
|
||||
|
||||
public async Task New(Context ctx)
|
||||
{
|
||||
if (ContextEntity != null) throw Errors.NotOwnSystemError;
|
||||
if (Context.SenderSystem != null) throw Errors.ExistingSystemError;
|
||||
ctx.CheckNoSystem();
|
||||
|
||||
var system = await Systems.Create(systemName);
|
||||
await Systems.Link(system, Context.User.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
||||
var system = await _systems.Create(ctx.RemainderOrNull());
|
||||
await _systems.Link(system, ctx.Author.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
||||
}
|
||||
|
||||
public async Task Name(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
[Command("name")]
|
||||
[Alias("rename", "changename")]
|
||||
[Remarks("system name <name>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Name([Remainder] string newSystemName = null) {
|
||||
var newSystemName = ctx.RemainderOrNull();
|
||||
if (newSystemName != null && newSystemName.Length > Limits.MaxSystemNameLength) throw Errors.SystemNameTooLongError(newSystemName.Length);
|
||||
|
||||
Context.SenderSystem.Name = newSystemName;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}.");
|
||||
ctx.System.Name = newSystemName;
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}.");
|
||||
}
|
||||
|
||||
public async Task Description(Context ctx) {
|
||||
ctx.CheckSystem();
|
||||
|
||||
[Command("description")]
|
||||
[Alias("desc")]
|
||||
[Remarks("system description <description>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Description([Remainder] string newDescription = null) {
|
||||
var newDescription = ctx.RemainderOrNull();
|
||||
if (newDescription != null && newDescription.Length > Limits.MaxDescriptionLength) throw Errors.DescriptionTooLongError(newDescription.Length);
|
||||
|
||||
Context.SenderSystem.Description = newDescription;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}.");
|
||||
ctx.System.Description = newDescription;
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}.");
|
||||
}
|
||||
|
||||
public async Task Tag(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
[Command("tag")]
|
||||
[Remarks("system tag <tag>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Tag([Remainder] string newTag = null) {
|
||||
|
||||
Context.SenderSystem.Tag = newTag;
|
||||
var newTag = ctx.RemainderOrNull();
|
||||
ctx.System.Tag = newTag;
|
||||
|
||||
if (newTag != null)
|
||||
{
|
||||
if (newTag.Length > Limits.MaxSystemTagLength) throw Errors.SystemNameTooLongError(newTag.Length);
|
||||
|
||||
// Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel)
|
||||
var unproxyableMembers = await Members.GetUnproxyableMembers(Context.SenderSystem);
|
||||
var unproxyableMembers = await _members.GetUnproxyableMembers(ctx.System);
|
||||
if (unproxyableMembers.Count > 0)
|
||||
{
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Warn} Changing your system tag to '{newTag}' will result in the following members being unproxyable, since the tag would bring their name over 32 characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name))}**\nDo you want to continue anyway?");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
|
||||
var msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} Changing your system tag to '{newTag.SanitizeMentions()}' will result in the following members being unproxyable, since the tag would bring their name over {Limits.MaxProxyNameLength} characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name.SanitizeMentions()))}**\nDo you want to continue anyway?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}.");
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("system avatar <avatar url>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemAvatar(IUser member)
|
||||
{
|
||||
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
Context.SenderSystem.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}.");
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(Context.SenderSystem.AvatarUrl).Build();
|
||||
await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Success} System avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the system's avatar will need to be re-set.", embed: embed);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("system avatar <avatar url>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemAvatar([Remainder] string avatarUrl = null)
|
||||
public async Task SystemAvatar(Context ctx)
|
||||
{
|
||||
string url = avatarUrl ?? Context.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await Context.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
ctx.CheckSystem();
|
||||
|
||||
Context.SenderSystem.AvatarUrl = url;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
|
||||
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Alias("remove", "destroy", "erase", "yeet")]
|
||||
[Remarks("system delete")]
|
||||
[MustHaveSystem]
|
||||
public async Task Delete() {
|
||||
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*.");
|
||||
|
||||
await Systems.Delete(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System deleted.");
|
||||
}
|
||||
|
||||
[Group("list")]
|
||||
[Alias("l", "members")]
|
||||
public class SystemListCommands: ModuleBase<PKCommandContext> {
|
||||
public MemberStore Members { get; set; }
|
||||
|
||||
[Command]
|
||||
[Remarks("system [system] list")]
|
||||
public async Task MemberShortList() {
|
||||
var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem;
|
||||
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}`";
|
||||
await Context.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name.toLower()).ToList(),
|
||||
25,
|
||||
embedTitle,
|
||||
(eb, ms) => eb.Description = string.Join("\n", ms.Select((m) => {
|
||||
if (m.HasProxyTags) return $"[`{m.Hid}`] **{m.Name}** *({m.ProxyString})*";
|
||||
return $"[`{m.Hid}`] **{m.Name}**";
|
||||
}))
|
||||
);
|
||||
var member = await ctx.MatchUser();
|
||||
if (member != null)
|
||||
{
|
||||
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
ctx.System.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(ctx.System.AvatarUrl).Build();
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} System avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the system's avatar will need to be re-set.", embed: embed);
|
||||
}
|
||||
else
|
||||
{
|
||||
string url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await ctx.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
|
||||
[Command("full")]
|
||||
[Alias("big", "details", "long")]
|
||||
[Remarks("system [system] list full")]
|
||||
public async Task MemberLongList() {
|
||||
var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
ctx.System.AvatarUrl = url;
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
var members = await Members.GetBySystem(system);
|
||||
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||
await Context.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name).ToList(),
|
||||
5,
|
||||
embedTitle,
|
||||
(eb, ms) => {
|
||||
foreach (var m in ms) {
|
||||
var profile = $"**ID**: {m.Hid}";
|
||||
if (m.Pronouns != null) profile += $"\n**Pronouns**: {m.Pronouns}";
|
||||
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
||||
if (m.Prefix != null || m.Suffix != null) profile += $"\n**Proxy tags**: {m.ProxyString}";
|
||||
if (m.Description != null) profile += $"\n\n{m.Description}";
|
||||
eb.AddField(m.Name, profile.Truncate(1024));
|
||||
}
|
||||
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
|
||||
await ctx.Reply($"{Emojis.Success} System avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
}
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public async Task Delete(Context ctx) {
|
||||
ctx.CheckSystem();
|
||||
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{ctx.System.Hid}`).\n**Note: this action is permanent.**");
|
||||
var reply = await ctx.AwaitMessage(ctx.Channel, ctx.Author, timeout: TimeSpan.FromMinutes(1));
|
||||
if (reply.Content != ctx.System.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{ctx.System.Hid}`) *verbatim*.");
|
||||
|
||||
await _systems.Delete(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System deleted.");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public async Task MemberShortList(Context ctx, PKSystem system) {
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var members = await _members.GetBySystem(system);
|
||||
var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||
await ctx.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name.ToLower()).ToList(),
|
||||
25,
|
||||
embedTitle,
|
||||
(eb, ms) => eb.Description = string.Join("\n", ms.Select((m) => {
|
||||
if (m.HasProxyTags) return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({m.ProxyString.SanitizeMentions()})*";
|
||||
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
public async Task MemberLongList(Context ctx, PKSystem system) {
|
||||
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}`";
|
||||
await ctx.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name.ToLower()).ToList(),
|
||||
5,
|
||||
embedTitle,
|
||||
(eb, ms) => {
|
||||
foreach (var m in ms) {
|
||||
var profile = $"**ID**: {m.Hid}";
|
||||
if (m.Pronouns != null) profile += $"\n**Pronouns**: {m.Pronouns}";
|
||||
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
||||
if (m.Prefix != null || m.Suffix != null) profile += $"\n**Proxy tags**: {m.ProxyString}";
|
||||
if (m.Description != null) profile += $"\n\n{m.Description}";
|
||||
eb.AddField(m.Name, profile.Truncate(1024));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Command("fronter")]
|
||||
[Alias("f", "front", "fronters")]
|
||||
[Remarks("system [system] fronter")]
|
||||
public async Task SystemFronter()
|
||||
|
||||
public async Task SystemFronter(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var sw = await Switches.GetLatestSwitch(system);
|
||||
var sw = await _switches.GetLatestSwitch(system);
|
||||
if (sw == null) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFronterEmbed(sw, system.Zone));
|
||||
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone));
|
||||
}
|
||||
|
||||
[Command("fronthistory")]
|
||||
[Alias("fh", "history", "switches")]
|
||||
[Remarks("system [system] fronthistory")]
|
||||
public async Task SystemFrontHistory()
|
||||
|
||||
public async Task SystemFrontHistory(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var sws = (await Switches.GetSwitches(system, 10)).ToList();
|
||||
var sws = (await _switches.GetSwitches(system, 10)).ToList();
|
||||
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontHistoryEmbed(sws, system.Zone));
|
||||
await ctx.Reply(embed: await _embeds.CreateFrontHistoryEmbed(sws, system.Zone));
|
||||
}
|
||||
|
||||
[Command("frontpercent")]
|
||||
[Alias("frontbreakdown", "frontpercent", "front%", "fp")]
|
||||
[Remarks("system [system] frontpercent [duration]")]
|
||||
public async Task SystemFrontPercent([Remainder] string durationStr = "30d")
|
||||
|
||||
public async Task SystemFrontPercent(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
string durationStr = ctx.RemainderOrNull() ?? "30d";
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
@@ -233,38 +209,37 @@ namespace PluralKit.Bot.Commands
|
||||
if (rangeStart == null) throw Errors.InvalidDateTime(durationStr);
|
||||
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
|
||||
|
||||
var frontpercent = await Switches.GetPerMemberSwitchDuration(system, rangeStart.Value.ToInstant(), now);
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontPercentEmbed(frontpercent, system.Zone));
|
||||
var frontpercent = await _switches.GetPerMemberSwitchDuration(system, rangeStart.Value.ToInstant(), now);
|
||||
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone));
|
||||
}
|
||||
|
||||
[Command("timezone")]
|
||||
[Alias("tz")]
|
||||
[Remarks("system timezone [timezone]")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemTimezone([Remainder] string zoneStr = null)
|
||||
|
||||
public async Task SystemTimezone(Context ctx)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
|
||||
var zoneStr = ctx.RemainderOrNull();
|
||||
if (zoneStr == null)
|
||||
{
|
||||
Context.SenderSystem.UiTz = "UTC";
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System time zone cleared.");
|
||||
ctx.System.UiTz = "UTC";
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System time zone cleared.");
|
||||
return;
|
||||
}
|
||||
|
||||
var zone = await FindTimeZone(zoneStr);
|
||||
var zone = await FindTimeZone(ctx, zoneStr);
|
||||
if (zone == null) throw Errors.InvalidTimeZone(zoneStr);
|
||||
|
||||
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
var msg = await ctx.Reply(
|
||||
$"This will change the system time zone to {zone.Id}. The current time is {Formats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
|
||||
Context.SenderSystem.UiTz = zone.Id;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
|
||||
ctx.System.UiTz = zone.Id;
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"System time zone changed to {zone.Id}.");
|
||||
await ctx.Reply($"System time zone changed to {zone.Id}.");
|
||||
}
|
||||
|
||||
public async Task<DateTimeZone> FindTimeZone(string zoneStr) {
|
||||
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
|
||||
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||
|
||||
@@ -312,7 +287,7 @@ namespace PluralKit.Bot.Commands
|
||||
return matchingZones.First();
|
||||
|
||||
// Otherwise, prompt and return!
|
||||
return await Context.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
|
||||
return await ctx.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
|
||||
z =>
|
||||
{
|
||||
if (TzdbDateTimeZoneSource.Default.Aliases.Contains(z.Id))
|
||||
@@ -320,12 +295,6 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
return $"**{z.Id}**";
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<PKSystem> ReadContextParameterAsync(string value)
|
||||
{
|
||||
var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services);
|
||||
return res.IsSuccess ? res.BestMatch as PKSystem : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user