Move command functions around

This commit is contained in:
Ske 2020-02-01 13:03:02 +01:00
parent a60be64551
commit 125ea81ec3
20 changed files with 950 additions and 807 deletions

View File

@ -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();

View File

@ -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<AutoproxyCommands>(Autoproxy, m => m.Autoproxy(ctx));
return ctx.Execute<Autoproxy>(Autoproxy, m => m.AutoproxyRoot(ctx));
if (ctx.Match("link"))
return ctx.Execute<LinkCommands>(Link, m => m.LinkSystem(ctx));
return ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx));
if (ctx.Match("unlink"))
return ctx.Execute<LinkCommands>(Unlink, m => m.UnlinkAccount(ctx));
return ctx.Execute<SystemLink>(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));
return ctx.Execute<Token>(TokenRefresh, m => m.RefreshToken(ctx));
else
return ctx.Execute<APICommands>(TokenGet, m => m.GetToken(ctx));
return ctx.Execute<Token>(TokenGet, m => m.GetToken(ctx));
if (ctx.Match("import"))
return ctx.Execute<ImportExportCommands>(Import, m => m.Import(ctx));
return ctx.Execute<ImportExport>(Import, m => m.Import(ctx));
if (ctx.Match("export"))
return ctx.Execute<ImportExportCommands>(Export, m => m.Export(ctx));
return ctx.Execute<ImportExport>(Export, m => m.Export(ctx));
if (ctx.Match("help"))
if (ctx.Match("commands"))
return ctx.Reply("For the list of commands, see the website: <https://pluralkit.me/commands>");
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<HelpCommands>(Help, m => m.HelpRoot(ctx));
else return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
if (ctx.Match("commands"))
return ctx.Reply("For the list of commands, see the website: <https://pluralkit.me/commands>");
if (ctx.Match("message", "msg"))
return ctx.Execute<ModCommands>(Message, m => m.GetMessage(ctx));
return ctx.Execute<Misc>(Message, m => m.GetMessage(ctx));
if (ctx.Match("log"))
if (ctx.Match("channel"))
return ctx.Execute<ModCommands>(LogChannel, m => m.SetLogChannel(ctx));
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx));
else if (ctx.Match("enable", "on"))
return ctx.Execute<ModCommands>(LogEnable, m => m.SetLogEnabled(ctx, true));
return ctx.Execute<ServerConfig>(LogEnable, m => m.SetLogEnabled(ctx, true));
else if (ctx.Match("disable", "off"))
return ctx.Execute<ModCommands>(LogDisable, m => m.SetLogEnabled(ctx, false));
return ctx.Execute<ServerConfig>(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<ModCommands>(BlacklistAdd, m => m.SetBlacklisted(ctx, true));
return ctx.Execute<ServerConfig>(BlacklistAdd, m => m.SetBlacklisted(ctx, true));
else if (ctx.Match("disable", "off", "remove", "allow"))
return ctx.Execute<ModCommands>(BlacklistRemove, m => m.SetBlacklisted(ctx, false));
return ctx.Execute<ServerConfig>(BlacklistRemove, m => m.SetBlacklisted(ctx, false));
else return PrintCommandExpectedError(ctx, BlacklistAdd, BlacklistRemove);
if (ctx.Match("proxy", "enable", "disable"))
return ctx.Execute<SystemCommands>(SystemProxy, m => m.SystemProxy(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("flash")) return ctx.Execute<MiscCommands>(null, m => m.Flash(ctx));
if (ctx.Match("stats")) return ctx.Execute<MiscCommands>(null, m => m.Stats(ctx));
return ctx.Execute<SystemEdit>(SystemProxy, m => m.SystemProxy(ctx));
if (ctx.Match("invite")) return ctx.Execute<Misc>(Invite, m => m.Invite(ctx));
if (ctx.Match("mn")) return ctx.Execute<Fun>(null, m => m.Mn(ctx));
if (ctx.Match("fire")) return ctx.Execute<Fun>(null, m => m.Fire(ctx));
if (ctx.Match("thunder")) return ctx.Execute<Fun>(null, m => m.Thunder(ctx));
if (ctx.Match("freeze")) return ctx.Execute<Fun>(null, m => m.Freeze(ctx));
if (ctx.Match("starstorm")) return ctx.Execute<Fun>(null, m => m.Starstorm(ctx));
if (ctx.Match("flash")) return ctx.Execute<Fun>(null, m => m.Flash(ctx));
if (ctx.Match("stats")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
if (ctx.Match("permcheck"))
return ctx.Execute<MiscCommands>(PermCheck, m => m.PermCheckGuild(ctx));
return ctx.Execute<Misc>(PermCheck, m => m.PermCheckGuild(ctx));
if (ctx.Match("random", "r"))
return ctx.Execute<MemberCommands>(MemberRandom, m => m.MemberRandom(ctx));
return ctx.Execute<Member>(MemberRandom, m => m.MemberRandom(ctx));
ctx.Reply(
$"{Emojis.Error} Unknown command `{ctx.PeekArgument().SanitizeMentions()}`. For a list of possible commands, see <https://pluralkit.me/commands>.");
@ -151,51 +151,51 @@ namespace PluralKit.Bot.Commands
{
// If we have no parameters, default to self-target
if (!ctx.HasNext())
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
await ctx.Execute<System>(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));
await ctx.Execute<System>(SystemNew, m => m.New(ctx));
else if (ctx.Match("name", "rename", "changename"))
await ctx.Execute<SystemCommands>(SystemRename, m => m.Name(ctx));
await ctx.Execute<SystemEdit>(SystemRename, m => m.Name(ctx));
else if (ctx.Match("tag"))
await ctx.Execute<SystemCommands>(SystemTag, m => m.Tag(ctx));
await ctx.Execute<SystemEdit>(SystemTag, m => m.Tag(ctx));
else if (ctx.Match("description", "desc", "bio"))
await ctx.Execute<SystemCommands>(SystemDesc, m => m.Description(ctx));
await ctx.Execute<SystemEdit>(SystemDesc, m => m.Description(ctx));
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
await ctx.Execute<SystemCommands>(SystemAvatar, m => m.SystemAvatar(ctx));
await ctx.Execute<SystemEdit>(SystemAvatar, m => m.Avatar(ctx));
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
await ctx.Execute<SystemCommands>(SystemDelete, m => m.Delete(ctx));
await ctx.Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx));
else if (ctx.Match("timezone", "tz"))
await ctx.Execute<SystemCommands>(SystemTimezone, m => m.SystemTimezone(ctx));
await ctx.Execute<SystemEdit>(SystemTimezone, m => m.SystemTimezone(ctx));
else if (ctx.Match("proxy"))
await ctx.Execute<SystemCommands>(SystemProxy, m => m.SystemProxy(ctx));
await ctx.Execute<SystemEdit>(SystemProxy, m => m.SystemProxy(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));
await ctx.Execute<SystemList>(SystemList, m => m.MemberLongList(ctx, ctx.System));
else
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, ctx.System));
await ctx.Execute<SystemList>(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));
await ctx.Execute<SystemFront>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
else if (ctx.Match("p", "percent", "%"))
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
await ctx.Execute<SystemFront>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
else
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, ctx.System));
await ctx.Execute<SystemFront>(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));
await ctx.Execute<SystemFront>(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));
await ctx.Execute<SystemFront>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
else if (ctx.Match("privacy"))
await ctx.Execute<SystemCommands>(SystemPrivacy, m => m.SystemPrivacy(ctx));
await ctx.Execute<SystemEdit>(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<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
await ctx.Execute<System>(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<SystemCommands>(SystemList, m => m.MemberLongList(ctx, target));
await ctx.Execute<SystemList>(SystemList, m => m.MemberLongList(ctx, target));
else
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, target));
await ctx.Execute<SystemList>(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));
await ctx.Execute<SystemFront>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
else if (ctx.Match("p", "percent", "%"))
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
await ctx.Execute<SystemFront>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
else
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, target));
await ctx.Execute<SystemFront>(SystemFronter, m => m.SystemFronter(ctx, target));
}
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
await ctx.Execute<SystemFront>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
await ctx.Execute<SystemFront>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
else if (ctx.Match("info", "view", "show"))
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
await ctx.Execute<System>(SystemInfo, m => m.Query(ctx, target));
else if (!ctx.HasNext())
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
await ctx.Execute<System>(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<MemberCommands>(MemberNew, m => m.NewMember(ctx));
await ctx.Execute<Member>(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 <member> delete)
if (ctx.Match("rename", "name", "changename", "setname"))
await ctx.Execute<MemberCommands>(MemberRename, m => m.RenameMember(ctx, target));
await ctx.Execute<MemberEdit>(MemberRename, m => m.Name(ctx, target));
else if (ctx.Match("description", "info", "bio", "text", "desc"))
await ctx.Execute<MemberCommands>(MemberDesc, m => m.MemberDescription(ctx, target));
await ctx.Execute<MemberEdit>(MemberDesc, m => m.Description(ctx, target));
else if (ctx.Match("pronouns", "pronoun"))
await ctx.Execute<MemberCommands>(MemberPronouns, m => m.MemberPronouns(ctx, target));
await ctx.Execute<MemberEdit>(MemberPronouns, m => m.Pronouns(ctx, target));
else if (ctx.Match("color", "colour"))
await ctx.Execute<MemberCommands>(MemberColor, m => m.MemberColor(ctx, target));
await ctx.Execute<MemberEdit>(MemberColor, m => m.Color(ctx, target));
else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate"))
await ctx.Execute<MemberCommands>(MemberBirthday, m => m.MemberBirthday(ctx, target));
await ctx.Execute<MemberEdit>(MemberBirthday, m => m.Birthday(ctx, target));
else if (ctx.Match("proxy", "tags", "proxytags", "brackets"))
await ctx.Execute<MemberCommands>(MemberProxy, m => m.MemberProxy(ctx, target));
await ctx.Execute<MemberProxy>(MemberProxy, m => m.Proxy(ctx, target));
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
await ctx.Execute<MemberCommands>(MemberDelete, m => m.MemberDelete(ctx, target));
await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target));
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
await ctx.Execute<MemberCommands>(MemberAvatar, m => m.MemberAvatar(ctx, target));
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target));
else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname"))
await ctx.Execute<MemberCommands>(MemberDisplayName, m => m.MemberDisplayName(ctx, target));
await ctx.Execute<MemberEdit>(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<MemberCommands>(MemberServerName, m => m.MemberServerName(ctx, target));
await ctx.Execute<MemberEdit>(MemberServerName, m => m.ServerName(ctx, target));
else if (ctx.Match("keepproxy", "keeptags", "showtags"))
await ctx.Execute<MemberCommands>(MemberKeepProxy, m => m.MemberKeepProxy(ctx, target));
await ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.KeepProxy(ctx, target));
else if (ctx.Match("private", "privacy", "hidden", "public"))
await ctx.Execute<MemberCommands>(MemberPrivacy, m => m.MemberPrivacy(ctx, target));
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target));
else if (!ctx.HasNext()) // Bare command
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
await ctx.Execute<Member>(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<SwitchCommands>(SwitchOut, m => m.SwitchOut(ctx));
await ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx));
else if (ctx.Match("move", "shift", "offset"))
await ctx.Execute<SwitchCommands>(SwitchMove, m => m.SwitchMove(ctx));
await ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx));
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
await ctx.Execute<SwitchCommands>(SwitchDelete, m => m.SwitchDelete(ctx));
await ctx.Execute<Switch>(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<SwitchCommands>(Switch, m => m.Switch(ctx));
await ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx));
else
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchDelete, SystemFronter, SystemFrontHistory);
}

View File

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

View File

@ -5,7 +5,7 @@ using PluralKit.Bot.CommandSystem;
namespace PluralKit.Bot.Commands
{
public class HelpCommands
public class Help
{
public async Task HelpRoot(Context ctx)
{

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PKMember>();

View File

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

View File

@ -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,33 +13,17 @@ 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)
@ -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;

View File

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

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -35,16 +35,23 @@ namespace PluralKit.Bot
// Commands
builder.RegisterType<CommandTree>().AsSelf();
builder.RegisterType<SystemCommands>().AsSelf();
builder.RegisterType<MemberCommands>().AsSelf();
builder.RegisterType<SwitchCommands>().AsSelf();
builder.RegisterType<LinkCommands>().AsSelf();
builder.RegisterType<APICommands>().AsSelf();
builder.RegisterType<ImportExportCommands>().AsSelf();
builder.RegisterType<HelpCommands>().AsSelf();
builder.RegisterType<ModCommands>().AsSelf();
builder.RegisterType<MiscCommands>().AsSelf();
builder.RegisterType<AutoproxyCommands>().AsSelf();
builder.RegisterType<Autoproxy>().AsSelf();
builder.RegisterType<Fun>().AsSelf();
builder.RegisterType<Help>().AsSelf();
builder.RegisterType<ImportExport>().AsSelf();
builder.RegisterType<Member>().AsSelf();
builder.RegisterType<MemberAvatar>().AsSelf();
builder.RegisterType<MemberEdit>().AsSelf();
builder.RegisterType<MemberProxy>().AsSelf();
builder.RegisterType<Misc>().AsSelf();
builder.RegisterType<ServerConfig>().AsSelf();
builder.RegisterType<Switch>().AsSelf();
builder.RegisterType<Commands.System>().AsSelf();
builder.RegisterType<SystemEdit>().AsSelf();
builder.RegisterType<SystemFront>().AsSelf();
builder.RegisterType<SystemLink>().AsSelf();
builder.RegisterType<SystemList>().AsSelf();
builder.RegisterType<Token>().AsSelf();
// Bot core
builder.RegisterType<Bot>().AsSelf().SingleInstance();