diff --git a/PluralKit.Bot/Commands/AutoproxyCommands.cs b/PluralKit.Bot/Commands/Autoproxy.cs similarity index 97% rename from PluralKit.Bot/Commands/AutoproxyCommands.cs rename to PluralKit.Bot/Commands/Autoproxy.cs index e748e6de..e8966d36 100644 --- a/PluralKit.Bot/Commands/AutoproxyCommands.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -8,18 +8,18 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class AutoproxyCommands + public class Autoproxy { private IDataStore _data; private AutoproxyCacheService _cache; - public AutoproxyCommands(IDataStore data, AutoproxyCacheService cache) + public Autoproxy(IDataStore data, AutoproxyCacheService cache) { _data = data; _cache = cache; } - public async Task Autoproxy(Context ctx) + public async Task AutoproxyRoot(Context ctx) { ctx.CheckSystem().CheckGuildContext(); diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index ded0e5b6..8ff7c6a2 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -89,58 +89,58 @@ namespace PluralKit.Bot.Commands if (ctx.Match("switch", "sw")) return HandleSwitchCommand(ctx); if (ctx.Match("ap", "autoproxy", "auto")) - return ctx.Execute(Autoproxy, m => m.Autoproxy(ctx)); + return ctx.Execute(Autoproxy, m => m.AutoproxyRoot(ctx)); if (ctx.Match("link")) - return ctx.Execute(Link, m => m.LinkSystem(ctx)); + return ctx.Execute(Link, m => m.LinkSystem(ctx)); if (ctx.Match("unlink")) - return ctx.Execute(Unlink, m => m.UnlinkAccount(ctx)); + return ctx.Execute(Unlink, m => m.UnlinkAccount(ctx)); if (ctx.Match("token")) if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) - return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); + return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); else - return ctx.Execute(TokenGet, m => m.GetToken(ctx)); + return ctx.Execute(TokenGet, m => m.GetToken(ctx)); if (ctx.Match("import")) - return ctx.Execute(Import, m => m.Import(ctx)); + return ctx.Execute(Import, m => m.Import(ctx)); if (ctx.Match("export")) - return ctx.Execute(Export, m => m.Export(ctx)); + return ctx.Execute(Export, m => m.Export(ctx)); if (ctx.Match("help")) if (ctx.Match("commands")) return ctx.Reply("For the list of commands, see the website: "); else if (ctx.Match("proxy")) return ctx.Reply("The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"); - else return ctx.Execute(Help, m => m.HelpRoot(ctx)); + else return ctx.Execute(Help, m => m.HelpRoot(ctx)); if (ctx.Match("commands")) return ctx.Reply("For the list of commands, see the website: "); if (ctx.Match("message", "msg")) - return ctx.Execute(Message, m => m.GetMessage(ctx)); + return ctx.Execute(Message, m => m.GetMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) - return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx)); + return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx)); else if (ctx.Match("enable", "on")) - return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true)); + return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true)); else if (ctx.Match("disable", "off")) - return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false)); + return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false)); else return PrintCommandExpectedError(ctx, LogCommands); if (ctx.Match("blacklist", "bl")) if (ctx.Match("enable", "on", "add", "deny")) - return ctx.Execute(BlacklistAdd, m => m.SetBlacklisted(ctx, true)); + return ctx.Execute(BlacklistAdd, m => m.SetBlacklisted(ctx, true)); else if (ctx.Match("disable", "off", "remove", "allow")) - return ctx.Execute(BlacklistRemove, m => m.SetBlacklisted(ctx, false)); + return ctx.Execute(BlacklistRemove, m => m.SetBlacklisted(ctx, false)); else return PrintCommandExpectedError(ctx, BlacklistAdd, BlacklistRemove); if (ctx.Match("proxy", "enable", "disable")) - return ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); - if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); - if (ctx.Match("mn")) return ctx.Execute(null, m => m.Mn(ctx)); - if (ctx.Match("fire")) return ctx.Execute(null, m => m.Fire(ctx)); - if (ctx.Match("thunder")) return ctx.Execute(null, m => m.Thunder(ctx)); - if (ctx.Match("freeze")) return ctx.Execute(null, m => m.Freeze(ctx)); - if (ctx.Match("starstorm")) return ctx.Execute(null, m => m.Starstorm(ctx)); - if (ctx.Match("flash")) return ctx.Execute(null, m => m.Flash(ctx)); - if (ctx.Match("stats")) return ctx.Execute(null, m => m.Stats(ctx)); + return ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); + if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); + if (ctx.Match("mn")) return ctx.Execute(null, m => m.Mn(ctx)); + if (ctx.Match("fire")) return ctx.Execute(null, m => m.Fire(ctx)); + if (ctx.Match("thunder")) return ctx.Execute(null, m => m.Thunder(ctx)); + if (ctx.Match("freeze")) return ctx.Execute(null, m => m.Freeze(ctx)); + if (ctx.Match("starstorm")) return ctx.Execute(null, m => m.Starstorm(ctx)); + if (ctx.Match("flash")) return ctx.Execute(null, m => m.Flash(ctx)); + if (ctx.Match("stats")) return ctx.Execute(null, m => m.Stats(ctx)); if (ctx.Match("permcheck")) - return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); + return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); if (ctx.Match("random", "r")) - return ctx.Execute(MemberRandom, m => m.MemberRandom(ctx)); + return ctx.Execute(MemberRandom, m => m.MemberRandom(ctx)); ctx.Reply( $"{Emojis.Error} Unknown command `{ctx.PeekArgument().SanitizeMentions()}`. For a list of possible commands, see ."); @@ -151,51 +151,51 @@ namespace PluralKit.Bot.Commands { // If we have no parameters, default to self-target if (!ctx.HasNext()) - await ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System)); + await ctx.Execute(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(SystemNew, m => m.New(ctx)); + await ctx.Execute(SystemNew, m => m.New(ctx)); else if (ctx.Match("name", "rename", "changename")) - await ctx.Execute(SystemRename, m => m.Name(ctx)); + await ctx.Execute(SystemRename, m => m.Name(ctx)); else if (ctx.Match("tag")) - await ctx.Execute(SystemTag, m => m.Tag(ctx)); + await ctx.Execute(SystemTag, m => m.Tag(ctx)); else if (ctx.Match("description", "desc", "bio")) - await ctx.Execute(SystemDesc, m => m.Description(ctx)); + await ctx.Execute(SystemDesc, m => m.Description(ctx)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - await ctx.Execute(SystemAvatar, m => m.SystemAvatar(ctx)); + await ctx.Execute(SystemAvatar, m => m.Avatar(ctx)); else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) - await ctx.Execute(SystemDelete, m => m.Delete(ctx)); + await ctx.Execute(SystemDelete, m => m.Delete(ctx)); else if (ctx.Match("timezone", "tz")) - await ctx.Execute(SystemTimezone, m => m.SystemTimezone(ctx)); + await ctx.Execute(SystemTimezone, m => m.SystemTimezone(ctx)); else if (ctx.Match("proxy")) - await ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); + await ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); else if (ctx.Match("list", "l", "members")) { if (ctx.Match("f", "full", "big", "details", "long")) - await ctx.Execute(SystemList, m => m.MemberLongList(ctx, ctx.System)); + await ctx.Execute(SystemList, m => m.MemberLongList(ctx, ctx.System)); else - await ctx.Execute(SystemList, m => m.MemberShortList(ctx, ctx.System)); + await ctx.Execute(SystemList, m => m.MemberShortList(ctx, ctx.System)); } else if (ctx.Match("f", "front", "fronter", "fronters")) { if (ctx.Match("h", "history")) - await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System)); + await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System)); else if (ctx.Match("p", "percent", "%")) - await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System)); + await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System)); else - await ctx.Execute(SystemFronter, m => m.SystemFronter(ctx, ctx.System)); + await ctx.Execute(SystemFronter, m => m.SystemFronter(ctx, ctx.System)); } else if (ctx.Match("fh", "fronthistory", "history", "switches")) - await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System)); + await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System)); else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System)); + await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System)); else if (ctx.Match("privacy")) - await ctx.Execute(SystemPrivacy, m => m.SystemPrivacy(ctx)); + await ctx.Execute(SystemPrivacy, m => m.SystemPrivacy(ctx)); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "systems", SystemCommands); else if (!ctx.HasNext()) // Bare command - await ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System)); + await ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System)); else await HandleSystemCommandTargeted(ctx); } @@ -213,27 +213,27 @@ namespace PluralKit.Bot.Commands else if (ctx.Match("list", "l", "members")) { if (ctx.Match("f", "full", "big", "details", "long")) - await ctx.Execute(SystemList, m => m.MemberLongList(ctx, target)); + await ctx.Execute(SystemList, m => m.MemberLongList(ctx, target)); else - await ctx.Execute(SystemList, m => m.MemberShortList(ctx, target)); + await ctx.Execute(SystemList, m => m.MemberShortList(ctx, target)); } else if (ctx.Match("f", "front", "fronter", "fronters")) { if (ctx.Match("h", "history")) - await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); + await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); else if (ctx.Match("p", "percent", "%")) - await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target)); + await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target)); else - await ctx.Execute(SystemFronter, m => m.SystemFronter(ctx, target)); + await ctx.Execute(SystemFronter, m => m.SystemFronter(ctx, target)); } else if (ctx.Match("fh", "fronthistory", "history", "switches")) - await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); + await ctx.Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target)); + await ctx.Execute(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target)); else if (ctx.Match("info", "view", "show")) - await ctx.Execute(SystemInfo, m => m.Query(ctx, target)); + await ctx.Execute(SystemInfo, m => m.Query(ctx, target)); else if (!ctx.HasNext()) - await ctx.Execute(SystemInfo, m => m.Query(ctx, target)); + await ctx.Execute(SystemInfo, m => m.Query(ctx, target)); else await PrintCommandNotFoundError(ctx, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemInfo); @@ -242,7 +242,7 @@ namespace PluralKit.Bot.Commands private async Task HandleMemberCommand(Context ctx) { if (ctx.Match("new", "n", "add", "create", "register")) - await ctx.Execute(MemberNew, m => m.NewMember(ctx)); + await ctx.Execute(MemberNew, m => m.NewMember(ctx)); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "members", MemberCommands); else if (await ctx.MatchMember() is PKMember target) @@ -258,31 +258,31 @@ namespace PluralKit.Bot.Commands { // Commands that have a member target (eg. pk;member delete) if (ctx.Match("rename", "name", "changename", "setname")) - await ctx.Execute(MemberRename, m => m.RenameMember(ctx, target)); + await ctx.Execute(MemberRename, m => m.Name(ctx, target)); else if (ctx.Match("description", "info", "bio", "text", "desc")) - await ctx.Execute(MemberDesc, m => m.MemberDescription(ctx, target)); + await ctx.Execute(MemberDesc, m => m.Description(ctx, target)); else if (ctx.Match("pronouns", "pronoun")) - await ctx.Execute(MemberPronouns, m => m.MemberPronouns(ctx, target)); + await ctx.Execute(MemberPronouns, m => m.Pronouns(ctx, target)); else if (ctx.Match("color", "colour")) - await ctx.Execute(MemberColor, m => m.MemberColor(ctx, target)); + await ctx.Execute(MemberColor, m => m.Color(ctx, target)); else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate")) - await ctx.Execute(MemberBirthday, m => m.MemberBirthday(ctx, target)); + await ctx.Execute(MemberBirthday, m => m.Birthday(ctx, target)); else if (ctx.Match("proxy", "tags", "proxytags", "brackets")) - await ctx.Execute(MemberProxy, m => m.MemberProxy(ctx, target)); + await ctx.Execute(MemberProxy, m => m.Proxy(ctx, target)); else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) - await ctx.Execute(MemberDelete, m => m.MemberDelete(ctx, target)); + await ctx.Execute(MemberDelete, m => m.Delete(ctx, target)); else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) - await ctx.Execute(MemberAvatar, m => m.MemberAvatar(ctx, target)); + await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname")) - await ctx.Execute(MemberDisplayName, m => m.MemberDisplayName(ctx, target)); + await ctx.Execute(MemberDisplayName, m => m.DisplayName(ctx, target)); else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname", "serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn")) - await ctx.Execute(MemberServerName, m => m.MemberServerName(ctx, target)); + await ctx.Execute(MemberServerName, m => m.ServerName(ctx, target)); else if (ctx.Match("keepproxy", "keeptags", "showtags")) - await ctx.Execute(MemberKeepProxy, m => m.MemberKeepProxy(ctx, target)); + await ctx.Execute(MemberKeepProxy, m => m.KeepProxy(ctx, target)); else if (ctx.Match("private", "privacy", "hidden", "public")) - await ctx.Execute(MemberPrivacy, m => m.MemberPrivacy(ctx, target)); + await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target)); else if (!ctx.HasNext()) // Bare command - await ctx.Execute(MemberInfo, m => m.ViewMember(ctx, target)); + await ctx.Execute(MemberInfo, m => m.ViewMember(ctx, target)); else await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName ,MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList); } @@ -290,15 +290,15 @@ namespace PluralKit.Bot.Commands private async Task HandleSwitchCommand(Context ctx) { if (ctx.Match("out")) - await ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)); + await ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)); else if (ctx.Match("move", "shift", "offset")) - await ctx.Execute(SwitchMove, m => m.SwitchMove(ctx)); + await ctx.Execute(SwitchMove, m => m.SwitchMove(ctx)); else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet")) - await ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx)); + await ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx)); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "switching", SwitchCommands); else if (ctx.HasNext()) // there are following arguments - await ctx.Execute(Switch, m => m.Switch(ctx)); + await ctx.Execute(Switch, m => m.SwitchDo(ctx)); else await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchDelete, SystemFronter, SystemFrontHistory); } diff --git a/PluralKit.Bot/Commands/Fun.cs b/PluralKit.Bot/Commands/Fun.cs new file mode 100644 index 00000000..92288d7a --- /dev/null +++ b/PluralKit.Bot/Commands/Fun.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class Fun + { + 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.*"); + public Task Flash(Context ctx) => ctx.Reply("*A ball of green light appears above your head and flies towards your enemy, exploding on contact.*"); + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/HelpCommands.cs b/PluralKit.Bot/Commands/Help.cs similarity index 99% rename from PluralKit.Bot/Commands/HelpCommands.cs rename to PluralKit.Bot/Commands/Help.cs index be5578a4..57fb7d7b 100644 --- a/PluralKit.Bot/Commands/HelpCommands.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -5,7 +5,7 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class HelpCommands + public class Help { public async Task HelpRoot(Context ctx) { diff --git a/PluralKit.Bot/Commands/ImportExportCommands.cs b/PluralKit.Bot/Commands/ImportExport.cs similarity index 98% rename from PluralKit.Bot/Commands/ImportExportCommands.cs rename to PluralKit.Bot/Commands/ImportExport.cs index 79dc90e5..1cc62a3d 100644 --- a/PluralKit.Bot/Commands/ImportExportCommands.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -12,10 +12,10 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class ImportExportCommands + public class ImportExport { private DataFileService _dataFiles; - public ImportExportCommands(DataFileService dataFiles) + public ImportExport(DataFileService dataFiles) { _dataFiles = dataFiles; } diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs new file mode 100644 index 00000000..7f729877 --- /dev/null +++ b/PluralKit.Bot/Commands/Member.cs @@ -0,0 +1,82 @@ +using System.Linq; +using System.Threading.Tasks; + +using PluralKit.Bot.CommandSystem; +using PluralKit.Core; + +namespace PluralKit.Bot.Commands +{ + public class Member + { + private IDataStore _data; + private EmbedService _embeds; + + private ProxyCacheService _proxyCache; + + public Member(IDataStore data, EmbedService embeds, ProxyCacheService proxyCache) + { + _data = data; + _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 there's already a member by this name + var existingMember = await _data.GetMemberByName(ctx.System, memberName); + if (existingMember != null) { + 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."); + } + + // Enforce per-system member limit + var memberCount = await _data.GetSystemMemberCount(ctx.System, true); + if (memberCount >= Limits.MaxMemberCount) + throw Errors.MemberLimitReachedError; + + // Create the member + var member = await _data.CreateMember(ctx.System, memberName); + memberCount++; + + // Send confirmation and space hint + 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}`)."); + if (memberCount >= Limits.MaxMemberCount) + await ctx.Reply($"{Emojis.Warn} You have reached the per-system member limit ({Limits.MaxMemberCount}). You will be unable to create additional members until existing members are deleted."); + else if (memberCount >= Limits.MaxMembersWarnThreshold) + await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {Limits.MaxMemberCount} members). Please review your member list for unused or duplicate members."); + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + public async Task MemberRandom(Context ctx) + { + ctx.CheckSystem(); + + var randGen = new global::System.Random(); + //Maybe move this somewhere else in the file structure since it doesn't need to get created at every command + + // TODO: don't buffer these, find something else to do ig + var members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberPrivacy == PrivacyLevel.Public).ToListAsync(); + if (members == null || !members.Any()) + throw Errors.NoMembersError; + var randInt = randGen.Next(members.Count); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); + + } + + + + public async Task ViewMember(Context ctx, PKMember target) + { + var system = await _data.GetSystemById(target.System); + await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs new file mode 100644 index 00000000..0cae2487 --- /dev/null +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -0,0 +1,87 @@ +using System.Linq; +using System.Threading.Tasks; + +using Discord; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class MemberAvatar + { + private IDataStore _data; + private ProxyCacheService _proxyCache; + + public MemberAvatar(IDataStore data, ProxyCacheService proxyCache) + { + _data = data; + _proxyCache = proxyCache; + } + + public async Task Avatar(Context ctx, PKMember target) + { + if (ctx.RemainderOrNull() == null && ctx.Message.Attachments.Count == 0) + { + if ((target.AvatarUrl?.Trim() ?? "").Length > 0) + { + var eb = new EmbedBuilder() + .WithTitle($"{target.Name.SanitizeMentions()}'s avatar") + .WithImageUrl(target.AvatarUrl); + if (target.System == ctx.System?.Id) + eb.WithDescription($"To clear, use `pk;member {target.Hid} avatar clear`."); + await ctx.Reply(embed: eb.Build()); + } + else + { + if (target.System == ctx.System?.Id) + throw new PKSyntaxError($"This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + throw new PKError($"This member does not have an avatar set."); + } + + return; + } + + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + if (ctx.Match("clear", "remove")) + { + target.AvatarUrl = null; + await _data.SaveMember(target); + await ctx.Reply($"{Emojis.Success} Member avatar cleared."); + } + else if (await ctx.MatchUser() is IUser user) + { + if (user.AvatarId == null) throw Errors.UserHasNoAvatar; + target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); + + await _data.SaveMember(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 _data.SaveMember(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 _data.SaveMember(target); + + await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working."); + } + // No-arguments no-attachment case covered by conditional at the very top + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberCommands.cs b/PluralKit.Bot/Commands/MemberCommands.cs deleted file mode 100644 index f3fbad7e..00000000 --- a/PluralKit.Bot/Commands/MemberCommands.cs +++ /dev/null @@ -1,461 +0,0 @@ -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Discord; -using NodaTime; - -using PluralKit.Bot.CommandSystem; -using PluralKit.Core; - -namespace PluralKit.Bot.Commands -{ - public class MemberCommands - { - private IDataStore _data; - private EmbedService _embeds; - - private ProxyCacheService _proxyCache; - - public MemberCommands(IDataStore data, EmbedService embeds, ProxyCacheService proxyCache) - { - _data = data; - _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 there's already a member by this name - var existingMember = await _data.GetMemberByName(ctx.System, memberName); - if (existingMember != null) { - 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."); - } - - // Enforce per-system member limit - var memberCount = await _data.GetSystemMemberCount(ctx.System, true); - if (memberCount >= Limits.MaxMemberCount) - throw Errors.MemberLimitReachedError; - - // Create the member - var member = await _data.CreateMember(ctx.System, memberName); - memberCount++; - - // Send confirmation and space hint - 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}`)."); - if (memberCount >= Limits.MaxMemberCount) - await ctx.Reply($"{Emojis.Warn} You have reached the per-system member limit ({Limits.MaxMemberCount}). You will be unable to create additional members until existing members are deleted."); - else if (memberCount >= Limits.MaxMembersWarnThreshold) - await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {Limits.MaxMemberCount} members). Please review your member list for unused or duplicate members."); - - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberRandom(Context ctx) - { - ctx.CheckSystem(); - - var randGen = new System.Random(); - //Maybe move this somewhere else in the file structure since it doesn't need to get created at every command - - // TODO: don't buffer these, find something else to do ig - var members = await _data.GetSystemMembers(ctx.System).Where(m => m.MemberPrivacy == PrivacyLevel.Public).ToListAsync(); - if (members == null || !members.Any()) - throw Errors.NoMembersError; - var randInt = randGen.Next(members.Count); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System))); - - } - - 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() ?? throw new PKSyntaxError("You must pass a new name for the member."); - - // Hard name length cap - if (newName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(newName.Length); - - // Warn if there's already a member by this name - var existingMember = await _data.GetMemberByName(ctx.System, newName); - if (existingMember != null) { - 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 - target.Name = newName; - await _data.SaveMember(target); - - 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."); - - if (ctx.Guild != null) - { - var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); - if (memberGuildConfig.DisplayName != null) - await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName.SanitizeMentions()}) in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name here."); - } - - 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; - - var description = ctx.RemainderOrNull(); - if (description.IsLongerThan(Limits.MaxDescriptionLength)) throw Errors.DescriptionTooLongError(description.Length); - - target.Description = description; - await _data.SaveMember(target); - - 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; - - var pronouns = ctx.RemainderOrNull(); - if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) throw Errors.MemberPronounsTooLongError(pronouns.Length); - - target.Pronouns = pronouns; - await _data.SaveMember(target); - - await ctx.Reply($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}."); - } - - 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); - } - - target.Color = color; - await _data.SaveMember(target); - - await ctx.Reply($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}."); - } - - 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); - } - - target.Birthday = date; - await _data.SaveMember(target); - - await ctx.Reply($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {target.BirthdayString}")}."); - } - - public async Task MemberProxy(Context ctx, PKMember target) - { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - ProxyTag ParseProxyTags(string exampleProxy) - { - // // Make sure there's one and only one instance of "text" in the example proxy given - var prefixAndSuffix = exampleProxy.Split("text"); - if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; - if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; - return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); - } - - async Task WarnOnConflict(ProxyTag newTag) - { - var conflicts = (await _data.GetConflictingProxies(ctx.System, newTag)) - .Where(m => m.Id != target.Id) - .ToList(); - - if (conflicts.Count <= 0) return true; - - var conflictList = conflicts.Select(m => $"- **{m.Name}**"); - var msg = await ctx.Reply( - $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"); - return await ctx.PromptYesNo(msg); - } - - // "Sub"command: no arguments clearing - // Also matches the pseudo-subcommand "text" which is equivalent to emoty proxy tags on both sides. - if (!ctx.HasNext() || ctx.Match("text")) - { - // If we already have multiple tags, this would clear everything, so prompt that - if (target.ProxyTags.Count > 1) - { - var msg = await ctx.Reply( - $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"); - if (!await ctx.PromptYesNo(msg)) - throw Errors.GenericCancelled(); - } - - target.ProxyTags = new ProxyTag[] { }; - - await _data.SaveMember(target); - await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); - } - // Subcommand: "add" - else if (ctx.Match("add")) - { - if (!ctx.HasNext()) throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); - - var tagToAdd = ParseProxyTags(ctx.RemainderOrNull()); - if (target.ProxyTags.Contains(tagToAdd)) - throw Errors.ProxyTagAlreadyExists(tagToAdd, target); - - if (!await WarnOnConflict(tagToAdd)) - throw Errors.GenericCancelled(); - - // It's not guaranteed the list's mutable, so we force it to be - target.ProxyTags = target.ProxyTags.ToList(); - target.ProxyTags.Add(tagToAdd); - - await _data.SaveMember(target); - await ctx.Reply($"{Emojis.Success} Added proxy tags `{tagToAdd.ProxyString.SanitizeMentions()}`."); - } - // Subcommand: "remove" - else if (ctx.Match("remove")) - { - if (!ctx.HasNext()) throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); - - var tagToRemove = ParseProxyTags(ctx.RemainderOrNull()); - if (!target.ProxyTags.Contains(tagToRemove)) - throw Errors.ProxyTagDoesNotExist(tagToRemove, target); - - // It's not guaranteed the list's mutable, so we force it to be - target.ProxyTags = target.ProxyTags.ToList(); - target.ProxyTags.Remove(tagToRemove); - - await _data.SaveMember(target); - await ctx.Reply($"{Emojis.Success} Removed proxy tags `{tagToRemove.ProxyString.SanitizeMentions()}`."); - } - // Subcommand: bare proxy tag given - else - { - if (!ctx.HasNext()) throw new PKSyntaxError("You must pass an example proxy to set (eg. `[text]` or `J:text`)."); - - var requestedTag = ParseProxyTags(ctx.RemainderOrNull()); - - // This is mostly a legacy command, so it's gonna error out if there's - // already more than one proxy tag. - if (target.ProxyTags.Count > 1) - throw Errors.LegacyAlreadyHasProxyTag(requestedTag, target); - - if (!await WarnOnConflict(requestedTag)) - throw Errors.GenericCancelled(); - - target.ProxyTags = new[] {requestedTag}; - - await _data.SaveMember(target); - await ctx.Reply($"{Emojis.Success} Member proxy tags set to `{requestedTag.ProxyString.SanitizeMentions()}`."); - } - - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberDelete(Context ctx, PKMember target) - { - 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 _data.DeleteMember(target); - await ctx.Reply($"{Emojis.Success} Member deleted."); - - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberAvatar(Context ctx, PKMember target) - { - if (ctx.RemainderOrNull() == null && ctx.Message.Attachments.Count == 0) - { - if ((target.AvatarUrl?.Trim() ?? "").Length > 0) - { - var eb = new EmbedBuilder() - .WithTitle($"{target.Name.SanitizeMentions()}'s avatar") - .WithImageUrl(target.AvatarUrl); - if (target.System == ctx.System?.Id) - eb.WithDescription($"To clear, use `pk;member {target.Hid} avatar clear`."); - await ctx.Reply(embed: eb.Build()); - } - else - { - if (target.System == ctx.System?.Id) - throw new PKSyntaxError($"This member does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); - throw new PKError($"This member does not have an avatar set."); - } - - return; - } - - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - if (ctx.Match("clear", "remove")) - { - target.AvatarUrl = null; - await _data.SaveMember(target); - await ctx.Reply($"{Emojis.Success} Member avatar cleared."); - } - else if (await ctx.MatchUser() is IUser user) - { - if (user.AvatarId == null) throw Errors.UserHasNoAvatar; - target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256); - - await _data.SaveMember(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 _data.SaveMember(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 _data.SaveMember(target); - - await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working."); - } - // No-arguments no-attachment case covered by conditional at the very top - - 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(); - - target.DisplayName = newDisplayName; - await _data.SaveMember(target); - - var successStr = $"{Emojis.Success} "; - if (newDisplayName != null) - successStr += $"Member display name changed. This member will now be proxied using the name \"{newDisplayName.SanitizeMentions()}\"."; - else - successStr += $"Member display name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\"."; - - if (ctx.Guild != null) - { - var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); - if (memberGuildConfig.DisplayName != null) - successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name, \"{memberGuildConfig.DisplayName.SanitizeMentions()}\", here."; - } - - await ctx.Reply(successStr); - - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberServerName(Context ctx, PKMember target) - { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - // TODO: allow setting server names for different servers/in DMs by ID - ctx.CheckGuildContext(); - - var newServerName = ctx.RemainderOrNull(); - - var guildSettings = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); - guildSettings.DisplayName = newServerName; - await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildSettings); - - var successStr = $"{Emojis.Success} "; - if (newServerName != null) - successStr += $"Member server name changed. This member will now be proxied using the name \"{newServerName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; - else if (target.DisplayName != null) - successStr += $"Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; - else - successStr += $"Member server name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; - - await ctx.Reply(successStr); - - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberKeepProxy(Context ctx, PKMember target) - { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - bool newValue; - if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; - else if (ctx.Match("off", "disabled", "false", "no")) newValue = false; - else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); - else newValue = !target.KeepProxy; - - target.KeepProxy = newValue; - await _data.SaveMember(target); - - if (newValue) - await ctx.Reply($"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying."); - else - await ctx.Reply($"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying."); - await _proxyCache.InvalidateResultsForSystem(ctx.System); - } - - public async Task MemberPrivacy(Context ctx, PKMember target) - { - if (ctx.System == null) throw Errors.NoSystemError; - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; - - bool newValue; - if (ctx.Match("private", "hide", "hidden", "on", "enable", "yes")) newValue = true; - else if (ctx.Match("public", "show", "shown", "displayed", "off", "disable", "no")) newValue = false; - else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"private\" or \"public\"."); - else newValue = target.MemberPrivacy != PrivacyLevel.Private; - - target.MemberPrivacy = newValue ? PrivacyLevel.Private : PrivacyLevel.Public; - await _data.SaveMember(target); - - if (newValue) - await ctx.Reply($"{Emojis.Success} Member privacy set to **private**. This member will no longer show up in member lists and will return limited information when queried by other accounts."); - else - await ctx.Reply($"{Emojis.Success} Member privacy set to **public**. This member will now show up in member lists and will return all information when queried by other accounts."); - } - - public async Task ViewMember(Context ctx, PKMember target) - { - var system = await _data.GetSystemById(target.System); - await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system))); - } - } -} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs new file mode 100644 index 00000000..54d00d9e --- /dev/null +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -0,0 +1,229 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using NodaTime; + +using PluralKit.Bot.CommandSystem; +using PluralKit.Core; + +namespace PluralKit.Bot.Commands +{ + public class MemberEdit + { + private IDataStore _data; + private ProxyCacheService _proxyCache; + + public MemberEdit(IDataStore data, ProxyCacheService proxyCache) + { + _data = data; + _proxyCache = proxyCache; + } + + public async Task Name(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() ?? throw new PKSyntaxError("You must pass a new name for the member."); + + // Hard name length cap + if (newName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(newName.Length); + + // Warn if there's already a member by this name + var existingMember = await _data.GetMemberByName(ctx.System, newName); + if (existingMember != null) { + 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 + target.Name = newName; + await _data.SaveMember(target); + + 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."); + + if (ctx.Guild != null) + { + var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + if (memberGuildConfig.DisplayName != null) + await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName.SanitizeMentions()}) in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name here."); + } + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + public async Task Description(Context ctx, PKMember target) { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + var description = ctx.RemainderOrNull(); + if (description.IsLongerThan(Limits.MaxDescriptionLength)) throw Errors.DescriptionTooLongError(description.Length); + + target.Description = description; + await _data.SaveMember(target); + + await ctx.Reply($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}."); + } + + public async Task Pronouns(Context ctx, PKMember target) { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + var pronouns = ctx.RemainderOrNull(); + if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) throw Errors.MemberPronounsTooLongError(pronouns.Length); + + target.Pronouns = pronouns; + await _data.SaveMember(target); + + await ctx.Reply($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}."); + } + + public async Task Color(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); + } + + target.Color = color; + await _data.SaveMember(target); + + await ctx.Reply($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}."); + } + + public async Task Birthday(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); + } + + target.Birthday = date; + await _data.SaveMember(target); + + await ctx.Reply($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {target.BirthdayString}")}."); + } + + public async Task DisplayName(Context ctx, PKMember target) + { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + var newDisplayName = ctx.RemainderOrNull(); + + target.DisplayName = newDisplayName; + await _data.SaveMember(target); + + var successStr = $"{Emojis.Success} "; + if (newDisplayName != null) + successStr += $"Member display name changed. This member will now be proxied using the name \"{newDisplayName.SanitizeMentions()}\"."; + else + successStr += $"Member display name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\"."; + + if (ctx.Guild != null) + { + var memberGuildConfig = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + if (memberGuildConfig.DisplayName != null) + successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name.SanitizeMentions()}), and will be proxied using that name, \"{memberGuildConfig.DisplayName.SanitizeMentions()}\", here."; + } + + await ctx.Reply(successStr); + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + public async Task ServerName(Context ctx, PKMember target) + { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + // TODO: allow setting server names for different servers/in DMs by ID + ctx.CheckGuildContext(); + + var newServerName = ctx.RemainderOrNull(); + + var guildSettings = await _data.GetMemberGuildSettings(target, ctx.Guild.Id); + guildSettings.DisplayName = newServerName; + await _data.SetMemberGuildSettings(target, ctx.Guild.Id, guildSettings); + + var successStr = $"{Emojis.Success} "; + if (newServerName != null) + successStr += $"Member server name changed. This member will now be proxied using the name \"{newServerName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; + else if (target.DisplayName != null) + successStr += $"Member server name cleared. This member will now be proxied using their global display name \"{target.DisplayName.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; + else + successStr += $"Member server name cleared. This member will now be proxied using their member name \"{target.Name.SanitizeMentions()}\" in this server ({ctx.Guild.Name.SanitizeMentions()})."; + + await ctx.Reply(successStr); + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + public async Task KeepProxy(Context ctx, PKMember target) + { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + bool newValue; + if (ctx.Match("on", "enabled", "true", "yes")) newValue = true; + else if (ctx.Match("off", "disabled", "false", "no")) newValue = false; + else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); + else newValue = !target.KeepProxy; + + target.KeepProxy = newValue; + await _data.SaveMember(target); + + if (newValue) + await ctx.Reply($"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying."); + else + await ctx.Reply($"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying."); + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + public async Task Privacy(Context ctx, PKMember target) + { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + bool newValue; + if (ctx.Match("private", "hide", "hidden", "on", "enable", "yes")) newValue = true; + else if (ctx.Match("public", "show", "shown", "displayed", "off", "disable", "no")) newValue = false; + else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"private\" or \"public\"."); + else newValue = target.MemberPrivacy != PrivacyLevel.Private; + + target.MemberPrivacy = newValue ? PrivacyLevel.Private : PrivacyLevel.Public; + await _data.SaveMember(target); + + if (newValue) + await ctx.Reply($"{Emojis.Success} Member privacy set to **private**. This member will no longer show up in member lists and will return limited information when queried by other accounts."); + else + await ctx.Reply($"{Emojis.Success} Member privacy set to **public**. This member will now show up in member lists and will return all information when queried by other accounts."); + } + + public async Task Delete(Context ctx, PKMember target) + { + 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 _data.DeleteMember(target); + await ctx.Reply($"{Emojis.Success} Member deleted."); + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs new file mode 100644 index 00000000..0539f55a --- /dev/null +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -0,0 +1,125 @@ +using System.Linq; +using System.Threading.Tasks; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class MemberProxy + { + private IDataStore _data; + private ProxyCacheService _proxyCache; + + public MemberProxy(IDataStore data, ProxyCacheService proxyCache) + { + _data = data; + _proxyCache = proxyCache; + } + + public async Task Proxy(Context ctx, PKMember target) + { + if (ctx.System == null) throw Errors.NoSystemError; + if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + + ProxyTag ParseProxyTags(string exampleProxy) + { + // // Make sure there's one and only one instance of "text" in the example proxy given + var prefixAndSuffix = exampleProxy.Split("text"); + if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; + if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; + return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); + } + + async Task WarnOnConflict(ProxyTag newTag) + { + var conflicts = (await _data.GetConflictingProxies(ctx.System, newTag)) + .Where(m => m.Id != target.Id) + .ToList(); + + if (conflicts.Count <= 0) return true; + + var conflictList = conflicts.Select(m => $"- **{m.Name}**"); + var msg = await ctx.Reply( + $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"); + return await ctx.PromptYesNo(msg); + } + + // "Sub"command: no arguments clearing + // Also matches the pseudo-subcommand "text" which is equivalent to emoty proxy tags on both sides. + if (!ctx.HasNext() || ctx.Match("text")) + { + // If we already have multiple tags, this would clear everything, so prompt that + if (target.ProxyTags.Count > 1) + { + var msg = await ctx.Reply( + $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"); + if (!await ctx.PromptYesNo(msg)) + throw Errors.GenericCancelled(); + } + + target.ProxyTags = new ProxyTag[] { }; + + await _data.SaveMember(target); + await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); + } + // Subcommand: "add" + else if (ctx.Match("add")) + { + if (!ctx.HasNext()) throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); + + var tagToAdd = ParseProxyTags(ctx.RemainderOrNull()); + if (target.ProxyTags.Contains(tagToAdd)) + throw Errors.ProxyTagAlreadyExists(tagToAdd, target); + + if (!await WarnOnConflict(tagToAdd)) + throw Errors.GenericCancelled(); + + // It's not guaranteed the list's mutable, so we force it to be + target.ProxyTags = target.ProxyTags.ToList(); + target.ProxyTags.Add(tagToAdd); + + await _data.SaveMember(target); + await ctx.Reply($"{Emojis.Success} Added proxy tags `{tagToAdd.ProxyString.SanitizeMentions()}`."); + } + // Subcommand: "remove" + else if (ctx.Match("remove")) + { + if (!ctx.HasNext()) throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); + + var tagToRemove = ParseProxyTags(ctx.RemainderOrNull()); + if (!target.ProxyTags.Contains(tagToRemove)) + throw Errors.ProxyTagDoesNotExist(tagToRemove, target); + + // It's not guaranteed the list's mutable, so we force it to be + target.ProxyTags = target.ProxyTags.ToList(); + target.ProxyTags.Remove(tagToRemove); + + await _data.SaveMember(target); + await ctx.Reply($"{Emojis.Success} Removed proxy tags `{tagToRemove.ProxyString.SanitizeMentions()}`."); + } + // Subcommand: bare proxy tag given + else + { + if (!ctx.HasNext()) throw new PKSyntaxError("You must pass an example proxy to set (eg. `[text]` or `J:text`)."); + + var requestedTag = ParseProxyTags(ctx.RemainderOrNull()); + + // This is mostly a legacy command, so it's gonna error out if there's + // already more than one proxy tag. + if (target.ProxyTags.Count > 1) + throw Errors.LegacyAlreadyHasProxyTag(requestedTag, target); + + if (!await WarnOnConflict(requestedTag)) + throw Errors.GenericCancelled(); + + target.ProxyTags = new[] {requestedTag}; + + await _data.SaveMember(target); + await ctx.Reply($"{Emojis.Success} Member proxy tags set to `{requestedTag.ProxyString.SanitizeMentions()}`."); + } + + await _proxyCache.InvalidateResultsForSystem(ctx.System); + } + + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MiscCommands.cs b/PluralKit.Bot/Commands/Misc.cs similarity index 87% rename from PluralKit.Bot/Commands/MiscCommands.cs rename to PluralKit.Bot/Commands/Misc.cs index 8f917c50..79a6fea8 100644 --- a/PluralKit.Bot/Commands/MiscCommands.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using App.Metrics; @@ -14,19 +15,23 @@ using PluralKit.Bot.CommandSystem; using PluralKit.Core; namespace PluralKit.Bot.Commands { - public class MiscCommands + public class Misc { private BotConfig _botConfig; private IMetrics _metrics; private CpuStatService _cpu; private ShardInfoService _shards; + private IDataStore _data; + private EmbedService _embeds; - public MiscCommands(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards) + public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, IDataStore data, EmbedService embeds) { _botConfig = botConfig; _metrics = metrics; _cpu = cpu; _shards = shards; + _data = data; + _embeds = embeds; } public async Task Invite(Context ctx) @@ -46,13 +51,6 @@ namespace PluralKit.Bot.Commands { 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.*"); - public Task Flash(Context ctx) => ctx.Reply("*A ball of green light appears above your head and flies towards your enemy, exploding on contact.*"); - public async Task Stats(Context ctx) { var msg = await ctx.Reply($"..."); @@ -176,5 +174,22 @@ namespace PluralKit.Bot.Commands { // Send! :) await ctx.Reply(embed: eb.Build()); } + + public async Task GetMessage(Context ctx) + { + 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 _data.GetMessage(messageId); + if (message == null) throw Errors.MessageNotFound(messageId); + + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); + } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ModCommands.cs b/PluralKit.Bot/Commands/ServerConfig.cs similarity index 78% rename from PluralKit.Bot/Commands/ModCommands.cs rename to PluralKit.Bot/Commands/ServerConfig.cs index 5426144c..aee16033 100644 --- a/PluralKit.Bot/Commands/ModCommands.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Discord; @@ -8,18 +7,12 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class ModCommands + public class ServerConfig { - private LogChannelService _logChannels; private IDataStore _data; - - private EmbedService _embeds; - - public ModCommands(LogChannelService logChannels, IDataStore data, EmbedService embeds) + public ServerConfig(IDataStore data) { - _logChannels = logChannels; _data = data; - _embeds = embeds; } public async Task SetLogChannel(Context ctx) @@ -89,24 +82,6 @@ namespace PluralKit.Bot.Commands await _data.SaveGuildConfig(guildCfg); await ctx.Reply($"{Emojis.Success} Channels {(onBlacklist ? "added to" : "removed from")} the proxy blacklist."); - - } - - public async Task GetMessage(Context ctx) - { - 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 _data.GetMessage(messageId); - if (message == null) throw Errors.MessageNotFound(messageId); - - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/SwitchCommands.cs b/PluralKit.Bot/Commands/Switch.cs similarity index 98% rename from PluralKit.Bot/Commands/SwitchCommands.cs rename to PluralKit.Bot/Commands/Switch.cs index c0c586ac..384abc05 100644 --- a/PluralKit.Bot/Commands/SwitchCommands.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -10,16 +10,16 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class SwitchCommands + public class Switch { private IDataStore _data; - public SwitchCommands(IDataStore data) + public Switch(IDataStore data) { _data = data; } - public async Task Switch(Context ctx) + public async Task SwitchDo(Context ctx) { ctx.CheckSystem(); var members = new List(); diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs new file mode 100644 index 00000000..8d5e7b82 --- /dev/null +++ b/PluralKit.Bot/Commands/System.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord; +using Humanizer; +using NodaTime; +using NodaTime.Text; +using NodaTime.TimeZones; + +using PluralKit.Bot.CommandSystem; +using PluralKit.Core; + +namespace PluralKit.Bot.Commands +{ + public class System + { + private IDataStore _data; + private EmbedService _embeds; + + public System(EmbedService embeds, IDataStore data) + { + _embeds = embeds; + _data = data; + } + + public async Task Query(Context ctx, PKSystem system) { + if (system == null) throw Errors.NoSystemError; + + await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system, ctx.LookupContextFor(system))); + } + + public async Task New(Context ctx) + { + ctx.CheckNoSystem(); + + var system = await _data.CreateSystem(ctx.RemainderOrNull()); + await _data.AddAccount(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."); + } + } +} diff --git a/PluralKit.Bot/Commands/SystemCommands.cs b/PluralKit.Bot/Commands/SystemEdit.cs similarity index 58% rename from PluralKit.Bot/Commands/SystemCommands.cs rename to PluralKit.Bot/Commands/SystemEdit.cs index a0a55933..f31ae5e2 100644 --- a/PluralKit.Bot/Commands/SystemCommands.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; + using Discord; -using Humanizer; + using NodaTime; using NodaTime.Text; using NodaTime.TimeZones; @@ -12,35 +13,19 @@ using PluralKit.Core; namespace PluralKit.Bot.Commands { - public class SystemCommands + public class SystemEdit { private IDataStore _data; private EmbedService _embeds; - private ProxyCacheService _proxyCache; - public SystemCommands(EmbedService embeds, ProxyCacheService proxyCache, IDataStore data) + public SystemEdit(IDataStore data, EmbedService embeds, ProxyCacheService proxyCache) { + _data = data; _embeds = embeds; _proxyCache = proxyCache; - _data = data; } - - public async Task Query(Context ctx, PKSystem system) { - if (system == null) throw Errors.NoSystemError; - await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system, ctx.LookupContextFor(system))); - } - - public async Task New(Context ctx) - { - ctx.CheckNoSystem(); - - var system = await _data.CreateSystem(ctx.RemainderOrNull()); - await _data.AddAccount(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(); @@ -81,7 +66,7 @@ namespace PluralKit.Bot.Commands await _proxyCache.InvalidateResultsForSystem(ctx.System); } - public async Task SystemAvatar(Context ctx) + public async Task Avatar(Context ctx) { ctx.CheckSystem(); @@ -147,198 +132,6 @@ namespace PluralKit.Bot.Commands await _proxyCache.InvalidateResultsForSystem(ctx.System); } - public async Task MemberShortList(Context ctx, PKSystem system) { - if (system == null) throw Errors.NoSystemError; - ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); - - var authCtx = ctx.LookupContextFor(system); - var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private"); - - var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; - - var memberCountPublic = _data.GetSystemMemberCount(system, false); - var memberCountAll = _data.GetSystemMemberCount(system, true); - await Task.WhenAll(memberCountPublic, memberCountAll); - - var memberCountDisplayed = shouldShowPrivate ? memberCountAll.Result : memberCountPublic.Result; - - var members = _data.GetSystemMembers(system) - .Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate) - .OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase); - var anyMembersHidden = !shouldShowPrivate && memberCountPublic.Result != memberCountAll.Result; - - await ctx.Paginate( - members, - memberCountDisplayed, - 25, - embedTitle, - (eb, ms) => - { - eb.Description = string.Join("\n", ms.Select((m) => - { - if (m.HasProxyTags) - { - var proxyTagsString = m.ProxyTagsString().SanitizeMentions(); - if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak? - proxyTagsString = "tags too long, see member card"; - - return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({proxyTagsString})*"; - } - - return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**"; - })); - - var footer = $"{memberCountDisplayed} total."; - if (anyMembersHidden && authCtx == LookupContext.ByOwner) - footer += "Private members have been hidden. type \"pk;system list all\" to include them."; - eb.WithFooter(footer); - - return Task.CompletedTask; - }); - } - - public async Task MemberLongList(Context ctx, PKSystem system) { - if (system == null) throw Errors.NoSystemError; - ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); - - var authCtx = ctx.LookupContextFor(system); - var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private"); - - var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; - - var memberCountPublic = _data.GetSystemMemberCount(system, false); - var memberCountAll = _data.GetSystemMemberCount(system, true); - await Task.WhenAll(memberCountPublic, memberCountAll); - - var memberCountDisplayed = shouldShowPrivate ? memberCountAll.Result : memberCountPublic.Result; - - var members = _data.GetSystemMembers(system) - .Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate) - .OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase); - var anyMembersHidden = !shouldShowPrivate && memberCountPublic.Result != memberCountAll.Result; - - await ctx.Paginate( - members, - memberCountDisplayed, - 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.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}"; - if (m.Description != null) profile += $"\n\n{m.Description}"; - if (m.MemberPrivacy == PrivacyLevel.Private) - profile += "*(this member is private)*"; - - eb.AddField(m.Name, profile.Truncate(1024)); - } - - var footer = $"{memberCountDisplayed} total."; - if (anyMembersHidden && authCtx == LookupContext.ByOwner) - footer += " Private members have been hidden. type \"pk;system list full all\" to include them."; - eb.WithFooter(footer); - return Task.CompletedTask; - } - ); - } - - public async Task SystemFronter(Context ctx, PKSystem system) - { - if (system == null) throw Errors.NoSystemError; - ctx.CheckSystemPrivacy(system, system.FrontPrivacy); - - var sw = await _data.GetLatestSwitch(system); - if (sw == null) throw Errors.NoRegisteredSwitches; - - await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone)); - } - - struct FrontHistoryEntry - { - public Instant? LastTime; - public PKSwitch ThisSwitch; - - public FrontHistoryEntry(Instant? lastTime, PKSwitch thisSwitch) - { - LastTime = lastTime; - ThisSwitch = thisSwitch; - } - } - - public async Task SystemFrontHistory(Context ctx, PKSystem system) - { - if (system == null) throw Errors.NoSystemError; - ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy); - - var sws = _data.GetSwitches(system) - .Scan(new FrontHistoryEntry(null, null), (lastEntry, newSwitch) => new FrontHistoryEntry(lastEntry.ThisSwitch?.Timestamp, newSwitch)); - var totalSwitches = await _data.GetSwitchCount(system); - if (totalSwitches == 0) throw Errors.NoRegisteredSwitches; - - var embedTitle = system.Name != null ? $"Front history of {system.Name} (`{system.Hid}`)" : $"Front history of `{system.Hid}`"; - - await ctx.Paginate( - sws, - totalSwitches, - 10, - embedTitle, - async (builder, switches) => - { - var outputStr = ""; - foreach (var entry in switches) - { - var lastSw = entry.LastTime; - - var sw = entry.ThisSwitch; - // Fetch member list and format - var members = await _data.GetSwitchMembers(sw).ToListAsync(); - var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.Name)) : "no fronter"; - - var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; - - // If this isn't the latest switch, we also show duration - string stringToAdd; - if (lastSw != null) - { - // Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one - var switchDuration = lastSw.Value - sw.Timestamp; - stringToAdd = - $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago, for {Formats.DurationFormat.Format(switchDuration)})\n"; - } - else - { - stringToAdd = - $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago)\n"; - } - - if (outputStr.Length + stringToAdd.Length > EmbedBuilder.MaxDescriptionLength) break; - outputStr += stringToAdd; - } - - builder.Description = outputStr; - } - ); - } - - public async Task SystemFrontPercent(Context ctx, PKSystem system) - { - if (system == null) throw Errors.NoSystemError; - ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy); - - string durationStr = ctx.RemainderOrNull() ?? "30d"; - - var now = SystemClock.Instance.GetCurrentInstant(); - - var rangeStart = PluralKit.Utils.ParseDateTime(durationStr, true, system.Zone); - if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); - if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; - - var frontpercent = await _data.GetFrontBreakdown(system, rangeStart.Value.ToInstant(), now); - await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone)); - } - public async Task SystemProxy(Context ctx) { ctx.CheckSystem().CheckGuildContext(); @@ -359,7 +152,7 @@ namespace PluralKit.Bot.Commands await ctx.Reply($"Message proxying in this server ({ctx.Guild.Name.EscapeMarkdown()}) is now **disabled** for your system."); } - public async Task SystemTimezone(Context ctx) + public async Task SystemTimezone(Context ctx) { if (ctx.System == null) throw Errors.NoSystemError; @@ -517,4 +310,4 @@ namespace PluralKit.Bot.Commands }); } } -} +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs new file mode 100644 index 00000000..06969543 --- /dev/null +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -0,0 +1,118 @@ +using System.Linq; +using System.Threading.Tasks; + +using Discord; + +using NodaTime; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class SystemFront + { + private IDataStore _data; + private EmbedService _embeds; + + public SystemFront(IDataStore data, EmbedService embeds) + { + _data = data; + _embeds = embeds; + } + + struct FrontHistoryEntry + { + public Instant? LastTime; + public PKSwitch ThisSwitch; + + public FrontHistoryEntry(Instant? lastTime, PKSwitch thisSwitch) + { + LastTime = lastTime; + ThisSwitch = thisSwitch; + } + } + + public async Task SystemFronter(Context ctx, PKSystem system) + { + if (system == null) throw Errors.NoSystemError; + ctx.CheckSystemPrivacy(system, system.FrontPrivacy); + + var sw = await _data.GetLatestSwitch(system); + if (sw == null) throw Errors.NoRegisteredSwitches; + + await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone)); + } + + public async Task SystemFrontHistory(Context ctx, PKSystem system) + { + if (system == null) throw Errors.NoSystemError; + ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy); + + var sws = _data.GetSwitches(system) + .Scan(new FrontHistoryEntry(null, null), (lastEntry, newSwitch) => new FrontHistoryEntry(lastEntry.ThisSwitch?.Timestamp, newSwitch)); + var totalSwitches = await _data.GetSwitchCount(system); + if (totalSwitches == 0) throw Errors.NoRegisteredSwitches; + + var embedTitle = system.Name != null ? $"Front history of {system.Name} (`{system.Hid}`)" : $"Front history of `{system.Hid}`"; + + await ctx.Paginate( + sws, + totalSwitches, + 10, + embedTitle, + async (builder, switches) => + { + var outputStr = ""; + foreach (var entry in switches) + { + var lastSw = entry.LastTime; + + var sw = entry.ThisSwitch; + // Fetch member list and format + var members = await _data.GetSwitchMembers(sw).ToListAsync(); + var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.Name)) : "no fronter"; + + var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; + + // If this isn't the latest switch, we also show duration + string stringToAdd; + if (lastSw != null) + { + // Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one + var switchDuration = lastSw.Value - sw.Timestamp; + stringToAdd = + $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago, for {Formats.DurationFormat.Format(switchDuration)})\n"; + } + else + { + stringToAdd = + $"**{membersStr}** ({Formats.ZonedDateTimeFormat.Format(sw.Timestamp.InZone(system.Zone))}, {Formats.DurationFormat.Format(switchSince)} ago)\n"; + } + + if (outputStr.Length + stringToAdd.Length > EmbedBuilder.MaxDescriptionLength) break; + outputStr += stringToAdd; + } + + builder.Description = outputStr; + } + ); + } + + public async Task SystemFrontPercent(Context ctx, PKSystem system) + { + if (system == null) throw Errors.NoSystemError; + ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy); + + string durationStr = ctx.RemainderOrNull() ?? "30d"; + + var now = SystemClock.Instance.GetCurrentInstant(); + + var rangeStart = PluralKit.Utils.ParseDateTime(durationStr, true, system.Zone); + if (rangeStart == null) throw Errors.InvalidDateTime(durationStr); + if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture; + + var frontpercent = await _data.GetFrontBreakdown(system, rangeStart.Value.ToInstant(), now); + await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone)); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/LinkCommands.cs b/PluralKit.Bot/Commands/SystemLink.cs similarity index 95% rename from PluralKit.Bot/Commands/LinkCommands.cs rename to PluralKit.Bot/Commands/SystemLink.cs index 83d86e11..d1fdc693 100644 --- a/PluralKit.Bot/Commands/LinkCommands.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -1,16 +1,15 @@ using System.Linq; using System.Threading.Tasks; -using Discord; using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class LinkCommands + public class SystemLink { private IDataStore _data; - public LinkCommands(IDataStore data) + public SystemLink(IDataStore data) { _data = data; } diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs new file mode 100644 index 00000000..78a87f6f --- /dev/null +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Humanizer; + +using PluralKit.Bot.CommandSystem; + +namespace PluralKit.Bot.Commands +{ + public class SystemList + { + private IDataStore _data; + + public SystemList(IDataStore data) + { + _data = data; + } + + public async Task MemberShortList(Context ctx, PKSystem system) { + if (system == null) throw Errors.NoSystemError; + ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); + + var authCtx = ctx.LookupContextFor(system); + var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private"); + + var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; + + var memberCountPublic = _data.GetSystemMemberCount(system, false); + var memberCountAll = _data.GetSystemMemberCount(system, true); + await Task.WhenAll(memberCountPublic, memberCountAll); + + var memberCountDisplayed = shouldShowPrivate ? memberCountAll.Result : memberCountPublic.Result; + + var members = _data.GetSystemMembers(system) + .Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate) + .OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase); + var anyMembersHidden = !shouldShowPrivate && memberCountPublic.Result != memberCountAll.Result; + + await ctx.Paginate( + members, + memberCountDisplayed, + 25, + embedTitle, + (eb, ms) => + { + eb.Description = string.Join("\n", ms.Select((m) => + { + if (m.HasProxyTags) + { + var proxyTagsString = m.ProxyTagsString().SanitizeMentions(); + if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak? + proxyTagsString = "tags too long, see member card"; + + return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({proxyTagsString})*"; + } + + return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**"; + })); + + var footer = $"{memberCountDisplayed} total."; + if (anyMembersHidden && authCtx == LookupContext.ByOwner) + footer += "Private members have been hidden. type \"pk;system list all\" to include them."; + eb.WithFooter(footer); + + return Task.CompletedTask; + }); + } + + public async Task MemberLongList(Context ctx, PKSystem system) { + if (system == null) throw Errors.NoSystemError; + ctx.CheckSystemPrivacy(system, system.MemberListPrivacy); + + var authCtx = ctx.LookupContextFor(system); + var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private"); + + var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; + + var memberCountPublic = _data.GetSystemMemberCount(system, false); + var memberCountAll = _data.GetSystemMemberCount(system, true); + await Task.WhenAll(memberCountPublic, memberCountAll); + + var memberCountDisplayed = shouldShowPrivate ? memberCountAll.Result : memberCountPublic.Result; + + var members = _data.GetSystemMembers(system) + .Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate) + .OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase); + var anyMembersHidden = !shouldShowPrivate && memberCountPublic.Result != memberCountAll.Result; + + await ctx.Paginate( + members, + memberCountDisplayed, + 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.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}"; + if (m.Description != null) profile += $"\n\n{m.Description}"; + if (m.MemberPrivacy == PrivacyLevel.Private) + profile += "*(this member is private)*"; + + eb.AddField(m.Name, profile.Truncate(1024)); + } + + var footer = $"{memberCountDisplayed} total."; + if (anyMembersHidden && authCtx == LookupContext.ByOwner) + footer += " Private members have been hidden. type \"pk;system list full all\" to include them."; + eb.WithFooter(footer); + return Task.CompletedTask; + } + ); + } + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/APICommands.cs b/PluralKit.Bot/Commands/Token.cs similarity index 97% rename from PluralKit.Bot/Commands/APICommands.cs rename to PluralKit.Bot/Commands/Token.cs index cd5d3f9d..e8c6fb1c 100644 --- a/PluralKit.Bot/Commands/APICommands.cs +++ b/PluralKit.Bot/Commands/Token.cs @@ -5,10 +5,10 @@ using PluralKit.Bot.CommandSystem; namespace PluralKit.Bot.Commands { - public class APICommands + public class Token { private IDataStore _data; - public APICommands(IDataStore data) + public Token(IDataStore data) { _data = data; } diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 62ce1eb8..039c063f 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -35,16 +35,23 @@ namespace PluralKit.Bot // Commands builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); - builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); // Bot core builder.RegisterType().AsSelf().SingleInstance();