This commit is contained in:
dawn 2026-01-26 04:15:54 +00:00 committed by GitHub
commit 6299718aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 8932 additions and 4828 deletions

View file

@ -1,58 +1,25 @@
using Humanizer;
using Myriad.Types;
using PluralKit.Core;
namespace PluralKit.Bot;
public partial class CommandTree
{
private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands)
private async Task PrintCommandList(Context ctx, string subject, string commands)
{
var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands);
await ctx.Reply(
$"{Emojis.Error} Unknown command `{ctx.DefaultPrefix}{ctx.FullCommand().Truncate(100)}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)
{
var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands);
await ctx.Reply(
$"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private static string CreatePotentialCommandList(string prefix, params Command[] potentialCommands)
{
return string.Join("\n", potentialCommands.Select(cmd => $"- **{prefix}{cmd.Usage}** - *{cmd.Description}*"));
}
private async Task PrintCommandList(Context ctx, string subject, params Command[] commands)
{
var str = CreatePotentialCommandList(ctx.DefaultPrefix, commands);
await ctx.Reply(
$"Here is a list of commands related to {subject}:",
embed: new Embed()
{
Description = $"{str}\nFor a full list of possible commands, see <https://pluralkit.me/commands>.",
Color = DiscordUtils.Blue,
}
);
}
private async Task<string> CreateSystemNotFoundError(Context ctx)
{
var input = ctx.PopArgument();
if (input.TryParseMention(out var id))
if (commands.Length == 0)
{
// Try to resolve the user ID to find the associated account,
// so we can print their username.
var user = await ctx.Rest.GetUser(id);
if (user != null)
return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered.";
return $"Account with ID `{id}` not found.";
await ctx.Reply($"No commands related to `{subject}` was found. For the full list of commands, see the website: <https://pluralkit.me/commands>");
return;
}
return $"System with ID {input.AsCode()} not found.";
await ctx.Reply(
components: [
new MessageComponent()
{
Type = ComponentType.Text,
Content = $"Here is a list of commands related to `{subject}`:\n{commands}\nFor a full list of possible commands, see <https://pluralkit.me/commands>.",
}
]
);
}
}

View file

@ -4,646 +4,326 @@ namespace PluralKit.Bot;
public partial class CommandTree
{
public Task ExecuteCommand(Context ctx)
public Task ExecuteCommand(Context ctx, Commands command)
{
if (ctx.Match("system", "s", "account", "acc"))
return HandleSystemCommand(ctx);
if (ctx.Match("member", "m"))
return HandleMemberCommand(ctx);
if (ctx.Match("group", "g"))
return HandleGroupCommand(ctx);
if (ctx.Match("switch", "sw"))
return HandleSwitchCommand(ctx);
if (ctx.Match("commands", "cmd", "c"))
return CommandHelpRoot(ctx);
if (ctx.Match("ap", "autoproxy", "auto"))
return HandleAutoproxyCommand(ctx);
if (ctx.Match("config", "cfg", "configure"))
return HandleConfigCommand(ctx);
if (ctx.Match("serverconfig", "guildconfig", "scfg"))
return HandleServerConfigCommand(ctx);
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls"))
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
if (ctx.Match("link"))
return ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx));
if (ctx.Match("unlink"))
return ctx.Execute<SystemLink>(Unlink, m => m.UnlinkAccount(ctx));
if (ctx.Match("token"))
if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen"))
return ctx.Execute<Api>(TokenRefresh, m => m.RefreshToken(ctx));
else
return ctx.Execute<Api>(TokenGet, m => m.GetToken(ctx));
if (ctx.Match("import"))
return ctx.Execute<ImportExport>(Import, m => m.Import(ctx));
if (ctx.Match("export"))
return ctx.Execute<ImportExport>(Export, m => m.Export(ctx));
if (ctx.Match("help", "h"))
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<Help>(Help, m => m.HelpRoot(ctx));
if (ctx.Match("explain"))
return ctx.Execute<Help>(Explain, m => m.Explain(ctx));
if (ctx.Match("message", "msg", "messageinfo"))
return ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx));
if (ctx.Match("edit", "e"))
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, false));
if (ctx.Match("x"))
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, true));
if (ctx.Match("reproxy", "rp", "crimes", "crime"))
return ctx.Execute<ProxiedMessage>(MessageReproxy, m => m.ReproxyMessage(ctx));
if (ctx.Match("log"))
if (ctx.Match("channel"))
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx), true);
else if (ctx.Match("enable", "on"))
return ctx.Execute<ServerConfig>(LogEnable, m => m.SetLogEnabled(ctx, true), true);
else if (ctx.Match("disable", "off"))
return ctx.Execute<ServerConfig>(LogDisable, m => m.SetLogEnabled(ctx, false), true);
else if (ctx.Match("list", "show"))
return ctx.Execute<ServerConfig>(LogShow, m => m.ShowLogDisabledChannels(ctx), true);
else
return ctx.Reply($"{Emojis.Warn} Message logging commands have moved to `{ctx.DefaultPrefix}serverconfig`.");
if (ctx.Match("logclean"))
return ctx.Execute<ServerConfig>(ServerConfigLogClean, m => m.SetLogCleanup(ctx), true);
if (ctx.Match("blacklist", "bl"))
if (ctx.Match("enable", "on", "add", "deny"))
return ctx.Execute<ServerConfig>(BlacklistAdd, m => m.SetProxyBlacklisted(ctx, true), true);
else if (ctx.Match("disable", "off", "remove", "allow"))
return ctx.Execute<ServerConfig>(BlacklistRemove, m => m.SetProxyBlacklisted(ctx, false), true);
else if (ctx.Match("list", "show"))
return ctx.Execute<ServerConfig>(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true);
else
return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`.");
if (ctx.Match("proxy"))
if (ctx.Match("debug"))
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
else
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("rool")) return ctx.Execute<Fun>(null, m => m.Rool(ctx));
if (ctx.Match("sus")) return ctx.Execute<Fun>(null, m => m.Sus(ctx));
if (ctx.Match("error")) return ctx.Execute<Fun>(null, m => m.Error(ctx));
if (ctx.Match("stats", "status")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
if (ctx.Match("permcheck"))
return ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
if (ctx.Match("proxycheck"))
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
if (ctx.Match("debug"))
return HandleDebugCommand(ctx);
if (ctx.Match("admin"))
return HandleAdminCommand(ctx);
if (ctx.Match("random", "rand", "r"))
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx, ctx.System));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, ctx.System));
if (ctx.Match("dashboard", "dash"))
return ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx));
// don't send an "invalid command" response if the guild has those turned off
if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true)
return Task.CompletedTask;
// remove compiler warning
return ctx.Reply(
$"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see <https://pluralkit.me/commands>.");
}
private async Task HandleAdminAbuseLogCommand(Context ctx)
{
ctx.AssertBotAdmin();
if (ctx.Match("n", "new", "create"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogCreate(ctx));
else
return command switch
{
AbuseLog? abuseLog = null!;
var account = await ctx.MatchUser();
if (account != null)
Commands.CommandsList(var param, _) => PrintCommandList(ctx, param.subject, Parameters.GetRelatedCommands(ctx.DefaultPrefix, param.subject)),
Commands.Dashboard => ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx)),
Commands.Explain => ctx.Execute<Help>(Explain, m => m.Explain(ctx)),
Commands.Help(_, var flags) => ctx.Execute<Help>(Help, m => m.HelpRoot(ctx, flags.show_embed)),
Commands.HelpCommands => ctx.Reply(
"For the list of commands, see the website: <https://pluralkit.me/commands>"),
Commands.HelpProxy => ctx.Reply(
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"),
Commands.Invite => ctx.Execute<Misc>(Invite, m => m.Invite(ctx)),
Commands.Stats => ctx.Execute<Misc>(null, m => m.Stats(ctx)),
Commands.MemberShow(var param, var flags) => ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, param.target, flags.show_embed)),
Commands.MemberNew(var param, var flags) => ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx, param.name, flags.yes)),
Commands.MemberSoulscream(var param, _) => ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, param.target)),
Commands.MemberAvatarShow(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberAvatarClear(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ClearAvatar(ctx, param.target, flags.yes)),
Commands.MemberAvatarUpdate(var param, _) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ChangeAvatar(ctx, param.target, param.avatar)),
Commands.MemberWebhookAvatarShow(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ShowWebhookAvatar(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberWebhookAvatarClear(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ClearWebhookAvatar(ctx, param.target, flags.yes)),
Commands.MemberWebhookAvatarUpdate(var param, _) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ChangeWebhookAvatar(ctx, param.target, param.avatar)),
Commands.MemberServerAvatarShow(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberServerAvatarClear(var param, var flags) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ClearServerAvatar(ctx, param.target, flags.yes)),
Commands.MemberServerAvatarUpdate(var param, _) => ctx.Execute<MemberAvatar>(MemberAvatar, m => m.ChangeServerAvatar(ctx, param.target, param.avatar)),
Commands.MemberPronounsShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberPronounsClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberPronouns, m => m.ClearPronouns(ctx, param.target, flags.yes)),
Commands.MemberPronounsUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberPronouns, m => m.ChangePronouns(ctx, param.target, param.pronouns)),
Commands.MemberDescShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberDesc, m => m.ShowDescription(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberDescClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberDesc, m => m.ClearDescription(ctx, param.target, flags.yes)),
Commands.MemberDescUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberDesc, m => m.ChangeDescription(ctx, param.target, param.description)),
Commands.MemberNameShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberInfo, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberNameUpdate(var param, var flags) => ctx.Execute<MemberEdit>(MemberInfo, m => m.ChangeName(ctx, param.target, param.name, flags.yes)),
Commands.MemberBannerShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberBannerClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberBannerImage, m => m.ClearBannerImage(ctx, param.target, flags.yes)),
Commands.MemberBannerUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberBannerImage, m => m.ChangeBannerImage(ctx, param.target, param.banner)),
Commands.MemberColorShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberColor, m => m.ShowColor(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberColorClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberColor, m => m.ClearColor(ctx, param.target, flags.yes)),
Commands.MemberColorUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberColor, m => m.ChangeColor(ctx, param.target, param.color)),
Commands.MemberBirthdayShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberBirthday, m => m.ShowBirthday(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberBirthdayClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberBirthday, m => m.ClearBirthday(ctx, param.target, flags.yes)),
Commands.MemberBirthdayUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberBirthday, m => m.ChangeBirthday(ctx, param.target, param.birthday)),
Commands.MemberDisplaynameShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberDisplayName, m => m.ShowDisplayName(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberDisplaynameClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberDisplayName, m => m.ClearDisplayName(ctx, param.target, flags.yes)),
Commands.MemberDisplaynameUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberDisplayName, m => m.ChangeDisplayName(ctx, param.target, param.name)),
Commands.MemberServernameShow(var param, var flags) => ctx.Execute<MemberEdit>(MemberServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())),
Commands.MemberServernameClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberServerName, m => m.ClearServerName(ctx, param.target, flags.yes)),
Commands.MemberServernameUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberServerName, m => m.ChangeServerName(ctx, param.target, param.name)),
Commands.MemberKeepproxyShow(var param, _) => ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.ShowKeepProxy(ctx, param.target)),
Commands.MemberKeepproxyUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.ChangeKeepProxy(ctx, param.target, param.value)),
Commands.MemberServerKeepproxyShow(var param, _) => ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ShowServerKeepProxy(ctx, param.target)),
Commands.MemberServerKeepproxyUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ChangeServerKeepProxy(ctx, param.target, param.value)),
Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)),
Commands.MemberProxyShow(var param, _) => ctx.Execute<MemberProxy>(MemberProxy, m => m.ShowProxy(ctx, param.target)),
Commands.MemberProxyClear(var param, var flags) => ctx.Execute<MemberProxy>(MemberProxy, m => m.ClearProxy(ctx, param.target, flags.yes)),
Commands.MemberProxyAdd(var param, var flags) => ctx.Execute<MemberProxy>(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag, flags.yes)),
Commands.MemberProxyRemove(var param, _) => ctx.Execute<MemberProxy>(MemberProxy, m => m.RemoveProxy(ctx, param.target, param.tag)),
Commands.MemberProxySet(var param, var flags) => ctx.Execute<MemberProxy>(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags, flags.yes)),
Commands.MemberTtsShow(var param, _) => ctx.Execute<MemberEdit>(MemberTts, m => m.ShowTts(ctx, param.target)),
Commands.MemberTtsUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberTts, m => m.ChangeTts(ctx, param.target, param.value)),
Commands.MemberAutoproxyShow(var param, _) => ctx.Execute<MemberEdit>(MemberAutoproxy, m => m.ShowAutoproxy(ctx, param.target)),
Commands.MemberAutoproxyUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberAutoproxy, m => m.ChangeAutoproxy(ctx, param.target, param.value)),
Commands.MemberDelete(var param, _) => ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, param.target)),
Commands.MemberPrivacyShow(var param, _) => ctx.Execute<MemberEdit>(MemberPrivacy, m => m.ShowPrivacy(ctx, param.target)),
Commands.MemberPrivacyUpdate(var param, _) => ctx.Execute<MemberEdit>(MemberPrivacy, m => m.ChangePrivacy(ctx, param.target, param.member_privacy_target, param.new_privacy_level)),
Commands.MemberGroupAdd(var param, _) => ctx.Execute<GroupMember>(MemberGroupAdd, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Add)),
Commands.MemberGroupRemove(var param, _) => ctx.Execute<GroupMember>(MemberGroupRemove, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Remove)),
Commands.MemberId(var param, _) => ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, param.target)),
Commands.CfgShow => ctx.Execute<Config>(null, m => m.ShowConfig(ctx)),
Commands.CfgApAccountShow => ctx.Execute<Config>(null, m => m.ViewAutoproxyAccount(ctx)),
Commands.CfgApAccountUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditAutoproxyAccount(ctx, param.toggle)),
Commands.CfgApTimeoutShow => ctx.Execute<Config>(null, m => m.ViewAutoproxyTimeout(ctx)),
Commands.CfgApTimeoutOff => ctx.Execute<Config>(null, m => m.DisableAutoproxyTimeout(ctx)),
Commands.CfgApTimeoutReset => ctx.Execute<Config>(null, m => m.ResetAutoproxyTimeout(ctx)),
Commands.CfgApTimeoutUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)),
Commands.CfgTimezoneShow => ctx.Execute<Config>(null, m => m.ViewSystemTimezone(ctx)),
Commands.CfgTimezoneReset => ctx.Execute<Config>(null, m => m.ResetSystemTimezone(ctx)),
Commands.CfgTimezoneUpdate(var param, var flags) => ctx.Execute<Config>(null, m => m.EditSystemTimezone(ctx, param.timezone, flags.yes)),
Commands.CfgPingShow => ctx.Execute<Config>(null, m => m.ViewSystemPing(ctx)),
Commands.CfgPingUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditSystemPing(ctx, param.toggle)),
Commands.CfgMemberPrivacyShow => ctx.Execute<Config>(null, m => m.ViewMemberDefaultPrivacy(ctx)),
Commands.CfgMemberPrivacyUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditMemberDefaultPrivacy(ctx, param.toggle)),
Commands.CfgGroupPrivacyShow => ctx.Execute<Config>(null, m => m.ViewGroupDefaultPrivacy(ctx)),
Commands.CfgGroupPrivacyUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditGroupDefaultPrivacy(ctx, param.toggle)),
Commands.CfgShowPrivateInfoShow => ctx.Execute<Config>(null, m => m.ViewShowPrivateInfo(ctx)),
Commands.CfgShowPrivateInfoUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditShowPrivateInfo(ctx, param.toggle)),
Commands.CfgCaseSensitiveProxyTagsShow => ctx.Execute<Config>(null, m => m.ViewCaseSensitiveProxyTags(ctx)),
Commands.CfgCaseSensitiveProxyTagsUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditCaseSensitiveProxyTags(ctx, param.toggle)),
Commands.CfgProxyErrorMessageShow => ctx.Execute<Config>(null, m => m.ViewProxyErrorMessageEnabled(ctx)),
Commands.CfgProxyErrorMessageUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditProxyErrorMessageEnabled(ctx, param.toggle)),
Commands.CfgHidSplitShow => ctx.Execute<Config>(null, m => m.ViewHidDisplaySplit(ctx)),
Commands.CfgHidSplitUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditHidDisplaySplit(ctx, param.toggle)),
Commands.CfgHidCapsShow => ctx.Execute<Config>(null, m => m.ViewHidDisplayCaps(ctx)),
Commands.CfgHidCapsUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditHidDisplayCaps(ctx, param.toggle)),
Commands.CfgHidPaddingShow => ctx.Execute<Config>(null, m => m.ViewHidListPadding(ctx)),
Commands.CfgHidPaddingUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditHidListPadding(ctx, param.padding)),
Commands.CfgCardShowColorHexShow => ctx.Execute<Config>(null, m => m.ViewCardShowColorHex(ctx)),
Commands.CfgCardShowColorHexUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditCardShowColorHex(ctx, param.toggle)),
Commands.CfgProxySwitchShow => ctx.Execute<Config>(null, m => m.ViewProxySwitch(ctx)),
Commands.CfgProxySwitchUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditProxySwitch(ctx, param.proxy_switch_action)),
Commands.CfgNameFormatShow => ctx.Execute<Config>(null, m => m.ViewNameFormat(ctx)),
Commands.CfgNameFormatReset => ctx.Execute<Config>(null, m => m.ResetNameFormat(ctx)),
Commands.CfgNameFormatUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditNameFormat(ctx, param.format)),
Commands.CfgServerNameFormatShow(_, var flags) => ctx.Execute<Config>(null, m => m.ViewServerNameFormat(ctx, flags.GetReplyFormat())),
Commands.CfgServerNameFormatReset => ctx.Execute<Config>(null, m => m.ResetServerNameFormat(ctx)),
Commands.CfgServerNameFormatUpdate(var param, _) => ctx.Execute<Config>(null, m => m.EditServerNameFormat(ctx, param.format)),
Commands.CfgLimitsUpdate => ctx.Execute<Config>(null, m => m.LimitUpdate(ctx)),
Commands.FunThunder => ctx.Execute<Fun>(null, m => m.Thunder(ctx)),
Commands.FunMeow => ctx.Execute<Fun>(null, m => m.Meow(ctx)),
Commands.FunPokemon => ctx.Execute<Fun>(null, m => m.Mn(ctx)),
Commands.FunFire => ctx.Execute<Fun>(null, m => m.Fire(ctx)),
Commands.FunFreeze => ctx.Execute<Fun>(null, m => m.Freeze(ctx)),
Commands.FunStarstorm => ctx.Execute<Fun>(null, m => m.Starstorm(ctx)),
Commands.FunFlash => ctx.Execute<Fun>(null, m => m.Flash(ctx)),
Commands.FunRool => ctx.Execute<Fun>(null, m => m.Rool(ctx)),
Commands.Amogus => ctx.Execute<Fun>(null, m => m.Sus(ctx)),
Commands.FunError => ctx.Execute<Fun>(null, m => m.Error(ctx)),
Commands.SystemInfo(var param, var flags) => ctx.Execute<System>(SystemInfo, m => m.Query(ctx, param.target ?? ctx.System, flags.all, flags.@public, flags.@private)),
Commands.SystemNew(var param, _) => ctx.Execute<System>(SystemNew, m => m.New(ctx, param.name)),
Commands.SystemShowName(var param, var flags) => ctx.Execute<SystemEdit>(SystemRename, m => m.ShowName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemRename(var param, _) => ctx.Execute<SystemEdit>(SystemRename, m => m.Rename(ctx, ctx.System, param.name)),
Commands.SystemShowServerName(var param, var flags) => ctx.Execute<SystemEdit>(SystemServerName, m => m.ShowServerName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearServerName(var param, var flags) => ctx.Execute<SystemEdit>(SystemServerName, m => m.ClearServerName(ctx, ctx.System, flags.yes)),
Commands.SystemRenameServerName(var param, _) => ctx.Execute<SystemEdit>(SystemServerName, m => m.RenameServerName(ctx, ctx.System, param.name)),
Commands.SystemShowDescription(var param, var flags) => ctx.Execute<SystemEdit>(SystemDesc, m => m.ShowDescription(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearDescription(var param, var flags) => ctx.Execute<SystemEdit>(SystemDesc, m => m.ClearDescription(ctx, ctx.System, flags.yes)),
Commands.SystemChangeDescription(var param, _) => ctx.Execute<SystemEdit>(SystemDesc, m => m.ChangeDescription(ctx, ctx.System, param.description)),
Commands.SystemShowColor(var param, var flags) => ctx.Execute<SystemEdit>(SystemColor, m => m.ShowColor(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearColor(var param, var flags) => ctx.Execute<SystemEdit>(SystemColor, m => m.ClearColor(ctx, ctx.System, flags.yes)),
Commands.SystemChangeColor(var param, _) => ctx.Execute<SystemEdit>(SystemColor, m => m.ChangeColor(ctx, ctx.System, param.color)),
Commands.SystemShowTag(var param, var flags) => ctx.Execute<SystemEdit>(SystemTag, m => m.ShowTag(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearTag(var param, var flags) => ctx.Execute<SystemEdit>(SystemTag, m => m.ClearTag(ctx, ctx.System, flags.yes)),
Commands.SystemChangeTag(var param, _) => ctx.Execute<SystemEdit>(SystemTag, m => m.ChangeTag(ctx, ctx.System, param.tag)),
Commands.SystemShowServerTag(var param, var flags) => ctx.Execute<SystemEdit>(SystemServerTag, m => m.ShowServerTag(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearServerTag(var param, var flags) => ctx.Execute<SystemEdit>(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System, flags.yes)),
Commands.SystemChangeServerTag(var param, _) => ctx.Execute<SystemEdit>(SystemServerTag, m => m.ChangeServerTag(ctx, ctx.System, param.tag)),
Commands.SystemShowPronouns(var param, var flags) => ctx.Execute<SystemEdit>(SystemPronouns, m => m.ShowPronouns(ctx, param.target ?? ctx.System, flags.GetReplyFormat())),
Commands.SystemClearPronouns(var param, var flags) => ctx.Execute<SystemEdit>(SystemPronouns, m => m.ClearPronouns(ctx, ctx.System, flags.yes)),
Commands.SystemChangePronouns(var param, _) => ctx.Execute<SystemEdit>(SystemPronouns, m => m.ChangePronouns(ctx, ctx.System, param.pronouns)),
Commands.SystemShowAvatar(var param, var flags) => ((Func<Task>)(() =>
{
abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id);
}
else
{
abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument()));
}
if (abuseLog == null)
{
await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query.");
return;
}
if (!ctx.HasNext())
await ctx.Execute<Admin>(Admin, a => a.AbuseLogShow(ctx, abuseLog));
else if (ctx.Match("au", "adduser"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogAddUser(ctx, abuseLog));
else if (ctx.Match("ru", "removeuser"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog));
else if (ctx.Match("desc", "description"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDescription(ctx, abuseLog));
else if (ctx.Match("deny", "deny-bot-usage"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog));
else if (ctx.Match("yeet", "remove", "delete"))
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDelete(ctx, abuseLog));
else
await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}.");
}
}
private async Task HandleAdminCommand(Context ctx)
{
if (ctx.Match("usid", "updatesystemid"))
await ctx.Execute<Admin>(Admin, a => a.UpdateSystemId(ctx));
else if (ctx.Match("umid", "updatememberid"))
await ctx.Execute<Admin>(Admin, a => a.UpdateMemberId(ctx));
else if (ctx.Match("ugid", "updategroupid"))
await ctx.Execute<Admin>(Admin, a => a.UpdateGroupId(ctx));
else if (ctx.Match("rsid", "rerollsystemid"))
await ctx.Execute<Admin>(Admin, a => a.RerollSystemId(ctx));
else if (ctx.Match("rmid", "rerollmemberid"))
await ctx.Execute<Admin>(Admin, a => a.RerollMemberId(ctx));
else if (ctx.Match("rgid", "rerollgroupid"))
await ctx.Execute<Admin>(Admin, a => a.RerollGroupId(ctx));
else if (ctx.Match("uml", "updatememberlimit"))
await ctx.Execute<Admin>(Admin, a => a.SystemMemberLimit(ctx));
else if (ctx.Match("ugl", "updategrouplimit"))
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
else if (ctx.Match("sr", "systemrecover"))
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
else if (ctx.Match("sd", "systemdelete"))
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
else if (ctx.Match("sendmsg", "sendmessage"))
await ctx.Execute<Admin>(Admin, a => a.SendAdminMessage(ctx));
else if (ctx.Match("al", "abuselog"))
await HandleAdminAbuseLogCommand(ctx);
else
await ctx.Reply($"{Emojis.Error} Unknown command.");
}
private async Task HandleDebugCommand(Context ctx)
{
var availableCommandsStr = "Available debug targets: `permissions`, `proxying`";
if (ctx.Match("permissions", "perms", "permcheck"))
if (ctx.Match("channel", "ch"))
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx));
else
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
else if (ctx.Match("channel"))
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx));
else if (ctx.Match("proxy", "proxying", "proxycheck"))
await ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
else if (!ctx.HasNext())
await ctx.Reply($"{Emojis.Error} You need to pass a command. {availableCommandsStr}");
else
await ctx.Reply(
$"{Emojis.Error} Unknown debug command {ctx.PeekArgument().AsCode()}. {availableCommandsStr}");
}
private async Task HandleSystemCommand(Context ctx)
{
// these commands never take a system target
if (ctx.Match("new", "create", "make", "add", "register", "init", "n"))
await ctx.Execute<System>(SystemNew, m => m.New(ctx));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "systems", SystemCommands);
// todo: these aren't deprecated but also shouldn't be here
else if (ctx.Match("webhook", "hook"))
await ctx.Execute<Api>(null, m => m.SystemWebhook(ctx));
else if (ctx.Match("proxy"))
await ctx.Execute<SystemEdit>(SystemProxy, m => m.SystemProxy(ctx));
// finally, parse commands that *can* take a system target
else
{
// try matching a system ID
var target = await ctx.MatchSystem();
var previousPtr = ctx.Parameters._ptr;
// if we have a parsed target and no more commands, don't bother with the command flow
// we skip the `target != null` check here since the argument isn't be popped if it's not a system
if (!ctx.HasNext())
{
await ctx.Execute<System>(SystemInfo, m => m.Query(ctx, target ?? ctx.System));
return;
}
// hacky, but we need to CheckSystem(target) which throws a PKError
// normally PKErrors are only handled in ctx.Execute
try
{
await HandleSystemCommandTargeted(ctx, target ?? ctx.System);
}
catch (PKError e)
{
await ctx.Reply($"{Emojis.Error} {e.Message}");
return;
}
// if we *still* haven't matched anything, the user entered an invalid command name or system reference
if (ctx.Parameters._ptr == previousPtr)
{
if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _))
if (param.target == null)
{
await PrintCommandNotFoundError(ctx, SystemCommands);
return;
// we want to change avatar if an attached image is passed
// we can't have a separate parsed command for this since the parser can't be aware of any attachments
var attachedImage = ctx.ExtractImageFromAttachment();
if (attachedImage is { } image)
return ctx.Execute<SystemEdit>(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, image));
}
var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands);
await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n"
+ $"Perhaps you meant to use one of the following commands?\n{list}");
}
}
}
private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target)
{
if (ctx.Match("name", "rename", "changename", "rn"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemRename, m => m.Name(ctx, target));
else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname",
"serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn"))
await ctx.Execute<SystemEdit>(SystemServerName, m => m.ServerName(ctx, target));
else if (ctx.Match("tag", "t"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemTag, m => m.Tag(ctx, target));
else if (ctx.Match("servertag", "st", "stag", "deer"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemServerTag, m => m.ServerTag(ctx, target));
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDesc, m => m.Description(ctx, target));
else if (ctx.Match("pronouns", "pronoun", "prns", "pn"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemPronouns, m => m.Pronouns(ctx, target));
else if (ctx.Match("color", "colour"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemColor, m => m.Color(ctx, target));
else if (ctx.Match("banner", "splash", "cover"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemBannerImage, m => m.BannerImage(ctx, target));
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemAvatar, m => m.Avatar(ctx, target));
else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic",
"guildavatar", "guildpic", "guildicon", "sicon", "spfp"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemServerAvatar, m => m.ServerAvatar(ctx, target));
else if (ctx.Match("list", "l", "members", "ls"))
await ctx.CheckSystem(target).Execute<SystemList>(SystemList, m => m.MemberList(ctx, target));
else if (ctx.Match("find", "search", "query", "fd", "s"))
await ctx.CheckSystem(target).Execute<SystemList>(SystemFind, m => m.MemberList(ctx, target));
else if (ctx.Match("f", "front", "fronter", "fronters"))
{
if (ctx.Match("h", "history"))
await ctx.CheckSystem(target).Execute<SystemFront>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
else if (ctx.Match("p", "percent", "%"))
await ctx.CheckSystem(target).Execute<SystemFront>(SystemFrontPercent, m => m.FrontPercent(ctx, system: target));
else
await ctx.CheckSystem(target).Execute<SystemFront>(SystemFronter, m => m.SystemFronter(ctx, target));
}
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
await ctx.CheckSystem(target).Execute<SystemFront>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
await ctx.CheckSystem(target).Execute<SystemFront>(SystemFrontPercent, m => m.FrontPercent(ctx, system: target));
else if (ctx.Match("info", "view", "show"))
await ctx.CheckSystem(target).Execute<System>(SystemInfo, m => m.Query(ctx, target));
else if (ctx.Match("groups", "gs"))
await ctx.CheckSystem(target).Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, target));
else if (ctx.Match("privacy"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemPrivacy, m => m.SystemPrivacy(ctx, target));
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx, target));
else if (ctx.Match("id"))
await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target));
else if (ctx.Match("random", "rand", "r"))
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
await ctx.CheckSystem(target).Execute<Random>(GroupRandom, r => r.Group(ctx, target));
else
await ctx.CheckSystem(target).Execute<Random>(MemberRandom, m => m.Member(ctx, target));
}
private async Task HandleMemberCommand(Context ctx)
{
if (ctx.Match("new", "n", "add", "create", "register"))
await ctx.Execute<Member>(MemberNew, m => m.NewMember(ctx));
else if (ctx.Match("list"))
await ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "members", MemberCommands);
else if (await ctx.MatchMember() is PKMember target)
await HandleMemberCommandTargeted(ctx, target);
else if (!ctx.HasNext())
await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName,
MemberServerName, MemberDesc, MemberPronouns,
MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar);
else
await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}");
}
private async Task HandleMemberCommandTargeted(Context ctx, PKMember target)
{
// Commands that have a member target (eg. pk;member <member> delete)
if (ctx.Match("rename", "name", "changename", "setname", "rn"))
await ctx.Execute<MemberEdit>(MemberRename, m => m.Name(ctx, target));
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
await ctx.Execute<MemberEdit>(MemberDesc, m => m.Description(ctx, target));
else if (ctx.Match("pronouns", "pronoun", "prns", "pn"))
await ctx.Execute<MemberEdit>(MemberPronouns, m => m.Pronouns(ctx, target));
else if (ctx.Match("color", "colour"))
await ctx.Execute<MemberEdit>(MemberColor, m => m.Color(ctx, target));
else if (ctx.Match("birthday", "birth", "bday", "birthdate", "cakeday", "bdate", "bd"))
await ctx.Execute<MemberEdit>(MemberBirthday, m => m.Birthday(ctx, target));
else if (ctx.Match("proxy", "tags", "proxytags", "brackets"))
await ctx.Execute<MemberProxy>(MemberProxy, m => m.Proxy(ctx, target));
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target));
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target));
else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp"))
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.WebhookAvatar(ctx, target));
else if (ctx.Match("banner", "splash", "cover"))
await ctx.Execute<MemberEdit>(MemberBannerImage, m => m.BannerImage(ctx, target));
else if (ctx.Match("group", "groups", "g"))
if (ctx.Match("add", "a"))
await ctx.Execute<GroupMember>(MemberGroupAdd,
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem"))
await ctx.Execute<GroupMember>(MemberGroupRemove,
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove));
else
await ctx.Execute<GroupMember>(MemberGroups, m => m.ListMemberGroups(ctx, target));
else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic",
"guildavatar", "guildpic", "guildicon", "sicon", "spfp"))
await ctx.Execute<MemberAvatar>(MemberServerAvatar, m => m.ServerAvatar(ctx, target));
else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname"))
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<MemberEdit>(MemberServerName, m => m.ServerName(ctx, target));
else if (ctx.Match("autoproxy", "ap"))
await ctx.Execute<MemberEdit>(MemberAutoproxy, m => m.MemberAutoproxy(ctx, target));
else if (ctx.Match("keepproxy", "keeptags", "showtags", "kp"))
await ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.KeepProxy(ctx, target));
else if (ctx.Match("texttospeech", "text-to-speech", "tts"))
await ctx.Execute<MemberEdit>(MemberTts, m => m.Tts(ctx, target));
else if (ctx.Match("serverkeepproxy", "servershowtags", "guildshowtags", "guildkeeptags", "serverkeeptags", "skp"))
await ctx.Execute<MemberEdit>(MemberServerKeepProxy, m => m.ServerKeepProxy(ctx, target));
else if (ctx.Match("id"))
await ctx.Execute<Member>(MemberId, m => m.DisplayId(ctx, target));
else if (ctx.Match("privacy"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, null));
else if (ctx.Match("private", "hidden", "hide"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private));
else if (ctx.Match("public", "shown", "show", "unhide", "unhidden"))
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public));
else if (ctx.Match("soulscream"))
await ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, target));
else if (!ctx.HasNext()) // Bare command
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);
}
private async Task HandleGroupCommand(Context ctx)
{
// Commands with no group argument
if (ctx.Match("n", "new"))
await ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx));
else if (ctx.Match("list", "l"))
await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "groups", GroupCommands);
else if (await ctx.MatchGroup() is { } target)
{
// Commands with group argument
if (ctx.Match("rename", "name", "changename", "setname", "rn"))
await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target));
else if (ctx.Match("nick", "dn", "displayname", "nickname"))
await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target));
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
else if (ctx.Match("add", "a"))
await ctx.Execute<GroupMember>(GroupAdd,
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem"))
await ctx.Execute<GroupMember>(GroupRemove,
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
else if (ctx.Match("members", "list", "ms", "l", "ls"))
await ctx.Execute<GroupMember>(GroupMemberList, g => g.ListGroupMembers(ctx, target));
else if (ctx.Match("random", "rand", "r"))
await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target));
else if (ctx.Match("privacy"))
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
else if (ctx.Match("public", "pub"))
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public));
else if (ctx.Match("private", "priv"))
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private));
else if (ctx.Match("delete", "destroy", "erase", "yeet"))
await ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, target));
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
await ctx.Execute<Groups>(GroupIcon, g => g.GroupIcon(ctx, target));
else if (ctx.Match("banner", "splash", "cover"))
await ctx.Execute<Groups>(GroupBannerImage, g => g.GroupBannerImage(ctx, target));
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
await ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, group: target));
else if (ctx.Match("color", "colour"))
await ctx.Execute<Groups>(GroupColor, g => g.GroupColor(ctx, target));
else if (ctx.Match("id"))
await ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, target));
else if (!ctx.HasNext())
await ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, target));
else
await PrintCommandNotFoundError(ctx, GroupCommandsTargeted);
}
else if (!ctx.HasNext())
await PrintCommandExpectedError(ctx, GroupCommands);
else
await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}");
}
private async Task HandleSwitchCommand(Context ctx)
{
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx));
else if (ctx.Match("move", "m", "shift", "offset"))
await ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx));
else if (ctx.Match("edit", "e", "replace"))
if (ctx.Match("out"))
await ctx.Execute<Switch>(SwitchEditOut, m => m.SwitchEditOut(ctx));
else
await ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx));
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
await ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx));
else if (ctx.Match("copy", "add", "duplicate", "dupe"))
await ctx.Execute<Switch>(SwitchCopy, m => m.SwitchEdit(ctx, true));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "switching", SwitchCommands);
else if (ctx.HasNext()) // there are following arguments
await ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx));
else
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
}
private async Task CommandHelpRoot(Context ctx)
{
if (!ctx.HasNext())
{
await ctx.Reply(
"Available command help targets: `system`, `member`, `group`, `switch`, `config`, `autoproxy`, `log`, `blacklist`."
+ $"\n- **{ctx.DefaultPrefix}commands <target>** - *View commands related to a help target.*"
+ "\n\nFor the full list of commands, see the website: <https://pluralkit.me/commands>");
return;
}
switch (ctx.PeekArgument())
{
case "system":
case "systems":
case "s":
case "account":
case "acc":
await PrintCommandList(ctx, "systems", SystemCommands);
break;
case "member":
case "members":
case "m":
await PrintCommandList(ctx, "members", MemberCommands);
break;
case "group":
case "groups":
case "g":
await PrintCommandList(ctx, "groups", GroupCommands);
break;
case "switch":
case "switches":
case "switching":
case "sw":
await PrintCommandList(ctx, "switching", SwitchCommands);
break;
case "log":
await PrintCommandList(ctx, "message logging", LogCommands);
break;
case "blacklist":
case "bl":
await PrintCommandList(ctx, "channel blacklisting", BlacklistCommands);
break;
case "config":
case "cfg":
await PrintCommandList(ctx, "settings", ConfigCommands);
break;
case "serverconfig":
case "guildconfig":
case "scfg":
await PrintCommandList(ctx, "server settings", ServerConfigCommands);
break;
case "autoproxy":
case "ap":
await PrintCommandList(ctx, "autoproxy", AutoproxyCommands);
break;
default:
await ctx.Reply("For the full list of commands, see the website: <https://pluralkit.me/commands>");
break;
}
}
private Task HandleAutoproxyCommand(Context ctx)
{
// ctx.CheckSystem();
// oops, that breaks stuff! PKErrors before ctx.Execute don't actually do anything.
// so we just emulate checking and throwing an error.
if (ctx.System == null)
return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError(ctx.DefaultPrefix).Message}");
return ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx));
}
private Task HandleConfigCommand(Context ctx)
{
if (ctx.System == null)
return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError(ctx.DefaultPrefix).Message}");
if (!ctx.HasNext())
return ctx.Execute<Config>(null, m => m.ShowConfig(ctx));
if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "account", "ac" }))
return ctx.Execute<Config>(null, m => m.AutoproxyAccount(ctx));
if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "timeout", "tm" }))
return ctx.Execute<Config>(null, m => m.AutoproxyTimeout(ctx));
if (ctx.Match("timezone", "zone", "tz"))
return ctx.Execute<Config>(null, m => m.SystemTimezone(ctx));
if (ctx.Match("ping"))
return ctx.Execute<Config>(null, m => m.SystemPing(ctx));
if (ctx.MatchMultiple(new[] { "private" }, new[] { "member" }) || ctx.Match("mp"))
return ctx.Execute<Config>(null, m => m.MemberDefaultPrivacy(ctx));
if (ctx.MatchMultiple(new[] { "private" }, new[] { "group" }) || ctx.Match("gp"))
return ctx.Execute<Config>(null, m => m.GroupDefaultPrivacy(ctx));
if (ctx.MatchMultiple(new[] { "show" }, new[] { "private" }) || ctx.Match("sp"))
return ctx.Execute<Config>(null, m => m.ShowPrivateInfo(ctx));
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "case" }))
return ctx.Execute<Config>(null, m => m.CaseSensitiveProxyTags(ctx));
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "error" }) || ctx.Match("pe"))
return ctx.Execute<Config>(null, m => m.ProxyErrorMessageEnabled(ctx));
if (ctx.MatchMultiple(new[] { "split" }, new[] { "id", "ids" }) || ctx.Match("sid", "sids"))
return ctx.Execute<Config>(null, m => m.HidDisplaySplit(ctx));
if (ctx.MatchMultiple(new[] { "cap", "caps", "capitalize", "capitalise" }, new[] { "id", "ids" }) || ctx.Match("capid", "capids"))
return ctx.Execute<Config>(null, m => m.HidDisplayCaps(ctx));
if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids"))
return ctx.Execute<Config>(null, m => m.HidListPadding(ctx));
if (ctx.MatchMultiple(new[] { "show" }, new[] { "color", "colour", "colors", "colours" }) || ctx.Match("showcolor", "showcolour", "showcolors", "showcolours", "colorcode", "colorhex"))
return ctx.Execute<Config>(null, m => m.CardShowColorHex(ctx));
if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf"))
return ctx.Execute<Config>(null, m => m.NameFormat(ctx));
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))
return ctx.Execute<Config>(null, m => m.LimitUpdate(ctx));
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "switch" }) || ctx.Match("proxyswitch", "ps"))
return ctx.Execute<Config>(null, m => m.ProxySwitch(ctx));
if (ctx.MatchMultiple(new[] { "server" }, new[] { "name" }, new[] { "format" }) || ctx.MatchMultiple(new[] { "server", "servername" }, new[] { "format", "nameformat", "nf" }) || ctx.Match("snf", "servernf", "servernameformat", "snameformat"))
return ctx.Execute<Config>(null, m => m.ServerNameFormat(ctx));
// todo: maybe add the list of configuration keys here?
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `{ctx.DefaultPrefix}commands config` for the list of possible config settings.");
}
private Task HandleServerConfigCommand(Context ctx)
{
if (!ctx.HasNext())
return ctx.Execute<ServerConfig>(null, m => m.ShowConfig(ctx));
if (ctx.MatchMultiple(new[] { "log" }, new[] { "cleanup", "clean" }) || ctx.Match("logclean"))
return ctx.Execute<ServerConfig>(null, m => m.SetLogCleanup(ctx));
if (ctx.MatchMultiple(new[] { "invalid", "unknown" }, new[] { "command" }, new[] { "error", "response" }) || ctx.Match("invalidcommanderror", "unknowncommanderror"))
return ctx.Execute<ServerConfig>(null, m => m.InvalidCommandResponse(ctx));
if (ctx.MatchMultiple(new[] { "require", "enforce" }, new[] { "tag", "systemtag" }) || ctx.Match("requiretag", "enforcetag"))
return ctx.Execute<ServerConfig>(null, m => m.RequireSystemTag(ctx));
if (ctx.MatchMultiple(new[] { "suppress" }, new[] { "notifications" }) || ctx.Match("proxysilent"))
return ctx.Execute<ServerConfig>(null, m => m.SuppressNotifications(ctx));
if (ctx.MatchMultiple(new[] { "log" }, new[] { "channel" }))
return ctx.Execute<ServerConfig>(null, m => m.SetLogChannel(ctx));
if (ctx.MatchMultiple(new[] { "log" }, new[] { "blacklist" }))
{
if (ctx.Match("enable", "on", "add", "deny"))
return ctx.Execute<ServerConfig>(null, m => m.SetLogBlacklisted(ctx, true));
else if (ctx.Match("disable", "off", "remove", "allow"))
return ctx.Execute<ServerConfig>(null, m => m.SetLogBlacklisted(ctx, false));
else
return ctx.Execute<ServerConfig>(null, m => m.ShowLogDisabledChannels(ctx));
}
if (ctx.MatchMultiple(new[] { "proxy", "proxying" }, new[] { "blacklist" }))
{
if (ctx.Match("enable", "on", "add", "deny"))
return ctx.Execute<ServerConfig>(null, m => m.SetProxyBlacklisted(ctx, true));
else if (ctx.Match("disable", "off", "remove", "allow"))
return ctx.Execute<ServerConfig>(null, m => m.SetProxyBlacklisted(ctx, false));
else
return ctx.Execute<ServerConfig>(null, m => m.ShowProxyBlacklisted(ctx));
}
// todo: maybe add the list of configuration keys here?
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `{ctx.DefaultPrefix}commands serverconfig` for the list of possible config settings.");
// if no attachment show the avatar like intended
return ctx.Execute<SystemEdit>(SystemAvatar, m => m.ShowAvatar(ctx, param.target ?? ctx.System, flags.GetReplyFormat()));
}))(),
Commands.SystemClearAvatar(var param, var flags) => ctx.Execute<SystemEdit>(SystemAvatar, m => m.ClearAvatar(ctx, ctx.System, flags.yes)),
Commands.SystemChangeAvatar(var param, _) => ctx.Execute<SystemEdit>(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, param.avatar)),
Commands.SystemShowServerAvatar(var param, var flags) => ((Func<Task>)(() =>
{
if (param.target == null)
{
// we want to change avatar if an attached image is passed
// we can't have a separate parsed command for this since the parser can't be aware of any attachments
var attachedImage = ctx.ExtractImageFromAttachment();
if (attachedImage is { } image)
return ctx.Execute<SystemEdit>(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, image));
}
// if no attachment show the avatar like intended
return ctx.Execute<SystemEdit>(SystemServerAvatar, m => m.ShowServerAvatar(ctx, param.target ?? ctx.System, flags.GetReplyFormat()));
}))(),
Commands.SystemClearServerAvatar(var param, var flags) => ctx.Execute<SystemEdit>(SystemServerAvatar, m => m.ClearServerAvatar(ctx, ctx.System, flags.yes)),
Commands.SystemChangeServerAvatar(var param, _) => ctx.Execute<SystemEdit>(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, param.avatar)),
Commands.SystemShowBanner(var param, var flags) => ((Func<Task>)(() =>
{
if (param.target == null)
{
// we want to change banner if an attached image is passed
// we can't have a separate parsed command for this since the parser can't be aware of any attachments
var attachedImage = ctx.ExtractImageFromAttachment();
if (attachedImage is { } image)
return ctx.Execute<SystemEdit>(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, image));
}
// if no attachment show the banner like intended
return ctx.Execute<SystemEdit>(SystemBannerImage, m => m.ShowBannerImage(ctx, param.target ?? ctx.System, flags.GetReplyFormat()));
}))(),
Commands.SystemClearBanner(var param, var flags) => ctx.Execute<SystemEdit>(SystemBannerImage, m => m.ClearBannerImage(ctx, ctx.System, flags.yes)),
Commands.SystemChangeBanner(var param, _) => ctx.Execute<SystemEdit>(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, param.banner)),
Commands.SystemDelete(_, var flags) => ctx.Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx, ctx.System, flags.no_export)),
Commands.SystemShowProxyCurrent(_, _) => ctx.Execute<SystemEdit>(SystemProxy, m => m.ShowSystemProxy(ctx, ctx.Guild)),
Commands.SystemShowProxy(var param, _) => ctx.Execute<SystemEdit>(SystemProxy, m => m.ShowSystemProxy(ctx, param.target)),
Commands.SystemToggleProxyCurrent(var param, _) => ctx.Execute<SystemEdit>(SystemProxy, m => m.ToggleSystemProxy(ctx, ctx.Guild, param.toggle)),
Commands.SystemToggleProxy(var param, _) => ctx.Execute<SystemEdit>(SystemProxy, m => m.ToggleSystemProxy(ctx, param.target, param.toggle)),
Commands.SystemShowPrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)),
Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)),
Commands.SystemChangePrivacy(var param, _) => ctx.Execute<SystemEdit>(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)),
Commands.SwitchOut(_, _) => ctx.Execute<Switch>(SwitchOut, m => m.SwitchOut(ctx)),
Commands.SwitchDo(var param, _) => ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx, param.targets)),
Commands.SwitchMove(var param, var flags) => ctx.Execute<Switch>(SwitchMove, m => m.SwitchMove(ctx, param.@string, flags.yes)),
Commands.SwitchEdit(var param, var flags) => ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend, flags.yes)),
Commands.SwitchEditOut(_, var flags) => ctx.Execute<Switch>(SwitchEditOut, m => m.SwitchEditOut(ctx, flags.yes)),
Commands.SwitchDelete(var param, var flags) => ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx, flags.all, flags.yes)),
Commands.SwitchCopy(var param, var flags) => ctx.Execute<Switch>(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend, false)),
Commands.SystemFronter(var param, var flags) => ctx.Execute<SystemFront>(SystemFronter, m => m.Fronter(ctx, param.target ?? ctx.System)),
Commands.SystemFronterHistory(var param, var flags) => ctx.Execute<SystemFront>(SystemFrontHistory, m => m.FrontHistory(ctx, param.target ?? ctx.System, flags.clear)),
Commands.SystemFronterPercent(var param, var flags) => ctx.Execute<SystemFront>(SystemFrontPercent, m => m.FrontPercent(ctx, param.target ?? ctx.System, flags.duration, flags.fronters_only, flags.flat)),
Commands.SystemDisplayId(var param, _) => ctx.Execute<System>(SystemId, m => m.DisplayId(ctx, param.target ?? ctx.System)),
Commands.SystemWebhookShow => ctx.Execute<Api>(null, m => m.GetSystemWebhook(ctx)),
Commands.SystemWebhookClear(_, var flags) => ctx.Execute<Api>(null, m => m.ClearSystemWebhook(ctx, flags.yes)),
Commands.SystemWebhookSet(var param, _) => ctx.Execute<Api>(null, m => m.SetSystemWebhook(ctx, param.url)),
Commands.RandomSelf(_, var flags) =>
flags.group
? ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed))
: ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, ctx.System, flags.all, flags.show_embed)),
Commands.RandomGroupSelf(_, var flags) => ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)),
Commands.RandomGroupMemberSelf(var param, var flags) => ctx.Execute<Random>(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)),
Commands.SystemRandom(var param, var flags) =>
flags.group
? ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed))
: ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)),
Commands.SystemRandomGroup(var param, var flags) =>
ctx.Execute<Random>(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)),
Commands.GroupRandomMember(var param, var flags) => ctx.Execute<Random>(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)),
Commands.SystemLink(var param, var flags) => ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx, param.account, flags.yes)),
Commands.SystemUnlink(var param, var flags) => ctx.Execute<SystemLink>(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)),
Commands.SystemMembers(var param, var flags) => ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, param.target ?? ctx.System, param.query, flags)),
Commands.MemberGroups(var param, var flags) => ctx.Execute<GroupMember>(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)),
Commands.GroupMembers(var param, var flags) => ctx.Execute<GroupMember>(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)),
Commands.SystemGroups(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, param.target ?? ctx.System, param.query, flags, flags.all)),
Commands.GroupsSelf(var param, var flags) => ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)),
Commands.GroupNew(var param, var flags) => ctx.Execute<Groups>(GroupNew, g => g.CreateGroup(ctx, param.name, flags.yes)),
Commands.GroupInfo(var param, var flags) => ctx.Execute<Groups>(GroupInfo, g => g.ShowGroupCard(ctx, param.target, flags.show_embed, flags.all)),
Commands.GroupShowName(var param, var flags) => ctx.Execute<Groups>(GroupRename, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearName(var param, var flags) => ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, param.target, null)),
Commands.GroupRename(var param, var flags) => ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, param.target, param.name, flags.yes)),
Commands.GroupShowDisplayName(var param, var flags) => ctx.Execute<Groups>(GroupDisplayName, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearDisplayName(var param, var flags) => ctx.Execute<Groups>(GroupDisplayName, g => g.ClearGroupDisplayName(ctx, param.target, flags.yes)),
Commands.GroupChangeDisplayName(var param, _) => ctx.Execute<Groups>(GroupDisplayName, g => g.ChangeGroupDisplayName(ctx, param.target, param.name)),
Commands.GroupShowDescription(var param, var flags) => ctx.Execute<Groups>(GroupDesc, g => g.ShowGroupDescription(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearDescription(var param, var flags) => ctx.Execute<Groups>(GroupDesc, g => g.ClearGroupDescription(ctx, param.target, flags.yes)),
Commands.GroupChangeDescription(var param, _) => ctx.Execute<Groups>(GroupDesc, g => g.ChangeGroupDescription(ctx, param.target, param.description)),
Commands.GroupShowIcon(var param, var flags) => ctx.Execute<Groups>(GroupIcon, g => g.ShowGroupIcon(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearIcon(var param, var flags) => ctx.Execute<Groups>(GroupIcon, g => g.ClearGroupIcon(ctx, param.target, flags.yes)),
Commands.GroupChangeIcon(var param, _) => ctx.Execute<Groups>(GroupIcon, g => g.ChangeGroupIcon(ctx, param.target, param.icon)),
Commands.GroupShowBanner(var param, var flags) => ctx.Execute<Groups>(GroupBannerImage, g => g.ShowGroupBanner(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearBanner(var param, var flags) => ctx.Execute<Groups>(GroupBannerImage, g => g.ClearGroupBanner(ctx, param.target, flags.yes)),
Commands.GroupChangeBanner(var param, _) => ctx.Execute<Groups>(GroupBannerImage, g => g.ChangeGroupBanner(ctx, param.target, param.banner)),
Commands.GroupShowColor(var param, var flags) => ctx.Execute<Groups>(GroupColor, g => g.ShowGroupColor(ctx, param.target, flags.GetReplyFormat())),
Commands.GroupClearColor(var param, var flags) => ctx.Execute<Groups>(GroupColor, g => g.ClearGroupColor(ctx, param.target, flags.yes)),
Commands.GroupChangeColor(var param, _) => ctx.Execute<Groups>(GroupColor, g => g.ChangeGroupColor(ctx, param.target, param.color)),
Commands.GroupAddMember(var param, var flags) => ctx.Execute<GroupMember>(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all)),
Commands.GroupRemoveMember(var param, var flags) => ctx.Execute<GroupMember>(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all, flags.yes)),
Commands.GroupShowPrivacy(var param, _) => ctx.Execute<Groups>(GroupPrivacy, g => g.ShowGroupPrivacy(ctx, param.target)),
Commands.GroupChangePrivacyAll(var param, _) => ctx.Execute<Groups>(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, param.level)),
Commands.GroupChangePrivacy(var param, _) => ctx.Execute<Groups>(GroupPrivacy, g => g.SetGroupPrivacy(ctx, param.target, param.privacy, param.level)),
Commands.GroupSetPublic(var param, _) => ctx.Execute<Groups>(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, PrivacyLevel.Public)),
Commands.GroupSetPrivate(var param, _) => ctx.Execute<Groups>(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, PrivacyLevel.Private)),
Commands.GroupDelete(var param, var flags) => ctx.Execute<Groups>(GroupDelete, g => g.DeleteGroup(ctx, param.target)),
Commands.GroupId(var param, _) => ctx.Execute<Groups>(GroupId, g => g.DisplayId(ctx, param.target)),
Commands.GroupFronterPercent(var param, var flags) => ctx.Execute<SystemFront>(GroupFrontPercent, g => g.FrontPercent(ctx, null, flags.duration, flags.fronters_only, flags.flat, param.target)),
Commands.TokenDisplay => ctx.Execute<Api>(TokenGet, m => m.GetToken(ctx)),
Commands.TokenRefresh => ctx.Execute<Api>(TokenRefresh, m => m.RefreshToken(ctx)),
Commands.AutoproxyShow => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, null)),
Commands.AutoproxyOff => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Off())),
Commands.AutoproxyLatch => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Latch())),
Commands.AutoproxyFront => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Front())),
Commands.AutoproxyMember(var param, _) => ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Member(param.target))),
Commands.PermcheckChannel(var param, _) => ctx.Execute<Checks>(PermCheck, m => m.PermCheckChannel(ctx, param.target)),
Commands.PermcheckGuild(var param, _) => ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx, param.target)),
Commands.MessageProxyCheck(var param, _) => ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)),
Commands.MessageInfo(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), flags.delete, flags.author, flags.show_embed)),
Commands.MessageAuthor(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), false, true, flags.show_embed)),
Commands.MessageDelete(var param, var flags) => ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), true, false, flags.show_embed)),
Commands.MessageEdit(var param, var flags) => ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, param.target, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)),
Commands.MessageReproxy(var param, _) => ctx.Execute<ProxiedMessage>(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg, param.member)),
Commands.Import(var param, var flags) => ctx.Execute<ImportExport>(Import, m => m.Import(ctx, param.url, flags.yes)),
Commands.Export(_, _) => ctx.Execute<ImportExport>(Export, m => m.Export(ctx)),
Commands.ServerConfigShow => ctx.Execute<ServerConfig>(null, m => m.ShowConfig(ctx)),
Commands.ServerConfigLogChannelShow => ctx.Execute<ServerConfig>(null, m => m.ShowLogChannel(ctx)),
Commands.ServerConfigLogChannelSet(var param, _) => ctx.Execute<ServerConfig>(null, m => m.SetLogChannel(ctx, param.channel)),
Commands.ServerConfigLogChannelClear(_, var flags) => ctx.Execute<ServerConfig>(null, m => m.ClearLogChannel(ctx, flags.yes)),
Commands.ServerConfigLogCleanupShow => ctx.Execute<ServerConfig>(null, m => m.ShowLogCleanup(ctx)),
Commands.ServerConfigLogCleanupSet(var param, _) => ctx.Execute<ServerConfig>(null, m => m.SetLogCleanup(ctx, param.toggle)),
Commands.ServerConfigLogBlacklistShow => ctx.Execute<ServerConfig>(null, m => m.ShowLogBlacklist(ctx)),
Commands.ServerConfigLogBlacklistAdd(var param, var flags) => ctx.Execute<ServerConfig>(null, m => m.AddLogBlacklist(ctx, param.channel, flags.all)),
Commands.ServerConfigLogBlacklistRemove(var param, var flags) => ctx.Execute<ServerConfig>(null, m => m.RemoveLogBlacklist(ctx, param.channel, flags.all)),
Commands.ServerConfigProxyBlacklistShow => ctx.Execute<ServerConfig>(null, m => m.ShowProxyBlacklist(ctx)),
Commands.ServerConfigProxyBlacklistAdd(var param, var flags) => ctx.Execute<ServerConfig>(null, m => m.AddProxyBlacklist(ctx, param.channel, flags.all)),
Commands.ServerConfigProxyBlacklistRemove(var param, var flags) => ctx.Execute<ServerConfig>(null, m => m.RemoveProxyBlacklist(ctx, param.channel, flags.all)),
Commands.ServerConfigInvalidCommandResponseShow => ctx.Execute<ServerConfig>(null, m => m.ShowInvalidCommandResponse(ctx)),
Commands.ServerConfigInvalidCommandResponseSet(var param, _) => ctx.Execute<ServerConfig>(null, m => m.SetInvalidCommandResponse(ctx, param.toggle)),
Commands.ServerConfigRequireSystemTagShow => ctx.Execute<ServerConfig>(null, m => m.ShowRequireSystemTag(ctx)),
Commands.ServerConfigRequireSystemTagSet(var param, _) => ctx.Execute<ServerConfig>(null, m => m.SetRequireSystemTag(ctx, param.toggle)),
Commands.ServerConfigSuppressNotificationsShow => ctx.Execute<ServerConfig>(null, m => m.ShowSuppressNotifications(ctx)),
Commands.ServerConfigSuppressNotificationsSet(var param, _) => ctx.Execute<ServerConfig>(null, m => m.SetSuppressNotifications(ctx, param.toggle)),
Commands.AdminUpdateSystemId(var param, var flags) => ctx.Execute<Admin>(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid, flags.yes)),
Commands.AdminUpdateMemberId(var param, var flags) => ctx.Execute<Admin>(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid, flags.yes)),
Commands.AdminUpdateGroupId(var param, var flags) => ctx.Execute<Admin>(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid, flags.yes)),
Commands.AdminRerollSystemId(var param, var flags) => ctx.Execute<Admin>(null, m => m.RerollSystemId(ctx, param.target, flags.yes)),
Commands.AdminRerollMemberId(var param, var flags) => ctx.Execute<Admin>(null, m => m.RerollMemberId(ctx, param.target, flags.yes)),
Commands.AdminRerollGroupId(var param, var flags) => ctx.Execute<Admin>(null, m => m.RerollGroupId(ctx, param.target, flags.yes)),
Commands.AdminSystemMemberLimit(var param, var flags) => ctx.Execute<Admin>(null, m => m.SystemMemberLimit(ctx, param.target, param.limit, flags.yes)),
Commands.AdminSystemGroupLimit(var param, var flags) => ctx.Execute<Admin>(null, m => m.SystemGroupLimit(ctx, param.target, param.limit, flags.yes)),
Commands.AdminSystemRecover(var param, var flags) => ctx.Execute<Admin>(null, m => m.SystemRecover(ctx, param.token, param.account, flags.reroll_token, flags.yes)),
Commands.AdminSystemDelete(var param, _) => ctx.Execute<Admin>(null, m => m.SystemDelete(ctx, param.target)),
Commands.AdminSendMessage(var param, _) => ctx.Execute<Admin>(null, m => m.SendAdminMessage(ctx, param.account, param.content)),
Commands.AdminAbuselogCreate(var param, var flags) => ctx.Execute<Admin>(null, m => m.AbuseLogCreate(ctx, param.account, flags.deny_boy_usage, param.description)),
Commands.AdminAbuselogShowAccount(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogShow(ctx, param.account, null)),
Commands.AdminAbuselogFlagDenyAccount(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogFlagDeny(ctx, param.account, null, param.value)),
Commands.AdminAbuselogDescriptionAccount(var param, var flags) => ctx.Execute<Admin>(null, m => m.AbuseLogDescription(ctx, param.account, null, param.desc, flags.clear, flags.yes)),
Commands.AdminAbuselogAddUserAccount(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogAddUser(ctx, param.account, null, ctx.Author)),
Commands.AdminAbuselogRemoveUserAccount(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogRemoveUser(ctx, param.account, null, ctx.Author)),
Commands.AdminAbuselogDeleteAccount(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogDelete(ctx, param.account, null)),
Commands.AdminAbuselogShowLogId(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogShow(ctx, null, param.log_id)),
Commands.AdminAbuselogFlagDenyLogId(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogFlagDeny(ctx, null, param.log_id, param.value)),
Commands.AdminAbuselogDescriptionLogId(var param, var flags) => ctx.Execute<Admin>(null, m => m.AbuseLogDescription(ctx, null, param.log_id, param.desc, flags.clear, flags.yes)),
Commands.AdminAbuselogAddUserLogId(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogAddUser(ctx, null, param.log_id, ctx.Author)),
Commands.AdminAbuselogRemoveUserLogId(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogRemoveUser(ctx, null, param.log_id, ctx.Author)),
Commands.AdminAbuselogDeleteLogId(var param, _) => ctx.Execute<Admin>(null, m => m.AbuseLogDelete(ctx, null, param.log_id)),
_ =>
// this should only ever occur when deving if commands are not implemented...
ctx.Reply(
$"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"),
};
}
}

View file

@ -26,11 +26,9 @@ public class Context
private readonly IMetrics _metrics;
private readonly CommandMessageService _commandMessageService;
private Command? _currentCommand;
public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message,
int commandParseOffset, PKSystem senderSystem, SystemConfig config,
GuildConfig? guildConfig, string[] prefixes)
GuildConfig? guildConfig, string[] prefixes, Parameters parameters)
{
Message = (Message)message;
ShardId = shardId;
@ -48,9 +46,9 @@ public class Context
_commandMessageService = provider.Resolve<CommandMessageService>();
CommandPrefix = message.Content?.Substring(0, commandParseOffset);
DefaultPrefix = prefixes[0];
Parameters = new Parameters(message.Content?.Substring(commandParseOffset));
Rest = provider.Resolve<DiscordApiClient>();
Cluster = provider.Resolve<Cluster>();
Parameters = parameters;
}
public readonly IDiscordCache Cache;
@ -156,8 +154,6 @@ public class Context
public async Task Execute<T>(Command? commandDef, Func<T, Task> handler, bool deprecated = false)
{
_currentCommand = commandDef;
if (deprecated && commandDef != null)
{
await Reply($"{Emojis.Warn} Server configuration has moved to `{DefaultPrefix}serverconfig`. The command you are trying to run is now `{DefaultPrefix}{commandDef.Key}`.");
@ -196,10 +192,11 @@ public class Context
public LookupContext DirectLookupContextFor(SystemId systemId)
=> System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner;
public LookupContext LookupContextFor(SystemId systemId)
public LookupContext LookupContextFor(SystemId systemId, bool? _hasPrivateOverride = null, bool? _hasPublicOverride = null)
{
var hasPrivateOverride = this.MatchFlag("private", "priv");
var hasPublicOverride = this.MatchFlag("public", "pub");
// todo(dusk): these should be passed as a parameter ideally
bool hasPrivateOverride = _hasPrivateOverride ?? Parameters.HasFlag("private", "priv");
bool hasPublicOverride = _hasPublicOverride ?? Parameters.HasFlag("public", "pub");
if (hasPrivateOverride && hasPublicOverride)
throw new PKError("Cannot match both public and private flags at the same time.");

View file

@ -2,201 +2,30 @@ using System.Text.RegularExpressions;
using Myriad.Types;
using PluralKit.Core;
namespace PluralKit.Bot;
public static class ContextArgumentsExt
{
public static string PopArgument(this Context ctx) =>
ctx.Parameters.Pop();
public static string PeekArgument(this Context ctx) =>
ctx.Parameters.Peek();
public static string RemainderOrNull(this Context ctx, bool skipFlags = true) =>
ctx.Parameters.Remainder(skipFlags).Length == 0 ? null : ctx.Parameters.Remainder(skipFlags);
public static bool HasNext(this Context ctx, bool skipFlags = true) =>
ctx.RemainderOrNull(skipFlags) != null;
public static string FullCommand(this Context ctx) =>
ctx.Parameters.FullCommand;
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords and pops it from the stack. Case-insensitive.
/// </summary>
public static bool Match(this Context ctx, ref string used, params string[] potentialMatches)
{
var arg = ctx.PeekArgument();
foreach (var match in potentialMatches)
if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase))
{
used = ctx.PopArgument();
return true;
}
return false;
}
/// <summary>
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
/// </summary>
public static bool Match(this Context ctx, params string[] potentialMatches)
{
string used = null; // Unused and unreturned, we just yeet it
return ctx.Match(ref used, potentialMatches);
}
/// <summary>
/// Checks if the next parameter (starting from `ptr`) is equal to one of the given keywords, and leaves it on the stack. Case-insensitive.
/// </summary>
public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches)
{
var arg = ctx.Parameters.PeekWithPtr(ref ptr);
foreach (var match in potentialMatches)
if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase))
return true;
return false;
}
/// <summary>
/// Matches the next *n* parameters against each parameter consecutively.
/// <br />
/// Note that this is handled differently than single-parameter Match:
/// each method parameter is an array of potential matches for the *n*th command string parameter.
/// </summary>
public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches)
{
int ptr = ctx.Parameters._ptr;
foreach (var param in potentialParametersMatches)
if (!ctx.PeekMatch(ref ptr, param)) return false;
ctx.Parameters._ptr = ptr;
return true;
}
public static bool MatchFlag(this Context ctx, params string[] potentialMatches)
{
// Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here.
// Can assume the caller array only contains lowercase *and* the set below only contains lowercase
var flags = ctx.Parameters.Flags();
return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch));
}
public static bool MatchClear(this Context ctx)
=> ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear");
public static ReplyFormat MatchFormat(this Context ctx)
{
if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
return ReplyFormat.Standard;
}
public static ReplyFormat PeekMatchFormat(this Context ctx)
{
int ptr1 = ctx.Parameters._ptr;
int ptr2 = ctx.Parameters._ptr;
if (ctx.PeekMatch(ref ptr1, new[] { "r", "raw" }) || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
if (ctx.PeekMatch(ref ptr2, new[] { "pt", "plaintext" }) || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
return ReplyFormat.Standard;
}
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
{
var value = ctx.MatchToggleOrNull(defaultValue);
if (value == null) throw new PKError("You must pass either \"on\" or \"off\" to this command.");
return value.Value;
}
public static bool? MatchToggleOrNull(this Context ctx, bool? defaultValue = null)
{
if (defaultValue != null && ctx.MatchClear())
return defaultValue.Value;
var yesToggles = new[] { "yes", "on", "enable", "enabled", "true" };
var noToggles = new[] { "no", "off", "disable", "disabled", "false" };
if (ctx.Match(yesToggles) || ctx.MatchFlag(yesToggles))
return true;
else if (ctx.Match(noToggles) || ctx.MatchFlag(noToggles))
return false;
else return null;
}
public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId)
public static Message.Reference? GetRepliedTo(this Context ctx)
{
if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null)
return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId);
return ctx.Message.MessageReference;
return null;
}
var word = ctx.PeekArgument();
if (word == null)
return (null, null);
if (parseRawMessageId && ulong.TryParse(word, out var mid))
public static (ulong? messageId, ulong? channelId) ParseMessage(this Context ctx, string maybeMessageRef, bool parseRawMessageId)
{
if (parseRawMessageId && ulong.TryParse(maybeMessageRef, out var mid))
return (mid, null);
var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
var match = Regex.Match(maybeMessageRef, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)");
if (!match.Success)
return (null, null);
var channelId = ulong.Parse(match.Groups[1].Value);
var messageId = ulong.Parse(match.Groups[2].Value);
ctx.PopArgument();
return (messageId, channelId);
}
public static async Task<List<PKMember>> ParseMemberList(this Context ctx, SystemId? restrictToSystem)
{
var members = new List<PKMember>();
// Loop through all the given arguments
while (ctx.HasNext())
{
// and attempt to match a member
var member = await ctx.MatchMember(restrictToSystem);
if (member == null)
// if we can't, big error. Every member name must be valid.
throw new PKError(ctx.CreateNotFoundError("Member", ctx.PopArgument()));
members.Add(member); // Then add to the final output list
}
if (members.Count == 0) throw new PKSyntaxError("You must input at least one member.");
return members;
}
public static async Task<List<PKGroup>> ParseGroupList(this Context ctx, SystemId? restrictToSystem)
{
var groups = new List<PKGroup>();
// Loop through all the given arguments
while (ctx.HasNext())
{
// and attempt to match a group
var group = await ctx.MatchGroup(restrictToSystem);
if (group == null)
// if we can't, big error. Every group name must be valid.
throw new PKError(ctx.CreateNotFoundError("Group", ctx.PopArgument()));
// todo: remove this, the database query enforces the restriction
if (restrictToSystem != null && group.System != restrictToSystem)
throw Errors.NotOwnGroupError; // TODO: name *which* group?
groups.Add(group); // Then add to the final output list
}
if (groups.Count == 0) throw new PKSyntaxError("You must input at least one group.");
return groups;
}
}
public enum ReplyFormat

View file

@ -6,34 +6,8 @@ namespace PluralKit.Bot;
public static class ContextAvatarExt
{
public static async Task<ParsedImage?> MatchImage(this Context ctx)
public static ParsedImage? ExtractImageFromAttachment(this Context ctx)
{
// If we have a user @mention/ID, use their avatar
if (await ctx.MatchUser() is { } user)
{
var url = user.AvatarUrl("png", 256);
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
}
// If we have raw or plaintext, don't try to parse as a URL
if (ctx.PeekMatchFormat() != ReplyFormat.Standard)
return null;
// If we have a positional argument, try to parse it as a URL
var arg = ctx.RemainderOrNull();
if (arg != null)
{
// Allow surrounding the URL with <angle brackets> to "de-embed"
if (arg.StartsWith("<") && arg.EndsWith(">"))
arg = arg.Substring(1, arg.Length - 2);
if (!Core.MiscUtils.TryMatchUri(arg, out var uri))
throw Errors.InvalidUrl;
// ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't
return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url };
}
// If we have an attachment, use that
if (ctx.Message.Attachments.FirstOrDefault() is { } attachment)
{
@ -51,6 +25,29 @@ public static class ContextAvatarExt
// and if there are no attachments (which would have been caught just before)
return null;
}
public static async Task<ParsedImage?> GetUserPfp(this Context ctx, string arg)
{
// If we have a user @mention/ID, use their avatar
if (await ctx.ParseUser(arg) is { } user)
{
var url = user.AvatarUrl("png", 256);
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
}
return null;
}
public static ParsedImage ParseImage(this Context ctx, string arg)
{
// Allow surrounding the URL with <angle brackets> to "de-embed"
if (arg.StartsWith("<") && arg.EndsWith(">"))
arg = arg.Substring(1, arg.Length - 2);
if (!Core.MiscUtils.TryMatchUri(arg, out var uri))
throw Errors.InvalidUrl;
// ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't
return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url };
}
}
public struct ParsedImage

View file

@ -1,52 +1,22 @@
using System.Text.RegularExpressions;
using Myriad.Extensions;
using Myriad.Types;
using PluralKit.Bot.Utils;
using PluralKit.Core;
namespace PluralKit.Bot;
public static class ContextEntityArgumentsExt
{
public static async Task<User> MatchUser(this Context ctx)
public static async Task<User> ParseUser(this Context ctx, string arg)
{
var text = ctx.PeekArgument();
if (text.TryParseMention(out var id))
{
var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
if (user != null) ctx.PopArgument();
return user;
}
if (arg.TryParseMention(out var id))
return await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
return null;
}
public static bool MatchUserRaw(this Context ctx, out ulong id)
public static async Task<PKSystem> ParseSystem(this Context ctx, string input)
{
id = 0;
var text = ctx.PeekArgument();
if (text.TryParseMention(out var mentionId))
id = mentionId;
return id != 0;
}
public static Task<PKSystem> PeekSystem(this Context ctx) => ctx.MatchSystemInner();
public static async Task<PKSystem> MatchSystem(this Context ctx)
{
var system = await ctx.MatchSystemInner();
if (system != null) ctx.PopArgument();
return system;
}
private static async Task<PKSystem> MatchSystemInner(this Context ctx)
{
var input = ctx.PeekArgument();
// System references can take three forms:
// - The direct user ID of an account connected to the system
// - A @mention of an account connected to the system (<@uid>)
@ -63,10 +33,8 @@ public static class ContextEntityArgumentsExt
return null;
}
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
public static async Task<PKMember?> ParseMember(this Context ctx, string input, bool byId)
{
var input = ctx.PeekArgument();
// Member references can have one of three forms, depending on
// whether you're in a system or not:
// - A member hid
@ -75,7 +43,7 @@ public static class ContextEntityArgumentsExt
// Skip name / display name matching if the user does not have a system
// or if they specifically request by-HID matching
if (ctx.System != null && !ctx.MatchFlag("id", "by-id"))
if (ctx.System != null && !byId)
{
// First, try finding by member name in system
if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName)
@ -98,55 +66,25 @@ public static class ContextEntityArgumentsExt
// If we are supposed to restrict it to a system anyway we can just do that
PKMember memberByHid = null;
if (restrictToSystem != null)
{
memberByHid = await ctx.Repository.GetMemberByHid(hid, restrictToSystem);
if (memberByHid != null)
return memberByHid;
}
// otherwise we try the querier's system and if that doesn't work we do global
else
{
memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id);
if (memberByHid != null)
return memberByHid;
memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id);
if (memberByHid != null)
return memberByHid;
// ff ctx.System was null then this would be a duplicate of above and we don't want to run it again
if (ctx.System != null)
{
memberByHid = await ctx.Repository.GetMemberByHid(hid);
if (memberByHid != null)
return memberByHid;
}
// ff ctx.System was null then this would be a duplicate of above and we don't want to run it again
if (ctx.System != null)
{
memberByHid = await ctx.Repository.GetMemberByHid(hid);
if (memberByHid != null)
return memberByHid;
}
// We didn't find anything, so we return null.
return null;
}
/// <summary>
/// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be
/// resolved by the next word in the argument stack, does *not* touch the stack, and returns null.
/// </summary>
public static async Task<PKMember> MatchMember(this Context ctx, SystemId? restrictToSystem = null)
public static async Task<PKGroup> ParseGroup(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null)
{
// First, peek a member
var member = await ctx.PeekMember(restrictToSystem);
// If the peek was successful, we've used up the next argument, so we pop that just to get rid of it.
if (member != null) ctx.PopArgument();
// Finally, we return the member value.
return member;
}
public static async Task<PKGroup> PeekGroup(this Context ctx, SystemId? restrictToSystem = null)
{
var input = ctx.PeekArgument();
// see PeekMember for an explanation of the logic used here
if (ctx.System != null && !ctx.MatchFlag("id", "by-id"))
if (ctx.System != null && !byId)
{
if (await ctx.Repository.GetGroupByName(ctx.System.Id, input) is { } byName)
return byName;
@ -163,16 +101,9 @@ public static class ContextEntityArgumentsExt
return null;
}
public static async Task<PKGroup> MatchGroup(this Context ctx, SystemId? restrictToSystem = null)
public static string CreateNotFoundError(this Context ctx, string entity, string input, bool byId = false)
{
var group = await ctx.PeekGroup(restrictToSystem);
if (group != null) ctx.PopArgument();
return group;
}
public static string CreateNotFoundError(this Context ctx, string entity, string input)
{
var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id");
var isIDOnlyQuery = ctx.System == null || byId;
var inputIsHid = HidUtils.ParseHid(input) != null;
if (isIDOnlyQuery)
@ -186,35 +117,4 @@ public static class ContextEntityArgumentsExt
return $"{entity} with ID or name \"{input}\" not found.";
return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long.";
}
public static async Task<Channel> MatchChannel(this Context ctx)
{
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
return null;
// todo: match channels in other guilds
var channel = await ctx.Cache.TryGetChannel(ctx.Guild!.Id, id);
if (channel == null)
channel = await ctx.Rest.GetChannelOrNull(id);
if (channel == null)
return null;
if (!DiscordUtils.IsValidGuildChannel(channel))
return null;
ctx.PopArgument();
return channel;
}
public static async Task<Guild> MatchGuild(this Context ctx)
{
if (!ulong.TryParse(ctx.PeekArgument(), out var id))
return null;
var guild = await ctx.Rest.GetGuildOrNull(id);
if (guild != null)
ctx.PopArgument();
return guild;
}
}

View file

@ -0,0 +1,54 @@
using PluralKit.Core;
namespace PluralKit.Bot;
public static class ContextFlagsExt
{
public static async Task<string?> FlagResolveOpaque(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.Opaque)?.value
);
}
public static async Task<PKMember?> FlagResolveMember(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.MemberRef)?.member
);
}
public static async Task<PKSystem?> FlagResolveSystem(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.SystemRef)?.system
);
}
public static async Task<MemberPrivacySubject?> FlagResolveMemberPrivacyTarget(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.MemberPrivacyTarget)?.target
);
}
public static async Task<PrivacyLevel?> FlagResolvePrivacyLevel(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.PrivacyLevel)?.level
);
}
public static async Task<bool?> FlagResolveToggle(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveFlag(
ctx, param_name,
param => (param as Parameter.Toggle)?.value
);
}
}

View file

@ -0,0 +1,151 @@
using PluralKit.Core;
using Myriad.Types;
namespace PluralKit.Bot;
public static class ContextParametersExt
{
public static async Task<string?> ParamResolveOpaque(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.Opaque)?.value
);
}
public static async Task<int?> ParamResolveNumber(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.Number)?.value
);
}
public static async Task<PKMember?> ParamResolveMember(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.MemberRef)?.member
);
}
public static async Task<List<PKMember>?> ParamResolveMembers(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.MemberRefs)?.members
);
}
public static async Task<PKGroup?> ParamResolveGroup(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.GroupRef)?.group
);
}
public static async Task<List<PKGroup>?> ParamResolveGroups(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.GroupRefs)?.groups
);
}
public static async Task<PKSystem?> ParamResolveSystem(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.SystemRef)?.system
);
}
public static async Task<User?> ParamResolveUser(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.UserRef)?.user
);
}
public static async Task<MemberPrivacySubject?> ParamResolveMemberPrivacyTarget(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.MemberPrivacyTarget)?.target
);
}
public static async Task<GroupPrivacySubject?> ParamResolveGroupPrivacyTarget(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.GroupPrivacyTarget)?.target
);
}
public static async Task<SystemPrivacySubject?> ParamResolveSystemPrivacyTarget(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.SystemPrivacyTarget)?.target
);
}
public static async Task<PrivacyLevel?> ParamResolvePrivacyLevel(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.PrivacyLevel)?.level
);
}
public static async Task<SystemConfig.ProxySwitchAction?> ParamResolveProxySwitchAction(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.ProxySwitchAction)?.action
);
}
public static async Task<bool?> ParamResolveToggle(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.Toggle)?.value
);
}
public static async Task<ParsedImage?> ParamResolveAvatar(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.Avatar)?.avatar
);
}
public static async Task<Myriad.Types.Message.Reference?> ParamResolveMessage(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.MessageRef)?.message
);
}
public static async Task<Myriad.Types.Channel?> ParamResolveChannel(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.ChannelRef)?.channel
);
}
public static async Task<Myriad.Types.Guild?> ParamResolveGuild(this Context ctx, string param_name)
{
return await ctx.Parameters.ResolveParameter(
ctx, param_name,
param => (param as Parameter.GuildRef)?.guild
);
}
}

View file

@ -1,51 +0,0 @@
using PluralKit.Core;
namespace PluralKit.Bot;
public static class ContextPrivacyExt
{
public static PrivacyLevel PopPrivacyLevel(this Context ctx)
{
if (ctx.Match("public", "pub", "show", "shown", "visible", "unhide", "unhidden"))
return PrivacyLevel.Public;
if (ctx.Match("private", "priv", "hide", "hidden"))
return PrivacyLevel.Private;
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a privacy level (`public` or `private`)");
throw new PKSyntaxError(
$"Invalid privacy level {ctx.PopArgument().AsCode()} (must be `public` or `private`).");
}
public static SystemPrivacySubject PopSystemPrivacySubject(this Context ctx)
{
if (!SystemPrivacyUtils.TryParseSystemPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError(
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `members`, `front`, `fronthistory`, `groups`, or `all`).");
ctx.PopArgument();
return subject;
}
public static MemberPrivacySubject PopMemberPrivacySubject(this Context ctx)
{
if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError(
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxy`, `metadata`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
public static GroupPrivacySubject PopGroupPrivacySubject(this Context ctx)
{
if (!GroupPrivacyUtils.TryParseGroupPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError(
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `icon`, `metadata`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
}

View file

@ -1,185 +1,205 @@
using Humanizer;
using Myriad.Types;
using Myriad.Extensions;
using PluralKit.Core;
using uniffi.commands;
namespace PluralKit.Bot;
// corresponds to the ffi Paramater type, but with stricter types (also avoiding exposing ffi types!)
public abstract record Parameter()
{
public record MemberRef(PKMember member): Parameter;
public record MemberRefs(List<PKMember> members): Parameter;
public record GroupRef(PKGroup group): Parameter;
public record GroupRefs(List<PKGroup> groups): Parameter;
public record SystemRef(PKSystem system): Parameter;
public record UserRef(User user): Parameter;
public record MessageRef(Message.Reference message): Parameter;
public record ChannelRef(Channel channel): Parameter;
public record GuildRef(Guild guild): Parameter;
public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter;
public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter;
public record SystemPrivacyTarget(SystemPrivacySubject target): Parameter;
public record PrivacyLevel(Core.PrivacyLevel level): Parameter;
public record Toggle(bool value): Parameter;
public record Opaque(string value): Parameter;
public record Number(int value): Parameter;
public record Avatar(ParsedImage avatar): Parameter;
public record ProxySwitchAction(SystemConfig.ProxySwitchAction action): Parameter;
}
public class Parameters
{
// Dictionary of (left, right) quote pairs
// Each char in the string is an individual quote, multi-char strings imply "one of the following chars"
private static readonly Dictionary<string, string> _quotePairs = new()
private string _cb { get; init; }
private Dictionary<string, uniffi.commands.Parameter?> _flags { get; init; }
private Dictionary<string, uniffi.commands.Parameter> _params { get; init; }
// just used for errors, temporarily
public string FullCommand { get; init; }
public Parameters(string prefix, string cmd)
{
// Basic
{ "'", "'" }, // ASCII single quotes
{ "\"", "\"" }, // ASCII double quotes
// "Smart quotes"
// Specifically ignore the left/right status of the quotes and match any combination of them
// Left string also includes "low" quotes to allow for the low-high style used in some locales
{ "\u201C\u201D\u201F\u201E", "\u201C\u201D\u201F" }, // double quotes
{ "\u2018\u2019\u201B\u201A", "\u2018\u2019\u201B" }, // single quotes
// Chevrons (normal and "fullwidth" variants)
{ "\u00AB\u300A", "\u00BB\u300B" }, // double chevrons, pointing away (<<text>>)
{ "\u00BB\u300B", "\u00AB\u300A" }, // double chevrons, pointing together (>>text<<)
{ "\u2039\u3008", "\u203A\u3009" }, // single chevrons, pointing away (<text>)
{ "\u203A\u3009", "\u2039\u3008" }, // single chevrons, pointing together (>text<)
// Other
{ "\u300C\u300E", "\u300D\u300F" } // corner brackets (Japanese/Chinese)
};
private ISet<string> _flags; // Only parsed when requested first time
public int _ptr;
public string FullCommand { get; }
private struct WordPosition
{
// Start of the word
internal readonly int startPos;
// End of the word
internal readonly int endPos;
// How much to advance word pointer afterwards to point at the start of the *next* word
internal readonly int advanceAfterWord;
internal readonly bool wasQuoted;
public WordPosition(int startPos, int endPos, int advanceAfterWord, bool wasQuoted)
FullCommand = cmd;
var result = CommandsMethods.ParseCommand(prefix, cmd);
if (result is CommandResult.Ok)
{
this.startPos = startPos;
this.endPos = endPos;
this.advanceAfterWord = advanceAfterWord;
this.wasQuoted = wasQuoted;
var command = ((CommandResult.Ok)result).@command;
_cb = command.@commandRef;
_flags = command.@flags;
_params = command.@params;
}
else
{
throw new PKError(((CommandResult.Err)result).@error);
}
}
public Parameters(string cmd)
public static string GetRelatedCommands(string prefix, string subject)
{
// This is a SUPER dirty hack to avoid having to match both spaces and newlines in the word detection below
// Instead, we just add a space before every newline (which then gets stripped out later).
FullCommand = cmd.Replace("\n", " \n");
_ptr = 0;
return CommandsMethods.GetRelatedCommands(prefix, subject);
}
private void ParseFlags()
public string Callback()
{
_flags = new HashSet<string>();
return _cb;
}
var ptr = 0;
while (NextWordPosition(ptr) is { } wp)
public bool HasFlag(params string[] potentialMatches)
{
return potentialMatches.Any(_flags.ContainsKey);
}
private async Task<Parameter?> ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param)
{
var byId = HasFlag("id", "by-id"); // this is added as a hidden flag to all command definitions
switch (ffi_param)
{
ptr = wp.endPos + wp.advanceAfterWord;
case uniffi.commands.Parameter.MemberRef memberRef:
return new Parameter.MemberRef(
await ctx.ParseMember(memberRef.member, byId)
?? throw new PKError(ctx.CreateNotFoundError("Member", memberRef.member, byId))
);
case uniffi.commands.Parameter.MemberRefs memberRefs:
return new Parameter.MemberRefs(
await memberRefs.members.ToAsyncEnumerable().SelectAwait(async m =>
await ctx.ParseMember(m, byId)
?? throw new PKError(ctx.CreateNotFoundError("Member", m, byId))
).ToListAsync()
);
case uniffi.commands.Parameter.GroupRef groupRef:
return new Parameter.GroupRef(
await ctx.ParseGroup(groupRef.group, byId)
?? throw new PKError(ctx.CreateNotFoundError("Group", groupRef.group))
);
case uniffi.commands.Parameter.GroupRefs groupRefs:
return new Parameter.GroupRefs(
await groupRefs.groups.ToAsyncEnumerable().SelectAwait(async g =>
await ctx.ParseGroup(g, byId)
?? throw new PKError(ctx.CreateNotFoundError("Group", g, byId))
).ToListAsync()
);
case uniffi.commands.Parameter.SystemRef systemRef:
// todo: do we need byId here?
return new Parameter.SystemRef(
await ctx.ParseSystem(systemRef.system)
?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system))
);
case uniffi.commands.Parameter.UserRef(var userId):
return new Parameter.UserRef(
await ctx.Cache.GetOrFetchUser(ctx.Rest, userId)
?? throw new PKError(ctx.CreateNotFoundError("User", userId.ToString()))
);
// todo(dusk): ideally generate enums for these from rust code in the cs glue
case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget:
// this should never really fail...
if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var memberPrivacy))
throw new PKError($"Invalid member privacy target {memberPrivacyTarget.target}");
return new Parameter.MemberPrivacyTarget(memberPrivacy);
case uniffi.commands.Parameter.GroupPrivacyTarget groupPrivacyTarget:
// this should never really fail...
if (!GroupPrivacyUtils.TryParseGroupPrivacy(groupPrivacyTarget.target, out var groupPrivacy))
throw new PKError($"Invalid group privacy target {groupPrivacyTarget.target}");
return new Parameter.GroupPrivacyTarget(groupPrivacy);
case uniffi.commands.Parameter.SystemPrivacyTarget systemPrivacyTarget:
// this should never really fail...
if (!SystemPrivacyUtils.TryParseSystemPrivacy(systemPrivacyTarget.target, out var systemPrivacy))
throw new PKError($"Invalid system privacy target {systemPrivacyTarget.target}");
return new Parameter.SystemPrivacyTarget(systemPrivacy);
case uniffi.commands.Parameter.PrivacyLevel privacyLevel:
return new Parameter.PrivacyLevel(privacyLevel.level == "public" ? PrivacyLevel.Public : privacyLevel.level == "private" ? PrivacyLevel.Private : throw new PKError($"Invalid privacy level {privacyLevel.level}"));
case uniffi.commands.Parameter.ProxySwitchAction(var action):
SystemConfig.ProxySwitchAction newVal;
// Is this word a *flag* (as in, starts with a - AND is not quoted)
if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word)
if (action.Equals("off", StringComparison.InvariantCultureIgnoreCase))
newVal = SystemConfig.ProxySwitchAction.Off;
else if (action.Equals("new", StringComparison.InvariantCultureIgnoreCase) || action.Equals("n", StringComparison.InvariantCultureIgnoreCase) || action.Equals("on", StringComparison.InvariantCultureIgnoreCase))
newVal = SystemConfig.ProxySwitchAction.New;
else if (action.Equals("add", StringComparison.InvariantCultureIgnoreCase) || action.Equals("a", StringComparison.InvariantCultureIgnoreCase))
newVal = SystemConfig.ProxySwitchAction.Add;
else
throw new PKError("You must pass either \"new\", \"add\", or \"off\" to this command.");
// Find the *end* of the flag start (technically allowing arbitrary amounts of dashes)
var flagNameStart = wp.startPos;
while (flagNameStart < FullCommand.Length && FullCommand[flagNameStart] == '-')
flagNameStart++;
// Then add the word to the flag set
var word = FullCommand.Substring(flagNameStart, wp.endPos - flagNameStart).Trim();
if (word.Length > 0)
_flags.Add(word.ToLowerInvariant());
return new Parameter.ProxySwitchAction(newVal);
case uniffi.commands.Parameter.Toggle toggle:
return new Parameter.Toggle(toggle.toggle);
case uniffi.commands.Parameter.OpaqueString opaque:
return new Parameter.Opaque(opaque.raw);
case uniffi.commands.Parameter.OpaqueInt number:
return new Parameter.Number(number.raw);
case uniffi.commands.Parameter.Avatar avatar:
return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar));
case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId):
return new Parameter.MessageRef(new Message.Reference(guildId, channelId, messageId));
case uniffi.commands.Parameter.ChannelRef(var channelId):
return new Parameter.ChannelRef(await ctx.Rest.GetChannelOrNull(channelId) ?? throw new PKError($"Channel {channelId} not found"));
case uniffi.commands.Parameter.GuildRef(var guildId):
return new Parameter.GuildRef(await ctx.Rest.GetGuildOrNull(guildId) ?? throw new PKError($"Guild {guildId} not found"));
case uniffi.commands.Parameter.Null:
return null;
}
return null;
}
public string Pop()
// resolves a single flag with value
private async Task<Parameter?> ResolveFlag(Context ctx, string flag_name)
{
// Loop to ignore and skip past flags
while (NextWordPosition(_ptr) is { } pos)
{
_ptr = pos.endPos + pos.advanceAfterWord;
if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue;
return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim();
}
return "";
if (!HasFlag(flag_name)) return null;
var flag_value = _flags[flag_name];
if (flag_value == null) return null;
var resolved = await ResolveFfiParam(ctx, flag_value);
if (resolved != null) return resolved;
// this should never happen, types are handled rust side
return null;
}
public string Peek()
// resolves a single parameter
private async Task<Parameter?> ResolveParameter(Context ctx, string param_name)
{
// temp ptr so we don't move the real ptr
int ptr = _ptr;
return PeekWithPtr(ref ptr);
if (!_params.ContainsKey(param_name)) return null;
var resolved = await ResolveFfiParam(ctx, _params[param_name]);
if (resolved != null) return resolved;
// this should never happen, types are handled rust side
return null;
}
public string PeekWithPtr(ref int ptr)
public async Task<T?> ResolveFlag<T>(Context ctx, string flag_name, Func<Parameter, T?> extract_func)
{
// Loop to ignore and skip past flags
while (NextWordPosition(ptr) is { } pos)
{
ptr = pos.endPos + pos.advanceAfterWord;
if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue;
return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim();
}
return "";
var param = await ResolveFlag(ctx, flag_name);
// todo: i think this should return null for everything...?
if (param == null) return default;
return extract_func(param)
// this should never happen unless codegen somehow uses a wrong name
?? throw new PKError($"Flag {flag_name.AsCode()} was not found or did not have a value defined for command {Callback().AsCode()} -- this is a bug!!");
}
public ISet<string> Flags()
public async Task<T> ResolveParameter<T>(Context ctx, string param_name, Func<Parameter, T?> extract_func)
{
if (_flags == null) ParseFlags();
return _flags;
}
public string Remainder(bool skipFlags = true)
{
if (skipFlags)
// Skip all *leading* flags when taking the remainder
while (NextWordPosition(_ptr) is { } wp)
{
if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) break;
_ptr = wp.endPos + wp.advanceAfterWord;
}
// *Then* get the remainder
return FullCommand.Substring(Math.Min(_ptr, FullCommand.Length)).Trim();
}
private WordPosition? NextWordPosition(int position)
{
// Skip leading spaces before actual content
while (position < FullCommand.Length && FullCommand[position] == ' ') position++;
// Is this the end of the string?
if (FullCommand.Length <= position) return null;
// Is this a quoted word?
if (TryCheckQuote(FullCommand[position], out var endQuotes))
{
// We found a quoted word - find an instance of one of the corresponding end quotes
var endQuotePosition = -1;
for (var i = position + 1; i < FullCommand.Length; i++)
if (endQuotePosition == -1 && endQuotes.Contains(FullCommand[i]))
endQuotePosition = i; // need a break; don't feel like brackets tho lol
// Position after the end quote should be EOL or a space
// Otherwise we fallthrough to the unquoted word handler below
if (FullCommand.Length == endQuotePosition + 1 || FullCommand[endQuotePosition + 1] == ' ')
return new WordPosition(position + 1, endQuotePosition, 2, true);
}
// Not a quoted word, just find the next space and return if it's the end of the command
var wordEnd = FullCommand.IndexOf(' ', position + 1);
return wordEnd == -1
? new WordPosition(position, FullCommand.Length, 0, false)
: new WordPosition(position, wordEnd, 1, false);
}
private bool TryCheckQuote(char potentialLeftQuote, out string correspondingRightQuotes)
{
foreach (var (left, right) in _quotePairs)
if (left.Contains(potentialLeftQuote))
{
correspondingRightQuotes = right;
return true;
}
correspondingRightQuotes = null;
return false;
var param = await ResolveParameter(ctx, param_name);
// todo: i think this should return null for everything...?
if (param == null) return default;
return extract_func(param)
// this should never happen unless codegen somehow uses a wrong name
?? throw new PKError($"Parameter {param_name.AsCode()} was not found for command {Callback().AsCode()} -- this is a bug!!");
}
}

View file

@ -1,5 +1,3 @@
using System.Text.RegularExpressions;
using Humanizer;
using Dapper;
using SqlKata;
@ -113,43 +111,27 @@ public class Admin
return eb.Build();
}
public async Task UpdateSystemId(Context ctx)
public async Task UpdateSystemId(Context ctx, PKSystem target, string newHid, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
var input = ctx.PopArgument();
if (!input.TryParseHid(out var newHid))
throw new PKError($"Invalid new system ID `{input}`.");
var existingSystem = await ctx.Repository.GetSystemByHid(newHid);
if (existingSystem != null)
throw new PKError($"Another system already exists with ID `{newHid}`.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change", flagValue: confirmYes))
throw new PKError("ID change cancelled.");
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Hid = newHid });
await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task UpdateMemberId(Context ctx)
public async Task UpdateMemberId(Context ctx, PKMember target, string newHid, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchMember();
if (target == null)
throw new PKError("Unknown member.");
var input = ctx.PopArgument();
if (!input.TryParseHid(out var newHid))
throw new PKError($"Invalid new member ID `{input}`.");
var existingMember = await ctx.Repository.GetMemberByHid(newHid);
if (existingMember != null)
throw new PKError($"Another member already exists with ID `{newHid}`.");
@ -159,7 +141,7 @@ public class Admin
if (!await ctx.PromptYesNo(
$"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?",
"Change"
"Change", flagValue: confirmYes
))
throw new PKError("ID change cancelled.");
@ -167,18 +149,10 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task UpdateGroupId(Context ctx)
public async Task UpdateGroupId(Context ctx, PKGroup target, string newHid, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchGroup();
if (target == null)
throw new PKError("Unknown group.");
var input = ctx.PopArgument();
if (!input.TryParseHid(out var newHid))
throw new PKError($"Invalid new group ID `{input}`.");
var existingGroup = await ctx.Repository.GetGroupByHid(newHid);
if (existingGroup != null)
throw new PKError($"Another group already exists with ID `{newHid}`.");
@ -187,7 +161,7 @@ public class Admin
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?",
"Change"
"Change", flagValue: confirmYes
))
throw new PKError("ID change cancelled.");
@ -195,17 +169,13 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollSystemId(Context ctx)
public async Task RerollSystemId(Context ctx, PKSystem target, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll"))
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll", flagValue: confirmYes))
throw new PKError("ID change cancelled.");
var query = new Query("systems").AsUpdate(new
@ -218,20 +188,16 @@ public class Admin
await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollMemberId(Context ctx)
public async Task RerollMemberId(Context ctx, PKMember target, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchMember();
if (target == null)
throw new PKError("Unknown member.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo(
$"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?",
"Reroll"
"Reroll", flagValue: confirmYes
))
throw new PKError("ID change cancelled.");
@ -245,19 +211,15 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task RerollGroupId(Context ctx)
public async Task RerollGroupId(Context ctx, PKGroup target, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchGroup();
if (target == null)
throw new PKError("Unknown group.");
var system = await ctx.Repository.GetSystem(target.System);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?",
"Change"
"Change", flagValue: confirmYes
))
throw new PKError("ID change cancelled.");
@ -271,71 +233,52 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task SystemMemberLimit(Context ctx)
public async Task SystemMemberLimit(Context ctx, PKSystem target, int? newLimit, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
var config = await ctx.Repository.GetSystemConfig(target.Id);
var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
if (!ctx.HasNext())
if (newLimit == null)
{
await ctx.Reply(null, await CreateEmbed(ctx, target));
return;
}
var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000");
if (!int.TryParse(newLimitStr, out var newLimit))
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update", flagValue: confirmYes))
throw new PKError("Member limit change cancelled.");
await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { MemberLimitOverride = newLimit });
await ctx.Reply($"{Emojis.Success} Member limit updated.");
}
public async Task SystemGroupLimit(Context ctx)
public async Task SystemGroupLimit(Context ctx, PKSystem target, int? newLimit, bool confirmYes)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
var config = await ctx.Repository.GetSystemConfig(target.Id);
var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
if (!ctx.HasNext())
if (newLimit == null)
{
await ctx.Reply(null, await CreateEmbed(ctx, target));
return;
}
var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000");
if (!int.TryParse(newLimitStr, out var newLimit))
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
await ctx.Reply(null, await CreateEmbed(ctx, target));
if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update"))
if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update", flagValue: confirmYes))
throw new PKError("Group limit change cancelled.");
await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { GroupLimitOverride = newLimit });
await ctx.Reply($"{Emojis.Success} Group limit updated.");
}
public async Task SystemRecover(Context ctx)
public async Task SystemRecover(Context ctx, string systemToken, User account, bool rerollToken, bool confirmYes)
{
ctx.AssertBotAdmin();
var rerollToken = ctx.MatchFlag("rt", "reroll-token");
var systemToken = ctx.PopArgument();
var systemId = await ctx.Database.Execute(conn => conn.QuerySingleOrDefaultAsync<SystemId?>(
"select id from systems where token = @token",
new { token = systemToken }
@ -344,10 +287,6 @@ public class Admin
if (systemId == null)
throw new PKError("Could not retrieve a system with that token.");
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to associate the system with (either ID or @mention).");
var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id);
if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config, ctx.DefaultPrefix);
@ -355,7 +294,7 @@ public class Admin
var system = await ctx.Repository.GetSystem(systemId.Value!);
await ctx.Reply(null, await CreateEmbed(ctx, system));
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account"))
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account", flagValue: confirmYes))
throw new PKError("System recovery cancelled.");
await ctx.Repository.AddAccount(system.Id, account.Id);
@ -378,14 +317,10 @@ public class Admin
});
}
public async Task SystemDelete(Context ctx)
public async Task SystemDelete(Context ctx, PKSystem target)
{
ctx.AssertBotAdmin();
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`",
await CreateEmbed(ctx, target));
if (!await ctx.ConfirmWithReply(target.Uuid.ToString()))
@ -396,18 +331,11 @@ public class Admin
await ctx.Reply($"{Emojis.Success} System deletion succesful.");
}
public async Task AbuseLogCreate(Context ctx)
public async Task AbuseLogCreate(Context ctx, User account, bool denyBotUsage, string? description)
{
var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage");
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
ctx.AssertBotAdmin();
string? desc = null!;
if (ctx.HasNext(false))
desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage);
var abuseLog = await ctx.Repository.CreateAbuseLog(description, denyBotUsage);
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
await ctx.Reply(
@ -415,14 +343,49 @@ public class Admin
await CreateAbuseLogEmbed(ctx, abuseLog));
}
public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog)
public async Task<AbuseLog?> GetAbuseLog(Context ctx, User? account, string? id)
{
ctx.AssertBotAdmin();
AbuseLog? abuseLog = null!;
if (account != null)
{
abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id);
}
else
{
abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(id));
}
if (abuseLog == null)
{
await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query.");
return null;
}
return abuseLog;
}
public async Task AbuseLogShow(Context ctx, User? account, string? id)
{
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id);
if (abuseLog == null)
return;
await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog));
}
public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog)
public async Task AbuseLogFlagDeny(Context ctx, User? account, string? id, bool? value)
{
if (!ctx.HasNext())
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id);
if (abuseLog == null)
return;
if (value == null)
{
await ctx.Reply(
$"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} "
@ -430,27 +393,31 @@ public class Admin
}
else
{
var value = ctx.MatchToggle(true);
if (abuseLog.DenyBotUsage != value)
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value });
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value.Value });
await ctx.Reply(
$"Bot usage is now **{(value ? "denied" : "allowed")}** "
$"Bot usage is now **{(value.Value ? "denied" : "allowed")}** "
+ $"for accounts associated with abuse log `{abuseLog.Uuid}`.");
}
}
public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog)
public async Task AbuseLogDescription(Context ctx, User? account, string? id, string? description, bool clear, bool confirmClear)
{
if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description"))
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id);
if (abuseLog == null)
return;
if (clear && await ctx.ConfirmClear("this abuse log description", confirmClear))
{
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null });
await ctx.Reply($"{Emojis.Success} Abuse log description cleared.");
}
else if (ctx.HasNext())
else if (description != null)
{
var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc });
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = description });
await ctx.Reply($"{Emojis.Success} Abuse log description updated.");
}
else
@ -461,11 +428,13 @@ public class Admin
}
}
public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog)
public async Task AbuseLogAddUser(Context ctx, User? accountToFind, string? id, User account)
{
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id);
if (abuseLog == null)
return;
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
await ctx.Reply(
@ -473,11 +442,13 @@ public class Admin
await CreateAbuseLogEmbed(ctx, abuseLog));
}
public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog)
public async Task AbuseLogRemoveUser(Context ctx, User? accountToFind, string? id, User account)
{
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention).");
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id);
if (abuseLog == null)
return;
await ctx.Repository.UpdateAccount(account.Id, new()
{
@ -489,8 +460,14 @@ public class Admin
await CreateAbuseLogEmbed(ctx, abuseLog));
}
public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog)
public async Task AbuseLogDelete(Context ctx, User? account, string? id)
{
ctx.AssertBotAdmin();
AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id);
if (abuseLog == null)
return;
if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false))
{
await ctx.Reply($"{Emojis.Error} Deletion cancelled.");
@ -501,17 +478,10 @@ public class Admin
await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry.");
}
public async Task SendAdminMessage(Context ctx)
public async Task SendAdminMessage(Context ctx, User account, string content)
{
ctx.AssertBotAdmin();
var account = await ctx.MatchUser();
if (account == null)
throw new PKError("You must pass an account to send an admin message to (either ID or @mention).");
if (!ctx.HasNext())
throw new PKError("You must provide a message to send.");
var content = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
var messageContent = $"## [Admin Message]\n\n{content}\n\nWe cannot read replies sent to this DM. If you wish to contact the staff team, please join the support server (<https://discord.gg/PczBt78>) or send us an email at <legal@pluralkit.me>.";
try

View file

@ -115,28 +115,32 @@ public class Api
}
}
public async Task SystemWebhook(Context ctx)
public async Task GetSystemWebhook(Context ctx)
{
ctx.CheckSystem().CheckDMContext();
if (!ctx.HasNext(false))
{
if (ctx.System.WebhookUrl == null)
await ctx.Reply($"Your system does not have a webhook URL set. Set one with `{ctx.DefaultPrefix}system webhook <url>`!");
else
await ctx.Reply($"Your system's webhook URL is <{ctx.System.WebhookUrl}>.");
if (ctx.System.WebhookUrl == null)
await ctx.Reply($"Your system does not have a webhook URL set. Set one with `{ctx.DefaultPrefix}system webhook <url>`!");
else
await ctx.Reply($"Your system's webhook URL is <{ctx.System.WebhookUrl}>.");
}
public async Task ClearSystemWebhook(Context ctx, bool confirmYes)
{
ctx.CheckSystem().CheckDMContext();
if (!await ctx.ConfirmClear("your system's webhook URL", confirmYes))
return;
}
if (ctx.MatchClear() && await ctx.ConfirmClear("your system's webhook URL"))
{
await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = null, WebhookToken = null });
await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = null, WebhookToken = null });
await ctx.Reply($"{Emojis.Success} System webhook URL removed.");
return;
}
await ctx.Reply($"{Emojis.Success} System webhook URL removed.");
}
public async Task SetSystemWebhook(Context ctx, string newUrl)
{
ctx.CheckSystem().CheckDMContext();
var newUrl = ctx.RemainderOrNull();
if (!await DispatchExt.ValidateUri(newUrl))
throw new PKError($"The URL {newUrl.AsCode()} is invalid or I cannot access it. Are you sure this is a valid, publicly accessible URL?");

View file

@ -11,37 +11,51 @@ public class Autoproxy
{
private readonly IClock _clock;
public abstract record Mode()
{
public record Off(): Mode;
public record Latch(): Mode;
public record Front(): Mode;
public record Member(PKMember member): Mode;
}
public Autoproxy(IClock clock)
{
_clock = clock;
}
public async Task SetAutoproxyMode(Context ctx)
public async Task SetAutoproxyMode(Context ctx, Mode? mode = null)
{
// no need to check account here, it's already done at CommandTree
ctx.CheckGuildContext();
ctx.CheckSystem().CheckGuildContext();
// for now, just for guild
// this also creates settings if there are none present
var settings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, ctx.Guild.Id, null);
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove"))
await AutoproxyOff(ctx, settings);
else if (ctx.Match("latch", "last", "proxy", "stick", "sticky", "l"))
await AutoproxyLatch(ctx, settings);
else if (ctx.Match("front", "fronter", "switch", "f"))
await AutoproxyFront(ctx, settings);
else if (ctx.Match("member"))
throw new PKSyntaxError($"Member-mode autoproxy must target a specific member. Use the `{ctx.DefaultPrefix}autoproxy <member>` command, where `member` is the name or ID of a member in your system.");
else if (await ctx.MatchMember() is PKMember member)
await AutoproxyMember(ctx, member);
else if (!ctx.HasNext())
await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings));
else
throw new PKSyntaxError($"Invalid autoproxy mode {ctx.PopArgument().AsCode()}.");
if (mode == null)
{
await AutoproxyShow(ctx, settings);
return;
}
switch (mode)
{
case Mode.Off:
await AutoproxyOff(ctx, settings);
break;
case Mode.Latch:
await AutoproxyLatch(ctx, settings);
break;
case Mode.Front:
await AutoproxyFront(ctx, settings);
break;
case Mode.Member(var member):
await AutoproxyMember(ctx, member);
break;
}
}
private async Task AutoproxyOff(Context ctx, AutoproxySettings settings)
public async Task AutoproxyOff(Context ctx, AutoproxySettings settings)
{
if (settings.AutoproxyMode == AutoproxyMode.Off)
{
@ -54,7 +68,7 @@ public class Autoproxy
}
}
private async Task AutoproxyLatch(Context ctx, AutoproxySettings settings)
public async Task AutoproxyLatch(Context ctx, AutoproxySettings settings)
{
if (settings.AutoproxyMode == AutoproxyMode.Latch)
{
@ -67,7 +81,7 @@ public class Autoproxy
}
}
private async Task AutoproxyFront(Context ctx, AutoproxySettings settings)
public async Task AutoproxyFront(Context ctx, AutoproxySettings settings)
{
if (settings.AutoproxyMode == AutoproxyMode.Front)
{
@ -80,7 +94,7 @@ public class Autoproxy
}
}
private async Task AutoproxyMember(Context ctx, PKMember member)
public async Task AutoproxyMember(Context ctx, PKMember member)
{
ctx.CheckOwnMember(member);
@ -90,6 +104,11 @@ public class Autoproxy
await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server.");
}
public async Task AutoproxyShow(Context ctx, AutoproxySettings settings)
{
await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings));
}
private async Task<Embed> CreateAutoproxyStatusEmbed(Context ctx, AutoproxySettings settings)
{
var commandList = $"**{ctx.DefaultPrefix}autoproxy latch** - Autoproxies as last-proxied member"

View file

@ -36,37 +36,11 @@ public class Checks
_cache = cache;
}
public async Task PermCheckGuild(Context ctx)
public async Task PermCheckGuild(Context ctx, Guild guild)
{
Guild guild;
GuildMemberPartial senderGuildUser = null;
if (ctx.Guild != null && !ctx.HasNext())
{
guild = ctx.Guild;
senderGuildUser = ctx.Member;
}
else
{
var guildIdStr = ctx.RemainderOrNull() ??
throw new PKSyntaxError("You must pass a server ID or run this command in a server.");
if (!ulong.TryParse(guildIdStr, out var guildId))
throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
try
{
guild = await _rest.GetGuild(guildId);
}
catch (ForbiddenException)
{
throw Errors.GuildNotFound(guildId);
}
if (guild != null)
senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id);
if (guild == null || senderGuildUser == null)
throw Errors.GuildNotFound(guildId);
}
var senderGuildUser = await _rest.GetGuildMember(guild.Id, ctx.Author.Id);
if (senderGuildUser == null)
throw Errors.GuildNotFound(guild.Id);
var guildMember = await _rest.GetGuildMember(guild.Id, _botConfig.ClientId);
@ -135,17 +109,13 @@ public class Checks
await ctx.Reply(embed: eb.Build());
}
public async Task PermCheckChannel(Context ctx)
public async Task PermCheckChannel(Context ctx, Channel channel)
{
if (!ctx.HasNext())
throw new PKSyntaxError("You need to specify a channel.");
var error = "Channel not found or you do not have permissions to access it.";
// todo: this breaks if channel is not in cache and bot does not have View Channel permissions
// with new cache it breaks if channel is not in current guild
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId == null)
if (channel.GuildId == null)
throw new PKError(error);
var guild = await _rest.GetGuildOrNull(channel.GuildId.Value);
@ -189,15 +159,16 @@ public class Checks
await ctx.Reply(embed: eb.Build());
}
public async Task MessageProxyCheck(Context ctx)
public async Task MessageProxyCheck(Context ctx, Message.Reference? messageReference)
{
if (!ctx.HasNext() && ctx.Message.MessageReference == null)
if (messageReference == null && ctx.Message.MessageReference == null)
throw new PKSyntaxError("You need to specify a message.");
var failedToGetMessage =
"Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you.";
var (messageId, channelId) = ctx.MatchMessage(false);
messageReference = ctx.GetRepliedTo();
var (messageId, channelId) = (messageReference?.MessageId, messageReference?.ChannelId);
if (messageId == null || channelId == null)
throw new PKError(failedToGetMessage);

View file

@ -197,17 +197,16 @@ public class Config
}
private string EnabledDisabled(bool value) => value ? "enabled" : "disabled";
public async Task AutoproxyAccount(Context ctx)
public async Task ViewAutoproxyAccount(Context ctx)
{
var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id);
if (!ctx.HasNext())
{
await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>.");
return;
}
await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>.");
}
var allow = ctx.MatchToggle(true);
public async Task EditAutoproxyAccount(Context ctx, bool allow)
{
var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id);
var statusString = EnabledDisabled(allow);
if (allowAutoproxy == allow)
@ -220,80 +219,87 @@ public class Config
await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.");
}
public async Task AutoproxyTimeout(Context ctx)
public async Task ViewAutoproxyTimeout(Context ctx)
{
if (!ctx.HasNext())
{
var timeout = ctx.Config.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.Config.LatchTimeout.Value)
: (Duration?)null;
var timeout = ctx.Config.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.Config.LatchTimeout.Value)
: (Duration?)null;
if (timeout == null)
await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}.");
else if (timeout == Duration.Zero)
await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out.");
else
await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}.");
return;
}
Duration? newTimeout;
Duration overflow = Duration.Zero;
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeout = Duration.Zero;
else if (ctx.MatchClear()) newTimeout = null;
if (timeout == null)
await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}.");
else if (timeout == Duration.Zero)
await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out.");
else
await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}.");
}
public async Task DisableAutoproxyTimeout(Context ctx)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int)Duration.Zero.TotalSeconds });
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out.");
}
public async Task ResetAutoproxyTimeout(Context ctx)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = null });
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
}
public async Task EditAutoproxyTimeout(Context ctx, string timeout)
{
Duration newTimeout;
Duration overflow = Duration.Zero;
// todo: we should parse date in the command parser
var timeoutStr = timeout;
var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr)
?? throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes).");
if (timeoutPeriod.TotalHours > 100000)
{
var timeoutStr = ctx.RemainderOrNull();
var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr);
if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes).");
if (timeoutPeriod.Value.TotalHours > 100000)
{
// sanity check to prevent seconds overflow if someone types in 999999999
overflow = timeoutPeriod.Value;
newTimeout = Duration.Zero;
}
else newTimeout = timeoutPeriod;
// sanity check to prevent seconds overflow if someone types in 999999999
overflow = timeoutPeriod;
newTimeout = Duration.Zero;
}
else newTimeout = timeoutPeriod;
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout?.TotalSeconds });
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout.TotalSeconds });
if (newTimeout == null)
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
else if (newTimeout == Duration.Zero && overflow != Duration.Zero)
if (newTimeout == Duration.Zero && overflow != Duration.Zero)
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)");
else if (newTimeout == Duration.Zero)
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out.");
else
await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}.");
await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.ToTimeSpan().Humanize(4)}.");
}
public async Task SystemTimezone(Context ctx)
public async Task ViewSystemTimezone(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
if (ctx.MatchClear())
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = "UTC" });
await ctx.Reply(
$"Your current system time zone is set to **{ctx.Config.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.Config.Zone)}** in that time zone. To change your system time zone, type `{ctx.DefaultPrefix}config tz <zone>`.");
}
await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC).");
return;
}
public async Task ResetSystemTimezone(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
var zoneStr = ctx.RemainderOrNull();
if (zoneStr == null)
{
await ctx.Reply(
$"Your current system time zone is set to **{ctx.Config.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.Config.Zone)}** in that time zone. To change your system time zone, type `{ctx.DefaultPrefix}config tz <zone>`.");
return;
}
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = "UTC" });
await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC).");
}
public async Task EditSystemTimezone(Context ctx, string zoneStr, bool confirmYes = false)
{
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
var zone = await FindTimeZone(ctx, zoneStr);
if (zone == null) throw Errors.InvalidTimeZone(zoneStr);
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?";
if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled;
if (!await ctx.PromptYesNo(msg, "Change Timezone", flagValue: confirmYes)) throw Errors.TimezoneChangeCancelled;
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = zone.Id });
@ -360,27 +366,24 @@ public class Config
});
}
public async Task SystemPing(Context ctx)
public async Task ViewSystemPing(Context ctx)
{
// note: this is here because this is also used in `pk;system ping`, which does not CheckSystem
ctx.CheckSystem();
// todo: move all the other config settings to this format
await ctx.Reply($"Reaction pings are currently **{EnabledDisabled(ctx.Config.PingsEnabled)}** for your system. " +
$"To {EnabledDisabled(!ctx.Config.PingsEnabled)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!ctx.Config.PingsEnabled)[..^1]}`.");
}
String Response(bool isError, bool val)
=> $"Reaction pings are {(isError ? "already" : "currently")} **{EnabledDisabled(val)}** for your system. "
+ $"To {EnabledDisabled(!val)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!val)[..^1]}`.";
if (!ctx.HasNext())
{
await ctx.Reply(Response(false, ctx.Config.PingsEnabled));
return;
}
var value = ctx.MatchToggle(true);
public async Task EditSystemPing(Context ctx, bool value)
{
ctx.CheckSystem();
if (ctx.Config.PingsEnabled == value)
await ctx.Reply(Response(true, ctx.Config.PingsEnabled));
{
await ctx.Reply($"Reaction pings are already **{EnabledDisabled(ctx.Config.PingsEnabled)}** for your system. " +
$"To {EnabledDisabled(!value)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!value)[..^1]}`.");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { PingsEnabled = value });
@ -388,230 +391,182 @@ public class Config
}
}
public async Task MemberDefaultPrivacy(Context ctx)
public async Task ViewMemberDefaultPrivacy(Context ctx)
{
if (!ctx.HasNext())
{
if (ctx.Config.MemberDefaultPrivate) { await ctx.Reply($"Newly created members will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private member off`"); }
else { await ctx.Reply($"Newly created members will currently have their privacy settings set to public. To automatically set new members' privacy settings to private, type `{ctx.DefaultPrefix}config private member on`"); }
}
if (ctx.Config.MemberDefaultPrivate)
await ctx.Reply($"Newly created members will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private member off`");
else
{
if (ctx.MatchToggle(false))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = true });
await ctx.Reply("Newly created members will now have their privacy settings set to private.");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = false });
await ctx.Reply("Newly created members will now have their privacy settings set to public.");
}
}
await ctx.Reply($"Newly created members will currently have their privacy settings set to public. To automatically set new members' privacy settings to private, type `{ctx.DefaultPrefix}config private member on`");
}
public async Task GroupDefaultPrivacy(Context ctx)
public async Task EditMemberDefaultPrivacy(Context ctx, bool value)
{
if (!ctx.HasNext())
{
if (ctx.Config.GroupDefaultPrivate) { await ctx.Reply($"Newly created groups will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private group off`"); }
else { await ctx.Reply($"Newly created groups will currently have their privacy settings set to public. To automatically set new groups' privacy settings to private, type `{ctx.DefaultPrefix}config private group on`"); }
}
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = value });
if (value)
await ctx.Reply("Newly created members will now have their privacy settings set to private.");
else
{
if (ctx.MatchToggle(false))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = true });
await ctx.Reply("Newly created groups will now have their privacy settings set to private.");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = false });
await ctx.Reply("Newly created groups will now have their privacy settings set to public.");
}
}
await ctx.Reply("Newly created members will now have their privacy settings set to public.");
}
public async Task ShowPrivateInfo(Context ctx)
public async Task ViewGroupDefaultPrivacy(Context ctx)
{
if (!ctx.HasNext())
{
if (ctx.Config.ShowPrivateInfo) await ctx.Reply("Private information is currently **shown** when looking up your own info. Use the `-public` flag to hide it.");
else await ctx.Reply("Private information is currently **hidden** when looking up your own info. Use the `-private` flag to show it.");
return;
}
if (ctx.Config.GroupDefaultPrivate)
await ctx.Reply($"Newly created groups will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private group off`");
else
await ctx.Reply($"Newly created groups will currently have their privacy settings set to public. To automatically set new groups' privacy settings to private, type `{ctx.DefaultPrefix}config private group on`");
}
if (ctx.MatchToggle(true))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = true });
public async Task EditGroupDefaultPrivacy(Context ctx, bool value)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = value });
if (value)
await ctx.Reply("Newly created groups will now have their privacy settings set to private.");
else
await ctx.Reply("Newly created groups will now have their privacy settings set to public.");
}
public async Task ViewShowPrivateInfo(Context ctx)
{
if (ctx.Config.ShowPrivateInfo)
await ctx.Reply("Private information is currently **shown** when looking up your own info. Use the `-public` flag to hide it.");
else
await ctx.Reply("Private information is currently **hidden** when looking up your own info. Use the `-private` flag to show it.");
}
public async Task EditShowPrivateInfo(Context ctx, bool value)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = value });
if (value)
await ctx.Reply("Private information will now be **shown** when looking up your own info. Use the `-public` flag to hide it.");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = false });
await ctx.Reply("Private information will now be **hidden** when looking up your own info. Use the `-private` flag to show it.");
}
}
public async Task CaseSensitiveProxyTags(Context ctx)
public async Task ViewCaseSensitiveProxyTags(Context ctx)
{
if (!ctx.HasNext())
{
if (ctx.Config.CaseSensitiveProxyTags) { await ctx.Reply("Proxy tags are currently case **sensitive**."); }
else { await ctx.Reply("Proxy tags are currently case **insensitive**."); }
return;
}
if (ctx.Config.CaseSensitiveProxyTags)
await ctx.Reply("Proxy tags are currently case **sensitive**.");
else
await ctx.Reply("Proxy tags are currently case **insensitive**.");
}
if (ctx.MatchToggle(true))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = true });
public async Task EditCaseSensitiveProxyTags(Context ctx, bool value)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = value });
if (value)
await ctx.Reply("Proxy tags are now case sensitive.");
}
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = false });
await ctx.Reply("Proxy tags are now case insensitive.");
}
}
public async Task ProxyErrorMessageEnabled(Context ctx)
public async Task ViewProxyErrorMessageEnabled(Context ctx)
{
if (!ctx.HasNext())
{
if (ctx.Config.ProxyErrorMessageEnabled) { await ctx.Reply("Proxy error messages are currently **enabled**."); }
else { await ctx.Reply("Proxy error messages are currently **disabled**. Messages that fail to proxy (due to message or attachment size) will not throw an error message."); }
return;
}
if (ctx.MatchToggle(true))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = true });
await ctx.Reply("Proxy error messages are now enabled.");
}
if (ctx.Config.ProxyErrorMessageEnabled)
await ctx.Reply("Proxy error messages are currently **enabled**.");
else
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = false });
await ctx.Reply("Proxy error messages are currently **disabled**. Messages that fail to proxy (due to message or attachment size) will not throw an error message.");
}
public async Task EditProxyErrorMessageEnabled(Context ctx, bool value)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = value });
if (value)
await ctx.Reply("Proxy error messages are now enabled.");
else
await ctx.Reply("Proxy error messages are now disabled. Messages that fail to proxy (due to message or attachment size) will not throw an error message.");
}
}
public async Task HidDisplaySplit(Context ctx)
public async Task ViewHidDisplaySplit(Context ctx)
{
if (!ctx.HasNext())
{
var msg = $"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = newVal });
await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(newVal)}.");
await ctx.Reply($"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**.");
}
public async Task HidDisplayCaps(Context ctx)
public async Task EditHidDisplaySplit(Context ctx, bool value)
{
if (!ctx.HasNext())
{
var msg = $"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = newVal });
await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(newVal)}.");
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = value });
await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(value)}.");
}
public async Task HidListPadding(Context ctx)
public async Task ViewHidDisplayCaps(Context ctx)
{
if (!ctx.HasNext())
{
string message;
switch (ctx.Config.HidListPadding)
{
case SystemConfig.HidPadFormat.None: message = "Padding 5-character IDs in lists is currently disabled."; break;
case SystemConfig.HidPadFormat.Left: message = "5-character IDs displayed in lists will have a padding space added to the beginning."; break;
case SystemConfig.HidPadFormat.Right: message = "5-character IDs displayed in lists will have a padding space added to the end."; break;
default: throw new Exception("unreachable");
}
await ctx.Reply(message);
return;
}
await ctx.Reply($"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**.");
}
public async Task EditHidDisplayCaps(Context ctx, bool value)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = value });
await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(value)}.");
}
public async Task ViewHidListPadding(Context ctx)
{
string message = ctx.Config.HidListPadding switch
{
SystemConfig.HidPadFormat.None => "Padding 5-character IDs in lists is currently disabled.",
SystemConfig.HidPadFormat.Left => "5-character IDs displayed in lists will have a padding space added to the beginning.",
SystemConfig.HidPadFormat.Right => "5-character IDs displayed in lists will have a padding space added to the end.",
_ => throw new Exception("unreachable")
};
await ctx.Reply(message);
}
public async Task EditHidListPadding(Context ctx, string padding)
{
var badInputError = "Valid padding settings are `left`, `right`, or `off`.";
var toggleOff = ctx.MatchToggleOrNull(false);
switch (toggleOff)
if (padding.Equals("off", StringComparison.InvariantCultureIgnoreCase))
{
case true: throw new PKError(badInputError);
case false:
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None });
await ctx.Reply("Padding 5-character IDs in lists has been disabled.");
return;
}
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None });
await ctx.Reply("Padding 5-character IDs in lists has been disabled.");
}
if (ctx.Match("left", "l"))
else if (padding.Equals("left", StringComparison.InvariantCultureIgnoreCase) || padding.Equals("l", StringComparison.InvariantCultureIgnoreCase))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Left });
await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the beginning.");
}
else if (ctx.Match("right", "r"))
else if (padding.Equals("right", StringComparison.InvariantCultureIgnoreCase) || padding.Equals("r", StringComparison.InvariantCultureIgnoreCase))
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Right });
await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the end.");
}
else throw new PKError(badInputError);
else
{
throw new PKError(badInputError);
}
}
public async Task CardShowColorHex(Context ctx)
public async Task ViewCardShowColorHex(Context ctx)
{
if (!ctx.HasNext())
{
var msg = $"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = newVal });
await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(newVal)}.");
await ctx.Reply($"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**.");
}
public async Task ProxySwitch(Context ctx)
public async Task EditCardShowColorHex(Context ctx, bool value)
{
if (!ctx.HasNext())
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = value });
await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(value)}.");
}
public async Task ViewProxySwitch(Context ctx)
{
string msg = ctx.Config.ProxySwitch switch
{
string msg = ctx.Config.ProxySwitch switch
{
SystemConfig.ProxySwitchAction.Off => "Currently, when you proxy as a member, no switches are logged or changed.",
SystemConfig.ProxySwitchAction.New => "When you proxy as a member, currently it makes a new switch.",
SystemConfig.ProxySwitchAction.Add => "When you proxy as a member, currently it adds them to the current switch.",
_ => throw new Exception("unreachable"),
};
await ctx.Reply(msg);
return;
}
SystemConfig.ProxySwitchAction.Off => "Currently, when you proxy as a member, no switches are logged or changed.",
SystemConfig.ProxySwitchAction.New => "When you proxy as a member, currently it makes a new switch.",
SystemConfig.ProxySwitchAction.Add => "When you proxy as a member, currently it adds them to the current switch.",
_ => throw new Exception("unreachable"),
};
await ctx.Reply(msg);
}
// toggle = false means off, toggle = true means new, otherwise if they said add that means add or if they said new they mean new. If none of those, error
var toggle = ctx.MatchToggleOrNull(false);
var newVal = toggle == false ? SystemConfig.ProxySwitchAction.Off : toggle == true ? SystemConfig.ProxySwitchAction.New : ctx.Match("add", "a") ? SystemConfig.ProxySwitchAction.Add : ctx.Match("new", "n") ? SystemConfig.ProxySwitchAction.New : throw new PKError("You must pass either \"new\", \"add\", or \"off\" to this command.");
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = newVal });
switch (newVal)
public async Task EditProxySwitch(Context ctx, SystemConfig.ProxySwitchAction action)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = action });
switch (action)
{
case SystemConfig.ProxySwitchAction.Off: await ctx.Reply("Now when you proxy as a member, no switches are logged or changed."); break;
case SystemConfig.ProxySwitchAction.New: await ctx.Reply("When you proxy as a member, it now makes a new switch."); break;
@ -620,65 +575,61 @@ public class Config
}
}
public async Task NameFormat(Context ctx)
public async Task ViewNameFormat(Context ctx)
{
var clearFlag = ctx.MatchClear();
if (!ctx.HasNext() && !clearFlag)
{
await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`");
return;
}
await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`");
}
string formatString;
if (clearFlag)
formatString = ProxyMember.DefaultFormat;
else
formatString = ctx.RemainderOrNull();
public async Task ResetNameFormat(Context ctx)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = ProxyMember.DefaultFormat });
await ctx.Reply($"Member names are now formatted as `{ProxyMember.DefaultFormat}`");
}
public async Task EditNameFormat(Context ctx, string formatString)
{
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = formatString });
await ctx.Reply($"Member names are now formatted as `{formatString}`");
}
public async Task ServerNameFormat(Context ctx)
public async Task ViewServerNameFormat(Context ctx, ReplyFormat format)
{
ctx.CheckGuildContext();
var clearFlag = ctx.MatchClear();
var format = ctx.MatchFormat();
var guildCfg = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id);
// if there's nothing next or what's next is raw/plaintext and we're not clearing, it's a query
if ((!ctx.HasNext() || format != ReplyFormat.Standard) && !clearFlag)
{
if (guildCfg.NameFormat == null)
await ctx.Reply("You do not have a specific name format set for this server and member names are formatted with your global name format.");
else
switch (format)
{
case ReplyFormat.Raw:
await ctx.Reply($"`{guildCfg.NameFormat}`");
break;
case ReplyFormat.Plaintext:
var eb = new EmbedBuilder()
.Description($"Showing guild Name Format for system {ctx.System.DisplayHid(ctx.Config)}");
await ctx.Reply(guildCfg.NameFormat, eb.Build());
break;
default:
await ctx.Reply($"Your member names in this server are currently formatted as `{guildCfg.NameFormat}`");
break;
}
return;
}
string? formatString = null;
if (!clearFlag)
{
formatString = ctx.RemainderOrNull();
}
await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = formatString });
if (formatString == null)
await ctx.Reply($"Member names are now formatted with your global name format in this server.");
if (guildCfg.NameFormat == null)
await ctx.Reply("You do not have a specific name format set for this server and member names are formatted with your global name format.");
else
await ctx.Reply($"Member names are now formatted as `{formatString}` in this server.");
switch (format)
{
case ReplyFormat.Raw:
await ctx.Reply($"`{guildCfg.NameFormat}`");
break;
case ReplyFormat.Plaintext:
var eb = new EmbedBuilder()
.Description($"Showing guild Name Format for system {ctx.System.DisplayHid(ctx.Config)}");
await ctx.Reply(guildCfg.NameFormat, eb.Build());
break;
default:
await ctx.Reply($"Your member names in this server are currently formatted as `{guildCfg.NameFormat}`");
break;
}
}
public async Task ResetServerNameFormat(Context ctx)
{
ctx.CheckGuildContext();
await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = null });
await ctx.Reply($"Member names are now formatted with your global name format in this server.");
}
public async Task EditServerNameFormat(Context ctx, string formatString)
{
ctx.CheckGuildContext();
await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = formatString });
await ctx.Reply($"Member names are now formatted as `{formatString}` in this server.");
}
public Task LimitUpdate(Context ctx)

View file

@ -34,20 +34,19 @@ public class Fun
public Task Sus(Context ctx) =>
ctx.Reply("\U0001F4EE");
public Task Error(Context ctx)
{
if (ctx.Match("message"))
return ctx.Reply("> **Error code:** `50f3c7b439d111ecab2023a5431fffbd`", new EmbedBuilder()
.Color(0xE74C3C)
.Title("Internal error occurred")
.Description(
"For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.")
.Footer(new Embed.EmbedFooter("50f3c7b439d111ecab2023a5431fffbd"))
.Timestamp(SystemClock.Instance.GetCurrentInstant().ToDateTimeOffset().ToString("O"))
.Build()
);
public Task Meow(Context ctx) =>
ctx.Reply("*mrrp :3*");
return ctx.Reply(
$"{Emojis.Error} Unknown command {"error".AsCode()}. For a list of possible commands, see <https://pluralkit.me/commands>.");
}
public Task ErrorMessage(Context ctx) => ctx.Reply("> **Error code:** `50f3c7b439d111ecab2023a5431fffbd`", new EmbedBuilder()
.Color(0xE74C3C)
.Title("Internal error occurred")
.Description(
"For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.")
.Footer(new Embed.EmbedFooter("50f3c7b439d111ecab2023a5431fffbd"))
.Timestamp(SystemClock.Instance.GetCurrentInstant().ToDateTimeOffset().ToString("O"))
.Build()
);
public Task Error(Context ctx) => ctx.Reply(
$"{Emojis.Error} Unknown command {"error".AsCode()}. For a list of possible commands, see <https://pluralkit.me/commands>.");
}

View file

@ -10,11 +10,11 @@ namespace PluralKit.Bot;
public class GroupMember
{
public async Task AddRemoveGroups(Context ctx, PKMember target, Groups.AddRemoveOperation op)
public async Task AddRemoveGroups(Context ctx, PKMember target, List<PKGroup> _groups, Groups.AddRemoveOperation op)
{
ctx.CheckSystem().CheckOwnMember(target);
var groups = (await ctx.ParseGroupList(ctx.System.Id))
var groups = _groups.FindAll(g => g.System == ctx.System.Id)
.Select(g => g.Id)
.Distinct()
.ToList();
@ -51,11 +51,12 @@ public class GroupMember
groups.Count - toAction.Count));
}
public async Task ListMemberGroups(Context ctx, PKMember target)
public async Task ListMemberGroups(Context ctx, PKMember target, string? query, IHasListOptions flags, bool all)
{
var targetSystem = await ctx.Repository.GetSystem(target.System);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
var opts = flags.GetListOptions(ctx, target.System);
opts.MemberFilter = target.Id;
opts.Search = query;
var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null)
@ -79,15 +80,15 @@ public class GroupMember
title.Append($" matching **{opts.Search.Truncate(100)}**");
await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(),
target.Color, opts);
target.Color, opts, all);
}
public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op)
public async Task AddRemoveMembers(Context ctx, PKGroup target, List<PKMember>? _members, Groups.AddRemoveOperation op, bool all, bool confirmYes = false)
{
ctx.CheckOwnGroup(target);
List<MemberId> members;
if (ctx.MatchFlag("all", "a"))
if (all)
{
members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
new DatabaseViewsExt.ListQueryOptions { })))
@ -97,10 +98,14 @@ public class GroupMember
}
else
{
members = (await ctx.ParseMemberList(ctx.System.Id))
.Select(m => m.Id)
.Distinct()
.ToList();
if (_members == null)
throw new PKError("Please provide a list of members to add/remove.");
members = _members
.FindAll(m => m.System == ctx.System.Id)
.Select(m => m.Id)
.Distinct()
.ToList();
}
var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
@ -124,7 +129,7 @@ public class GroupMember
.Where(m => existingMembersInGroup.Contains(m.Value))
.ToList();
if (ctx.MatchFlag("all", "a") && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled();
if (all && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group", flagValue: confirmYes)) throw Errors.GenericCancelled();
await ctx.Repository.RemoveMembersFromGroup(target.Id, toAction);
}
@ -137,15 +142,16 @@ public class GroupMember
members.Count - toAction.Count));
}
public async Task ListGroupMembers(Context ctx, PKGroup target)
public async Task ListGroupMembers(Context ctx, PKGroup target, string? query, IHasListOptions flags)
{
// see global system list for explanation of how privacy settings are used here
var targetSystem = await GetGroupSystem(ctx, target);
ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
var opts = flags.GetListOptions(ctx, target.System);
opts.GroupFilter = target.Id;
opts.Search = query;
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
if (ctx.Guild != null)

View file

@ -32,12 +32,11 @@ public class Groups
_avatarHosting = avatarHosting;
}
public async Task CreateGroup(Context ctx)
public async Task CreateGroup(Context ctx, string groupName, bool confirmYes = false)
{
ctx.CheckSystem();
// Check group name length
var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name.");
if (groupName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters).");
@ -54,7 +53,7 @@ public class Groups
{
var msg =
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to create another group with the same name?";
if (!await ctx.PromptYesNo(msg, "Create"))
if (!await ctx.PromptYesNo(msg, "Create", flagValue: confirmYes))
throw new PKError("Group creation cancelled.");
}
@ -99,12 +98,11 @@ public class Groups
await ctx.Reply(replyStr, eb.Build());
}
public async Task RenameGroup(Context ctx, PKGroup target)
public async Task RenameGroup(Context ctx, PKGroup target, string? newName, bool confirmYes = false)
{
ctx.CheckOwnGroup(target);
// Check group name length
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new group name.");
if (newName.Length > Limits.MaxGroupNameLength)
throw new PKError(
$"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters).");
@ -115,7 +113,7 @@ public class Groups
{
var msg =
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to rename this group to that name too?";
if (!await ctx.PromptYesNo(msg, "Rename"))
if (!await ctx.PromptYesNo(msg, "Rename", flagValue: confirmYes))
throw new PKError("Group rename cancelled.");
}
@ -124,7 +122,7 @@ public class Groups
await ctx.Reply($"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}** (using {newName.Length}/{Limits.MaxGroupNameLength} characters).");
}
public async Task GroupDisplayName(Context ctx, PKGroup target)
public async Task ShowGroupDisplayName(Context ctx, PKGroup target, ReplyFormat format)
{
var noDisplayNameSetMessage = "This group does not have a display name set" +
(ctx.System?.Id == target.System
@ -134,8 +132,6 @@ public class Groups
// Whether displayname is shown or not should depend on if group name privacy is set.
// If name privacy is on then displayname should look like name.
var format = ctx.MatchFormat();
// if we're doing a raw or plaintext query check for null
if (format != ReplyFormat.Standard)
if (target.DisplayName == null || !target.NamePrivacy.CanAccess(ctx.DirectLookupContextFor(target.System)))
@ -157,69 +153,65 @@ public class Groups
return;
}
if (!ctx.HasNext(false))
{
var showDisplayName = target.NamePrivacy.CanAccess(ctx.LookupContextFor(target.System)) && target.DisplayName != null;
var showDisplayName = target.NamePrivacy.CanAccess(ctx.LookupContextFor(target.System)) && target.DisplayName != null;
var eb = new EmbedBuilder()
.Title("Group names")
.Field(new Embed.Field("Name", target.NameFor(ctx)))
.Field(new Embed.Field("Display Name", showDisplayName ? target.DisplayName : "*(no displayname set or name is private)*"));
var eb2 = new EmbedBuilder()
.Title("Group names")
.Field(new Embed.Field("Name", target.NameFor(ctx)))
.Field(new Embed.Field("Display Name", showDisplayName ? target.DisplayName : "*(no displayname set or name is private)*"));
var reference = target.Reference(ctx);
var reference = target.Reference(ctx);
if (ctx.System?.Id == target.System)
eb.Description(
$"To change display name, type `{ctx.DefaultPrefix}group {reference} displayname <display name>`.\n"
+ $"To clear it, type `{ctx.DefaultPrefix}group {reference} displayname -clear`.\n"
+ $"To print the raw display name, type `{ctx.DefaultPrefix}group {reference} displayname -raw`.");
if (ctx.System?.Id == target.System)
eb2.Description(
$"To change display name, type `{ctx.DefaultPrefix}group {reference} displayname <display name>`.\n"
+ $"To clear it, type `{ctx.DefaultPrefix}group {reference} displayname -clear`.\n"
+ $"To print the raw display name, type `{ctx.DefaultPrefix}group {reference} displayname -raw`.");
if (ctx.System?.Id == target.System && showDisplayName)
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
if (ctx.System?.Id == target.System && showDisplayName)
eb2.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
await ctx.Reply(embed: eb.Build());
return;
}
ctx.CheckOwnGroup(target);
if (ctx.MatchClear() && await ctx.ConfirmClear("this group's display name"))
{
var patch = new GroupPatch { DisplayName = Partial<string>.Null() };
await ctx.Repository.UpdateGroup(target.Id, patch);
var replyStr = $"{Emojis.Success} Group display name cleared.";
if (target.NamePrivacy == PrivacyLevel.Private)
replyStr += $"\n{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**.";
await ctx.Reply(replyStr);
}
else
{
var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
if (newDisplayName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Group display name changed (using {newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
}
await ctx.Reply(embed: eb2.Build());
}
public async Task GroupDescription(Context ctx, PKGroup target)
public async Task ClearGroupDisplayName(Context ctx, PKGroup target, bool confirmYes = false)
{
ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy);
ctx.CheckOwnGroup(target);
var noDescriptionSetMessage = "This group does not have a description set.";
if (ctx.System?.Id == target.System)
noDescriptionSetMessage +=
$" To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description <description>`.";
if (!await ctx.ConfirmClear("this group's display name", confirmYes))
return;
var format = ctx.MatchFormat();
var patch = new GroupPatch { DisplayName = Partial<string>.Null() };
await ctx.Repository.UpdateGroup(target.Id, patch);
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
var replyStr = $"{Emojis.Success} Group display name cleared.";
if (target.NamePrivacy == PrivacyLevel.Private)
replyStr += $"\n{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**.";
await ctx.Reply(replyStr);
}
public async Task ChangeGroupDisplayName(Context ctx, PKGroup target, string newDisplayName)
{
ctx.CheckOwnGroup(target);
if (newDisplayName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Group display name changed (using {newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
}
public async Task ShowGroupDescription(Context ctx, PKGroup target, ReplyFormat format)
{
var noDescriptionSetMessage = "This group does not have a description set" +
(ctx.System?.Id == target.System
? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description <description>`."
: ".");
// if we're doing a raw or plaintext query check for null
if (format != ReplyFormat.Standard)
if (target.Description == null)
{
await ctx.Reply(noDescriptionSetMessage);
@ -239,246 +231,291 @@ public class Groups
return;
}
if (!ctx.HasNext(false))
if (target.Description == null)
{
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group description")
.Description(target.Description)
.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description -raw`."
+ (ctx.System?.Id == target.System
? $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description -clear`."
: "")
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
.Build());
await ctx.Reply(noDescriptionSetMessage);
return;
}
var eb2 = new EmbedBuilder()
.Title("Group description")
.Description(target.Description);
var reference = target.Reference(ctx);
if (ctx.System?.Id == target.System)
eb2.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`."
+ $" To clear it, type `{ctx.DefaultPrefix}group {reference} description -clear`."
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."));
else
eb2.Field(new Embed.Field("\u200B",
$"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`."
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."));
await ctx.Reply(embed: eb2.Build());
}
public async Task ClearGroupDescription(Context ctx, PKGroup target, bool confirmYes = false)
{
ctx.CheckOwnGroup(target);
if (ctx.MatchClear() && await ctx.ConfirmClear("this group's description"))
{
var patch = new GroupPatch { Description = Partial<string>.Null() };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Group description cleared.");
}
else
{
var description = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
if (description.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.StringTooLongError("Description", description.Length, Limits.MaxDescriptionLength);
if (!await ctx.ConfirmClear("this group's description", confirmYes))
return;
var patch = new GroupPatch { Description = Partial<string>.Present(description) };
await ctx.Repository.UpdateGroup(target.Id, patch);
var patch = new GroupPatch { Description = Partial<string>.Null() };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Group description changed (using {description.Length}/{Limits.MaxDescriptionLength} characters).");
}
await ctx.Reply($"{Emojis.Success} Group description cleared.");
}
public async Task GroupIcon(Context ctx, PKGroup target)
public async Task ChangeGroupDescription(Context ctx, PKGroup target, string newDescription)
{
async Task ClearIcon()
{
await ctx.ConfirmClear("this group's icon");
ctx.CheckOwnGroup(target);
ctx.CheckOwnGroup(target);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null });
await ctx.Reply($"{Emojis.Success} Group icon cleared.");
}
if (newDescription.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.StringTooLongError("Description", newDescription.Length, Limits.MaxDescriptionLength);
async Task SetIcon(ParsedImage img)
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch { Description = Partial<string>.Present(newDescription) };
await ctx.Repository.UpdateGroup(target.Id, patch);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Reply($"{Emojis.Success} Group description changed (using {newDescription.Length}/{Limits.MaxDescriptionLength} characters).");
}
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url });
public async Task ShowGroupIcon(Context ctx, PKGroup target, ReplyFormat format)
{
var noIconSetMessage = "This group does not have an avatar set" +
(ctx.System?.Id == target.System
? ". Set one by attaching an image to this command, or by passing an image URL or @mention."
: ".");
var msg = img.Source switch
ctx.CheckSystemPrivacy(target.System, target.IconPrivacy);
// if we're doing a raw or plaintext query check for null
if (format != ReplyFormat.Standard)
if ((target.Icon?.Trim() ?? "").Length == 0)
{
AvatarSource.User =>
$"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.",
AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.",
AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.",
AvatarSource.Attachment =>
$"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
await ctx.Reply(noIconSetMessage);
return;
}
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
await (hasEmbed
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
: ctx.Reply(msg));
}
async Task ShowIcon()
if (format == ReplyFormat.Raw)
{
ctx.CheckSystemPrivacy(target.System, target.IconPrivacy);
if ((target.Icon?.Trim() ?? "").Length > 0)
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.Icon.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.Icon.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("Group icon")
.Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} icon -clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
throw new PKSyntaxError(
"This group does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
await ctx.Reply($"`{target.Icon.TryGetCleanCdnUrl()}`");
return;
}
if (ctx.MatchClear())
await ClearIcon();
else if (await ctx.MatchImage() is { } img)
await SetIcon(img);
else
await ShowIcon();
}
public async Task GroupBannerImage(Context ctx, PKGroup target)
{
async Task ClearBannerImage()
if (format == ReplyFormat.Plaintext)
{
ctx.CheckOwnGroup(target);
await ctx.ConfirmClear("this group's banner image");
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null });
await ctx.Reply($"{Emojis.Success} Group banner image cleared.");
}
async Task SetBannerImage(ParsedImage img)
{
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(img.Url, true);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url });
var msg = img.Source switch
{
AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.",
AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.",
AvatarSource.Attachment =>
$"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.",
AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."),
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
await (hasEmbed
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
: ctx.Reply(msg));
}
async Task ShowBannerImage()
{
ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy);
if ((target.BannerImage?.Trim() ?? "").Length > 0)
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("Group banner image")
.Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
throw new PKSyntaxError(
"This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
}
if (ctx.MatchClear())
await ClearBannerImage();
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);
else
await ShowBannerImage();
}
public async Task GroupColor(Context ctx, PKGroup target)
{
var isOwnSystem = ctx.System?.Id == target.System;
var matchedFormat = ctx.MatchFormat();
var matchedClear = ctx.MatchClear();
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
{
if (target.Color == null)
await ctx.Reply(
"This group does not have a color set." + (isOwnSystem ? $" To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color <color>`." : ""));
else if (matchedFormat == ReplyFormat.Raw)
await ctx.Reply("```\n#" + target.Color + "\n```");
else if (matchedFormat == ReplyFormat.Plaintext)
await ctx.Reply(target.Color);
else
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group color")
.Color(target.Color.ToDiscordColor())
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
.Description($"This group's color is **#{target.Color}**."
+ (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`." : ""))
.Build(),
files: [MiscUtils.GenerateColorPreview(target.Color)]);
var ebP = new EmbedBuilder()
.Description($"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.Icon.TryGetCleanCdnUrl()}>", embed: ebP.Build());
return;
}
ctx.CheckSystem().CheckOwnGroup(target);
if (matchedClear)
if ((target.Icon?.Trim() ?? "").Length == 0)
{
await ctx.Repository.UpdateGroup(target.Id, new() { Color = Partial<string>.Null() });
await ctx.Reply($"{Emojis.Success} Group color cleared.");
await ctx.Reply(noIconSetMessage);
return;
}
else
{
var color = ctx.RemainderOrNull();
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new GroupPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply(embed: new EmbedBuilder()
.Title($"{Emojis.Success} Group color changed.")
.Color(color.ToDiscordColor())
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
.Build(),
files: [MiscUtils.GenerateColorPreview(color)]);
}
var ebS = new EmbedBuilder()
.Title("Group icon")
.Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} icon -clear`.");
await ctx.Reply(embed: ebS.Build());
}
public async Task ListSystemGroups(Context ctx, PKSystem system)
public async Task ClearGroupIcon(Context ctx, PKGroup target, bool confirmYes)
{
ctx.CheckOwnGroup(target);
await ctx.ConfirmClear("this group's icon", confirmYes);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null });
await ctx.Reply($"{Emojis.Success} Group icon cleared.");
}
public async Task ChangeGroupIcon(Context ctx, PKGroup target, ParsedImage img)
{
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url });
var msg = img.Source switch
{
AvatarSource.User =>
$"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.",
AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.",
AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.",
AvatarSource.Attachment =>
$"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
await (hasEmbed
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
: ctx.Reply(msg));
}
public async Task ShowGroupBanner(Context ctx, PKGroup target, ReplyFormat format)
{
var noBannerSetMessage = "This group does not have a banner image set" +
(ctx.System?.Id == target.System
? ". Set one by attaching an image to this command, or by passing an image URL or @mention."
: ".");
ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy);
// if we're doing a raw or plaintext query check for null
if (format != ReplyFormat.Standard)
if ((target.BannerImage?.Trim() ?? "").Length == 0)
{
await ctx.Reply(noBannerSetMessage);
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`");
return;
}
if (format == ReplyFormat.Plaintext)
{
var ebP = new EmbedBuilder()
.Description($"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build());
return;
}
if ((target.BannerImage?.Trim() ?? "").Length == 0)
{
await ctx.Reply(noBannerSetMessage);
return;
}
var ebS = new EmbedBuilder()
.Title("Group banner image")
.Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: ebS.Build());
}
public async Task ClearGroupBanner(Context ctx, PKGroup target, bool confirmYes)
{
ctx.CheckOwnGroup(target);
await ctx.ConfirmClear("this group's banner image", confirmYes);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null });
await ctx.Reply($"{Emojis.Success} Group banner image cleared.");
}
public async Task ChangeGroupBanner(Context ctx, PKGroup target, ParsedImage img)
{
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(img.Url, true);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url });
var msg = img.Source switch
{
AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.",
AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.",
AvatarSource.Attachment =>
$"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.",
AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."),
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
await (hasEmbed
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
: ctx.Reply(msg));
}
public async Task ShowGroupColor(Context ctx, PKGroup target, ReplyFormat format)
{
var noColorSetMessage = "This group does not have a color set" +
(ctx.System?.Id == target.System
? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color <color>`."
: ".");
// if we're doing a raw or plaintext query check for null
if (format != ReplyFormat.Standard)
if (target.Color == null)
{
await ctx.Reply(noColorSetMessage);
return;
}
if (format == ReplyFormat.Raw)
{
await ctx.Reply("```\n#" + target.Color + "\n```");
return;
}
if (format == ReplyFormat.Plaintext)
{
await ctx.Reply(target.Color);
return;
}
if (target.Color == null)
{
await ctx.Reply(noColorSetMessage);
return;
}
var eb = new EmbedBuilder()
.Title("Group color")
.Color(target.Color.ToDiscordColor())
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
.Description($"This group's color is **#{target.Color}**.");
if (ctx.System?.Id == target.System)
eb.Description(eb.Build().Description + $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`.");
await ctx.Reply(embed: eb.Build(), files: [MiscUtils.GenerateColorPreview(target.Color)]);
}
public async Task ClearGroupColor(Context ctx, PKGroup target, bool confirmYes = false)
{
ctx.CheckOwnGroup(target);
if (!await ctx.ConfirmClear("this group's color", confirmYes))
return;
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Color = Partial<string>.Null() });
await ctx.Reply($"{Emojis.Success} Group color cleared.");
}
public async Task ChangeGroupColor(Context ctx, PKGroup target, string color)
{
ctx.CheckOwnGroup(target);
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new GroupPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await ctx.Repository.UpdateGroup(target.Id, patch);
await ctx.Reply(embed: new EmbedBuilder()
.Title($"{Emojis.Success} Group color changed.")
.Color(color.ToDiscordColor())
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
.Build(),
files: [MiscUtils.GenerateColorPreview(color)]);
}
public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags, bool all)
{
if (system == null)
{
@ -492,13 +529,16 @@ public class Groups
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
// - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id));
var opts = flags.GetListOptions(ctx, system.Id);
opts.Search = query;
await ctx.RenderGroupList(
ctx.LookupContextFor(system.Id),
system.Id,
GetEmbedTitle(ctx, system, opts),
system.Color,
opts
opts,
all
);
}
@ -517,114 +557,109 @@ public class Groups
return title.ToString();
}
public async Task ShowGroupCard(Context ctx, PKGroup target)
public async Task ShowGroupCard(Context ctx, PKGroup target, bool showEmbed, bool all)
{
var system = await GetGroupSystem(ctx, target);
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target));
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target, all));
return;
}
await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target));
await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target, all));
}
public async Task GroupPrivacy(Context ctx, PKGroup target, PrivacyLevel? newValueFromCommand)
public async Task ShowGroupPrivacy(Context ctx, PKGroup target)
{
ctx.CheckSystem().CheckOwnGroup(target);
// Display privacy settings
if (!ctx.HasNext() && newValueFromCommand == null)
{
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.Name}")
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
.Field(new Embed.Field("Icon", target.IconPrivacy.Explanation()))
.Field(new Embed.Field("Member list", target.ListPrivacy.Explanation()))
.Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation()))
.Field(new Embed.Field("Visibility", target.Visibility.Explanation()))
.Description(
$"To edit privacy settings, use the command:\n> {ctx.DefaultPrefix}group **{target.Reference(ctx)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
return;
}
async Task SetAll(PrivacyLevel level)
{
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithAllPrivacy(level));
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.Name}")
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
.Field(new Embed.Field("Icon", target.IconPrivacy.Explanation()))
.Field(new Embed.Field("Member list", target.ListPrivacy.Explanation()))
.Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation()))
.Field(new Embed.Field("Visibility", target.Visibility.Explanation()))
.Description(
$"To edit privacy settings, use the command:\n> {ctx.DefaultPrefix}group **{target.Reference(ctx)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
}
if (level == PrivacyLevel.Private)
await ctx.Reply(
$"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card.");
else
await ctx.Reply(
$"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card.");
}
public async Task SetAllGroupPrivacy(Context ctx, PKGroup target, PrivacyLevel level)
{
ctx.CheckOwnGroup(target);
async Task SetLevel(GroupPrivacySubject subject, PrivacyLevel level)
{
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithPrivacy(subject, level));
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithAllPrivacy(level));
var subjectName = subject switch
{
GroupPrivacySubject.Name => "name privacy",
GroupPrivacySubject.Description => "description privacy",
GroupPrivacySubject.Banner => "banner privacy",
GroupPrivacySubject.Icon => "icon privacy",
GroupPrivacySubject.List => "member list",
GroupPrivacySubject.Metadata => "metadata",
GroupPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(GroupPrivacySubject.Name, PrivacyLevel.Private) =>
"This group's name is now hidden from other systems, and will be replaced by the group's display name.",
(GroupPrivacySubject.Description, PrivacyLevel.Private) =>
"This group's description is now hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Private) =>
"This group's banner is now hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Private) =>
"This group's icon is now hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) =>
"This group is now hidden from group lists and member cards.",
(GroupPrivacySubject.Metadata, PrivacyLevel.Private) =>
"This group's metadata (eg. creation date) is now hidden from other systems.",
(GroupPrivacySubject.List, PrivacyLevel.Private) =>
"This group's member list is now hidden from other systems.",
(GroupPrivacySubject.Name, PrivacyLevel.Public) =>
"This group's name is no longer hidden from other systems.",
(GroupPrivacySubject.Description, PrivacyLevel.Public) =>
"This group's description is no longer hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Public) =>
"This group's banner is no longer hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Public) =>
"This group's icon is no longer hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) =>
"This group is no longer hidden from group lists and member cards.",
(GroupPrivacySubject.Metadata, PrivacyLevel.Public) =>
"This group's metadata (eg. creation date) is no longer hidden from other systems.",
(GroupPrivacySubject.List, PrivacyLevel.Public) =>
"This group's member list is no longer hidden from other systems.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
var replyStr = $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}";
if (subject == GroupPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null)
replyStr += $"\n{Emojis.Warn} This group does not have a display name set, and name privacy **will not take effect**.";
await ctx.Reply(replyStr);
}
if (ctx.Match("all") || newValueFromCommand != null)
await SetAll(newValueFromCommand ?? ctx.PopPrivacyLevel());
if (level == PrivacyLevel.Private)
await ctx.Reply(
$"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card.");
else
await SetLevel(ctx.PopGroupPrivacySubject(), ctx.PopPrivacyLevel());
await ctx.Reply(
$"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card.");
}
public async Task SetGroupPrivacy(Context ctx, PKGroup target, GroupPrivacySubject subject, PrivacyLevel level)
{
ctx.CheckOwnGroup(target);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithPrivacy(subject, level));
var subjectName = subject switch
{
GroupPrivacySubject.Name => "name privacy",
GroupPrivacySubject.Description => "description privacy",
GroupPrivacySubject.Banner => "banner privacy",
GroupPrivacySubject.Icon => "icon privacy",
GroupPrivacySubject.List => "member list",
GroupPrivacySubject.Metadata => "metadata",
GroupPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(GroupPrivacySubject.Name, PrivacyLevel.Private) =>
"This group's name is now hidden from other systems, and will be replaced by the group's display name.",
(GroupPrivacySubject.Description, PrivacyLevel.Private) =>
"This group's description is now hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Private) =>
"This group's banner is now hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Private) =>
"This group's icon is now hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) =>
"This group is now hidden from group lists and member cards.",
(GroupPrivacySubject.Metadata, PrivacyLevel.Private) =>
"This group's metadata (eg. creation date) is now hidden from other systems.",
(GroupPrivacySubject.List, PrivacyLevel.Private) =>
"This group's member list is now hidden from other systems.",
(GroupPrivacySubject.Name, PrivacyLevel.Public) =>
"This group's name is no longer hidden from other systems.",
(GroupPrivacySubject.Description, PrivacyLevel.Public) =>
"This group's description is no longer hidden from other systems.",
(GroupPrivacySubject.Banner, PrivacyLevel.Public) =>
"This group's banner is no longer hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Public) =>
"This group's icon is no longer hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) =>
"This group is no longer hidden from group lists and member cards.",
(GroupPrivacySubject.Metadata, PrivacyLevel.Public) =>
"This group's metadata (eg. creation date) is no longer hidden from other systems.",
(GroupPrivacySubject.List, PrivacyLevel.Public) =>
"This group's member list is no longer hidden from other systems.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
var replyStr = $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}";
if (subject == GroupPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null)
replyStr += $"\n{Emojis.Warn} This group does not have a display name set, and name privacy **will not take effect**.";
await ctx.Reply(replyStr);
}
public async Task DeleteGroup(Context ctx, PKGroup target)

View file

@ -7,9 +7,9 @@ namespace PluralKit.Bot;
public class Help
{
public Task HelpRoot(Context ctx)
public Task HelpRoot(Context ctx, bool showEmbed = false)
{
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
return HelpRootOld(ctx);
return ctx.Reply(BuildComponents(ctx.Author.Id, Help.Description.Replace("{prefix}", ctx.DefaultPrefix), -1));

View file

@ -31,9 +31,9 @@ public class ImportExport
_dmCache = dmCache;
}
public async Task Import(Context ctx)
public async Task Import(Context ctx, string? inputUrl, bool confirmYes)
{
var inputUrl = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url;
inputUrl = inputUrl ?? ctx.Message.Attachments.FirstOrDefault()?.Url;
if (inputUrl == null) throw Errors.NoImportFilePassed;
if (!Core.MiscUtils.TryMatchUri(inputUrl, out var url))
@ -77,7 +77,7 @@ public class ImportExport
async Task ConfirmImport(string message)
{
var msg = $"{message}\n\nDo you want to proceed with the import?";
if (!await ctx.PromptYesNo(msg, "Proceed"))
if (!await ctx.PromptYesNo(msg, "Proceed", flagValue: confirmYes))
throw Errors.ImportCancelled;
}
@ -86,7 +86,7 @@ public class ImportExport
&& data.Value<JArray>("accounts").Contains(ctx.Author.Id.ToString()))
{
var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(msg, "Import")) throw Errors.ImportCancelled;
if (!await ctx.PromptYesNo(msg, "Import", flagValue: confirmYes)) throw Errors.ImportCancelled;
}
var result = await _dataFiles.ImportSystem(ctx.Author.Id, ctx.System, data, ConfirmImport);

View file

@ -9,95 +9,13 @@ using PluralKit.Core;
namespace PluralKit.Bot;
public interface IHasListOptions
{
ListOptions GetListOptions(Context ctx, SystemId system);
}
public static class ContextListExt
{
public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext)
{
var p = new ListOptions();
// Short or long list? (parse this first, as it can potentially take a positional argument)
var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
p.Type = isFull ? ListType.Long : ListType.Short;
// Search query
if (ctx.HasNext())
p.Search = ctx.RemainderOrNull();
// Include description in search?
if (ctx.MatchFlag(
"search-description",
"filter-description",
"in-description",
"sd",
"description",
"desc"
))
p.SearchDescription = true;
// Sort property (default is by name, but adding a flag anyway, 'cause why not)
if (ctx.MatchFlag("by-name", "bn")) p.SortProperty = SortProperty.Name;
if (ctx.MatchFlag("by-display-name", "bdn")) p.SortProperty = SortProperty.DisplayName;
if (ctx.MatchFlag("by-id", "bid")) p.SortProperty = SortProperty.Hid;
if (ctx.MatchFlag("by-message-count", "bmc")) p.SortProperty = SortProperty.MessageCount;
if (ctx.MatchFlag("by-created", "bc", "bcd")) p.SortProperty = SortProperty.CreationDate;
if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls"))
p.SortProperty = SortProperty.LastSwitch;
if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage;
if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate;
if (ctx.MatchFlag("random", "rand")) p.SortProperty = SortProperty.Random;
// Sort reverse?
if (ctx.MatchFlag("r", "rev", "reverse"))
p.Reverse = true;
// Privacy filter (default is public only)
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private;
// PERM CHECK: If we're trying to access non-public members of another system, error
if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw Errors.NotOwnInfo;
//this is for searching
p.Context = lookupContext;
// Additional fields to include in the search results
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
p.IncludeLastSwitch = true;
if (ctx.MatchFlag("with-last-message", "with-last-proxy", "wlm", "wlp"))
p.IncludeLastMessage = true;
if (ctx.MatchFlag("with-message-count", "wmc"))
p.IncludeMessageCount = true;
if (ctx.MatchFlag("with-created", "wc"))
p.IncludeCreated = true;
if (ctx.MatchFlag("with-avatar", "with-image", "with-icon", "wa", "wi", "ia", "ii", "img"))
p.IncludeAvatar = true;
if (ctx.MatchFlag("with-pronouns", "wp", "wprns"))
p.IncludePronouns = true;
if (ctx.MatchFlag("with-displayname", "wdn"))
p.IncludeDisplayName = true;
if (ctx.MatchFlag("with-birthday", "wbd", "wb"))
p.IncludeBirthday = true;
// Always show the sort property, too (unless this is the short list and we are already showing something else)
if (p.Type != ListType.Short || p.includedCount == 0)
{
if (p.SortProperty == SortProperty.DisplayName) p.IncludeDisplayName = true;
if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true;
if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true;
if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true;
if (p.SortProperty == SortProperty.Birthdate) p.IncludeBirthday = true;
}
// Make sure the options are valid
p.AssertIsValid();
// Done!
return p;
}
public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx,
SystemId system, string embedTitle, string color, ListOptions opts)
{
@ -212,7 +130,7 @@ public static class ContextListExt
}
public static async Task RenderGroupList(this Context ctx, LookupContext lookupCtx,
SystemId system, string embedTitle, string color, ListOptions opts)
SystemId system, string embedTitle, string color, ListOptions opts, bool all)
{
// We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime
// We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout)
@ -286,7 +204,7 @@ public static class ContextListExt
{
if (g.ListPrivacy == PrivacyLevel.Public || lookupCtx == LookupContext.ByOwner)
{
if (ctx.MatchFlag("all", "a"))
if (all)
{
ret += $"({"member".ToQuantity(g.TotalMemberCount)})";
}
@ -324,7 +242,7 @@ public static class ContextListExt
if (g.ListPrivacy == PrivacyLevel.Public || lookupCtx == LookupContext.ByOwner)
{
if (ctx.MatchFlag("all", "a") && ctx.DirectLookupContextFor(system) == LookupContext.ByOwner)
if (all && ctx.DirectLookupContextFor(system) == LookupContext.ByOwner)
profile.Append($"\n**Member Count:** {g.TotalMemberCount}");
else
profile.Append($"\n**Member Count:** {g.PublicMemberCount}");

View file

@ -184,6 +184,7 @@ public static class ListOptionsExt
// the check for multiple *sorting* property flags is done in SortProperty setter
}
}
public enum SortProperty

View file

@ -1,4 +1,5 @@
using System.Net;
using System.Reflection.Metadata;
using System.Web;
using Dapper;
@ -27,10 +28,10 @@ public class Member
_avatarHosting = avatarHosting;
}
public async Task NewMember(Context ctx)
public async Task NewMember(Context ctx, string? memberName, bool confirmYes = false)
{
if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
memberName = memberName ?? throw new PKSyntaxError("You must pass a member name.");
// Hard name length cap
if (memberName.Length > Limits.MaxMemberNameLength)
@ -41,7 +42,7 @@ public class Member
if (existingMember != null)
{
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.DisplayHid(ctx.Config)}`). Do you want to create another member with the same name?";
if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled.");
if (!await ctx.PromptYesNo(msg, "Create", flagValue: confirmYes)) throw new PKError("Member creation cancelled.");
}
await using var conn = await ctx.Database.Obtain();
@ -119,10 +120,10 @@ public class Member
await ctx.Reply(replyStr);
}
public async Task ViewMember(Context ctx, PKMember target)
public async Task ViewMember(Context ctx, PKMember target, bool showEmbed = false)
{
var system = await ctx.Repository.GetSystem(target.System);
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,

View file

@ -17,8 +17,11 @@ public class MemberAvatar
_avatarHosting = avatarHosting;
}
private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs, bool confirmYes)
{
ctx.CheckSystem().CheckOwnMember(target);
await ctx.ConfirmClear("this member's " + location.Name(), confirmYes);
await UpdateAvatar(location, ctx, target, null);
if (location == MemberAvatarLocation.Server)
{
@ -47,7 +50,7 @@ public class MemberAvatar
}
private async Task AvatarShow(MemberAvatarLocation location, Context ctx, PKMember target,
MemberGuildSettings? guildData)
MemberGuildSettings? guildData, ReplyFormat format)
{
// todo: this privacy code is really confusing
// for now, we skip privacy flag/config parsing for this, but it would be good to fix that at some point
@ -86,7 +89,6 @@ public class MemberAvatar
if (location == MemberAvatarLocation.Server)
field += $" (for {ctx.Guild.Name})";
var format = ctx.MatchFormat();
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"`{currentValue?.TryGetCleanCdnUrl()}`");
@ -110,58 +112,89 @@ public class MemberAvatar
else throw new PKError("Format Not Recognized");
}
public async Task ServerAvatar(Context ctx, PKMember target)
private async Task AvatarChange(MemberAvatarLocation location, Context ctx, PKMember target,
MemberGuildSettings? guildData, ParsedImage avatar)
{
ctx.CheckGuildContext();
var guildData = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
await AvatarCommandTree(MemberAvatarLocation.Server, ctx, target, guildData);
}
public async Task Avatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
await AvatarCommandTree(MemberAvatarLocation.Member, ctx, target, guildData);
}
public async Task WebhookAvatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
await AvatarCommandTree(MemberAvatarLocation.MemberWebhook, ctx, target, guildData);
}
private async Task AvatarCommandTree(MemberAvatarLocation location, Context ctx, PKMember target,
MemberGuildSettings? guildData)
{
// First, see if we need to *clear*
if (ctx.MatchClear())
{
ctx.CheckSystem().CheckOwnMember(target);
await ctx.ConfirmClear("this member's " + location.Name());
await AvatarClear(location, ctx, target, guildData);
return;
}
// Then, parse an image from the command (from various sources...)
var avatarArg = await ctx.MatchImage();
if (avatarArg == null)
{
// If we didn't get any, just show the current avatar
await AvatarShow(location, ctx, target, guildData);
return;
}
ctx.CheckSystem().CheckOwnMember(target);
avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(avatarArg.Value.Url);
await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url);
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);
avatar = await _avatarHosting.TryRehostImage(avatar, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await _avatarHosting.VerifyAvatarOrThrow(avatar.Url);
await UpdateAvatar(location, ctx, target, avatar.CleanUrl ?? avatar.Url);
await PrintResponse(location, ctx, target, avatar, guildData);
}
private Task<MemberGuildSettings> GetServerAvatarGuildData(Context ctx, PKMember target)
{
ctx.CheckGuildContext();
return ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
}
private async Task<MemberGuildSettings?> GetAvatarGuildData(Context ctx, PKMember target)
{
return ctx.Guild != null
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
}
private async Task<MemberGuildSettings?> GetWebhookAvatarGuildData(Context ctx, PKMember target)
{
return ctx.Guild != null
? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id)
: null;
}
public async Task ShowServerAvatar(Context ctx, PKMember target, ReplyFormat format)
{
var guildData = await GetServerAvatarGuildData(ctx, target);
await AvatarShow(MemberAvatarLocation.Server, ctx, target, guildData, format);
}
public async Task ClearServerAvatar(Context ctx, PKMember target, bool confirmYes)
{
var guildData = await GetServerAvatarGuildData(ctx, target);
await AvatarClear(MemberAvatarLocation.Server, ctx, target, guildData, confirmYes);
}
public async Task ChangeServerAvatar(Context ctx, PKMember target, ParsedImage avatar)
{
var guildData = await GetServerAvatarGuildData(ctx, target);
await AvatarChange(MemberAvatarLocation.Server, ctx, target, guildData, avatar);
}
public async Task ShowAvatar(Context ctx, PKMember target, ReplyFormat format)
{
var guildData = await GetAvatarGuildData(ctx, target);
await AvatarShow(MemberAvatarLocation.Member, ctx, target, guildData, format);
}
public async Task ClearAvatar(Context ctx, PKMember target, bool confirmYes)
{
var guildData = await GetAvatarGuildData(ctx, target);
await AvatarClear(MemberAvatarLocation.Member, ctx, target, guildData, confirmYes);
}
public async Task ChangeAvatar(Context ctx, PKMember target, ParsedImage avatar)
{
var guildData = await GetAvatarGuildData(ctx, target);
await AvatarChange(MemberAvatarLocation.Member, ctx, target, guildData, avatar);
}
public async Task ShowWebhookAvatar(Context ctx, PKMember target, ReplyFormat format)
{
var guildData = await GetWebhookAvatarGuildData(ctx, target);
await AvatarShow(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, format);
}
public async Task ClearWebhookAvatar(Context ctx, PKMember target, bool confirmYes)
{
var guildData = await GetWebhookAvatarGuildData(ctx, target);
await AvatarClear(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, confirmYes);
}
public async Task ChangeWebhookAvatar(Context ctx, PKMember target, ParsedImage avatar)
{
var guildData = await GetWebhookAvatarGuildData(ctx, target);
await AvatarChange(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, avatar);
}
private Task PrintResponse(MemberAvatarLocation location, Context ctx, PKMember target, ParsedImage avatar,

File diff suppressed because it is too large Load diff

View file

@ -6,133 +6,120 @@ namespace PluralKit.Bot;
public class MemberProxy
{
public async Task Proxy(Context ctx, PKMember target)
public async Task ShowProxy(Context ctx, PKMember target)
{
if (target.ProxyTags.Count == 0)
await ctx.Reply("This member does not have any proxy tags.");
else
await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}");
}
public async Task ClearProxy(Context ctx, PKMember target, bool confirmYes = false)
{
ctx.CheckSystem().CheckOwnMember(target);
ProxyTag ParseProxyTags(string exampleProxy)
// If we already have multiple tags, this would clear everything, so prompt that
if (target.ProxyTags.Count > 1)
{
// // Make sure there's one and only one instance of "text" in the example proxy given
var prefixAndSuffix = exampleProxy.Split("text");
if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].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 query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing";
var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync<PKMember>(query,
new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList();
if (conflicts.Count <= 0) return true;
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
var msg = $"{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, "Proceed");
}
// "Sub"command: clear flag
if (ctx.MatchClear())
{
// If we already have multiple tags, this would clear everything, so prompt that
if (target.ProxyTags.Count > 1)
{
var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?";
if (!await ctx.PromptYesNo(msg, "Clear"))
throw Errors.GenericCancelled();
}
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0]) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Proxy tags cleared.");
}
// "Sub"command: no arguments; will print proxy tags
else if (!ctx.HasNext(false))
{
if (target.ProxyTags.Count == 0)
await ctx.Reply("This member does not have any proxy tags.");
else
await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}");
}
// Subcommand: "add"
else if (ctx.Match("add", "append"))
{
if (!ctx.HasNext(false))
throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (target.ProxyTags.Contains(tagToAdd))
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError(
$"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(tagToAdd))
var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?";
if (!await ctx.PromptYesNo(msg, "Clear", flagValue: confirmYes))
throw Errors.GenericCancelled();
var newTags = target.ProxyTags.ToList();
newTags.Add(tagToAdd);
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters).");
}
// Subcommand: "remove"
else if (ctx.Match("remove", "delete"))
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0]) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Proxy tags cleared.");
}
public async Task AddProxy(Context ctx, PKMember target, string proxyString, bool confirmYes = false)
{
ctx.CheckSystem().CheckOwnMember(target);
var tagToAdd = ParseProxyTag(proxyString);
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (target.ProxyTags.Contains(tagToAdd))
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError(
$"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(ctx, target, tagToAdd, confirmYes))
throw Errors.GenericCancelled();
var newTags = target.ProxyTags.ToList();
newTags.Add(tagToAdd);
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters).");
}
public async Task RemoveProxy(Context ctx, PKMember target, string proxyString)
{
ctx.CheckSystem().CheckOwnMember(target);
var tagToRemove = ParseProxyTag(proxyString);
if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (!target.ProxyTags.Contains(tagToRemove))
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
var newTags = target.ProxyTags.ToList();
newTags.Remove(tagToRemove);
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}.");
}
public async Task SetProxy(Context ctx, PKMember target, string proxyString, bool confirmYes = false)
{
ctx.CheckSystem().CheckOwnMember(target);
var requestedTag = ParseProxyTag(proxyString);
if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (target.ProxyTags.Count > 1)
{
if (!ctx.HasNext(false))
throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`).");
var remainder = ctx.RemainderOrNull(false);
var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing());
if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
if (!target.ProxyTags.Contains(tagToRemove))
{
// Legacy support for when line endings weren't normalized
tagToRemove = ParseProxyTags(remainder);
if (!target.ProxyTags.Contains(tagToRemove))
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
}
var newTags = target.ProxyTags.ToList();
newTags.Remove(tagToRemove);
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}.");
}
// Subcommand: bare proxy tag given
else
{
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
// This is mostly a legacy command, so it's gonna warn if there's
// already more than one proxy tag.
if (target.ProxyTags.Count > 1)
{
var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?";
if (!await ctx.PromptYesNo(msg, "Replace"))
throw Errors.GenericCancelled();
}
if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError(
$"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(requestedTag))
var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?";
if (!await ctx.PromptYesNo(msg, "Replace", flagValue: confirmYes))
throw Errors.GenericCancelled();
var newTags = new[] { requestedTag };
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters).");
}
if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError(
$"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(ctx, target, requestedTag, confirmYes))
throw Errors.GenericCancelled();
var newTags = new[] { requestedTag };
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags) };
await ctx.Repository.UpdateMember(target.Id, patch);
await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters).");
}
private ProxyTag ParseProxyTag(string proxyString)
{
// Make sure there's one and only one instance of "text" in the example proxy given
var prefixAndSuffix = proxyString.Split("text");
if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT");
if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText;
if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText;
return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]);
}
private async Task<bool> WarnOnConflict(Context ctx, PKMember target, ProxyTag newTag, bool confirmYes = false)
{
var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing";
var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync<PKMember>(query,
new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList();
if (conflicts.Count <= 0) return true;
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
var msg = $"{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, "Proceed", flagValue: confirmYes);
}
}

View file

@ -58,16 +58,14 @@ public class ProxiedMessage
_redisService = redisService;
}
public async Task ReproxyMessage(Context ctx)
public async Task ReproxyMessage(Context ctx, Message.Reference? messageRef, PKMember target)
{
var (msg, systemId) = await GetMessageToEdit(ctx, ReproxyTimeout, true);
var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId ?? ctx.GetRepliedTo()?.MessageId, ReproxyTimeout, true);
if (ctx.System.Id != systemId)
throw new PKError("Can't reproxy a message sent by a different system.");
// Get target member ID
var target = await ctx.MatchMember(restrictToSystem: ctx.System.Id);
if (target == null)
if (target == null || target.System != ctx.System.Id)
throw new PKError("Could not find a member to reproxy the message with.");
// Fetch members and get the ProxyMember for `target`
@ -93,9 +91,9 @@ public class ProxiedMessage
}
}
public async Task EditMessage(Context ctx, bool useRegex)
public async Task EditMessage(Context ctx, Message.Reference? messageRef, string? newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments)
{
var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false);
var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId ?? ctx.GetRepliedTo()?.MessageId, EditTimeout, false);
if (ctx.System.Id != systemId)
throw new PKError("Can't edit a message sent by a different system.");
@ -104,21 +102,12 @@ public class ProxiedMessage
if (originalMsg == null)
throw new PKError("Could not edit message.");
// Regex flag
useRegex = useRegex || ctx.MatchFlag("regex", "x");
// Check if we should append or prepend
var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " ";
var append = ctx.MatchFlag("append", "a");
var prepend = ctx.MatchFlag("prepend", "p");
var mutateSpace = noSpace ? "" : " ";
// Grab the original message content and new message content
var originalContent = originalMsg.Content;
var newContent = ctx.RemainderOrNull()?.NormalizeLineEndSpacing();
// Should we clear embeds?
var clearEmbeds = ctx.MatchFlag("clear-embed", "ce");
var clearAttachments = ctx.MatchFlag("clear-attachments", "ca");
if ((clearEmbeds || clearAttachments) && newContent == null)
newContent = originalMsg.Content!;
@ -249,14 +238,13 @@ public class ProxiedMessage
}
}
private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, Duration timeout, bool isReproxy)
private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, ulong? referencedMessage, Duration timeout, bool isReproxy)
{
var editType = isReproxy ? "reproxy" : "edit";
var editTypeAction = isReproxy ? "reproxied" : "edited";
PKMessage? msg = null;
var (referencedMessage, _) = ctx.MatchMessage(false);
if (referencedMessage != null)
{
await using var conn = await ctx.Database.Obtain();
@ -332,22 +320,20 @@ public class ProxiedMessage
return lastMessage;
}
public async Task GetMessage(Context ctx)
public async Task GetMessage(Context ctx, Message.Reference? messageRef, ReplyFormat format, bool isDelete, bool author, bool showEmbed)
{
var (messageId, _) = ctx.MatchMessage(true);
if (messageId == null)
if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null)
messageRef = ctx.Message.MessageReference;
if (messageRef == null || messageRef.MessageId == null)
{
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a message ID or link.");
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
throw new PKSyntaxError("You must pass a message ID or link.");
}
var isDelete = ctx.Match("delete") || ctx.MatchFlag("delete");
var message = await ctx.Repository.GetFullMessage(messageId.Value);
var message = await ctx.Repository.GetFullMessage(messageRef.MessageId.Value);
if (message == null)
{
await GetCommandMessage(ctx, messageId.Value, isDelete);
await GetCommandMessage(ctx, messageRef.MessageId.Value, isDelete, showEmbed);
return;
}
@ -360,8 +346,6 @@ public class ProxiedMessage
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
var format = ctx.MatchFormat();
if (format != ReplyFormat.Standard)
{
var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid);
@ -423,10 +407,10 @@ public class ProxiedMessage
return;
}
if (ctx.Match("author") || ctx.MatchFlag("author"))
if (author)
{
var user = await _rest.GetUser(message.Message.Sender);
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
var eb = new EmbedBuilder()
.Author(new Embed.EmbedAuthor(
@ -446,7 +430,7 @@ public class ProxiedMessage
return;
}
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
return;
@ -455,7 +439,7 @@ public class ProxiedMessage
await ctx.Reply(components: await _embeds.CreateMessageInfoMessageComponents(message, showContent, ctx.Config));
}
private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete)
private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete, bool showEmbed)
{
var msg = await _repo.GetCommandMessage(messageId);
if (msg == null)
@ -484,7 +468,7 @@ public class ProxiedMessage
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
return;

View file

@ -15,7 +15,7 @@ public class Random
// todo: get postgresql to return one random member/group instead of querying all members/groups
public async Task Member(Context ctx, PKSystem target)
public async Task Member(Context ctx, PKSystem target, bool all, bool showEmbed = false)
{
if (target == null)
throw Errors.NoSystemError(ctx.DefaultPrefix);
@ -24,7 +24,7 @@ public class Random
var members = await ctx.Repository.GetSystemMembers(target.Id).ToListAsync();
if (!ctx.MatchFlag("all", "a"))
if (!all)
members = members.Where(m => m.MemberVisibility == PrivacyLevel.Public).ToList();
else
ctx.CheckOwnSystem(target);
@ -37,7 +37,7 @@ public class Random
var randInt = randGen.Next(members.Count);
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
@ -49,7 +49,7 @@ public class Random
components: await _embeds.CreateMemberMessageComponents(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone));
}
public async Task Group(Context ctx, PKSystem target)
public async Task Group(Context ctx, PKSystem target, bool all, bool showEmbed = false)
{
if (target == null)
throw Errors.NoSystemError(ctx.DefaultPrefix);
@ -57,7 +57,7 @@ public class Random
ctx.CheckSystemPrivacy(target.Id, target.GroupListPrivacy);
var groups = await ctx.Repository.GetSystemGroups(target.Id).ToListAsync();
if (!ctx.MatchFlag("all", "a"))
if (!all)
groups = groups.Where(g => g.Visibility == PrivacyLevel.Public).ToList();
else
ctx.CheckOwnSystem(target);
@ -70,23 +70,23 @@ public class Random
var randInt = randGen.Next(groups.Count());
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,
embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt]));
embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt], all));
return;
}
await ctx.Reply(
components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt]));
components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt], all));
}
public async Task GroupMember(Context ctx, PKGroup group)
public async Task GroupMember(Context ctx, PKGroup group, bool all, bool show_embed, IHasListOptions flags)
{
ctx.CheckSystemPrivacy(group.System, group.ListPrivacy);
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System));
var opts = flags.GetListOptions(ctx, group.System);
opts.GroupFilter = group.Id;
var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions()));
@ -96,7 +96,7 @@ public class Random
"This group has no members!"
+ (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : ""));
if (!ctx.MatchFlag("all", "a"))
if (!all)
members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public);
else
ctx.CheckOwnGroup(group);
@ -112,7 +112,7 @@ public class Random
var randInt = randGen.Next(ms.Count);
if (ctx.MatchFlag("show-embed", "se"))
if (show_embed)
{
await ctx.Reply(
text: EmbedService.LEGACY_EMBED_WARNING,

View file

@ -110,34 +110,27 @@ public class ServerConfig
);
}
public async Task SetLogChannel(Context ctx)
public async Task ShowLogChannel(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var settings = await ctx.Repository.GetGuild(ctx.Guild.Id);
if (ctx.MatchClear() && await ctx.ConfirmClear("the server log channel"))
if (settings.LogChannel == null)
{
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogChannel = null });
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
await ctx.Reply("This server does not have a log channel set.");
return;
}
if (!ctx.HasNext())
{
if (settings.LogChannel == null)
{
await ctx.Reply("This server does not have a log channel set.");
return;
}
await ctx.Reply($"This server's log channel is currently set to <#{settings.LogChannel}>.");
}
await ctx.Reply($"This server's log channel is currently set to <#{settings.LogChannel}>.");
return;
}
public async Task SetLogChannel(Context ctx, Channel channel)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (channel.GuildId != ctx.Guild.Id)
throw Errors.ChannelNotFound(channel.Id.ToString());
Channel channel = null;
var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildPublicThread && channel.Type != Channel.ChannelType.GuildPrivateThread)
throw new PKError("PluralKit cannot log messages to this type of channel.");
@ -151,46 +144,18 @@ public class ServerConfig
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to <#{channel.Id}>.");
}
// legacy behaviour: enable/disable logging for commands
// new behaviour is add/remove from log blacklist (see #LogBlacklistNew)
public async Task SetLogEnabled(Context ctx, bool enable)
public async Task ClearLogChannel(Context ctx, bool confirmYes)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
.Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else
while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
if (!await ctx.ConfirmClear("the server log channel", confirmYes))
return;
ulong? logChannel = null;
var config = await ctx.Repository.GetGuild(ctx.Guild.Id);
logChannel = config.LogChannel;
var blacklist = config.LogBlacklist.ToHashSet();
if (enable)
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
else
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} Message logging for the given channels {(enable ? "enabled" : "disabled")}." +
(logChannel == null
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`."
: ""));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogChannel = null });
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
}
public async Task ShowProxyBlacklisted(Context ctx)
public async Task ShowProxyBlacklist(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
@ -240,14 +205,73 @@ public class ServerConfig
});
}
public async Task ShowLogDisabledChannels(Context ctx)
public async Task AddProxyBlacklist(Context ctx, Channel? channel, bool all)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (all)
{
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
}
else if (channel != null)
{
if (channel.GuildId != ctx.Guild.Id)
throw Errors.ChannelNotFound(channel.Id.ToString());
affectedChannels.Add(channel);
}
else
{
throw new PKSyntaxError("You must specify a channel or use the --all flag.");
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.Blacklist.ToHashSet();
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() });
await ctx.Reply($"{Emojis.Success} {(all ? "All channels" : "Channel")} added to the proxy blacklist.");
}
public async Task RemoveProxyBlacklist(Context ctx, Channel? channel, bool all)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (all)
{
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
}
else if (channel != null)
{
if (channel.GuildId != ctx.Guild.Id)
throw Errors.ChannelNotFound(channel.Id.ToString());
affectedChannels.Add(channel);
}
else
{
throw new PKSyntaxError("You must specify a channel or use the --all flag.");
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.Blacklist.ToHashSet();
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() });
await ctx.Reply($"{Emojis.Success} {(all ? "All channels" : "Channel")} removed from the proxy blacklist.");
}
public async Task ShowLogBlacklist(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var config = await ctx.Repository.GetGuild(ctx.Guild.Id);
// Resolve all channels from the cache and order by position
// todo: GetAllChannels?
var channels = (await Task.WhenAll(config.LogBlacklist
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
.Where(c => c != null)
@ -291,78 +315,75 @@ public class ServerConfig
});
}
public async Task SetProxyBlacklisted(Context ctx, bool shouldAdd)
public async Task AddLogBlacklist(Context ctx, Channel? channel, bool all)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
if (all)
{
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
// All the channel types you can proxy in
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
}
else if (channel != null)
{
if (channel.GuildId != ctx.Guild.Id)
throw Errors.ChannelNotFound(channel.Id.ToString());
affectedChannels.Add(channel);
}
else
while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
{
throw new PKSyntaxError("You must specify a channel or use the --all flag.");
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.Blacklist.ToHashSet();
if (shouldAdd)
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
else
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist.");
}
public async Task SetLogBlacklisted(Context ctx, bool shouldAdd)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
// All the channel types you can proxy in
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else
while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.LogBlacklist.ToHashSet();
if (shouldAdd)
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
else
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the logging blacklist." +
$"{Emojis.Success} {(all ? "All channels" : "Channel")} added to the logging blacklist." +
(guild.LogChannel == null
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`."
: ""));
}
public async Task SetLogCleanup(Context ctx)
public async Task RemoveLogBlacklist(Context ctx, Channel? channel, bool all)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (all)
{
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
}
else if (channel != null)
{
if (channel.GuildId != ctx.Guild.Id)
throw Errors.ChannelNotFound(channel.Id.ToString());
affectedChannels.Add(channel);
}
else
{
throw new PKSyntaxError("You must specify a channel or use the --all flag.");
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.LogBlacklist.ToHashSet();
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} {(all ? "All channels" : "Channel")} removed from the logging blacklist." +
(guild.LogChannel == null
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`."
: ""));
}
public async Task ShowLogCleanup(Context ctx)
{
var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant()));
var eb = new EmbedBuilder()
@ -377,74 +398,77 @@ public class ServerConfig
}
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
bool? newValue = ctx.MatchToggleOrNull();
if (newValue == null)
{
if (ctx.GuildConfig!.LogCleanupEnabled)
eb.Description(
$"Log cleanup is currently **on** for this server. To disable it, type `{ctx.DefaultPrefix}serverconfig logclean off`.");
else
eb.Description(
$"Log cleanup is currently **off** for this server. To enable it, type `{ctx.DefaultPrefix}serverconfig logclean on`.");
await ctx.Reply(embed: eb.Build());
return;
}
if (ctx.GuildConfig!.LogCleanupEnabled)
eb.Description(
$"Log cleanup is currently **on** for this server. To disable it, type `{ctx.DefaultPrefix}serverconfig logclean off`.");
else
eb.Description(
$"Log cleanup is currently **off** for this server. To enable it, type `{ctx.DefaultPrefix}serverconfig logclean on`.");
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = newValue.Value });
await ctx.Reply(embed: eb.Build());
}
if (newValue.Value)
public async Task SetLogCleanup(Context ctx, bool value)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant()));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = value });
if (value)
await ctx.Reply(
$"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts.");
else
await ctx.Reply($"{Emojis.Success} Log cleanup has been **disabled** for this server.");
}
public async Task InvalidCommandResponse(Context ctx)
public async Task ShowInvalidCommandResponse(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (!ctx.HasNext())
{
var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = newVal });
await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(newVal)}.");
var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**.";
await ctx.Reply(msg);
}
public async Task RequireSystemTag(Context ctx)
public async Task SetInvalidCommandResponse(Context ctx, bool value)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (!ctx.HasNext())
{
var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server.";
await ctx.Reply(msg);
return;
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = newVal });
await ctx.Reply($"System tags are now **{(newVal ? "required" : "not required")}** for PluralKit users in this server.");
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = value });
await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(value)}.");
}
public async Task SuppressNotifications(Context ctx)
public async Task ShowRequireSystemTag(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (!ctx.HasNext())
{
var msg = $"Suppressing notifications for proxied messages is currently **{EnabledDisabled(ctx.GuildConfig!.SuppressNotifications)}**.";
await ctx.Reply(msg);
return;
}
var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server.";
await ctx.Reply(msg);
}
var newVal = ctx.MatchToggle(false);
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { SuppressNotifications = newVal });
await ctx.Reply($"Suppressing notifications for proxied messages is now {EnabledDisabled(newVal)}.");
public async Task SetRequireSystemTag(Context ctx, bool value)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = value });
await ctx.Reply($"System tags are now **{(value ? "required" : "not required")}** for PluralKit users in this server.");
}
public async Task ShowSuppressNotifications(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var msg = $"Suppressing notifications for proxied messages is currently **{EnabledDisabled(ctx.GuildConfig!.SuppressNotifications)}**.";
await ctx.Reply(msg);
}
public async Task SetSuppressNotifications(Context ctx, bool value)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { SuppressNotifications = value });
await ctx.Reply($"Suppressing notifications for proxied messages is now {EnabledDisabled(value)}.");
}
}

View file

@ -8,11 +8,10 @@ namespace PluralKit.Bot;
public class Switch
{
public async Task SwitchDo(Context ctx)
public async Task SwitchDo(Context ctx, ICollection<PKMember> members)
{
ctx.CheckSystem();
var members = await ctx.ParseMemberList(ctx.System.Id);
await DoSwitchCommand(ctx, members);
}
@ -21,11 +20,12 @@ public class Switch
ctx.CheckSystem();
// Switch with no members = switch-out
await DoSwitchCommand(ctx, new PKMember[] { });
await DoSwitchCommand(ctx, []);
}
private async Task DoSwitchCommand(Context ctx, ICollection<PKMember> members)
private async Task DoSwitchCommand(Context ctx, ICollection<PKMember>? members)
{
if (members == null) members = new List<PKMember>();
// Make sure there are no dupes in the list
// We do this by checking if removing duplicate member IDs results in a list of different length
if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers;
@ -57,16 +57,14 @@ public class Switch
$"{Emojis.Success} Switch registered. Current fronters are now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}.");
}
public async Task SwitchMove(Context ctx)
public async Task SwitchMove(Context ctx, string str, bool confirmYes = false)
{
ctx.CheckSystem();
var timeToMove = ctx.RemainderOrNull() ??
throw new PKSyntaxError("Must pass a date or time to move the switch to.");
var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC");
var result = DateUtils.ParseDateTime(timeToMove, true, tz);
if (result == null) throw Errors.InvalidDateTime(timeToMove);
var result = DateUtils.ParseDateTime(str, true, tz);
if (result == null) throw Errors.InvalidDateTime(str);
var time = result.Value;
@ -97,18 +95,18 @@ public class Switch
// yeet
var msg =
$"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from <t:{lastSwitchTime}> ({lastSwitchDeltaStr} ago) to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago). Is this OK?";
if (!await ctx.PromptYesNo(msg, "Move Switch")) throw Errors.SwitchMoveCancelled;
if (!await ctx.PromptYesNo(msg, "Move Switch", flagValue: confirmYes)) throw Errors.SwitchMoveCancelled;
// aaaand *now* we do the move
await ctx.Repository.MoveSwitch(lastTwoSwitches[0].Id, time.ToInstant());
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
}
public async Task SwitchEdit(Context ctx, bool newSwitch = false)
public async Task SwitchEdit(Context ctx, List<PKMember>? newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false, bool confirmYes = false)
{
ctx.CheckSystem();
var newMembers = await ctx.ParseMemberList(ctx.System.Id);
if (newMembers == null) newMembers = new List<PKMember>();
await using var conn = await ctx.Database.Obtain();
var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id);
@ -116,24 +114,24 @@ public class Switch
throw Errors.NoRegisteredSwitches;
var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask();
if (ctx.MatchFlag("first", "f"))
if (first)
newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers);
else if (ctx.MatchFlag("remove", "r"))
else if (remove)
newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("append", "a"))
else if (append)
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
else if (ctx.MatchFlag("prepend", "p"))
else if (prepend)
newMembers = PrependToSwitch(newMembers, currentSwitchMembers);
if (newSwitch)
{
// if there's no edit flag, assume we're appending
if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p"))
if (!prepend && !append && !remove && !first)
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
await DoSwitchCommand(ctx, newMembers);
}
else
await DoEditCommand(ctx, newMembers);
await DoEditCommand(ctx, newMembers, confirmYes);
}
public List<PKMember> PrependToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
@ -169,14 +167,16 @@ public class Switch
return members;
}
public async Task SwitchEditOut(Context ctx)
public async Task SwitchEditOut(Context ctx, bool confirmYes)
{
ctx.CheckSystem();
await DoEditCommand(ctx, new PKMember[] { });
await DoEditCommand(ctx, [], confirmYes);
}
public async Task DoEditCommand(Context ctx, ICollection<PKMember> members)
public async Task DoEditCommand(Context ctx, ICollection<PKMember>? members, bool confirmYes)
{
if (members == null) members = new List<PKMember>();
// Make sure there are no dupes in the list
// We do this by checking if removing duplicate member IDs results in a list of different length
if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers;
@ -203,7 +203,7 @@ public class Switch
msg = $"{Emojis.Warn} This will turn the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago) into a switch-out. Is this okay?";
else
msg = $"{Emojis.Warn} This will change the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago) to {newSwitchMemberStr}. Is this okay?";
if (!await ctx.PromptYesNo(msg, "Edit")) throw Errors.SwitchEditCancelled;
if (!await ctx.PromptYesNo(msg, "Edit", flagValue: confirmYes)) throw Errors.SwitchEditCancelled;
// Actually edit the switch
await ctx.Repository.EditSwitch(conn, lastSwitch.Id, members.Select(m => m.Id).ToList());
@ -217,16 +217,16 @@ public class Switch
await ctx.Reply($"{Emojis.Success} Switch edited. Current fronters are now {newSwitchMemberStr}.");
}
public async Task SwitchDelete(Context ctx)
public async Task SwitchDelete(Context ctx, bool all = false, bool confirmYes = false)
{
ctx.CheckSystem();
if (ctx.Match("all", "clear") || ctx.MatchFlag("all", "clear", "c"))
if (all)
{
// Subcommand: "delete all"
var purgeMsg =
$"{Emojis.Warn} This will delete *all registered switches* in your system. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(purgeMsg, "Clear Switches"))
if (!await ctx.PromptYesNo(purgeMsg, "Clear Switches", flagValue: confirmYes))
throw Errors.GenericCancelled();
await ctx.Repository.DeleteAllSwitches(ctx.System.Id);
await ctx.Reply($"{Emojis.Success} Cleared system switches!");
@ -258,7 +258,7 @@ public class Switch
msg = $"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr} ({secondSwitchDeltaStr} ago). Is this okay?";
}
if (!await ctx.PromptYesNo(msg, "Delete Switch")) throw Errors.SwitchDeleteCancelled;
if (!await ctx.PromptYesNo(msg, "Delete Switch", flagValue: confirmYes)) throw Errors.SwitchDeleteCancelled;
await ctx.Repository.DeleteSwitch(lastTwoSwitches[0].Id);
await ctx.Reply($"{Emojis.Success} Switch deleted.");

View file

@ -14,23 +14,22 @@ public class System
_embeds = embeds;
}
public async Task Query(Context ctx, PKSystem system)
public async Task Query(Context ctx, PKSystem system, bool all, bool @public, bool @private, bool showEmbed = false)
{
if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
if (ctx.MatchFlag("show-embed", "se"))
if (showEmbed)
{
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id)));
await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id), all));
return;
}
await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id)));
await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id), all));
}
public async Task New(Context ctx)
public async Task New(Context ctx, string? systemName)
{
ctx.CheckNoSystem();
var systemName = ctx.RemainderOrNull();
if (systemName != null && systemName.Length > Limits.MaxSystemNameLength)
throw Errors.StringTooLongError("System name", systemName.Length, Limits.MaxSystemNameLength);

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ public class SystemFront
_embeds = embeds;
}
public async Task SystemFronter(Context ctx, PKSystem system)
public async Task Fronter(Context ctx, PKSystem system)
{
if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
ctx.CheckSystemPrivacy(system.Id, system.FrontPrivacy);
@ -26,11 +26,11 @@ public class SystemFront
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, ctx.Zone, ctx.LookupContextFor(system.Id)));
}
public async Task SystemFrontHistory(Context ctx, PKSystem system)
public async Task FrontHistory(Context ctx, PKSystem system, bool showMemberId, bool clear = false)
{
if (ctx.MatchFlag("clear", "c") || ctx.PeekArgument() == "clear")
if (clear)
{
await new Switch().SwitchDelete(ctx);
await new Switch().SwitchDelete(ctx, true);
return;
}
@ -55,8 +55,6 @@ public class SystemFront
embedTitle = $"Front history of {guildSettings.DisplayName} (`{system.Hid}`)";
}
var showMemberId = ctx.MatchFlag("with-id", "wid");
await ctx.Paginate(
sws,
totalSwitches,
@ -106,7 +104,7 @@ public class SystemFront
);
}
public async Task FrontPercent(Context ctx, PKSystem? system = null, PKGroup? group = null)
public async Task FrontPercent(Context ctx, PKSystem? system, string? durationStr, bool ignoreNoFronters = false, bool showFlat = false, PKGroup? group = null)
{
if (system == null && group == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
if (system == null) system = await GetGroupSystem(ctx, group);
@ -116,10 +114,8 @@ public class SystemFront
var totalSwitches = await ctx.Repository.GetSwitchCount(system.Id);
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
var ignoreNoFronters = ctx.MatchFlag("fo", "fronters-only");
var showFlat = ctx.MatchFlag("flat");
var durationStr = ctx.RemainderOrNull() ?? "30d";
if (durationStr == null)
durationStr = "30d";
// Picked the UNIX epoch as a random date
// even though we don't store switch timestamps in UNIX time

View file

@ -1,4 +1,5 @@
using Myriad.Extensions;
using Myriad.Types;
using PluralKit.Core;
@ -6,12 +7,10 @@ namespace PluralKit.Bot;
public class SystemLink
{
public async Task LinkSystem(Context ctx)
public async Task LinkSystem(Context ctx, User account, bool confirmYes = false)
{
ctx.CheckSystem();
var account = await ctx.MatchUser() ??
throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
var accountIds = await ctx.Repository.GetSystemAccounts(ctx.System.Id);
if (accountIds.Contains(account.Id))
throw Errors.AccountAlreadyLinked;
@ -21,17 +20,17 @@ public class SystemLink
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config, ctx.DefaultPrefix);
var msg = $"{account.Mention()}, please confirm the link.";
if (!await ctx.PromptYesNo(msg, "Confirm", account, false)) throw Errors.MemberLinkCancelled;
if (!await ctx.PromptYesNo(msg, "Confirm", account, true, confirmYes)) throw Errors.MemberLinkCancelled;
await ctx.Repository.AddAccount(ctx.System.Id, account.Id);
await ctx.Reply($"{Emojis.Success} Account linked to system.");
}
public async Task UnlinkAccount(Context ctx)
public async Task UnlinkAccount(Context ctx, string idRaw, bool confirmYes)
{
ctx.CheckSystem();
ulong id;
if (!ctx.MatchUserRaw(out id))
if (!idRaw.TryParseMention(out id))
throw new PKSyntaxError("You must pass an account to unlink from (either ID or @mention).");
var accountIds = (await ctx.Repository.GetSystemAccounts(ctx.System.Id)).ToList();
@ -39,7 +38,7 @@ public class SystemLink
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount(ctx.DefaultPrefix);
var msg = $"Are you sure you want to unlink <@{id}> from your system?";
if (!await ctx.PromptYesNo(msg, "Unlink")) throw Errors.MemberUnlinkCancelled;
if (!await ctx.PromptYesNo(msg, "Unlink", flagValue: confirmYes)) throw Errors.MemberUnlinkCancelled;
await ctx.Repository.RemoveAccount(ctx.System.Id, id);
await ctx.Reply($"{Emojis.Success} Account unlinked.");

View file

@ -8,16 +8,20 @@ namespace PluralKit.Bot;
public class SystemList
{
public async Task MemberList(Context ctx, PKSystem target)
public async Task MemberList(Context ctx, PKSystem target, string? query, IHasListOptions flags)
{
ctx.CheckSystem(target);
if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix);
ctx.CheckSystemPrivacy(target.Id, target.MemberListPrivacy);
var opts = flags.GetListOptions(ctx, target.Id);
opts.Search = query;
// explanation of privacy lookup here:
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
// - RenderMemberList checks the indivual privacy for each member (NameFor, etc)
// the own system is always allowed to look up their list
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id));
await ctx.RenderMemberList(
ctx.LookupContextFor(target.Id),
target.Id,

View file

@ -140,7 +140,40 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null;
await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes));
// parse parameters
Parameters parameters;
try
{
parameters = new Parameters(evt.Content?.Substring(0, cmdStart), evt.Content?.Substring(cmdStart));
}
catch (PKError e)
{
// don't send an "invalid command" response if the guild has those turned off
// TODO: only dont send command not found, not every parse error (eg. missing params, syntax error...)
if (!(guildConfig != null && guildConfig!.InvalidCommandResponseEnabled != true))
{
await _rest.CreateMessage(channel.Id, new MessageRequest
{
Content = $"{Emojis.Error} {e.Message}",
});
}
throw;
}
var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters);
Commands command;
try
{
command = await Commands.FromContext(ctx);
}
catch (PKError e)
{
await ctx.Reply($"{Emojis.Error} {e.Message}");
throw;
}
await _tree.ExecuteCommand(ctx, command);
}
catch (PKError)
{

View file

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>annotations</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View file

@ -43,7 +43,7 @@ public class EmbedService
return Task.WhenAll(ids.Select(Inner));
}
public async Task<MessageComponent[]> CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx)
public async Task<MessageComponent[]> CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx, bool countctxByOwner)
{
// Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(system.Id);
@ -55,7 +55,7 @@ public class EmbedService
};
var countctx = LookupContext.ByNonOwner;
if (cctx.MatchFlag("a", "all"))
if (countctxByOwner)
{
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
@ -206,14 +206,14 @@ public class EmbedService
];
}
public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx)
public async Task<Embed> CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx, bool countctxByOwner)
{
// Fetch/render info for all accounts simultaneously
var accounts = await _repo.GetSystemAccounts(system.Id);
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})");
var countctx = LookupContext.ByNonOwner;
if (cctx.MatchFlag("a", "all"))
if (countctxByOwner)
{
if (system.Id == cctx.System?.Id)
countctx = LookupContext.ByOwner;
@ -560,7 +560,7 @@ public class EmbedService
return eb.Build();
}
public async Task<MessageComponent[]> CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target)
public async Task<MessageComponent[]> CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target, bool all)
{
var pctx = ctx.LookupContextFor(system.Id);
var name = target.NameFor(ctx);
@ -568,7 +568,7 @@ public class EmbedService
var systemName = (ctx.Guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx);
var countctx = LookupContext.ByNonOwner;
if (ctx.MatchFlag("a", "all"))
if (all)
{
if (system.Id == ctx.System?.Id)
countctx = LookupContext.ByOwner;
@ -673,12 +673,12 @@ public class EmbedService
];
}
public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
public async Task<Embed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target, bool all)
{
var pctx = ctx.LookupContextFor(system.Id);
var countctx = LookupContext.ByNonOwner;
if (ctx.MatchFlag("a", "all"))
if (all)
{
if (system.Id == ctx.System?.Id)
countctx = LookupContext.ByOwner;

View file

@ -16,17 +16,17 @@ namespace PluralKit.Bot;
public static class ContextUtils
{
public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
public static async Task<bool> ConfirmClear(this Context ctx, string toClear, bool confirmYes)
{
if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear"))
if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear", flagValue: confirmYes))
throw Errors.GenericCancelled();
return true;
}
public static async Task<bool> PromptYesNo(this Context ctx, string msgString, string acceptButton,
User user = null, bool matchFlag = true)
User user = null, bool matchFlag = true, bool flagValue = false)
{
if (matchFlag && ctx.MatchFlag("y", "yes")) return true;
if (matchFlag && flagValue) return true;
var prompt = new YesNoPrompt(ctx)
{