mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
Merge 498d657cd4 into 4a947c01fc
This commit is contained in:
commit
6299718aa0
87 changed files with 8932 additions and 4828 deletions
20
.github/workflows/dotnet-docker.yml
vendored
20
.github/workflows/dotnet-docker.yml
vendored
|
|
@ -1,23 +1,23 @@
|
|||
name: Build and push Docker image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/dotnet-docker.yml'
|
||||
- 'ci/Dockerfile.dotnet'
|
||||
- 'ci/dotnet-version.sh'
|
||||
- 'Myriad/**'
|
||||
- 'PluralKit.API/**'
|
||||
- 'PluralKit.Bot/**'
|
||||
- 'PluralKit.Core/**'
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/dotnet-docker.yml"
|
||||
- "ci/Dockerfile.dotnet"
|
||||
- "ci/dotnet-version.sh"
|
||||
- "Myriad/**"
|
||||
- "PluralKit.API/**"
|
||||
- "PluralKit.Bot/**"
|
||||
- "PluralKit.Core/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: '.net docker build'
|
||||
name: ".net docker build"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
if: github.repository == 'PluralKit/PluralKit'
|
||||
steps:
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
|
|
|
|||
35
.github/workflows/dotnet.yml
vendored
35
.github/workflows/dotnet.yml
vendored
|
|
@ -3,22 +3,22 @@ name: .net checks
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/dotnet.yml
|
||||
- 'Myriad/**'
|
||||
- 'PluralKit.API/**'
|
||||
- 'PluralKit.Bot/**'
|
||||
- 'PluralKit.Core/**'
|
||||
- .github/workflows/dotnet.yml
|
||||
- "Myriad/**"
|
||||
- "PluralKit.API/**"
|
||||
- "PluralKit.Bot/**"
|
||||
- "PluralKit.Core/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/dotnet.yml
|
||||
- 'Myriad/**'
|
||||
- 'PluralKit.API/**'
|
||||
- 'PluralKit.Bot/**'
|
||||
- 'PluralKit.Core/**'
|
||||
- .github/workflows/dotnet.yml
|
||||
- "Myriad/**"
|
||||
- "PluralKit.API/**"
|
||||
- "PluralKit.Bot/**"
|
||||
- "PluralKit.Core/**"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 'run .net tests'
|
||||
name: "run .net tests"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -30,6 +30,19 @@ jobs:
|
|||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ""
|
||||
|
||||
- name: install uniffi
|
||||
run: cargo install uniffi-bindgen-cs --git https://github.com/90-008/uniffi-bindgen-cs
|
||||
|
||||
- name: generate command parser bindings
|
||||
run: |
|
||||
cargo -Z unstable-options build --package commands --lib --release --artifact-dir obj/
|
||||
uniffi-bindgen-cs "obj/libcommands.so" --library --out-dir="./PluralKit.Bot"
|
||||
cargo run --package commands --bin write_cs_glue -- "./PluralKit.Bot"/commandtypes.cs
|
||||
|
||||
- name: Run automated tests
|
||||
run: dotnet test --configuration Release
|
||||
|
||||
|
|
|
|||
20
.github/workflows/rust-docker.yml
vendored
20
.github/workflows/rust-docker.yml
vendored
|
|
@ -1,22 +1,22 @@
|
|||
name: Build and push Rust service Docker images
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'crates/**'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/rust.yml'
|
||||
- 'ci/Dockerfile.rust'
|
||||
- 'ci/rust-docker-target.sh'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- "crates/**"
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/rust.yml"
|
||||
- "ci/Dockerfile.rust"
|
||||
- "ci/rust-docker-target.sh"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'rust docker build'
|
||||
name: "rust docker build"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
if: github.repository == 'PluralKit/PluralKit'
|
||||
steps:
|
||||
- uses: docker/login-action@v1
|
||||
if: ${{ !env.ACT }}
|
||||
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
# https://github.com/docker/build-push-action/issues/378
|
||||
context: .
|
||||
file: ci/Dockerfile.rust
|
||||
push: false
|
||||
push: false
|
||||
cache-from: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust
|
||||
cache-to: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust,mode=max
|
||||
outputs: .docker-bin
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ target/
|
|||
.idea/
|
||||
.run/
|
||||
.vscode/
|
||||
.zed/
|
||||
.mono/
|
||||
tags/
|
||||
.DS_Store
|
||||
|
|
@ -31,6 +32,8 @@ logs/
|
|||
.version
|
||||
recipe.json
|
||||
.docker-bin/
|
||||
PluralKit.Bot/commands.cs
|
||||
PluralKit.Bot/commandtypes.cs
|
||||
|
||||
# nix
|
||||
.nix-process-compose
|
||||
|
|
|
|||
1842
Cargo.lock
generated
1842
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./crates/*"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
|
|
|
|||
|
|
@ -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>.",
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
54
PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs
Normal file
54
PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
151
PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs
Normal file
151
PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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!!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?");
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>.");
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ public static class ListOptionsExt
|
|||
|
||||
// the check for multiple *sorting* property flags is done in SortProperty setter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum SortProperty
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>annotations</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y curl build-essential && \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
ENV RUSTFLAGS='-C link-arg=-s'
|
||||
|
||||
# Install uniffi-bindgen-cs
|
||||
RUN cargo install uniffi-bindgen-cs --git https://github.com/90-008/uniffi-bindgen-cs
|
||||
|
||||
# Restore/fetch dependencies excluding app code to make use of caching
|
||||
COPY PluralKit.sln /app/
|
||||
COPY Myriad/Myriad.csproj /app/Myriad/
|
||||
|
|
@ -13,8 +22,21 @@ COPY .git/ /app/.git
|
|||
COPY Serilog/ /app/Serilog/
|
||||
RUN dotnet restore PluralKit.sln
|
||||
|
||||
# Copy the rest of the code and build
|
||||
# Copy the rest of the code
|
||||
COPY . /app
|
||||
|
||||
# copy parser code
|
||||
COPY Cargo.toml /app/
|
||||
COPY Cargo.lock /app/
|
||||
|
||||
COPY crates/ /app/crates
|
||||
|
||||
# Generate command parser bindings
|
||||
RUN mkdir -p /app/bin && cargo -Z unstable-options build --package commands --lib --release --artifact-dir /app/bin/
|
||||
RUN uniffi-bindgen-cs "/app/bin/libcommands.so" --library --out-dir="/app/PluralKit.Bot"
|
||||
RUN cargo run --package commands --bin write_cs_glue -- "/app/PluralKit.Bot/commandtypes.cs"
|
||||
|
||||
# build bot
|
||||
RUN dotnet build -c Release -o bin
|
||||
|
||||
# Build runtime stage (doesn't include SDK)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ async fn verify(
|
|||
)
|
||||
.await?;
|
||||
|
||||
process::process_async(result.data, req.kind).await?;
|
||||
let _ = process::process_async(result.data, req.kind).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
7
crates/command_definitions/Cargo.toml
Normal file
7
crates/command_definitions/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "command_definitions"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
command_parser = { path = "../command_parser"}
|
||||
75
crates/command_definitions/src/admin.rs
Normal file
75
crates/command_definitions/src/admin.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use super::*;
|
||||
|
||||
pub fn admin() -> &'static str {
|
||||
"admin"
|
||||
}
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let admin = admin();
|
||||
|
||||
let abuselog = tokens!(admin, ("abuselog", ["al"]));
|
||||
let make_abuselog_cmds = |log_param: Parameter| {
|
||||
[
|
||||
command!(abuselog, ("show", ["s"]), log_param => format!("admin_abuselog_show_{}", log_param.name()))
|
||||
.help("Shows an abuse log entry"),
|
||||
command!(abuselog, ("flagdeny", ["fd"]), log_param, Optional(("value", Toggle)) => format!("admin_abuselog_flag_deny_{}", log_param.name()))
|
||||
.help("Sets the deny flag on an abuse log entry"),
|
||||
command!(abuselog, ("description", ["desc"]), log_param, Optional(Remainder(("desc", OpaqueString))) => format!("admin_abuselog_description_{}", log_param.name()))
|
||||
.flag(CLEAR)
|
||||
.flag(YES)
|
||||
.help("Sets the description of an abuse log entry"),
|
||||
command!(abuselog, ("adduser", ["au"]), log_param => format!("admin_abuselog_add_user_{}", log_param.name()))
|
||||
.help("Adds a user to an abuse log entry"),
|
||||
command!(abuselog, ("removeuser", ["ru"]), log_param => format!("admin_abuselog_remove_user_{}", log_param.name()))
|
||||
.help("Removes a user from an abuse log entry"),
|
||||
command!(abuselog, ("delete", ["d"]), log_param => format!("admin_abuselog_delete_{}", log_param.name()))
|
||||
.help("Deletes an abuse log entry"),
|
||||
]
|
||||
};
|
||||
let abuselog_cmds = [
|
||||
command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(Remainder(("description", OpaqueString))) => "admin_abuselog_create")
|
||||
.flag(("deny-boy-usage", ["deny"]))
|
||||
.help("Creates an abuse log entry")
|
||||
]
|
||||
.into_iter()
|
||||
.chain(make_abuselog_cmds(Skip(("account", UserRef)).into())) // falls through to log_id
|
||||
.chain(make_abuselog_cmds(("log_id", OpaqueString).into()));
|
||||
|
||||
[
|
||||
command!(admin, ("updatesystemid", ["usid"]), SystemRef, ("new_hid", OpaqueString) => "admin_update_system_id")
|
||||
.flag(YES)
|
||||
.help("Updates a system's ID"),
|
||||
command!(admin, ("updatememberid", ["umid"]), MemberRef, ("new_hid", OpaqueString) => "admin_update_member_id")
|
||||
.flag(YES)
|
||||
.help("Updates a member's ID"),
|
||||
command!(admin, ("updategroupid", ["ugid"]), GroupRef, ("new_hid", OpaqueString) => "admin_update_group_id")
|
||||
.flag(YES)
|
||||
.help("Updates a group's ID"),
|
||||
command!(admin, ("rerollsystemid", ["rsid"]), SystemRef => "admin_reroll_system_id")
|
||||
.flag(YES)
|
||||
.help("Rerolls a system's ID"),
|
||||
command!(admin, ("rerollmemberid", ["rmid"]), MemberRef => "admin_reroll_member_id")
|
||||
.flag(YES)
|
||||
.help("Rerolls a member's ID"),
|
||||
command!(admin, ("rerollgroupid", ["rgid"]), GroupRef => "admin_reroll_group_id")
|
||||
.flag(YES)
|
||||
.help("Rerolls a group's ID"),
|
||||
command!(admin, ("updatememberlimit", ["uml"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_member_limit")
|
||||
.flag(YES)
|
||||
.help("Updates a system's member limit"),
|
||||
command!(admin, ("updategrouplimit", ["ugl"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_group_limit")
|
||||
.flag(YES)
|
||||
.help("Updates a system's group limit"),
|
||||
command!(admin, ("systemrecover", ["sr"]), ("token", OpaqueString), ("account", UserRef) => "admin_system_recover")
|
||||
.flag(YES)
|
||||
.flag(("reroll-token", ["rt"]))
|
||||
.help("Recovers a system"),
|
||||
command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete")
|
||||
.help("Deletes a system"),
|
||||
command!(admin, ("sendmessage", ["sendmsg"]), ("account", UserRef), Remainder(("content", OpaqueString)) => "admin_send_message")
|
||||
.help("Sends a message to a user"),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(abuselog_cmds)
|
||||
.map(|cmd| cmd.show_in_suggestions(false))
|
||||
}
|
||||
9
crates/command_definitions/src/api.rs
Normal file
9
crates/command_definitions/src/api.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
[
|
||||
command!("token" => "token_display").help("Gets your system's API token"),
|
||||
command!("token", ("refresh", ["renew", "regen", "reroll"]) => "token_refresh")
|
||||
.help("Generates a new API token and invalidates the old one"),
|
||||
]
|
||||
}
|
||||
20
crates/command_definitions/src/autoproxy.rs
Normal file
20
crates/command_definitions/src/autoproxy.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use super::*;
|
||||
|
||||
pub fn autoproxy() -> (&'static str, [&'static str; 2]) {
|
||||
("autoproxy", ["ap", "auto"])
|
||||
}
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let ap = autoproxy();
|
||||
|
||||
[
|
||||
command!(ap => "autoproxy_show").help("Shows your current autoproxy settings"),
|
||||
command!(ap, ("off", ["stop", "cancel", "no", "disable", "remove"]) => "autoproxy_off")
|
||||
.help("Disables autoproxying for your system in the current server"),
|
||||
command!(ap, ("latch", ["last", "proxy", "stick", "sticky", "l"]) => "autoproxy_latch")
|
||||
.help("Sets your system's autoproxy in this server to proxy the last manually proxied member"),
|
||||
command!(ap, ("front", ["fronter", "switch", "f"]) => "autoproxy_front")
|
||||
.help("Sets your system's autoproxy in this server to proxy the first member currently registered as front"),
|
||||
command!(ap, MemberRef => "autoproxy_member").help("Sets your system's autoproxy in this server to proxy a specific member"),
|
||||
]
|
||||
}
|
||||
239
crates/command_definitions/src/config.rs
Normal file
239
crates/command_definitions/src/config.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use command_parser::parameter;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let cfg = ("config", ["cfg", "configure"]);
|
||||
|
||||
let base = [command!(cfg => "cfg_show").help("Shows the current configuration")];
|
||||
|
||||
let ap = tokens!(cfg, ("autoproxy", ["ap"]));
|
||||
let ap_account = tokens!(ap, ("account", ["ac"]));
|
||||
let ap_timeout = tokens!(ap, ("timeout", ["tm"]));
|
||||
let autoproxy = [
|
||||
command!(ap_account => "cfg_ap_account_show")
|
||||
.help("Shows autoproxy status for the account"),
|
||||
command!(ap_account, Toggle => "cfg_ap_account_update")
|
||||
.help("Toggles autoproxy globally for the current account"),
|
||||
command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"),
|
||||
command!(ap_timeout, RESET => "cfg_ap_timeout_reset")
|
||||
.help("Resets the autoproxy timeout"),
|
||||
command!(ap_timeout, parameter::Toggle::Off => "cfg_ap_timeout_off")
|
||||
.help("Disables the autoproxy timeout"),
|
||||
command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update")
|
||||
.help("Sets the latch timeout duration for your system"),
|
||||
];
|
||||
|
||||
let timezone_tokens = tokens!(cfg, ("timezone", ["zone", "tz"]));
|
||||
let timezone = [
|
||||
command!(timezone_tokens => "cfg_timezone_show").help("Shows the system timezone"),
|
||||
command!(timezone_tokens, RESET => "cfg_timezone_reset")
|
||||
.help("Resets the system timezone"),
|
||||
command!(timezone_tokens, ("timezone", OpaqueString) => "cfg_timezone_update")
|
||||
.flag(YES)
|
||||
.help("Changes your system's time zone"),
|
||||
];
|
||||
|
||||
let ping_tokens = tokens!(cfg, "ping");
|
||||
let ping = [
|
||||
command!(ping_tokens => "cfg_ping_show").help("Shows ping preferences"),
|
||||
command!(ping_tokens, Toggle => "cfg_ping_update")
|
||||
.help("Changes your system's ping preferences"),
|
||||
];
|
||||
|
||||
let priv_ = ("private", ["priv"]);
|
||||
let member_privacy = tokens!(cfg, priv_, ("member", ["mem"]));
|
||||
let member_privacy_short = tokens!(cfg, "mp");
|
||||
let group_privacy = tokens!(cfg, priv_, ("group", ["grp"]));
|
||||
let group_privacy_short = tokens!(cfg, "gp");
|
||||
let privacy = [
|
||||
command!(member_privacy => "cfg_member_privacy_show")
|
||||
.help("Shows the default privacy for new members"),
|
||||
command!(member_privacy, Toggle => "cfg_member_privacy_update")
|
||||
.help("Sets whether member privacy is automatically set to private when creating a new member"),
|
||||
command!(member_privacy_short => "cfg_member_privacy_show")
|
||||
.help("Shows the default privacy for new members"),
|
||||
command!(member_privacy_short, Toggle => "cfg_member_privacy_update")
|
||||
.help("Sets whether member privacy is automatically set to private when creating a new member"),
|
||||
command!(group_privacy => "cfg_group_privacy_show")
|
||||
.help("Shows the default privacy for new groups"),
|
||||
command!(group_privacy, Toggle => "cfg_group_privacy_update")
|
||||
.help("Sets whether group privacy is automatically set to private when creating a new group"),
|
||||
command!(group_privacy_short => "cfg_group_privacy_show")
|
||||
.help("Shows the default privacy for new groups"),
|
||||
command!(group_privacy_short, Toggle => "cfg_group_privacy_update")
|
||||
.help("Sets whether group privacy is automatically set to private when creating a new group"),
|
||||
];
|
||||
|
||||
let show = "show";
|
||||
let show_private = tokens!(cfg, show, priv_);
|
||||
let show_private_short = tokens!(cfg, "sp");
|
||||
let private_info = [
|
||||
command!(show_private => "cfg_show_private_info_show")
|
||||
.help("Shows whether private info is shown"),
|
||||
command!(show_private, Toggle => "cfg_show_private_info_update")
|
||||
.help("Sets whether private information is shown to linked accounts by default"),
|
||||
command!(show_private_short => "cfg_show_private_info_show")
|
||||
.help("Shows whether private info is shown"),
|
||||
command!(show_private_short, Toggle => "cfg_show_private_info_update")
|
||||
.help("Sets whether private information is shown to linked accounts by default"),
|
||||
];
|
||||
|
||||
let proxy = ("proxy", ["px"]);
|
||||
let proxy_case = tokens!(cfg, proxy, ("case", ["caps", "capitalize", "capitalise"]));
|
||||
let proxy_error = tokens!(cfg, proxy, ("error", ["errors"]));
|
||||
let proxy_error_short = tokens!(cfg, "pe");
|
||||
let proxy_switch = tokens!(cfg, proxy, "switch");
|
||||
let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["ps"]));
|
||||
let proxy_settings = [
|
||||
command!(proxy_case => "cfg_case_sensitive_proxy_tags_show")
|
||||
.help("Shows whether proxy tags are case-sensitive"),
|
||||
command!(proxy_case, Toggle => "cfg_case_sensitive_proxy_tags_update")
|
||||
.help("Toggles case sensitivity for proxy tags"),
|
||||
command!(proxy_error => "cfg_proxy_error_message_show")
|
||||
.help("Shows whether proxy error messages are enabled"),
|
||||
command!(proxy_error, Toggle => "cfg_proxy_error_message_update")
|
||||
.help("Toggles proxy error messages"),
|
||||
command!(proxy_error_short => "cfg_proxy_error_message_show")
|
||||
.help("Shows whether proxy error messages are enabled"),
|
||||
command!(proxy_error_short, Toggle => "cfg_proxy_error_message_update")
|
||||
.help("Toggles proxy error messages"),
|
||||
command!(proxy_switch => "cfg_proxy_switch_show").help("Shows the proxy switch behavior"),
|
||||
command!(proxy_switch, ProxySwitchAction => "cfg_proxy_switch_update")
|
||||
.help("Sets the switching behavior when proxy tags are used"),
|
||||
command!(proxy_switch_short => "cfg_proxy_switch_show")
|
||||
.help("Shows the proxy switch behavior"),
|
||||
command!(proxy_switch_short, ProxySwitchAction => "cfg_proxy_switch_update")
|
||||
.help("Sets the switching behavior when proxy tags are used"),
|
||||
];
|
||||
|
||||
let id = ("id", ["ids"]);
|
||||
let split_id = tokens!(cfg, "split", id);
|
||||
let split_id_short = tokens!(cfg, ("sid", ["sids"]));
|
||||
let cap_id = tokens!(cfg, ("cap", ["caps", "capitalize", "capitalise"]), id);
|
||||
let cap_id_short = tokens!(cfg, ("capid", ["capids"]));
|
||||
let pad = ("pad", ["padding"]);
|
||||
let pad_id = tokens!(cfg, pad, id);
|
||||
let id_pad = tokens!(cfg, id, pad);
|
||||
let id_pad_short = tokens!(cfg, ("idpad", ["padid", "padids"]));
|
||||
let id_settings = [
|
||||
command!(split_id => "cfg_hid_split_show").help("Shows whether IDs are split in lists"),
|
||||
command!(split_id, Toggle => "cfg_hid_split_update").help("Toggles splitting IDs in lists"),
|
||||
command!(split_id_short => "cfg_hid_split_show")
|
||||
.help("Shows whether IDs are split in lists"),
|
||||
command!(split_id_short, Toggle => "cfg_hid_split_update")
|
||||
.help("Toggles splitting IDs in lists"),
|
||||
command!(cap_id => "cfg_hid_caps_show").help("Shows whether IDs are capitalized in lists"),
|
||||
command!(cap_id, Toggle => "cfg_hid_caps_update")
|
||||
.help("Toggles capitalization of IDs in lists"),
|
||||
command!(cap_id_short => "cfg_hid_caps_show")
|
||||
.help("Shows whether IDs are capitalized in lists"),
|
||||
command!(cap_id_short, Toggle => "cfg_hid_caps_update")
|
||||
.help("Toggles capitalization of IDs in lists"),
|
||||
command!(pad_id => "cfg_hid_padding_show").help("Shows the ID padding for lists"),
|
||||
command!(pad_id, ("padding", OpaqueString) => "cfg_hid_padding_update")
|
||||
.help("Sets the ID padding for lists"),
|
||||
command!(id_pad => "cfg_hid_padding_show").help("Shows the ID padding for lists"),
|
||||
command!(id_pad, ("padding", OpaqueString) => "cfg_hid_padding_update")
|
||||
.help("Sets the ID padding for lists"),
|
||||
command!(id_pad_short => "cfg_hid_padding_show").help("Shows the ID padding for lists"),
|
||||
command!(id_pad_short, ("padding", OpaqueString) => "cfg_hid_padding_update")
|
||||
.help("Sets the ID padding for lists"),
|
||||
];
|
||||
|
||||
let show_color = tokens!(cfg, show, ("color", ["colour", "colors", "colours"]));
|
||||
let show_color_short = tokens!(
|
||||
cfg,
|
||||
(
|
||||
"showcolor",
|
||||
[
|
||||
"showcolour",
|
||||
"showcolors",
|
||||
"showcolours",
|
||||
"colorcode",
|
||||
"colorhex"
|
||||
]
|
||||
)
|
||||
);
|
||||
let color_settings = [
|
||||
command!(show_color => "cfg_card_show_color_hex_show")
|
||||
.help("Shows whether color hex codes are shown on cards"),
|
||||
command!(show_color, Toggle => "cfg_card_show_color_hex_update")
|
||||
.help("Toggles showing color hex codes on cards"),
|
||||
command!(show_color_short => "cfg_card_show_color_hex_show")
|
||||
.help("Shows whether color hex codes are shown on cards"),
|
||||
command!(show_color_short, Toggle => "cfg_card_show_color_hex_update")
|
||||
.help("Toggles showing color hex codes on cards"),
|
||||
];
|
||||
|
||||
let format = "format";
|
||||
let name_format = tokens!(cfg, "name", format);
|
||||
let name_format_short = tokens!(cfg, ("nameformat", ["nf"]));
|
||||
let name_formatting = [
|
||||
command!(name_format => "cfg_name_format_show").help("Shows the name format"),
|
||||
command!(name_format, RESET => "cfg_name_format_reset")
|
||||
.help("Resets the name format"),
|
||||
command!(name_format, ("format", OpaqueString) => "cfg_name_format_update")
|
||||
.help("Changes your system's username formatting"),
|
||||
command!(name_format_short => "cfg_name_format_show").help("Shows the name format"),
|
||||
command!(name_format_short, RESET => "cfg_name_format_reset")
|
||||
.help("Resets the name format"),
|
||||
command!(name_format_short, ("format", OpaqueString) => "cfg_name_format_update")
|
||||
.help("Changes your system's username formatting"),
|
||||
];
|
||||
|
||||
let server = "server";
|
||||
let server_name_format = tokens!(cfg, server, "name", format);
|
||||
let server_format = tokens!(
|
||||
cfg,
|
||||
("server", ["servername"]),
|
||||
("format", ["nameformat", "nf"])
|
||||
);
|
||||
let server_format_short = tokens!(
|
||||
cfg,
|
||||
("snf", ["servernf", "servernameformat", "snameformat"])
|
||||
);
|
||||
let server_name_formatting = [
|
||||
command!(server_name_format => "cfg_server_name_format_show")
|
||||
.help("Shows the server name format"),
|
||||
command!(server_name_format, RESET => "cfg_server_name_format_reset")
|
||||
.help("Resets the server name format"),
|
||||
command!(server_name_format, ("format", OpaqueString) => "cfg_server_name_format_update")
|
||||
.help("Changes your system's username formatting in the current server"),
|
||||
command!(server_format => "cfg_server_name_format_show")
|
||||
.help("Shows the server name format"),
|
||||
command!(server_format, RESET => "cfg_server_name_format_reset")
|
||||
.help("Resets the server name format"),
|
||||
command!(server_format, ("format", OpaqueString) => "cfg_server_name_format_update")
|
||||
.help("Changes your system's username formatting in the current server"),
|
||||
command!(server_format_short => "cfg_server_name_format_show")
|
||||
.help("Shows the server name format"),
|
||||
command!(server_format_short, RESET => "cfg_server_name_format_reset")
|
||||
.help("Resets the server name format"),
|
||||
command!(server_format_short, ("format", OpaqueString) => "cfg_server_name_format_update")
|
||||
.help("Changes your system's username formatting in the current server"),
|
||||
];
|
||||
|
||||
let limit_ = ("limit", ["lim"]);
|
||||
let member_limit = tokens!(cfg, ("member", ["mem"]), limit_);
|
||||
let group_limit = tokens!(cfg, ("group", ["grp"]), limit_);
|
||||
let limit = tokens!(cfg, limit_);
|
||||
let limits = [
|
||||
command!(member_limit => "cfg_limits_update").help("Refreshes member/group limits"),
|
||||
command!(group_limit => "cfg_limits_update").help("Refreshes member/group limits"),
|
||||
command!(limit => "cfg_limits_update").help("Refreshes member/group limits"),
|
||||
];
|
||||
|
||||
base.into_iter()
|
||||
.chain(autoproxy)
|
||||
.chain(timezone)
|
||||
.chain(ping)
|
||||
.chain(privacy)
|
||||
.chain(private_info)
|
||||
.chain(proxy_settings)
|
||||
.chain(id_settings)
|
||||
.chain(color_settings)
|
||||
.chain(name_formatting)
|
||||
.chain(server_name_formatting)
|
||||
.chain(limits)
|
||||
}
|
||||
20
crates/command_definitions/src/debug.rs
Normal file
20
crates/command_definitions/src/debug.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use command_parser::parameter::MESSAGE_REF;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn debug() -> (&'static str, [&'static str; 1]) {
|
||||
("debug", ["dbg"])
|
||||
}
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let debug = debug();
|
||||
let perms = ("permissions", ["perms", "permcheck"]);
|
||||
[
|
||||
command!(debug, perms, ("channel", ["ch"]), ChannelRef => "permcheck_channel")
|
||||
.help("Checks if PluralKit has the required permissions in a channel"),
|
||||
command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild")
|
||||
.help("Checks whether a server's permission setup is correct"),
|
||||
command!(debug, ("proxy", ["proxying", "proxycheck"]), MESSAGE_REF => "message_proxy_check")
|
||||
.help("Checks why a message has not been proxied"),
|
||||
]
|
||||
}
|
||||
17
crates/command_definitions/src/fun.rs
Normal file
17
crates/command_definitions/src/fun.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
[
|
||||
command!("thunder" => "fun_thunder").help("Vanquishes your opponent with a lightning bolt"),
|
||||
command!("meow" => "fun_meow").help("mrrp :3"),
|
||||
command!("mn" => "fun_pokemon").help("Gotta catch 'em all!"),
|
||||
command!("fire" => "fun_fire").help("Engulfs your opponent in a pillar of fire"),
|
||||
command!("freeze" => "fun_freeze").help("Freezes your opponent solid"),
|
||||
command!("starstorm" => "fun_starstorm")
|
||||
.help("Summons a storm of meteors to strike your opponent"),
|
||||
command!("flash" => "fun_flash").help("Explodes your opponent with a ball of green light"),
|
||||
command!("rool" => "fun_rool").help("\"What the fuck is a Pokémon?\""),
|
||||
command!("sus" => "amogus").help("ඞ"),
|
||||
command!("error" => "fun_error").help("Shows a fake error message"),
|
||||
]
|
||||
}
|
||||
182
crates/command_definitions/src/group.rs
Normal file
182
crates/command_definitions/src/group.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
use std::iter::once;
|
||||
|
||||
use command_parser::token::TokensIterator;
|
||||
|
||||
use crate::utils::get_list_flags;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn group() -> (&'static str, [&'static str; 1]) {
|
||||
("group", ["g"])
|
||||
}
|
||||
|
||||
pub fn targeted() -> TokensIterator {
|
||||
tokens!(group(), GroupRef)
|
||||
}
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
let group = group();
|
||||
let group_target = targeted();
|
||||
|
||||
let group_new = tokens!(group, ("new", ["n"]));
|
||||
let group_new_cmd = once(
|
||||
command!(group_new, Remainder(("name", OpaqueString)) => "group_new")
|
||||
.flag(YES)
|
||||
.help("Creates a new group"),
|
||||
);
|
||||
|
||||
let group_info_cmd = once(
|
||||
command!(group_target => "group_info")
|
||||
.flag(ALL)
|
||||
.help("Looks up information about a group"),
|
||||
);
|
||||
|
||||
let group_name = tokens!(
|
||||
group_target,
|
||||
("name", ["rename", "changename", "setname", "rn"])
|
||||
);
|
||||
let group_name_cmd = [
|
||||
command!(group_name => "group_show_name").help("Shows the group's name"),
|
||||
command!(group_name, CLEAR => "group_clear_name")
|
||||
.flag(YES)
|
||||
.help("Clears the group's name"),
|
||||
command!(group_name, Remainder(("name", OpaqueString)) => "group_rename")
|
||||
.flag(YES)
|
||||
.help("Renames a group"),
|
||||
];
|
||||
|
||||
let group_display_name = tokens!(group_target, ("displayname", ["dn", "nick", "nickname"]));
|
||||
let group_display_name_cmd = [
|
||||
command!(group_display_name => "group_show_display_name")
|
||||
.help("Shows the group's display name"),
|
||||
command!(group_display_name, CLEAR => "group_clear_display_name")
|
||||
.flag(YES)
|
||||
.help("Clears the group's display name"),
|
||||
command!(group_display_name, Remainder(("name", OpaqueString)) => "group_change_display_name")
|
||||
.help("Changes the group's display name"),
|
||||
];
|
||||
|
||||
let group_description = tokens!(
|
||||
group_target,
|
||||
(
|
||||
"description",
|
||||
["desc", "describe", "d", "bio", "info", "text", "intro"]
|
||||
)
|
||||
);
|
||||
let group_description_cmd = [
|
||||
command!(group_description => "group_show_description")
|
||||
.help("Shows the group's description"),
|
||||
command!(group_description, CLEAR => "group_clear_description")
|
||||
.flag(YES)
|
||||
.help("Clears the group's description"),
|
||||
command!(group_description, Remainder(("description", OpaqueString)) => "group_change_description")
|
||||
.help("Changes the group's description"),
|
||||
];
|
||||
|
||||
let group_icon = tokens!(
|
||||
group_target,
|
||||
("icon", ["avatar", "picture", "image", "pic", "pfp"])
|
||||
);
|
||||
let group_icon_cmd = [
|
||||
command!(group_icon => "group_show_icon").help("Shows the group's icon"),
|
||||
command!(group_icon, CLEAR => "group_clear_icon")
|
||||
.flag(YES)
|
||||
.help("Clears the group's icon"),
|
||||
command!(group_icon, ("icon", Avatar) => "group_change_icon")
|
||||
.help("Changes a group's icon"),
|
||||
];
|
||||
|
||||
let group_banner = tokens!(group_target, ("banner", ["splash", "cover"]));
|
||||
let group_banner_cmd = [
|
||||
command!(group_banner => "group_show_banner").help("Sets the group's banner image"),
|
||||
command!(group_banner, CLEAR => "group_clear_banner")
|
||||
.flag(YES)
|
||||
.help("Clears the group's banner"),
|
||||
command!(group_banner, ("banner", Avatar) => "group_change_banner")
|
||||
.help("Sets the group's banner image"),
|
||||
];
|
||||
|
||||
let group_color = tokens!(group_target, ("color", ["colour"]));
|
||||
let group_color_cmd = [
|
||||
command!(group_color => "group_show_color").help("Shows the group's color"),
|
||||
command!(group_color, CLEAR => "group_clear_color")
|
||||
.flag(YES)
|
||||
.help("Clears the group's color"),
|
||||
command!(group_color, ("color", OpaqueString) => "group_change_color")
|
||||
.help("Changes a group's color"),
|
||||
];
|
||||
|
||||
let group_privacy = tokens!(group_target, ("privacy", ["priv"]));
|
||||
let group_privacy_cmd = [
|
||||
command!(group_privacy => "group_show_privacy")
|
||||
.help("Shows the group's privacy settings"),
|
||||
command!(group_privacy, ALL, ("level", PrivacyLevel) => "group_change_privacy_all")
|
||||
.help("Changes all privacy settings for the group"),
|
||||
command!(group_privacy, ("privacy", GroupPrivacyTarget), ("level", PrivacyLevel) => "group_change_privacy")
|
||||
.help("Changes a specific privacy setting for the group"),
|
||||
];
|
||||
|
||||
let group_public_cmd = [
|
||||
command!(group_target, ("public", ["pub"]) => "group_set_public")
|
||||
.help("Sets the group to public"),
|
||||
];
|
||||
|
||||
let group_private_cmd = [
|
||||
command!(group_target, ("private", ["priv"]) => "group_set_private")
|
||||
.help("Sets the group to private"),
|
||||
];
|
||||
|
||||
let group_delete_cmd = [
|
||||
command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete")
|
||||
.help("Deletes a group"),
|
||||
];
|
||||
|
||||
let group_id_cmd = [command!(group_target, "id" => "group_id").help("Prints a group's ID")];
|
||||
|
||||
let group_front = tokens!(group_target, ("front", ["fronter", "fronters", "f"]));
|
||||
let group_front_cmd = [
|
||||
command!(group_front, ("percent", ["p", "%"]) => "group_fronter_percent")
|
||||
.help("Shows a group's front breakdown")
|
||||
.flag(("duration", OpaqueString))
|
||||
.flag(("fronters-only", ["fo"]))
|
||||
.flag("flat"),
|
||||
];
|
||||
|
||||
let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags());
|
||||
let search_param = Optional(Remainder(("query", OpaqueString)));
|
||||
|
||||
let group_list_members_cmd =
|
||||
once(command!(group_target, ("members", ["list", "ls"]), search_param => "group_members"))
|
||||
.map(apply_list_opts);
|
||||
|
||||
let group_modify_members_cmd = [
|
||||
command!(group_target, "add", Optional(MemberRefs) => "group_add_member")
|
||||
.help("Adds one or more members to a group")
|
||||
.flag(ALL),
|
||||
command!(group_target, ("remove", ["rem", "rm"]), Optional(MemberRefs) => "group_remove_member")
|
||||
.help("Removes one or more members from a group")
|
||||
.flag(ALL).flag(YES),
|
||||
];
|
||||
|
||||
let system_groups_cmd =
|
||||
once(command!(group, ("list", ["ls", "l"]), search_param => "groups_self"))
|
||||
.map(apply_list_opts);
|
||||
|
||||
system_groups_cmd
|
||||
.chain(group_new_cmd)
|
||||
.chain(group_info_cmd)
|
||||
.chain(group_name_cmd)
|
||||
.chain(group_display_name_cmd)
|
||||
.chain(group_description_cmd)
|
||||
.chain(group_icon_cmd)
|
||||
.chain(group_banner_cmd)
|
||||
.chain(group_color_cmd)
|
||||
.chain(group_privacy_cmd)
|
||||
.chain(group_public_cmd)
|
||||
.chain(group_private_cmd)
|
||||
.chain(group_front_cmd)
|
||||
.chain(group_modify_members_cmd)
|
||||
.chain(group_delete_cmd)
|
||||
.chain(group_id_cmd)
|
||||
.chain(group_list_members_cmd)
|
||||
}
|
||||
15
crates/command_definitions/src/help.rs
Normal file
15
crates/command_definitions/src/help.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let help = ("help", ["h"]);
|
||||
[
|
||||
command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list")
|
||||
.help("Lists all commands or commands in a specific category"),
|
||||
command!(("dashboard", ["dash"]) => "dashboard")
|
||||
.help("Gets a link to the PluralKit web dashboard"),
|
||||
command!("explain" => "explain").help("Explains the basics of systems and proxying"),
|
||||
command!(help => "help").help("Shows the help command"),
|
||||
command!(help, "commands" => "help_commands").help("help commands"),
|
||||
command!(help, "proxy" => "help_proxy").help("help proxy"),
|
||||
]
|
||||
}
|
||||
10
crates/command_definitions/src/import_export.rs
Normal file
10
crates/command_definitions/src/import_export.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
[
|
||||
command!("import", Optional(Remainder(("url", OpaqueString))) => "import")
|
||||
.help("Imports system information from a data file")
|
||||
.flag(YES),
|
||||
command!("export" => "export").help("Exports system information to a file"),
|
||||
]
|
||||
}
|
||||
59
crates/command_definitions/src/lib.rs
Normal file
59
crates/command_definitions/src/lib.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
pub mod admin;
|
||||
pub mod api;
|
||||
pub mod autoproxy;
|
||||
pub mod config;
|
||||
pub mod debug;
|
||||
pub mod fun;
|
||||
pub mod group;
|
||||
pub mod help;
|
||||
pub mod import_export;
|
||||
pub mod member;
|
||||
pub mod message;
|
||||
pub mod misc;
|
||||
pub mod random;
|
||||
pub mod server_config;
|
||||
pub mod switch;
|
||||
pub mod system;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use command_parser::{
|
||||
command,
|
||||
command::Command,
|
||||
parameter::{Optional, Parameter, ParameterKind::*, Remainder, Skip},
|
||||
tokens,
|
||||
};
|
||||
|
||||
pub fn all() -> impl Iterator<Item = Command> {
|
||||
std::iter::empty()
|
||||
.chain(help::cmds())
|
||||
.chain(system::cmds())
|
||||
.chain(group::cmds())
|
||||
.chain(member::cmds())
|
||||
.chain(config::cmds())
|
||||
.chain(server_config::cmds())
|
||||
.chain(fun::cmds())
|
||||
.chain(switch::cmds())
|
||||
.chain(random::cmds())
|
||||
.chain(api::cmds())
|
||||
.chain(autoproxy::cmds())
|
||||
.chain(debug::cmds())
|
||||
.chain(message::cmds())
|
||||
.chain(import_export::cmds())
|
||||
.chain(admin::cmds())
|
||||
.chain(misc::cmds())
|
||||
.map(|cmd| {
|
||||
cmd.hidden_flag(("plaintext", ["pt"]))
|
||||
.hidden_flag(("raw", ["r"]))
|
||||
.hidden_flag(("show-embed", ["se"]))
|
||||
.hidden_flag(("by-id", ["id"]))
|
||||
.hidden_flag(("private", ["priv"]))
|
||||
.hidden_flag(("public", ["pub"]))
|
||||
})
|
||||
}
|
||||
|
||||
pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]);
|
||||
|
||||
pub const CLEAR: (&str, [&str; 1]) = ("clear", ["c"]);
|
||||
pub const YES: (&str, [&str; 1]) = ("yes", ["y"]);
|
||||
pub const ALL: (&str, [&str; 1]) = ("all", ["a"]);
|
||||
331
crates/command_definitions/src/member.rs
Normal file
331
crates/command_definitions/src/member.rs
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
use std::iter::once;
|
||||
|
||||
use command_parser::token::TokensIterator;
|
||||
|
||||
use crate::utils::get_list_flags;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn member() -> (&'static str, [&'static str; 1]) {
|
||||
("member", ["m"])
|
||||
}
|
||||
|
||||
pub fn targetted() -> TokensIterator {
|
||||
tokens!(member(), MemberRef)
|
||||
}
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
let member = member();
|
||||
let member_target = targetted();
|
||||
|
||||
let name = ("name", ["n"]);
|
||||
let description = ("description", ["desc"]);
|
||||
let pronouns = ("pronouns", ["pronoun", "prns", "pn"]);
|
||||
let privacy = ("privacy", ["priv"]);
|
||||
let new = ("new", ["n"]);
|
||||
let banner = ("banner", ["bn"]);
|
||||
let color = ("color", ["colour"]);
|
||||
let birthday = ("birthday", ["bday", "bd"]);
|
||||
let display_name = ("displayname", ["dname", "dn"]);
|
||||
let server_name = ("servername", ["sname", "sn"]);
|
||||
let keep_proxy = ("keepproxy", ["kp"]);
|
||||
let server_keep_proxy = ("serverkeepproxy", ["skp"]);
|
||||
let autoproxy = ("autoproxy", ["ap"]);
|
||||
let proxy = ("proxy", ["tags", "proxytags", "brackets"]);
|
||||
let tts = ("tts", ["texttospeech"]);
|
||||
let delete = ("delete", ["del", "remove"]);
|
||||
|
||||
let member_new_cmd = once(
|
||||
command!(member, new, ("name", OpaqueString) => "member_new")
|
||||
.flag(YES)
|
||||
.help("Creates a new member"),
|
||||
);
|
||||
|
||||
let member_info_cmd = once(
|
||||
command!(member_target => "member_show")
|
||||
.flag("pt")
|
||||
.help("Looks up information about a member"),
|
||||
);
|
||||
|
||||
let member_name_cmd = {
|
||||
let member_name = tokens!(member_target, name);
|
||||
[
|
||||
command!(member_name => "member_name_show").help("Shows a member's name"),
|
||||
command!(member_name, Remainder(("name", OpaqueString)) => "member_name_update")
|
||||
.flag(YES)
|
||||
.help("Renames a member"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_description_cmd = {
|
||||
let member_desc = tokens!(member_target, description);
|
||||
[
|
||||
command!(member_desc => "member_desc_show").help("Shows a member's description"),
|
||||
command!(member_desc, CLEAR => "member_desc_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's description"),
|
||||
command!(member_desc, Remainder(("description", OpaqueString)) => "member_desc_update")
|
||||
.help("Changes a member's description"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_privacy_cmd = {
|
||||
let member_privacy = tokens!(member_target, privacy);
|
||||
[
|
||||
command!(member_privacy => "member_privacy_show")
|
||||
.help("Displays a member's current privacy settings"),
|
||||
command!(
|
||||
member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel)
|
||||
=> "member_privacy_update"
|
||||
)
|
||||
.help("Changes a member's privacy settings"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_pronouns_cmd = {
|
||||
let member_pronouns = tokens!(member_target, pronouns);
|
||||
[
|
||||
command!(member_pronouns => "member_pronouns_show")
|
||||
.help("Shows a member's pronouns"),
|
||||
command!(member_pronouns, Remainder(("pronouns", OpaqueString)) => "member_pronouns_update")
|
||||
.help("Changes a member's pronouns"),
|
||||
command!(member_pronouns, CLEAR => "member_pronouns_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's pronouns"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_banner_cmd = {
|
||||
let member_banner = tokens!(member_target, banner);
|
||||
[
|
||||
command!(member_banner => "member_banner_show").help("Shows a member's banner image"),
|
||||
command!(member_banner, ("banner", Avatar) => "member_banner_update")
|
||||
.help("Sets the member's banner image"),
|
||||
command!(member_banner, CLEAR => "member_banner_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's banner image"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_color_cmd = {
|
||||
let member_color = tokens!(member_target, color);
|
||||
[
|
||||
command!(member_color => "member_color_show").help("Shows a member's color"),
|
||||
command!(member_color, ("color", OpaqueString) => "member_color_update")
|
||||
.help("Changes a member's color"),
|
||||
command!(member_color, CLEAR => "member_color_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's color"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_birthday_cmd = {
|
||||
let member_birthday = tokens!(member_target, birthday);
|
||||
[
|
||||
command!(member_birthday => "member_birthday_show").help("Shows a member's birthday"),
|
||||
command!(member_birthday, ("birthday", OpaqueString) => "member_birthday_update")
|
||||
.help("Changes a member's birthday"),
|
||||
command!(member_birthday, CLEAR => "member_birthday_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's birthday"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_display_name_cmd = {
|
||||
let member_display_name = tokens!(member_target, display_name);
|
||||
[
|
||||
command!(member_display_name => "member_displayname_show")
|
||||
.help("Shows a member's display name"),
|
||||
command!(member_display_name, Remainder(("name", OpaqueString)) => "member_displayname_update")
|
||||
.help("Changes a member's display name"),
|
||||
command!(member_display_name, CLEAR => "member_displayname_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's display name"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_server_name_cmd = {
|
||||
let member_server_name = tokens!(member_target, server_name);
|
||||
[
|
||||
command!(member_server_name => "member_servername_show")
|
||||
.help("Shows a member's server name"),
|
||||
command!(member_server_name, Remainder(("name", OpaqueString)) => "member_servername_update")
|
||||
.help("Changes a member's display name in the current server"),
|
||||
command!(member_server_name, CLEAR => "member_servername_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's server name"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_proxy_cmd = {
|
||||
let member_proxy = tokens!(member_target, proxy);
|
||||
[
|
||||
command!(member_proxy => "member_proxy_show")
|
||||
.help("Shows a member's proxy tags"),
|
||||
command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add")
|
||||
.flag(YES)
|
||||
.help("Adds proxy tag to a member"),
|
||||
command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove")
|
||||
.help("Removes proxy tag from a member"),
|
||||
command!(member_proxy, CLEAR => "member_proxy_clear")
|
||||
.flag(YES)
|
||||
.help("Clears all proxy tags from a member"),
|
||||
command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set")
|
||||
.flag(YES)
|
||||
.help("Sets a member's proxy tags"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_proxy_settings_cmd = {
|
||||
let member_keep_proxy = tokens!(member_target, keep_proxy);
|
||||
let member_server_keep_proxy = tokens!(member_target, server_keep_proxy);
|
||||
[
|
||||
command!(member_keep_proxy => "member_keepproxy_show")
|
||||
.help("Shows a member's keep-proxy setting"),
|
||||
command!(member_keep_proxy, ("value", Toggle) => "member_keepproxy_update")
|
||||
.help("Sets whether to include a member's proxy tags when proxying"),
|
||||
command!(member_server_keep_proxy => "member_server_keepproxy_show")
|
||||
.help("Shows a member's server-specific keep-proxy setting"),
|
||||
command!(member_server_keep_proxy, CLEAR => "member_server_keepproxy_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's server-specific keep-proxy setting"),
|
||||
command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update")
|
||||
.help("Sets whether to include a member's proxy tags when proxying in the current server"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_message_settings_cmd = {
|
||||
let member_tts = tokens!(member_target, tts);
|
||||
let member_autoproxy = tokens!(member_target, autoproxy);
|
||||
[
|
||||
command!(member_tts => "member_tts_show")
|
||||
.help("Shows whether a member's messages are sent as TTS"),
|
||||
command!(member_tts, ("value", Toggle) => "member_tts_update")
|
||||
.help("Sets whether to send a member's messages as text-to-speech messages"),
|
||||
command!(member_autoproxy => "member_autoproxy_show")
|
||||
.help("Shows whether a member can be autoproxied"),
|
||||
command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update")
|
||||
.help("Sets whether a member will be autoproxied when autoproxy is set to latch or front mode"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_avatar_cmd = {
|
||||
let member_avatar = tokens!(
|
||||
member_target,
|
||||
(
|
||||
"avatar",
|
||||
["profile", "picture", "icon", "image", "pfp", "pic"]
|
||||
)
|
||||
);
|
||||
[
|
||||
command!(member_avatar => "member_avatar_show").help("Shows a member's avatar"),
|
||||
command!(member_avatar, ("avatar", Avatar) => "member_avatar_update")
|
||||
.help("Changes a member's avatar"),
|
||||
command!(member_avatar, CLEAR => "member_avatar_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's avatar"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_webhook_avatar_cmd = {
|
||||
let member_webhook_avatar = tokens!(
|
||||
member_target,
|
||||
(
|
||||
"proxyavatar",
|
||||
[
|
||||
"proxypfp",
|
||||
"webhookavatar",
|
||||
"webhookpfp",
|
||||
"pa",
|
||||
"pavatar",
|
||||
"ppfp"
|
||||
]
|
||||
)
|
||||
);
|
||||
[
|
||||
command!(member_webhook_avatar => "member_webhook_avatar_show")
|
||||
.help("Shows a member's proxy avatar"),
|
||||
command!(member_webhook_avatar, ("avatar", Avatar) => "member_webhook_avatar_update")
|
||||
.help("Changes a member's proxy avatar"),
|
||||
command!(member_webhook_avatar, CLEAR => "member_webhook_avatar_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's proxy avatar"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_server_avatar_cmd = {
|
||||
let member_server_avatar = tokens!(
|
||||
member_target,
|
||||
(
|
||||
"serveravatar",
|
||||
[
|
||||
"sa",
|
||||
"servericon",
|
||||
"serverimage",
|
||||
"serverpfp",
|
||||
"serverpic",
|
||||
"savatar",
|
||||
"spic",
|
||||
"guildavatar",
|
||||
"guildpic",
|
||||
"guildicon",
|
||||
"sicon",
|
||||
"spfp"
|
||||
]
|
||||
)
|
||||
);
|
||||
[
|
||||
command!(member_server_avatar => "member_server_avatar_show")
|
||||
.help("Shows a member's server-specific avatar"),
|
||||
command!(member_server_avatar, ("avatar", Avatar) => "member_server_avatar_update")
|
||||
.help("Changes a member's avatar in the current server"),
|
||||
command!(member_server_avatar, CLEAR => "member_server_avatar_clear")
|
||||
.flag(YES)
|
||||
.help("Clears a member's server-specific avatar"),
|
||||
]
|
||||
};
|
||||
|
||||
let member_group = tokens!(member_target, ("groups", ["group", "g"]));
|
||||
let member_list_group_cmds = once(
|
||||
command!(member_group, Optional(Remainder(("query", OpaqueString))) => "member_groups"),
|
||||
)
|
||||
.map(|cmd| cmd.flags(get_list_flags()));
|
||||
let member_add_remove_group_cmds = [
|
||||
command!(member_group, "add", Optional(("groups", GroupRefs)) => "member_group_add")
|
||||
.help("Adds a member to one or more groups"),
|
||||
command!(member_group, ("remove", ["rem"]), Optional(("groups", GroupRefs)) => "member_group_remove")
|
||||
.help("Removes a member from one or more groups"),
|
||||
];
|
||||
|
||||
let member_display_id_cmd =
|
||||
[command!(member_target, "id" => "member_id").help("Prints a member's ID")];
|
||||
|
||||
let member_delete_cmd =
|
||||
[command!(member_target, delete => "member_delete").help("Deletes a member")];
|
||||
|
||||
let member_easter_eggs =
|
||||
[command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)];
|
||||
|
||||
member_new_cmd
|
||||
.chain(member_info_cmd)
|
||||
.chain(member_name_cmd)
|
||||
.chain(member_description_cmd)
|
||||
.chain(member_privacy_cmd)
|
||||
.chain(member_pronouns_cmd)
|
||||
.chain(member_banner_cmd)
|
||||
.chain(member_color_cmd)
|
||||
.chain(member_birthday_cmd)
|
||||
.chain(member_display_name_cmd)
|
||||
.chain(member_server_name_cmd)
|
||||
.chain(member_proxy_cmd)
|
||||
.chain(member_avatar_cmd)
|
||||
.chain(member_webhook_avatar_cmd)
|
||||
.chain(member_server_avatar_cmd)
|
||||
.chain(member_proxy_settings_cmd)
|
||||
.chain(member_message_settings_cmd)
|
||||
.chain(member_display_id_cmd)
|
||||
.chain(member_delete_cmd)
|
||||
.chain(member_easter_eggs)
|
||||
.chain(member_add_remove_group_cmds)
|
||||
.chain(member_list_group_cmds)
|
||||
}
|
||||
44
crates/command_definitions/src/message.rs
Normal file
44
crates/command_definitions/src/message.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use command_parser::{
|
||||
parameter::{MESSAGE_LINK, MESSAGE_REF},
|
||||
token::TokensIterator,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let message = tokens!(("message", ["msg", "messageinfo"]), Optional(MESSAGE_REF));
|
||||
|
||||
let author = ("author", ["sender", "a"]);
|
||||
let delete = ("delete", ["del", "d"]);
|
||||
let reproxy = ("reproxy", ["rp", "crimes", "crime"]);
|
||||
|
||||
let edit = ("edit", ["e"]);
|
||||
let new_content_param = Optional(Remainder(("new_content", OpaqueString)));
|
||||
let edit_short_subcmd = tokens!(Optional(MESSAGE_LINK), new_content_param);
|
||||
|
||||
let apply_edit = |cmd: Command| {
|
||||
cmd.flag(("append", ["a"]))
|
||||
.flag(("prepend", ["p"]))
|
||||
.flag(("regex", ["r"]))
|
||||
.flag(("no-space", ["nospace", "ns"]))
|
||||
.flag(("clear-embeds", ["clear-embed", "ce"]))
|
||||
.flag(("clear-attachments", ["clear-attachment", "ca"]))
|
||||
.help("Edits a previously proxied message")
|
||||
};
|
||||
let make_edit_cmd = |tokens: TokensIterator| apply_edit(command!(tokens => "message_edit"));
|
||||
|
||||
[
|
||||
make_edit_cmd(tokens!(edit, edit_short_subcmd)),
|
||||
// this one always does regex
|
||||
make_edit_cmd(tokens!("x", edit_short_subcmd)).flag_value("regex", None),
|
||||
command!(reproxy, Optional(("msg", MESSAGE_REF)), ("member", MemberRef) => "message_reproxy")
|
||||
.help("Reproxies a previously proxied message with a different member"),
|
||||
command!(message, author => "message_author").help("Shows the author of a proxied message"),
|
||||
command!(message, delete => "message_delete").help("Deletes a proxied message"),
|
||||
make_edit_cmd(tokens!(message, edit, new_content_param)),
|
||||
command!(message => "message_info")
|
||||
.flag(delete)
|
||||
.flag(author)
|
||||
.help("Shows information about a proxied message"),
|
||||
]
|
||||
}
|
||||
9
crates/command_definitions/src/misc.rs
Normal file
9
crates/command_definitions/src/misc.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
[
|
||||
command!("invite" => "invite").help("Gets a link to invite PluralKit to other servers"),
|
||||
command!(("stats", ["status"]) => "stats")
|
||||
.help("Shows statistics and information about PluralKit"),
|
||||
]
|
||||
}
|
||||
31
crates/command_definitions/src/random.rs
Normal file
31
crates/command_definitions/src/random.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::utils::get_list_flags;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
let random = ("random", ["rand", "r"]);
|
||||
let group = group::group();
|
||||
let member = member::member();
|
||||
|
||||
[
|
||||
command!(random => "random_self")
|
||||
.help("Shows the info card of a randomly selected member in your system")
|
||||
.flag(group),
|
||||
command!(random, member => "random_self"),
|
||||
command!(random, group => "random_group_self")
|
||||
.help("Shows the info card of a randomly selected group in your system"),
|
||||
command!(random, group::targeted() => "random_group_member_self")
|
||||
.help("Shows the info card of a randomly selected member in a group in your system")
|
||||
.flags(get_list_flags()),
|
||||
command!(system::targeted(), random => "system_random")
|
||||
.help("Shows the info card of a randomly selected member in a system")
|
||||
.flag(group),
|
||||
command!(system::targeted(), random, group => "system_random_group")
|
||||
.help("Shows the info card of a randomly selected group in a system"),
|
||||
command!(group::targeted(), random => "group_random_member")
|
||||
.help("Shows the info card of a randomly selected member in a group")
|
||||
.flags(get_list_flags()),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|cmd| cmd.flag(ALL))
|
||||
}
|
||||
140
crates/command_definitions/src/server_config.rs
Normal file
140
crates/command_definitions/src/server_config.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use std::iter::once;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
let server_config = ("serverconfig", ["guildconfig", "scfg", "gcfg"]);
|
||||
|
||||
let log = tokens!(server_config, ("log", ["log", "logging"]));
|
||||
let log_channel = tokens!(log, ("channel", ["ch", "chan"]));
|
||||
let log_cleanup = tokens!(log, ("cleanup", ["clean"]));
|
||||
let log_cleanup_short = tokens!(server_config, ("logclean", ["logclean", "logcleanup"]));
|
||||
let log_blacklist = tokens!(log, ("blacklist", ["bl", "ignore"]));
|
||||
|
||||
let proxy = tokens!(server_config, ("proxy", ["proxy", "proxying"]));
|
||||
let proxy_blacklist = tokens!(proxy, ("blacklist", ["bl", "ignore", "disable"]));
|
||||
|
||||
let invalid = tokens!(
|
||||
server_config,
|
||||
("invalid", ["invalid", "unknown"]),
|
||||
("command", ["command", "cmd"]),
|
||||
("error", ["error", "response"])
|
||||
);
|
||||
let invalid_short = tokens!(
|
||||
server_config,
|
||||
(
|
||||
"invalidcommanderror",
|
||||
["invalidcommanderror", "unknowncommanderror", "ice"]
|
||||
)
|
||||
);
|
||||
|
||||
let require_tag = tokens!(
|
||||
server_config,
|
||||
("require", ["require", "enforce"]),
|
||||
("tag", ["tag", "systemtag"])
|
||||
);
|
||||
let require_tag_short = tokens!(server_config, ("requiretag", ["requiretag", "enforcetag"]));
|
||||
|
||||
let suppress = tokens!(
|
||||
server_config,
|
||||
("suppress", ["suppress"]),
|
||||
("notifications", ["notifications", "notifs"])
|
||||
);
|
||||
let suppress_short = tokens!(server_config, ("proxysilent", ["proxysilent", "silent"]));
|
||||
|
||||
// Common tokens for add/remove operations
|
||||
let add = ("add", ["enable", "on", "deny"]);
|
||||
let remove = ("remove", ["disable", "off", "allow"]);
|
||||
|
||||
let log_channel_cmds = [
|
||||
command!(log_channel => "server_config_log_channel_show")
|
||||
.help("Shows the current log channel"),
|
||||
command!(log_channel, ("channel", ChannelRef) => "server_config_log_channel_set")
|
||||
.help("Designates a channel to post proxied messages to"),
|
||||
command!(log_channel, CLEAR => "server_config_log_channel_clear")
|
||||
.flag(YES)
|
||||
.help("Clears the log channel"),
|
||||
];
|
||||
|
||||
let log_cleanup_cmds = [
|
||||
command!(log_cleanup => "server_config_log_cleanup_show")
|
||||
.help("Shows whether log cleanup is enabled"),
|
||||
command!(log_cleanup, Toggle => "server_config_log_cleanup_set")
|
||||
.help("Toggles whether to clean up other bots' log channels"),
|
||||
command!(log_cleanup_short => "server_config_log_cleanup_show")
|
||||
.help("Shows whether log cleanup is enabled"),
|
||||
command!(log_cleanup_short, Toggle => "server_config_log_cleanup_set")
|
||||
.help("Toggles whether to clean up other bots' log channels"),
|
||||
];
|
||||
|
||||
let log_blacklist_cmds = [
|
||||
command!(log_blacklist => "server_config_log_blacklist_show")
|
||||
.help("Shows channels where logging is disabled"),
|
||||
command!(log_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_add")
|
||||
.flag(ALL)
|
||||
.help("Disables message logging in certain channels"),
|
||||
command!(log_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_remove")
|
||||
.flag(ALL)
|
||||
.help("Enables message logging in certain channels"),
|
||||
];
|
||||
|
||||
let proxy_blacklist_cmds = [
|
||||
command!(proxy_blacklist => "server_config_proxy_blacklist_show")
|
||||
.help("Shows channels where proxying is disabled"),
|
||||
command!(proxy_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_add")
|
||||
.flag(ALL)
|
||||
.help("Disables message proxying in certain channels"),
|
||||
command!(proxy_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_remove")
|
||||
.flag(ALL)
|
||||
.help("Enables message proxying in certain channels"),
|
||||
];
|
||||
|
||||
let invalid_cmds = [
|
||||
command!(invalid => "server_config_invalid_command_response_show")
|
||||
.help("Shows whether error responses for invalid commands are enabled"),
|
||||
command!(invalid, Toggle => "server_config_invalid_command_response_set")
|
||||
.help("Sets whether to show an error message when an unknown command is sent"),
|
||||
command!(invalid_short => "server_config_invalid_command_response_show")
|
||||
.help("Shows whether error responses for invalid commands are enabled"),
|
||||
command!(invalid_short, Toggle => "server_config_invalid_command_response_set")
|
||||
.help("Sets whether to show an error message when an unknown command is sent"),
|
||||
];
|
||||
|
||||
let require_tag_cmds = [
|
||||
command!(require_tag => "server_config_require_system_tag_show")
|
||||
.help("Shows whether system tags are required"),
|
||||
command!(require_tag, Toggle => "server_config_require_system_tag_set").help(
|
||||
"Sets whether server users are required to have a system tag on proxied messages",
|
||||
),
|
||||
command!(require_tag_short => "server_config_require_system_tag_show")
|
||||
.help("Shows whether system tags are required"),
|
||||
command!(require_tag_short, Toggle => "server_config_require_system_tag_set").help(
|
||||
"Sets whether server users are required to have a system tag on proxied messages",
|
||||
),
|
||||
];
|
||||
|
||||
let suppress_cmds = [
|
||||
command!(suppress => "server_config_suppress_notifications_show")
|
||||
.help("Shows whether notifications are suppressed for proxied messages"),
|
||||
command!(suppress, Toggle => "server_config_suppress_notifications_set")
|
||||
.help("Sets whether all proxied messages will have notifications suppressed (sent as `@silent` messages)"),
|
||||
command!(suppress_short => "server_config_suppress_notifications_show")
|
||||
.help("Shows whether notifications are suppressed for proxied messages"),
|
||||
command!(suppress_short, Toggle => "server_config_suppress_notifications_set")
|
||||
.help("Sets whether all proxied messages will have notifications suppressed (sent as `@silent` messages)"),
|
||||
];
|
||||
|
||||
let main_cmd = once(
|
||||
command!(server_config => "server_config_show")
|
||||
.help("Shows the current server configuration"),
|
||||
);
|
||||
|
||||
main_cmd
|
||||
.chain(log_channel_cmds)
|
||||
.chain(log_cleanup_cmds)
|
||||
.chain(log_blacklist_cmds)
|
||||
.chain(proxy_blacklist_cmds)
|
||||
.chain(invalid_cmds)
|
||||
.chain(require_tag_cmds)
|
||||
.chain(suppress_cmds)
|
||||
}
|
||||
42
crates/command_definitions/src/switch.rs
Normal file
42
crates/command_definitions/src/switch.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl IntoIterator<Item = Command> {
|
||||
let switch = ("switch", ["sw"]);
|
||||
|
||||
let edit = ("edit", ["e", "replace"]);
|
||||
let r#move = ("move", ["m", "shift", "offset"]);
|
||||
let delete = ("delete", ["remove", "erase", "cancel", "yeet"]);
|
||||
let copy = ("copy", ["add", "duplicate", "dupe"]);
|
||||
let out = "out";
|
||||
|
||||
let edit_flags = [
|
||||
("first", ["f"]),
|
||||
("remove", ["r"]),
|
||||
("append", ["a"]),
|
||||
("prepend", ["p"]),
|
||||
];
|
||||
|
||||
[
|
||||
command!(switch, ("commands", ["help"]) => "switch_commands")
|
||||
.help("Shows help for switch commands"),
|
||||
command!(switch, out => "switch_out").help("Registers a switch with no members"),
|
||||
command!(switch, delete => "switch_delete")
|
||||
.flag(YES)
|
||||
.help("Deletes the latest switch")
|
||||
.flag(("all", ["clear", "c"])),
|
||||
command!(switch, r#move, Remainder(OpaqueString) => "switch_move")
|
||||
.flag(YES)
|
||||
.help("Moves the latest switch in time"), // TODO: datetime parsing
|
||||
command!(switch, edit, out => "switch_edit_out")
|
||||
.help("Turns the latest switch into a switch-out")
|
||||
.flag(YES),
|
||||
command!(switch, edit, Optional(MemberRefs) => "switch_edit")
|
||||
.flag(YES)
|
||||
.help("Edits the members in the latest switch")
|
||||
.flags(edit_flags),
|
||||
command!(switch, copy, Optional(MemberRefs) => "switch_copy")
|
||||
.help("Makes a new switch with the listed members added")
|
||||
.flags(edit_flags),
|
||||
command!(switch, MemberRefs => "switch_do").help("Registers a switch"),
|
||||
]
|
||||
}
|
||||
315
crates/command_definitions/src/system.rs
Normal file
315
crates/command_definitions/src/system.rs
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
use std::iter::once;
|
||||
|
||||
use command_parser::token::TokensIterator;
|
||||
|
||||
use crate::utils::get_list_flags;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
edit()
|
||||
}
|
||||
|
||||
pub fn system() -> (&'static str, [&'static str; 1]) {
|
||||
("system", ["s"])
|
||||
}
|
||||
|
||||
pub fn targeted() -> TokensIterator {
|
||||
tokens!(system(), SystemRef)
|
||||
}
|
||||
|
||||
pub fn edit() -> impl Iterator<Item = Command> {
|
||||
let system = system();
|
||||
|
||||
let system_new_cmd =
|
||||
once(
|
||||
command!(system, ("new", ["n"]), Optional(Remainder(("name", OpaqueString))) => "system_new")
|
||||
.help("Creates a new system")
|
||||
);
|
||||
|
||||
let system_webhook = tokens!(system, ("webhook", ["hook"]));
|
||||
let system_webhook_cmd = [
|
||||
command!(system_webhook => "system_webhook_show").help("Shows your system's webhook URL"),
|
||||
command!(system_webhook, CLEAR => "system_webhook_clear")
|
||||
.flag(YES)
|
||||
.help("Clears your system's webhook URL"),
|
||||
command!(system_webhook, ("url", OpaqueString) => "system_webhook_set")
|
||||
.help("Sets your system's webhook URL"),
|
||||
];
|
||||
|
||||
let add_info_flags = |cmd: Command| {
|
||||
cmd.flag(("public", ["pub"]))
|
||||
.flag(("private", ["priv"]))
|
||||
.flag(ALL)
|
||||
};
|
||||
let system_info_cmd = [
|
||||
command!(system, Optional(SystemRef) => "system_info")
|
||||
.help("Shows information about your system"),
|
||||
command!(system, Optional(SystemRef), ("info", ["show", "view"]) => "system_info")
|
||||
.help("Shows information about your system"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(add_info_flags);
|
||||
|
||||
let name = "name";
|
||||
let system_name_cmd = once(
|
||||
command!(system, Optional(SystemRef), name => "system_show_name")
|
||||
.help("Shows the systems name"),
|
||||
);
|
||||
let system_name_self = tokens!(system, ("rename", [name]));
|
||||
let system_name_self_cmd = [
|
||||
command!(system_name_self, CLEAR => "system_clear_name")
|
||||
.flag(YES)
|
||||
.help("Clears your system's name"),
|
||||
command!(system_name_self, Remainder(("name", OpaqueString)) => "system_rename")
|
||||
.help("Renames your system"),
|
||||
];
|
||||
|
||||
let server_name = ("servername", ["sn", "guildname"]);
|
||||
let system_server_name_cmd = once(
|
||||
command!(system, Optional(SystemRef), server_name => "system_show_server_name")
|
||||
.help("Shows the system's server name"),
|
||||
);
|
||||
let system_server_name_self = tokens!(system, server_name);
|
||||
let system_server_name_self_cmd = [
|
||||
command!(system_server_name_self, CLEAR => "system_clear_server_name")
|
||||
.flag(YES)
|
||||
.help("Clears your system's server name"),
|
||||
command!(system_server_name_self, Remainder(("name", OpaqueString)) => "system_rename_server_name")
|
||||
.help("Renames your system's server name"),
|
||||
];
|
||||
|
||||
let description = ("description", ["desc", "d"]);
|
||||
let system_description_cmd = once(
|
||||
command!(system, Optional(SystemRef), description => "system_show_description")
|
||||
.help("Shows the system's description"),
|
||||
);
|
||||
let system_description_self = tokens!(system, description);
|
||||
let system_description_self_cmd = [
|
||||
command!(system_description_self, CLEAR => "system_clear_description")
|
||||
.flag(YES)
|
||||
.help("Clears your system's description"),
|
||||
command!(system_description_self, Remainder(("description", OpaqueString)) => "system_change_description")
|
||||
.help("Changes your system's description"),
|
||||
];
|
||||
|
||||
let color = ("color", ["colour"]);
|
||||
let system_color_cmd = once(
|
||||
command!(system, Optional(SystemRef), color => "system_show_color")
|
||||
.help("Shows the system's color"),
|
||||
);
|
||||
let system_color_self = tokens!(system, color);
|
||||
let system_color_self_cmd = [
|
||||
command!(system_color_self, CLEAR => "system_clear_color")
|
||||
.flag(YES)
|
||||
.help("Clears your system's color"),
|
||||
command!(system_color_self, ("color", OpaqueString) => "system_change_color")
|
||||
.help("Changes your system's color"),
|
||||
];
|
||||
|
||||
let tag = ("tag", ["suffix"]);
|
||||
let system_tag_cmd = once(
|
||||
command!(system, Optional(SystemRef), tag => "system_show_tag")
|
||||
.help("Shows the system's tag"),
|
||||
);
|
||||
let system_tag_self = tokens!(system, tag);
|
||||
let system_tag_self_cmd = [
|
||||
command!(system_tag_self, CLEAR => "system_clear_tag")
|
||||
.flag(YES)
|
||||
.help("Clears your system's tag"),
|
||||
command!(system_tag_self, Remainder(("tag", OpaqueString)) => "system_change_tag")
|
||||
.help("Changes your system's tag"),
|
||||
];
|
||||
|
||||
let servertag = ("servertag", ["st", "guildtag", "stag", "deer"]);
|
||||
let system_server_tag_cmd = once(
|
||||
command!(system, Optional(SystemRef), servertag => "system_show_server_tag")
|
||||
.help("Shows the system's server tag"),
|
||||
);
|
||||
let system_server_tag_self = tokens!(system, servertag);
|
||||
let system_server_tag_self_cmd = [
|
||||
command!(system_server_tag_self, CLEAR => "system_clear_server_tag")
|
||||
.flag(YES)
|
||||
.help("Clears your system's server tag"),
|
||||
command!(system_server_tag_self, Remainder(("tag", OpaqueString)) => "system_change_server_tag")
|
||||
.help("Changes your system's server tag"),
|
||||
];
|
||||
|
||||
let pronouns = ("pronouns", ["prns"]);
|
||||
let system_pronouns_cmd = once(
|
||||
command!(system, Optional(SystemRef), pronouns => "system_show_pronouns")
|
||||
.help("Shows the system's pronouns"),
|
||||
);
|
||||
let system_pronouns_self = tokens!(system, pronouns);
|
||||
let system_pronouns_self_cmd = [
|
||||
command!(system_pronouns_self, CLEAR => "system_clear_pronouns")
|
||||
.flag(YES)
|
||||
.help("Clears your system's pronouns"),
|
||||
command!(system_pronouns_self, Remainder(("pronouns", OpaqueString)) => "system_change_pronouns")
|
||||
.help("Changes your system's pronouns"),
|
||||
];
|
||||
|
||||
let avatar = ("avatar", ["pfp"]);
|
||||
let system_avatar_cmd = once(
|
||||
command!(system, Optional(SystemRef), avatar => "system_show_avatar")
|
||||
.help("Shows the system's avatar"),
|
||||
);
|
||||
let system_avatar_self = tokens!(system, avatar);
|
||||
let system_avatar_self_cmd = [
|
||||
command!(system_avatar_self, CLEAR => "system_clear_avatar")
|
||||
.flag(YES)
|
||||
.help("Clears your system's avatar"),
|
||||
command!(system_avatar_self, ("avatar", Avatar) => "system_change_avatar")
|
||||
.help("Changes your system's avatar"),
|
||||
];
|
||||
|
||||
let serveravatar = ("serveravatar", ["spfp"]);
|
||||
let system_server_avatar_cmd = once(
|
||||
command!(system, Optional(SystemRef), serveravatar => "system_show_server_avatar")
|
||||
.help("Shows the system's server avatar"),
|
||||
);
|
||||
let system_server_avatar_self = tokens!(system, serveravatar);
|
||||
let system_server_avatar_self_cmd = [
|
||||
command!(system_server_avatar_self, CLEAR => "system_clear_server_avatar")
|
||||
.flag(YES)
|
||||
.help("Clears your system's server avatar"),
|
||||
command!(system_server_avatar_self, ("avatar", Avatar) => "system_change_server_avatar")
|
||||
.help("Changes your system's server avatar"),
|
||||
];
|
||||
|
||||
let banner = ("banner", ["cover"]);
|
||||
let system_banner_cmd = once(
|
||||
command!(system, Optional(SystemRef), banner => "system_show_banner")
|
||||
.help("Shows the system's banner"),
|
||||
);
|
||||
let system_banner_self = tokens!(system, banner);
|
||||
let system_banner_self_cmd = [
|
||||
command!(system_banner_self, CLEAR => "system_clear_banner")
|
||||
.flag(YES)
|
||||
.help("Clears your system's banner"),
|
||||
command!(system_banner_self, ("banner", Avatar) => "system_change_banner")
|
||||
.help("Changes your system's banner"),
|
||||
];
|
||||
|
||||
let system_proxy = tokens!(system, "proxy");
|
||||
let system_proxy_cmd = [
|
||||
command!(system_proxy => "system_show_proxy_current")
|
||||
.help("Shows your system's proxy setting for the guild you are in"),
|
||||
command!(system_proxy, Skip(Toggle) => "system_toggle_proxy_current")
|
||||
.help("Toggle your system's proxy for the guild you are in"),
|
||||
command!(system_proxy, GuildRef => "system_show_proxy")
|
||||
.help("Shows your system's proxy setting for a guild"),
|
||||
command!(system_proxy, GuildRef, Toggle => "system_toggle_proxy")
|
||||
.help("Toggle your system's proxy for a guild"),
|
||||
];
|
||||
|
||||
let system_privacy = tokens!(system, ("privacy", ["priv"]));
|
||||
let system_privacy_cmd = [
|
||||
command!(system_privacy => "system_show_privacy")
|
||||
.help("Shows your system's privacy settings"),
|
||||
command!(system_privacy, ALL, ("level", PrivacyLevel) => "system_change_privacy_all")
|
||||
.help("Changes all privacy settings for your system"),
|
||||
command!(system_privacy, ("privacy", SystemPrivacyTarget), ("level", PrivacyLevel) => "system_change_privacy")
|
||||
.help("Changes a specific privacy setting for your system"),
|
||||
];
|
||||
|
||||
let front = ("front", ["fronter", "fronters", "f"]);
|
||||
let make_front_history = |subcmd: TokensIterator| {
|
||||
command!(system, Optional(SystemRef), subcmd => "system_fronter_history")
|
||||
.help("Shows a system's front history")
|
||||
.flag(CLEAR)
|
||||
};
|
||||
let make_front_percent = |subcmd: TokensIterator| {
|
||||
command!(system, Optional(SystemRef), subcmd => "system_fronter_percent")
|
||||
.help("Shows a system's front breakdown")
|
||||
.flag(("duration", OpaqueString))
|
||||
.flag(("fronters-only", ["fo"]))
|
||||
.flag("flat")
|
||||
};
|
||||
let system_front_cmd = [
|
||||
command!(system, Optional(SystemRef), front => "system_fronter")
|
||||
.help("Shows a system's fronter(s)"),
|
||||
make_front_history(tokens!(front, ("history", ["h"]))),
|
||||
make_front_history(tokens!(("fronthistory", ["fh", "history", "switches"]))),
|
||||
make_front_percent(tokens!(front, ("percent", ["p", "%"]))),
|
||||
make_front_percent(tokens!((
|
||||
"frontpercent",
|
||||
["fp", "front%", "frontbreakdown"]
|
||||
))),
|
||||
];
|
||||
|
||||
let search_param = Optional(Remainder(("query", OpaqueString)));
|
||||
let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags());
|
||||
|
||||
let members_subcmd = tokens!(
|
||||
(
|
||||
"members",
|
||||
["l", "ls", "list", "find", "search", "query", "fd"]
|
||||
),
|
||||
search_param
|
||||
);
|
||||
let system_members_cmd = [
|
||||
command!(system, Optional(SystemRef), members_subcmd => "system_members")
|
||||
.help("Lists a system's members"),
|
||||
command!(members_subcmd => "system_members").help("Lists your system's members"),
|
||||
]
|
||||
.map(apply_list_opts);
|
||||
|
||||
let system_groups_cmd = once(
|
||||
command!(system, Optional(SystemRef), ("groups", ["g"]), search_param => "system_groups")
|
||||
.help("Lists groups in a system"),
|
||||
)
|
||||
.map(apply_list_opts);
|
||||
|
||||
let system_display_id_cmd = once(
|
||||
command!(system, Optional(SystemRef), "id" => "system_display_id")
|
||||
.help("Prints a system's ID"),
|
||||
);
|
||||
|
||||
let system_delete = once(
|
||||
command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete")
|
||||
.flag(("no-export", ["ne"]))
|
||||
.help("Deletes the system"),
|
||||
);
|
||||
|
||||
let system_link = [
|
||||
command!("link", ("account", UserRef) => "system_link")
|
||||
.flag(YES)
|
||||
.help("Links another Discord account to your system"),
|
||||
command!("unlink", ("account", OpaqueString) => "system_unlink")
|
||||
.help("Unlinks a Discord account from your system")
|
||||
.flag(YES),
|
||||
];
|
||||
|
||||
system_new_cmd
|
||||
.chain(system_webhook_cmd)
|
||||
.chain(system_name_self_cmd)
|
||||
.chain(system_server_name_self_cmd)
|
||||
.chain(system_description_self_cmd)
|
||||
.chain(system_color_self_cmd)
|
||||
.chain(system_tag_self_cmd)
|
||||
.chain(system_server_tag_self_cmd)
|
||||
.chain(system_pronouns_self_cmd)
|
||||
.chain(system_avatar_self_cmd)
|
||||
.chain(system_server_avatar_self_cmd)
|
||||
.chain(system_banner_self_cmd)
|
||||
.chain(system_delete)
|
||||
.chain(system_privacy_cmd)
|
||||
.chain(system_proxy_cmd)
|
||||
.chain(system_name_cmd)
|
||||
.chain(system_server_name_cmd)
|
||||
.chain(system_description_cmd)
|
||||
.chain(system_color_cmd)
|
||||
.chain(system_tag_cmd)
|
||||
.chain(system_server_tag_cmd)
|
||||
.chain(system_pronouns_cmd)
|
||||
.chain(system_avatar_cmd)
|
||||
.chain(system_server_avatar_cmd)
|
||||
.chain(system_banner_cmd)
|
||||
.chain(system_info_cmd)
|
||||
.chain(system_front_cmd)
|
||||
.chain(system_link)
|
||||
.chain(system_members_cmd)
|
||||
.chain(system_groups_cmd)
|
||||
.chain(system_display_id_cmd)
|
||||
}
|
||||
54
crates/command_definitions/src/utils.rs
Normal file
54
crates/command_definitions/src/utils.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use command_parser::flag::Flag;
|
||||
|
||||
use crate::ALL;
|
||||
|
||||
pub fn get_list_flags() -> [Flag; 22] {
|
||||
[
|
||||
// Short or long list
|
||||
Flag::from(("full", ["f", "big", "details", "long"])),
|
||||
// Search description
|
||||
Flag::from((
|
||||
"search-description",
|
||||
[
|
||||
"filter-description",
|
||||
"in-description",
|
||||
"sd",
|
||||
"description",
|
||||
"desc",
|
||||
],
|
||||
)),
|
||||
// Sort properties
|
||||
Flag::from(("by-name", ["bn"])),
|
||||
Flag::from(("by-display-name", ["by-displayname", "bdn"])),
|
||||
Flag::from(("by-id", ["bid"])),
|
||||
Flag::from(("by-message-count", ["bmc"])),
|
||||
Flag::from(("by-created", ["bc", "bcd"])),
|
||||
Flag::from((
|
||||
"by-last-fronted",
|
||||
["by-last-front", "by-last-switch", "blf", "bls"],
|
||||
)),
|
||||
Flag::from(("by-last-message", ["blm", "blp"])),
|
||||
Flag::from(("by-birthday", ["by-birthdate", "bbd"])),
|
||||
Flag::from(("random", ["rand"])),
|
||||
// Sort reverse
|
||||
Flag::from(("reverse", ["r", "rev"])),
|
||||
// Privacy filter
|
||||
Flag::from(ALL),
|
||||
Flag::from(("private-only", ["po"])),
|
||||
// Additional fields to include
|
||||
Flag::from((
|
||||
"with-last-switch",
|
||||
["with-last-fronted", "with-last-front", "wls", "wlf"],
|
||||
)),
|
||||
Flag::from(("with-last-message", ["with-last-proxy", "wlm", "wlp"])),
|
||||
Flag::from(("with-message-count", ["wmc"])),
|
||||
Flag::from(("with-created", ["wc"])),
|
||||
Flag::from((
|
||||
"with-avatar",
|
||||
["with-image", "with-icon", "wa", "wi", "ia", "ii", "img"],
|
||||
)),
|
||||
Flag::from(("with-pronouns", ["wp", "wprns"])),
|
||||
Flag::from(("with-display-name", ["with-displayname", "wdn"])),
|
||||
Flag::from(("with-birthday", ["wbd", "wb"])),
|
||||
]
|
||||
}
|
||||
12
crates/command_parser/Cargo.toml
Normal file
12
crates/command_parser/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "command_parser"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = { workspace = true }
|
||||
smol_str = "0.3.2"
|
||||
ordermap = "0.5"
|
||||
regex = "1"
|
||||
strsim = "0.11"
|
||||
log = "0.4"
|
||||
162
crates/command_parser/src/command.rs
Normal file
162
crates/command_parser/src/command.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::{Debug, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::{flag::Flag, parameter::ParameterValue, token::Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Command {
|
||||
// TODO: fix hygiene
|
||||
pub tokens: Vec<Token>,
|
||||
pub flags: HashSet<Flag>,
|
||||
pub help: SmolStr,
|
||||
pub cb: SmolStr,
|
||||
pub show_in_suggestions: bool,
|
||||
pub parse_flags_before: usize,
|
||||
pub hidden_flags: HashSet<SmolStr>,
|
||||
pub flag_values: HashMap<SmolStr, Option<ParameterValue>>,
|
||||
pub original: Option<Arc<Command>>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(tokens: impl IntoIterator<Item = Token>, cb: impl Into<SmolStr>) -> Self {
|
||||
let tokens = tokens.into_iter().collect::<Vec<_>>();
|
||||
assert!(tokens.len() > 0);
|
||||
// figure out which token to parse / put flags after
|
||||
// (by default, put flags after the last token)
|
||||
let mut parse_flags_before = tokens.len();
|
||||
for (idx, token) in tokens.iter().enumerate().rev() {
|
||||
match token {
|
||||
// we want flags to go before any parameters
|
||||
Token::Parameter(_) => parse_flags_before = idx,
|
||||
Token::Value { .. } => break,
|
||||
}
|
||||
}
|
||||
Self {
|
||||
flags: HashSet::new(),
|
||||
help: SmolStr::new_static("<no help text>"),
|
||||
cb: cb.into(),
|
||||
show_in_suggestions: true,
|
||||
parse_flags_before,
|
||||
tokens,
|
||||
hidden_flags: HashSet::new(),
|
||||
flag_values: HashMap::new(),
|
||||
original: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help(mut self, v: impl Into<SmolStr>) -> Self {
|
||||
self.help = v.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn show_in_suggestions(mut self, v: bool) -> Self {
|
||||
self.show_in_suggestions = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn flags(mut self, flags: impl IntoIterator<Item = impl Into<Flag>>) -> Self {
|
||||
self.flags.extend(flags.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn flag(mut self, flag: impl Into<Flag>) -> Self {
|
||||
self.flags.insert(flag.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hidden_flag(mut self, flag: impl Into<Flag>) -> Self {
|
||||
let flag = flag.into();
|
||||
self.hidden_flags.insert(flag.get_name().into());
|
||||
self.flags.insert(flag);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn flag_value(
|
||||
mut self,
|
||||
flag_name: impl Into<SmolStr>,
|
||||
value: impl Into<Option<ParameterValue>>,
|
||||
) -> Self {
|
||||
self.flag_values.insert(flag_name.into(), value.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Command {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cb == other.cb
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Command {}
|
||||
|
||||
impl std::hash::Hash for Command {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.cb.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let visible_flags = self
|
||||
.flags
|
||||
.iter()
|
||||
.filter(|flag| !self.hidden_flags.contains(flag.get_name()))
|
||||
.collect::<Vec<_>>();
|
||||
let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| {
|
||||
if visible_flags.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
write!(f, "{}(", space.then_some(" ").unwrap_or(""))?;
|
||||
let mut written = 0;
|
||||
let max_flags = visible_flags.len().min(5);
|
||||
for flag in &visible_flags {
|
||||
if written > max_flags {
|
||||
break;
|
||||
}
|
||||
write!(f, "{flag}")?;
|
||||
if max_flags > written {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
written += 1;
|
||||
}
|
||||
if visible_flags.len() > written {
|
||||
let rest_count = visible_flags.len() - written;
|
||||
write!(
|
||||
f,
|
||||
" ...and {rest_count} flag{}...",
|
||||
(rest_count > 1).then_some("s").unwrap_or(""),
|
||||
)?;
|
||||
}
|
||||
write!(f, "){}", space.then_some("").unwrap_or(" "))
|
||||
};
|
||||
|
||||
for (idx, token) in self.tokens.iter().enumerate() {
|
||||
if idx == self.parse_flags_before {
|
||||
write_flags(f, false)?;
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"{token}{}",
|
||||
(idx < self.tokens.len() - 1).then_some(" ").unwrap_or("")
|
||||
)?;
|
||||
}
|
||||
if self.tokens.len() == self.parse_flags_before {
|
||||
write_flags(f, true)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// a macro is required because generic cant be different types at the same time (which means you couldnt have ["member", MemberRef, "subcmd"] etc)
|
||||
// (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway)
|
||||
#[macro_export]
|
||||
macro_rules! command {
|
||||
($($v:expr),+ => $cb:expr$(,)*) => {
|
||||
$crate::command::Command::new($crate::tokens!($($v),+), $cb)
|
||||
};
|
||||
}
|
||||
137
crates/command_parser/src/flag.rs
Normal file
137
crates/command_parser/src/flag.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use std::{fmt::Display, hash::Hash};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::parameter::{Parameter, ParameterKind, ParameterValue};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlagValueMatchError {
|
||||
ValueMissing,
|
||||
InvalidValue { raw: SmolStr, msg: SmolStr },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Flag {
|
||||
name: SmolStr,
|
||||
aliases: Vec<SmolStr>,
|
||||
value: Option<Parameter>,
|
||||
}
|
||||
|
||||
impl Display for Flag {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "-{}", self.name)?;
|
||||
if let Some(value) = self.value.as_ref() {
|
||||
write!(f, "=")?;
|
||||
value.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Flag {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name.eq(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Flag {}
|
||||
|
||||
impl Hash for Flag {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlagMatchError {
|
||||
ValueMatchFailed(FlagValueMatchError),
|
||||
}
|
||||
|
||||
type TryMatchFlagResult = Option<Result<Option<ParameterValue>, FlagMatchError>>;
|
||||
|
||||
impl Flag {
|
||||
pub fn new(name: impl Into<SmolStr>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
aliases: Vec::new(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(mut self, param: impl Into<Parameter>) -> Self {
|
||||
self.value = Some(param.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alias(mut self, alias: impl Into<SmolStr>) -> Self {
|
||||
self.aliases.push(alias.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn get_value(&self) -> Option<&Parameter> {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_aliases(&self) -> impl Iterator<Item = &str> {
|
||||
self.aliases.iter().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult {
|
||||
// if not matching the name or any aliases then skip anymore matching
|
||||
if self.name != input_name && self.get_aliases().all(|s| s.ne(input_name)) {
|
||||
return None;
|
||||
}
|
||||
// get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value)
|
||||
let Some(value) = self.value.as_ref() else {
|
||||
return Some(Ok(None));
|
||||
};
|
||||
// check if we have a non-empty flag value, we return error if not (because flag requested a value)
|
||||
let Some(input_value) = input_value else {
|
||||
return Some(Err(FlagMatchError::ValueMatchFailed(
|
||||
FlagValueMatchError::ValueMissing,
|
||||
)));
|
||||
};
|
||||
// try matching the value
|
||||
match value.match_value(input_value) {
|
||||
Ok(param) => Some(Ok(Some(param))),
|
||||
Err(err) => Some(Err(FlagMatchError::ValueMatchFailed(
|
||||
FlagValueMatchError::InvalidValue {
|
||||
raw: input_value.into(),
|
||||
msg: err,
|
||||
},
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Flag {
|
||||
fn from(name: &str) -> Self {
|
||||
Flag::new(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, ParameterKind)> for Flag {
|
||||
fn from((name, value): (&str, ParameterKind)) -> Self {
|
||||
Flag::new(name).value(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> From<(&str, [&str; L])> for Flag {
|
||||
fn from((name, aliases): (&str, [&str; L])) -> Self {
|
||||
let mut flag = Flag::new(name);
|
||||
for alias in aliases {
|
||||
flag = flag.alias(alias);
|
||||
}
|
||||
flag
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> From<((&str, [&str; L]), ParameterKind)> for Flag {
|
||||
fn from(((name, aliases), value): ((&str, [&str; L]), ParameterKind)) -> Self {
|
||||
Flag::from((name, aliases)).value(value)
|
||||
}
|
||||
}
|
||||
566
crates/command_parser/src/lib.rs
Normal file
566
crates/command_parser/src/lib.rs
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
#![feature(anonymous_lifetime_in_impl_trait)]
|
||||
#![feature(round_char_boundary)]
|
||||
#![feature(iter_intersperse)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod command;
|
||||
pub mod flag;
|
||||
pub mod parameter;
|
||||
mod string;
|
||||
pub mod token;
|
||||
pub mod tree;
|
||||
|
||||
use core::panic;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Not;
|
||||
use std::{collections::HashMap, usize};
|
||||
|
||||
use command::Command;
|
||||
use flag::{Flag, FlagMatchError, FlagValueMatchError};
|
||||
use log::debug;
|
||||
use parameter::ParameterValue;
|
||||
use smol_str::SmolStr;
|
||||
use string::MatchedFlag;
|
||||
use token::{Token, TokenMatchResult};
|
||||
|
||||
// todo: this should come from the bot probably
|
||||
const MAX_SUGGESTIONS: usize = 5;
|
||||
|
||||
pub type Tree = tree::TreeBranch;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedCommand {
|
||||
pub command_def: Arc<Command>,
|
||||
pub parameters: HashMap<String, ParameterValue>,
|
||||
pub flags: HashMap<String, Option<ParameterValue>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MatchedTokenState {
|
||||
tree: Arc<Tree>,
|
||||
token: Token,
|
||||
match_result: TokenMatchResult,
|
||||
start_pos: usize,
|
||||
filtered_tokens: Vec<Token>,
|
||||
}
|
||||
|
||||
pub fn parse_command(
|
||||
command_tree: impl Into<Arc<Tree>>,
|
||||
prefix: String,
|
||||
input: String,
|
||||
) -> Result<ParsedCommand, String> {
|
||||
let input: SmolStr = input.into();
|
||||
let mut local_tree = command_tree.into();
|
||||
|
||||
// end position of all currently matched tokens
|
||||
let mut current_pos: usize = 0;
|
||||
let mut current_token_idx: usize = 0;
|
||||
let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new();
|
||||
|
||||
let mut matched_tokens: Vec<MatchedTokenState> = Vec::new();
|
||||
let mut filtered_tokens: Vec<Token> = Vec::new(); // these are tokens that we've already tried (and failed)
|
||||
|
||||
let mut last_optional_param_error: Option<(SmolStr, SmolStr)> = None;
|
||||
|
||||
// track the best attempt at parsing (deepest matched tokens)
|
||||
// so we can use it for error messages/suggestions even if we backtrack later
|
||||
let mut best_attempt: Option<(Arc<Tree>, Vec<MatchedTokenState>, usize)> = None;
|
||||
|
||||
loop {
|
||||
let mut possible_tokens = local_tree
|
||||
.possible_tokens()
|
||||
.filter(|t| !filtered_tokens.contains(t))
|
||||
.collect::<Vec<_>>();
|
||||
// sort so parameters come last
|
||||
// we always want to test values first
|
||||
// parameters that parse the remainder come last (otherwise they would always match)
|
||||
possible_tokens.sort_by(|a, b| match (a, b) {
|
||||
(Token::Parameter(param), _) if param.is_remainder() => std::cmp::Ordering::Greater,
|
||||
(_, Token::Parameter(param)) if param.is_remainder() => std::cmp::Ordering::Less,
|
||||
(Token::Parameter(_), Token::Parameter(_)) => std::cmp::Ordering::Equal,
|
||||
(Token::Parameter(_), _) => std::cmp::Ordering::Greater,
|
||||
(_, Token::Parameter(_)) => std::cmp::Ordering::Less,
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
});
|
||||
debug!("possible: {:?}", possible_tokens);
|
||||
let next = next_token(possible_tokens.iter().cloned(), &input, current_pos);
|
||||
debug!("next: {:?}", next);
|
||||
match &next {
|
||||
Some((found_token, result, new_pos)) => {
|
||||
match &result {
|
||||
// todo: better error messages for these?
|
||||
TokenMatchResult::MissingParameter { name } => {
|
||||
return Err(format!(
|
||||
"Expected parameter `{name}` in command `{prefix}{input} {found_token}`."
|
||||
));
|
||||
}
|
||||
TokenMatchResult::ParameterMatchError { input: raw, msg } => {
|
||||
// we can try other branches if the parameter is optional or skip-on-error parameter
|
||||
if matches!(found_token, Token::Parameter(param) if param.is_optional() || param.is_skip())
|
||||
&& possible_tokens.len() > 1
|
||||
{
|
||||
// save error for later, will be used if no other tokens match
|
||||
last_optional_param_error = Some((raw.clone(), msg.clone()));
|
||||
// try the other branches first
|
||||
filtered_tokens.push(found_token.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(format!(
|
||||
"Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}."
|
||||
));
|
||||
}
|
||||
// don't use a catch-all here, we want to make sure compiler errors when new errors are added
|
||||
TokenMatchResult::MatchedParameter { .. } | TokenMatchResult::MatchedValue => {
|
||||
// clear the error since we successfully matched forward, we dont need it anymore
|
||||
last_optional_param_error = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let TokenMatchResult::MatchedParameter { .. } = result {
|
||||
// we don't add params here, but wait until we matched a full command
|
||||
// then we can use matched_tokens to extract the params
|
||||
// this is so we don't have to keep track of "params" when trying branches
|
||||
}
|
||||
|
||||
// move to the next branch
|
||||
if let Some(next_tree) = local_tree.get_branch(&found_token) {
|
||||
matched_tokens.push(MatchedTokenState {
|
||||
tree: local_tree.clone(),
|
||||
token: found_token.clone(),
|
||||
match_result: result.clone(),
|
||||
start_pos: current_pos,
|
||||
filtered_tokens: filtered_tokens.clone(),
|
||||
});
|
||||
|
||||
// update best attempt if we're deeper
|
||||
if best_attempt.as_ref().map(|x| x.1.len()).unwrap_or(0) < matched_tokens.len()
|
||||
{
|
||||
best_attempt = Some((next_tree.clone(), matched_tokens.clone(), *new_pos));
|
||||
}
|
||||
|
||||
filtered_tokens.clear(); // new branch, new tokens
|
||||
local_tree = next_tree.clone();
|
||||
} else {
|
||||
panic!("found token {found_token:?} could not match tree, at {input}");
|
||||
}
|
||||
|
||||
// advance our position on the input
|
||||
current_pos = *new_pos;
|
||||
current_token_idx += 1;
|
||||
}
|
||||
None => {
|
||||
// redo the previous branches if we didnt match on a parameter
|
||||
// this is a bit of a hack, but its necessary for making parameters on the same depth work
|
||||
if let Some(state) = matched_tokens
|
||||
.pop()
|
||||
.and_then(|m| matches!(m.token, Token::Parameter(_)).then_some(m))
|
||||
{
|
||||
debug!("redoing previous branch: {:?}", state.token);
|
||||
local_tree = state.tree;
|
||||
current_pos = state.start_pos; // reset position to previous branch's start
|
||||
filtered_tokens = state.filtered_tokens; // reset filtered tokens to the previous branch's
|
||||
filtered_tokens.push(state.token);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((raw, msg)) = last_optional_param_error {
|
||||
return Err(format!(
|
||||
"Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}."
|
||||
));
|
||||
}
|
||||
|
||||
// restore best attempt if it's deeper than current state
|
||||
// this helps when we backtracked out of the correct path because of a later error
|
||||
if let Some((best_tree, best_matched, best_pos)) = best_attempt {
|
||||
if best_matched.len() > matched_tokens.len() {
|
||||
local_tree = best_tree;
|
||||
matched_tokens = best_matched;
|
||||
current_pos = best_pos;
|
||||
}
|
||||
}
|
||||
|
||||
let mut error = format!("Unknown command `{prefix}{input}`.");
|
||||
|
||||
// normalize input by replacing parameters with placeholders
|
||||
let mut normalized_input = String::new();
|
||||
for state in &matched_tokens {
|
||||
write!(&mut normalized_input, "{} ", state.token).unwrap();
|
||||
}
|
||||
normalized_input.push_str(&input[current_pos..].trim_start());
|
||||
|
||||
let input_tokens = input.split_whitespace().collect::<Vec<_>>();
|
||||
let mut possible_commands = rank_possible_commands(
|
||||
&normalized_input,
|
||||
local_tree.possible_commands(usize::MAX),
|
||||
&input_tokens,
|
||||
);
|
||||
|
||||
// checks if we matched a parameter last
|
||||
// if we did, we might have matched a parameter "by accident" (ie. `pk;s renam` matched `s <system>`)
|
||||
// so we also want to suggest commands from the *previous* branch
|
||||
if let Some(state) = matched_tokens.last()
|
||||
&& matches!(state.token, Token::Parameter(_))
|
||||
{
|
||||
let mut parent_input = String::new();
|
||||
// recreate input string up to the parameter
|
||||
for parent_state in matched_tokens.iter().take(matched_tokens.len() - 1) {
|
||||
write!(&mut parent_input, "{} ", parent_state.token).unwrap();
|
||||
}
|
||||
// assume the user intended to type a command here, so we use the raw input
|
||||
// (eg. `s renam` -> `s renam`)
|
||||
parent_input.push_str(&input[state.start_pos..].trim_start());
|
||||
|
||||
let input_tokens = parent_input.split_whitespace().collect::<Vec<_>>();
|
||||
let parent_commands = rank_possible_commands(
|
||||
&parent_input,
|
||||
state.tree.possible_commands(usize::MAX),
|
||||
&input_tokens,
|
||||
);
|
||||
possible_commands.extend(parent_commands);
|
||||
|
||||
// re-deduplicate
|
||||
possible_commands.dedup_by(|a, b| {
|
||||
let cmd_a = a.0.original.as_deref().unwrap_or(&a.0);
|
||||
let cmd_b = b.0.original.as_deref().unwrap_or(&b.0);
|
||||
cmd_a == cmd_b
|
||||
});
|
||||
// re-sort after extending
|
||||
possible_commands
|
||||
.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||
}
|
||||
|
||||
if possible_commands.is_empty().not() {
|
||||
error.push_str(" Perhaps you meant one of the following commands:\n");
|
||||
fmt_commands_list(&mut error, &prefix, possible_commands);
|
||||
} else {
|
||||
// add a space between the unknown command and "for a list of all possible commands"
|
||||
// message if we didn't add any possible suggestions
|
||||
error.push_str(" ");
|
||||
}
|
||||
|
||||
error.push_str(
|
||||
"For a list of all possible commands, see <https://pluralkit.me/commands>.",
|
||||
);
|
||||
|
||||
// todo: check if last token is a common incorrect unquote (multi-member names etc)
|
||||
// todo: check if this is a system name in pk;s command
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
// match flags until there are none left
|
||||
while let Some(matched_flag) = string::next_flag(&input, current_pos) {
|
||||
current_pos = matched_flag.next_pos;
|
||||
debug!("flag matched {matched_flag:?}");
|
||||
raw_flags.push((current_token_idx, matched_flag));
|
||||
}
|
||||
// if we have a command, stop parsing and return it (only if there is no remaining input)
|
||||
if current_pos >= input.len()
|
||||
&& let Some(command) = local_tree.command()
|
||||
{
|
||||
// match the flags against this commands flags
|
||||
let mut flags: HashMap<String, Option<ParameterValue>> = HashMap::new();
|
||||
let mut misplaced_flags: Vec<MatchedFlag> = Vec::new();
|
||||
let mut invalid_flags: Vec<MatchedFlag> = Vec::new();
|
||||
|
||||
let mut params: HashMap<String, ParameterValue> = HashMap::new();
|
||||
for state in &matched_tokens {
|
||||
if let TokenMatchResult::MatchedParameter { name, value } = &state.match_result {
|
||||
params.insert(name.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (token_idx, raw_flag) in raw_flags {
|
||||
let Some(matched_flag) = match_flag(command.flags.iter(), raw_flag.clone()) else {
|
||||
invalid_flags.push(raw_flag);
|
||||
continue;
|
||||
};
|
||||
|
||||
if token_idx != command.parse_flags_before {
|
||||
misplaced_flags.push(raw_flag);
|
||||
continue;
|
||||
}
|
||||
|
||||
match matched_flag {
|
||||
// a flag was matched
|
||||
Ok((name, value)) => {
|
||||
flags.insert(name.into(), value);
|
||||
}
|
||||
Err((flag, err)) => {
|
||||
let error = match err {
|
||||
FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => {
|
||||
format!(
|
||||
"Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.",
|
||||
name = flag.get_name()
|
||||
)
|
||||
}
|
||||
FlagMatchError::ValueMatchFailed(
|
||||
FlagValueMatchError::InvalidValue { msg, raw },
|
||||
) => {
|
||||
format!(
|
||||
"Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.",
|
||||
name = flag.get_name()
|
||||
)
|
||||
}
|
||||
};
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let full_cmd = command.original.as_ref().unwrap_or(&command);
|
||||
if misplaced_flags.is_empty().not() {
|
||||
let mut error = format!(
|
||||
"Flag{} ",
|
||||
(misplaced_flags.len() > 1).then_some("s").unwrap_or("")
|
||||
);
|
||||
for (idx, matched_flag) in misplaced_flags.iter().enumerate() {
|
||||
write!(&mut error, "`-{}`", matched_flag.name).expect("oom");
|
||||
if idx < misplaced_flags.len() - 1 {
|
||||
error.push_str(", ");
|
||||
}
|
||||
}
|
||||
write!(
|
||||
&mut error,
|
||||
" in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.",
|
||||
(misplaced_flags.len() > 1).then_some("are").unwrap_or("is"),
|
||||
command = full_cmd
|
||||
).expect("oom");
|
||||
return Err(error);
|
||||
}
|
||||
if invalid_flags.is_empty().not() {
|
||||
let mut error = format!(
|
||||
"Flag{} ",
|
||||
(invalid_flags.len() > 1).then_some("s").unwrap_or("")
|
||||
);
|
||||
for (idx, matched_flag) in invalid_flags.iter().enumerate() {
|
||||
write!(&mut error, "`-{}`", matched_flag.name).expect("oom");
|
||||
if idx < invalid_flags.len() - 1 {
|
||||
error.push_str(", ");
|
||||
}
|
||||
}
|
||||
write!(
|
||||
&mut error,
|
||||
" {} seem to be applicable in this command (`{prefix}{command}`).",
|
||||
(invalid_flags.len() > 1)
|
||||
.then_some("don't")
|
||||
.unwrap_or("doesn't"),
|
||||
command = full_cmd
|
||||
)
|
||||
.expect("oom");
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
for (name, value) in &full_cmd.flag_values {
|
||||
flags.insert(name.to_string(), value.clone());
|
||||
}
|
||||
|
||||
debug!("{} {flags:?} {params:?}", full_cmd.cb);
|
||||
return Ok(ParsedCommand {
|
||||
command_def: full_cmd.clone(),
|
||||
flags,
|
||||
parameters: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_flag<'a>(
|
||||
possible_flags: impl Iterator<Item = &'a Flag>,
|
||||
matched_flag: MatchedFlag<'a>,
|
||||
) -> Option<Result<(SmolStr, Option<ParameterValue>), (&'a Flag, FlagMatchError)>> {
|
||||
// check for all (possible) flags, see if token matches
|
||||
for flag in possible_flags {
|
||||
debug!("matching flag {flag:?}");
|
||||
match flag.try_match(matched_flag.name, matched_flag.value) {
|
||||
Some(Ok(param)) => return Some(Ok((flag.get_name().into(), param))),
|
||||
Some(Err(err)) => return Some(Err((flag, err))),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the next token from an either raw or partially parsed command string
|
||||
///
|
||||
/// Returns:
|
||||
/// - nothing (none matched)
|
||||
/// - matched token, to move deeper into the tree
|
||||
/// - matched value (if this command matched an user-provided value such as a member name)
|
||||
/// - end position of matched token
|
||||
/// - error when matching
|
||||
fn next_token<'a>(
|
||||
possible_tokens: impl Iterator<Item = &'a Token>,
|
||||
input: &str,
|
||||
current_pos: usize,
|
||||
) -> Option<(Token, TokenMatchResult, usize)> {
|
||||
// get next parameter, matching quotes
|
||||
let matched = string::next_param(&input, current_pos);
|
||||
debug!("matched: {matched:?}\n---");
|
||||
|
||||
// iterate over tokens and run try_match
|
||||
for token in possible_tokens {
|
||||
let is_match_remaining_token =
|
||||
|token: &Token| matches!(token, Token::Parameter(param) if param.is_remainder());
|
||||
// check if this is a token that matches the rest of the input
|
||||
let match_remaining = is_match_remaining_token(token);
|
||||
// either use matched param or rest of the input if matching remaining
|
||||
let input_to_match = matched.as_ref().map(|v| {
|
||||
match_remaining
|
||||
.then_some(&input[current_pos..])
|
||||
.unwrap_or(v.value)
|
||||
});
|
||||
let next_pos = match matched {
|
||||
// return last possible pos if we matched remaining,
|
||||
Some(_) if match_remaining => input.len(),
|
||||
// otherwise use matched param next pos,
|
||||
Some(ref param) => param.next_pos,
|
||||
// and if didnt match anything we stay where we are
|
||||
None => current_pos,
|
||||
};
|
||||
match token.try_match(input_to_match) {
|
||||
Some(result) => {
|
||||
//debug!("matched token: {}", token);
|
||||
return Some((token.clone(), result, next_pos));
|
||||
}
|
||||
None => {} // continue matching until we exhaust all tokens
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// todo: should probably move this somewhere else
|
||||
fn rank_possible_commands(
|
||||
input: &str,
|
||||
possible_commands: impl IntoIterator<Item = &Command>,
|
||||
input_tokens: &[&str],
|
||||
) -> Vec<(Command, String, f64, bool)> {
|
||||
let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands
|
||||
.into_iter()
|
||||
.map(|cmd| cmd.original.as_deref().unwrap_or(cmd))
|
||||
.filter(|cmd| cmd.show_in_suggestions)
|
||||
.flat_map(|cmd| {
|
||||
let versions = generate_command_versions(cmd, input_tokens);
|
||||
versions
|
||||
.into_iter()
|
||||
.map(move |(display, scoring, is_alias)| {
|
||||
let similarity = strsim::jaro_winkler(&input, &scoring);
|
||||
// if similarity > 0.7 {
|
||||
// debug!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity);
|
||||
// }
|
||||
(cmd, display, similarity, is_alias)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
commands_with_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
// remove duplicate commands
|
||||
let mut seen_commands = std::collections::HashSet::new();
|
||||
let mut best_commands = Vec::new();
|
||||
for (cmd, version, score, is_alias) in commands_with_scores {
|
||||
if seen_commands.insert(cmd) {
|
||||
best_commands.push((cmd, version, score, is_alias));
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_SCORE_THRESHOLD: f64 = 0.8;
|
||||
if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// if score falls off too much, don't show
|
||||
let falloff_threshold: f64 = 0.2;
|
||||
let best_score = best_commands[0].2;
|
||||
|
||||
let mut commands_to_show = Vec::new();
|
||||
for (command, version, score, is_alias) in best_commands.into_iter().take(MAX_SUGGESTIONS) {
|
||||
let delta = best_score - score;
|
||||
if delta > falloff_threshold {
|
||||
break;
|
||||
}
|
||||
commands_to_show.push((command.clone(), version, score, is_alias));
|
||||
}
|
||||
|
||||
commands_to_show
|
||||
}
|
||||
|
||||
fn fmt_commands_list(
|
||||
f: &mut String,
|
||||
prefix: &str,
|
||||
commands_to_show: Vec<(Command, String, f64, bool)>,
|
||||
) {
|
||||
for (command, version, _, is_alias) in commands_to_show {
|
||||
writeln!(
|
||||
f,
|
||||
"- **{prefix}{version}**{alias} - *{help}*",
|
||||
help = command.help,
|
||||
alias = is_alias
|
||||
.then(|| format!(
|
||||
" (alias of **{prefix}{base_version}**)",
|
||||
base_version = build_command_string(&command, None, &[])
|
||||
))
|
||||
.unwrap_or_else(String::new),
|
||||
)
|
||||
.expect("oom");
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_command_versions(cmd: &Command, input_tokens: &[&str]) -> Vec<(String, String, bool)> {
|
||||
let mut versions = Vec::new();
|
||||
|
||||
// Start with base version using primary names
|
||||
let base_display = build_command_string(cmd, None, &[]);
|
||||
let base_scoring = build_command_string(cmd, None, input_tokens);
|
||||
versions.push((base_display, base_scoring, false));
|
||||
|
||||
// Generate versions for each alias combination
|
||||
for (idx, token) in cmd.tokens.iter().enumerate() {
|
||||
if let Token::Value { aliases, .. } = token {
|
||||
for alias in aliases {
|
||||
let display = build_command_string(cmd, Some((idx, alias.as_str())), &[]);
|
||||
let scoring = build_command_string(cmd, Some((idx, alias.as_str())), input_tokens);
|
||||
versions.push((display, scoring, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
versions
|
||||
}
|
||||
|
||||
fn build_command_string(
|
||||
cmd: &Command,
|
||||
alias_replacement: Option<(usize, &str)>,
|
||||
input_tokens: &[&str],
|
||||
) -> String {
|
||||
let mut result = String::new();
|
||||
for (idx, token) in cmd.tokens.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
// Check if we should use an alias for this token
|
||||
let replacement = alias_replacement
|
||||
.filter(|(i, _)| *i == idx)
|
||||
.map(|(_, alias)| alias);
|
||||
|
||||
match token {
|
||||
Token::Value { name, .. } => {
|
||||
result.push_str(replacement.unwrap_or(name));
|
||||
}
|
||||
Token::Parameter(param) => {
|
||||
// if we have an input token at this position, use it
|
||||
// otherwise use the placeholder
|
||||
if let Some(input_token) = input_tokens.get(idx) {
|
||||
result.push_str(input_token);
|
||||
} else {
|
||||
write!(&mut result, "{param}").unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
520
crates/command_parser/src/parameter.rs
Normal file
520
crates/command_parser/src/parameter.rs
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use smol_str::{SmolStr, format_smolstr};
|
||||
|
||||
use crate::token::{Token, TokenMatchResult};
|
||||
|
||||
pub const MESSAGE_REF: ParameterKind = ParameterKind::MessageRef {
|
||||
id: true,
|
||||
link: true,
|
||||
};
|
||||
pub const MESSAGE_LINK: ParameterKind = ParameterKind::MessageRef {
|
||||
id: false,
|
||||
link: true,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ParameterKind {
|
||||
OpaqueString,
|
||||
OpaqueInt,
|
||||
MemberRef,
|
||||
MemberRefs,
|
||||
GroupRef,
|
||||
GroupRefs,
|
||||
SystemRef,
|
||||
UserRef,
|
||||
MessageRef { id: bool, link: bool },
|
||||
ChannelRef,
|
||||
GuildRef,
|
||||
MemberPrivacyTarget,
|
||||
GroupPrivacyTarget,
|
||||
SystemPrivacyTarget,
|
||||
PrivacyLevel,
|
||||
Toggle,
|
||||
Avatar,
|
||||
ProxySwitchAction,
|
||||
}
|
||||
|
||||
impl ParameterKind {
|
||||
pub(crate) fn default_name(&self) -> &str {
|
||||
match self {
|
||||
ParameterKind::OpaqueString => "string",
|
||||
ParameterKind::OpaqueInt => "number",
|
||||
ParameterKind::MemberRef => "target",
|
||||
ParameterKind::MemberRefs => "targets",
|
||||
ParameterKind::GroupRef => "target",
|
||||
ParameterKind::GroupRefs => "targets",
|
||||
ParameterKind::SystemRef => "target",
|
||||
ParameterKind::UserRef => "target",
|
||||
ParameterKind::MessageRef { .. } => "target",
|
||||
ParameterKind::ChannelRef => "target",
|
||||
ParameterKind::GuildRef => "target",
|
||||
ParameterKind::MemberPrivacyTarget => "member_privacy_target",
|
||||
ParameterKind::GroupPrivacyTarget => "group_privacy_target",
|
||||
ParameterKind::SystemPrivacyTarget => "system_privacy_target",
|
||||
ParameterKind::PrivacyLevel => "privacy_level",
|
||||
ParameterKind::Toggle => "toggle",
|
||||
ParameterKind::Avatar => "avatar",
|
||||
ParameterKind::ProxySwitchAction => "proxy_switch_action",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParameterValue {
|
||||
OpaqueString(String),
|
||||
OpaqueInt(i32),
|
||||
MemberRef(String),
|
||||
MemberRefs(Vec<String>),
|
||||
GroupRef(String),
|
||||
GroupRefs(Vec<String>),
|
||||
SystemRef(String),
|
||||
UserRef(u64),
|
||||
MessageRef(Option<u64>, Option<u64>, u64),
|
||||
ChannelRef(u64),
|
||||
GuildRef(u64),
|
||||
MemberPrivacyTarget(String),
|
||||
GroupPrivacyTarget(String),
|
||||
SystemPrivacyTarget(String),
|
||||
PrivacyLevel(String),
|
||||
Toggle(bool),
|
||||
Avatar(String),
|
||||
ProxySwitchAction(ProxySwitchAction),
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Parameter {
|
||||
name: SmolStr,
|
||||
kind: ParameterKind,
|
||||
remainder: bool,
|
||||
optional: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ParameterKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn remainder(mut self) -> Self {
|
||||
self.remainder = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn optional(mut self) -> Self {
|
||||
self.optional = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn skip(mut self) -> Self {
|
||||
self.skip = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_remainder(&self) -> bool {
|
||||
self.remainder
|
||||
}
|
||||
|
||||
pub fn is_optional(&self) -> bool {
|
||||
self.optional
|
||||
}
|
||||
|
||||
pub fn is_skip(&self) -> bool {
|
||||
self.skip
|
||||
}
|
||||
|
||||
pub fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
match self.kind {
|
||||
// TODO: actually parse image url
|
||||
ParameterKind::OpaqueString => Ok(ParameterValue::OpaqueString(input.into())),
|
||||
ParameterKind::OpaqueInt => input
|
||||
.parse::<i32>()
|
||||
.map(|num| ParameterValue::OpaqueInt(num))
|
||||
.map_err(|err| format_smolstr!("invalid integer: {err}")),
|
||||
ParameterKind::GroupRef => Ok(ParameterValue::GroupRef(input.into())),
|
||||
ParameterKind::GroupRefs => Ok(ParameterValue::GroupRefs(
|
||||
input.split(' ').map(|s| s.trim().to_string()).collect(),
|
||||
)),
|
||||
ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())),
|
||||
ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs(
|
||||
input.split(' ').map(|s| s.trim().to_string()).collect(),
|
||||
)),
|
||||
ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())),
|
||||
ParameterKind::UserRef => parse_user_ref(input),
|
||||
ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input)
|
||||
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())),
|
||||
ParameterKind::GroupPrivacyTarget => GroupPrivacyTargetKind::from_str(input)
|
||||
.map(|target| ParameterValue::GroupPrivacyTarget(target.as_ref().into())),
|
||||
ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input)
|
||||
.map(|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into())),
|
||||
ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input)
|
||||
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())),
|
||||
ParameterKind::Toggle => {
|
||||
Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into()))
|
||||
}
|
||||
ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())),
|
||||
ParameterKind::MessageRef { id, link } => {
|
||||
if id {
|
||||
if let Ok(message_id) = input.parse::<u64>() {
|
||||
return Ok(ParameterValue::MessageRef(None, None, message_id));
|
||||
}
|
||||
}
|
||||
|
||||
if link {
|
||||
static SERVER_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(
|
||||
|| {
|
||||
regex::Regex::new(
|
||||
r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(?P<guild>\d+)/(?P<channel>\d+)/(?P<message>\d+)",
|
||||
)
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
static DM_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(
|
||||
|| {
|
||||
regex::Regex::new(
|
||||
r"https://(?:\w+\.)?discord(?:app)?\.com/channels/@me/(?P<channel>\d+)/(?P<message>\d+)",
|
||||
)
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(captures) = SERVER_RE.captures(input) {
|
||||
let guild_id = captures.parse_id("guild")?;
|
||||
let channel_id = captures.parse_id("channel")?;
|
||||
let message_id = captures.parse_id("message")?;
|
||||
|
||||
Ok(ParameterValue::MessageRef(
|
||||
Some(guild_id),
|
||||
Some(channel_id),
|
||||
message_id,
|
||||
))
|
||||
} else if let Some(captures) = DM_RE.captures(input) {
|
||||
let channel_id = captures.parse_id("channel")?;
|
||||
let message_id = captures.parse_id("message")?;
|
||||
|
||||
Ok(ParameterValue::MessageRef(
|
||||
None,
|
||||
Some(channel_id),
|
||||
message_id,
|
||||
))
|
||||
} else {
|
||||
Err(SmolStr::new("invalid message reference"))
|
||||
}
|
||||
} else {
|
||||
unreachable!("link and id both cant be false")
|
||||
}
|
||||
}
|
||||
ParameterKind::ChannelRef => {
|
||||
let mut text = input;
|
||||
|
||||
if text.len() > 3 && text.starts_with("<#") && text.ends_with('>') {
|
||||
text = &text[2..text.len() - 1];
|
||||
}
|
||||
|
||||
text.parse::<u64>()
|
||||
.map(ParameterValue::ChannelRef)
|
||||
.map_err(|_| SmolStr::new("invalid channel ID"))
|
||||
}
|
||||
ParameterKind::GuildRef => input
|
||||
.parse::<u64>()
|
||||
.map(ParameterValue::GuildRef)
|
||||
.map_err(|_| SmolStr::new("invalid guild ID")),
|
||||
ParameterKind::ProxySwitchAction => ProxySwitchAction::from_str(input)
|
||||
.map(ParameterValue::ProxySwitchAction)
|
||||
.map_err(|_| SmolStr::new("invalid proxy switch action, must be new/add/off")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Parameter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
ParameterKind::OpaqueString => {
|
||||
write!(f, "[{}]", self.name)
|
||||
}
|
||||
ParameterKind::OpaqueInt => {
|
||||
write!(f, "[{}]", self.name)
|
||||
}
|
||||
ParameterKind::MemberRef => write!(f, "<target member>"),
|
||||
ParameterKind::MemberRefs => write!(f, "<member 1> <member 2> <member 3>"),
|
||||
ParameterKind::GroupRef => write!(f, "<target group>"),
|
||||
ParameterKind::GroupRefs => write!(f, "<group 1> <group 2> <group 3>"),
|
||||
ParameterKind::SystemRef => write!(f, "<target system>"),
|
||||
ParameterKind::UserRef => write!(f, "<target user>"),
|
||||
ParameterKind::MessageRef { link, id } => write!(
|
||||
f,
|
||||
"<target message {}>",
|
||||
link.then_some("link")
|
||||
.into_iter()
|
||||
.chain(id.then_some("id"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
),
|
||||
ParameterKind::ChannelRef => write!(f, "<target channel>"),
|
||||
ParameterKind::GuildRef => write!(f, "<target guild>"),
|
||||
ParameterKind::MemberPrivacyTarget => write!(f, "<privacy target>"),
|
||||
ParameterKind::GroupPrivacyTarget => write!(f, "<privacy target>"),
|
||||
ParameterKind::SystemPrivacyTarget => write!(f, "<privacy target>"),
|
||||
ParameterKind::PrivacyLevel => write!(f, "[privacy level]"),
|
||||
ParameterKind::Toggle => write!(f, "<on|off>"),
|
||||
ParameterKind::Avatar => write!(f, "<url|@mention>"),
|
||||
ParameterKind::ProxySwitchAction => write!(f, "<new|add|off>"),
|
||||
}?;
|
||||
if self.is_remainder() {
|
||||
write!(f, "...")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_remainder(kind: ParameterKind) -> bool {
|
||||
matches!(kind, ParameterKind::MemberRefs | ParameterKind::GroupRefs)
|
||||
}
|
||||
|
||||
impl From<ParameterKind> for Parameter {
|
||||
fn from(value: ParameterKind) -> Self {
|
||||
Parameter {
|
||||
name: value.default_name().into(),
|
||||
kind: value,
|
||||
remainder: is_remainder(value),
|
||||
optional: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, ParameterKind)> for Parameter {
|
||||
fn from((name, kind): (&str, ParameterKind)) -> Self {
|
||||
Parameter {
|
||||
name: name.into(),
|
||||
kind,
|
||||
remainder: is_remainder(kind),
|
||||
optional: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// if no input is left to parse, this parameter matches to Null
|
||||
#[derive(Clone)]
|
||||
pub struct Optional<P: Into<Parameter>>(pub P);
|
||||
|
||||
impl<P: Into<Parameter>> From<Optional<P>> for Parameter {
|
||||
fn from(value: Optional<P>) -> Self {
|
||||
let p = value.0.into();
|
||||
p.optional()
|
||||
}
|
||||
}
|
||||
|
||||
/// tells the parser to use the remainder of the input as the input to this parameter
|
||||
#[derive(Clone)]
|
||||
pub struct Remainder<P: Into<Parameter>>(pub P);
|
||||
|
||||
impl<P: Into<Parameter>> From<Remainder<P>> for Parameter {
|
||||
fn from(value: Remainder<P>) -> Self {
|
||||
let p = value.0.into();
|
||||
p.remainder()
|
||||
}
|
||||
}
|
||||
|
||||
// todo: this should ideally be removed in favor of making Token::Parameter take multiple parameters
|
||||
/// skips the branch this parameter is in if it does not match
|
||||
#[derive(Clone)]
|
||||
pub struct Skip<P: Into<Parameter>>(pub P);
|
||||
|
||||
impl<P: Into<Parameter>> From<Skip<P>> for Parameter {
|
||||
fn from(value: Skip<P>) -> Self {
|
||||
let p = value.0.into();
|
||||
p.skip()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_user_ref(input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
if let Ok(user_id) = input.parse::<u64>() {
|
||||
return Ok(ParameterValue::UserRef(user_id));
|
||||
}
|
||||
|
||||
static RE: std::sync::LazyLock<Regex> =
|
||||
std::sync::LazyLock::new(|| Regex::new(r"<@!?(\d{17,19})>").unwrap());
|
||||
if let Some(captures) = RE.captures(&input) {
|
||||
return captures[1]
|
||||
.parse::<u64>()
|
||||
.map(|id| ParameterValue::UserRef(id))
|
||||
.map_err(|_| SmolStr::new("invalid user ID"));
|
||||
}
|
||||
|
||||
Err(SmolStr::new("invalid user ID"))
|
||||
}
|
||||
|
||||
macro_rules! define_enum {
|
||||
($name:ident ($pretty_name:expr): $($variant:ident),* $(,)?) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum $name {
|
||||
$($variant),*
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub const PRETTY_NAME: &'static str = $pretty_name;
|
||||
|
||||
pub fn variants() -> impl Iterator<Item = Self> {
|
||||
[$(Self::$variant),*].into_iter()
|
||||
}
|
||||
|
||||
pub fn variants_str() -> impl Iterator<Item = &'static str> {
|
||||
[$(Self::$variant.as_ref()),*].into_iter()
|
||||
}
|
||||
|
||||
pub fn get_error() -> SmolStr {
|
||||
let pretty_name = Self::PRETTY_NAME;
|
||||
let vars = Self::variants_str().intersperse("/").collect::<SmolStr>();
|
||||
format_smolstr!("invalid {pretty_name}, must be one of {vars}")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! str_enum {
|
||||
($name:ident: $($variant:ident = $variant_str:literal),* $(,)?) => {
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
$(Self::$variant => $variant_str),*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_enum {
|
||||
($name:ident ($pretty_name:expr): $($variant:ident = $variant_str:literal $(| $variant_matches:literal)*),* $(,)?) => {
|
||||
define_enum!($name($pretty_name): $($variant),*);
|
||||
|
||||
str_enum!($name: $($variant = $variant_str),*);
|
||||
|
||||
impl FromStr for $name {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
$($variant_str $(| $variant_matches)* => Ok(Self::$variant),)*
|
||||
_ => Err(Self::get_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
auto_enum! {
|
||||
MemberPrivacyTargetKind("member privacy target"):
|
||||
Visibility = "visibility",
|
||||
Name = "name",
|
||||
Description = "description",
|
||||
Banner = "banner",
|
||||
Avatar = "avatar",
|
||||
Birthday = "birthday",
|
||||
Pronouns = "pronouns",
|
||||
Proxy = "proxy",
|
||||
Metadata = "metadata",
|
||||
}
|
||||
|
||||
auto_enum! {
|
||||
GroupPrivacyTargetKind("group privacy target"):
|
||||
Name = "name",
|
||||
Icon = "icon" | "avatar",
|
||||
Description = "description",
|
||||
Banner = "banner",
|
||||
List = "list",
|
||||
Metadata = "metadata",
|
||||
Visibility = "visibility",
|
||||
}
|
||||
|
||||
auto_enum! {
|
||||
SystemPrivacyTargetKind("system privacy target"):
|
||||
Name = "name",
|
||||
Avatar = "avatar" | "pfp" | "pic" | "icon",
|
||||
Description = "description" | "desc" | "bio" | "info",
|
||||
Banner = "banner" | "splash" | "cover",
|
||||
Pronouns = "pronouns" | "prns" | "pn",
|
||||
MemberList = "members" | "memberlist" | "list",
|
||||
GroupList = "groups" | "gs",
|
||||
Front = "front" | "fronter" | "fronters",
|
||||
FrontHistory = "fronthistory" | "fh" | "switches",
|
||||
}
|
||||
|
||||
auto_enum! {
|
||||
PrivacyLevelKind("privacy level"):
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
define_enum!(Toggle("toggle"): On, Off);
|
||||
str_enum!(Toggle: On = "on", Off = "off");
|
||||
|
||||
impl FromStr for Toggle {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let matches_self = |toggle: &Self| {
|
||||
matches!(
|
||||
Token::from(*toggle).try_match(Some(s)),
|
||||
Some(TokenMatchResult::MatchedValue)
|
||||
)
|
||||
};
|
||||
Self::variants()
|
||||
.find(matches_self)
|
||||
.ok_or_else(Self::get_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Toggle> for Token {
|
||||
fn from(toggle: Toggle) -> Self {
|
||||
match toggle {
|
||||
Toggle::On => Self::from(("on", ["yes", "true", "enable", "enabled"])),
|
||||
Toggle::Off => Self::from(("off", ["no", "false", "disable", "disabled"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<bool> for Toggle {
|
||||
fn into(self) -> bool {
|
||||
match self {
|
||||
Toggle::On => true,
|
||||
Toggle::Off => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
define_enum!(ProxySwitchAction("proxy switch action"): New, Add, Off);
|
||||
str_enum!(ProxySwitchAction: New = "new", Add = "add", Off = "off");
|
||||
|
||||
impl FromStr for ProxySwitchAction {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::variants()
|
||||
.find(|action| action.as_ref() == s)
|
||||
.ok_or_else(Self::get_error)
|
||||
}
|
||||
}
|
||||
|
||||
trait ParseMessageLink {
|
||||
fn parse_id(&self, name: &str) -> Result<u64, SmolStr>;
|
||||
}
|
||||
|
||||
impl ParseMessageLink for regex::Captures<'_> {
|
||||
fn parse_id(&self, name: &str) -> Result<u64, SmolStr> {
|
||||
self.name(name)
|
||||
.and_then(|m| m.as_str().parse::<u64>().ok())
|
||||
.ok_or_else(|| SmolStr::new(format!("invalid {} ID in message link", name)))
|
||||
}
|
||||
}
|
||||
186
crates/command_parser/src/string.rs
Normal file
186
crates/command_parser/src/string.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use log::debug;
|
||||
use smol_str::{SmolStr, ToSmolStr};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// Dictionary of (left, right) quote pairs
|
||||
// Each char in the string is an individual quote, multi-char strings imply "one of the following chars"
|
||||
// Certain languages can have quote patterns that have a different character for open and close
|
||||
pub static ref QUOTE_PAIRS: HashMap<SmolStr, SmolStr> = {
|
||||
let mut pairs: HashMap<SmolStr, SmolStr> = HashMap::new();
|
||||
|
||||
let mut insert_pair = |a: &'static str, b: &'static str| {
|
||||
let a = SmolStr::new_static(a);
|
||||
let b = SmolStr::new_static(b);
|
||||
pairs.insert(a.clone(), b.clone());
|
||||
// make it easier to look up right quotes
|
||||
for char in a.chars() {
|
||||
pairs.insert(char.to_smolstr(), b.clone());
|
||||
}
|
||||
};
|
||||
|
||||
// Basic
|
||||
insert_pair( "'", "'" ); // ASCII single quotes
|
||||
insert_pair( "\"", "\"" ); // 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
|
||||
insert_pair( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes
|
||||
insert_pair( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes
|
||||
|
||||
// Chevrons (normal and "fullwidth" variants)
|
||||
insert_pair( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<<text>>)
|
||||
insert_pair( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<)
|
||||
insert_pair( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away (<text>)
|
||||
insert_pair( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<)
|
||||
|
||||
// Other
|
||||
insert_pair( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese)
|
||||
|
||||
pairs
|
||||
};
|
||||
}
|
||||
|
||||
// very very simple quote matching
|
||||
// expects match_str to be trimmed (no whitespace, from the start at least)
|
||||
// returns the position of an end quote if any is found
|
||||
// quotes need to be at start/end of words, and are ignored if a closing quote is not present
|
||||
// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
|
||||
fn find_quotes(match_str: &str) -> Option<usize> {
|
||||
if let Some(right) = QUOTE_PAIRS.get(&match_str[0..match_str.ceil_char_boundary(1)]) {
|
||||
// try matching end quote
|
||||
for possible_quote in right.chars() {
|
||||
for (pos, _) in match_str.match_indices(possible_quote) {
|
||||
if match_str.len() == pos + 1
|
||||
|| match_str.chars().nth(pos + 1).unwrap().is_whitespace()
|
||||
{
|
||||
return Some(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct MatchedParam<'a> {
|
||||
pub(super) value: &'a str,
|
||||
pub(super) next_pos: usize,
|
||||
#[allow(dead_code)] // this'll prolly be useful sometime later
|
||||
pub(super) in_quotes: bool,
|
||||
}
|
||||
|
||||
pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option<MatchedParam<'a>> {
|
||||
if input.len() == current_pos {
|
||||
return None;
|
||||
}
|
||||
|
||||
let leading_whitespace_count =
|
||||
input[..current_pos].len() - input[..current_pos].trim_start().len();
|
||||
let substr_to_match = &input[current_pos + leading_whitespace_count..];
|
||||
debug!("stuff: {input} {current_pos} {leading_whitespace_count}");
|
||||
debug!("to match: {substr_to_match}");
|
||||
|
||||
if let Some(end_quote_pos) = find_quotes(substr_to_match) {
|
||||
// return quoted string, without quotes
|
||||
return Some(MatchedParam {
|
||||
value: &substr_to_match[1..end_quote_pos],
|
||||
next_pos: current_pos + end_quote_pos + 1,
|
||||
in_quotes: true,
|
||||
});
|
||||
}
|
||||
|
||||
// find next whitespace character
|
||||
for (pos, char) in substr_to_match.char_indices() {
|
||||
if char.is_whitespace() {
|
||||
return Some(MatchedParam {
|
||||
value: &substr_to_match[..pos],
|
||||
next_pos: current_pos + pos + 1,
|
||||
in_quotes: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if we're here, we went to EOF and didn't match any whitespace
|
||||
// so we return the whole string
|
||||
Some(MatchedParam {
|
||||
value: substr_to_match,
|
||||
next_pos: current_pos + substr_to_match.len(),
|
||||
in_quotes: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct MatchedFlag<'a> {
|
||||
pub(super) name: &'a str,
|
||||
pub(super) value: Option<&'a str>,
|
||||
pub(super) next_pos: usize,
|
||||
}
|
||||
|
||||
pub(super) fn next_flag<'a>(input: &'a str, mut current_pos: usize) -> Option<MatchedFlag<'a>> {
|
||||
if input.len() == current_pos {
|
||||
return None;
|
||||
}
|
||||
|
||||
let leading_whitespace_count =
|
||||
input[..current_pos].len() - input[..current_pos].trim_start().len();
|
||||
let substr_to_match = &input[current_pos + leading_whitespace_count..];
|
||||
|
||||
// if the param is quoted, it should not be processed as a flag
|
||||
if find_quotes(substr_to_match).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
debug!("flag input {substr_to_match}");
|
||||
// strip the -
|
||||
let original_len = substr_to_match.len();
|
||||
let substr_without_dashes = substr_to_match.trim_start_matches('-');
|
||||
let dash_count = original_len - substr_without_dashes.len();
|
||||
|
||||
if dash_count == 0 || dash_count > 2 {
|
||||
// if it doesn't have one, then it is not a flag
|
||||
// or if it has more dashes than 2, assume its not a flag
|
||||
return None;
|
||||
}
|
||||
|
||||
let substr_to_match = substr_without_dashes;
|
||||
current_pos += dash_count;
|
||||
|
||||
// try finding = or whitespace
|
||||
for (pos, char) in substr_to_match.char_indices() {
|
||||
debug!("flag find char {char} at {pos}");
|
||||
if char == '=' {
|
||||
let name = &substr_to_match[..pos];
|
||||
debug!("flag find {name}");
|
||||
// try to get the value
|
||||
let Some(param) = next_param(input, current_pos + pos + 1) else {
|
||||
return Some(MatchedFlag {
|
||||
name,
|
||||
value: Some(""),
|
||||
next_pos: current_pos + pos + 1,
|
||||
});
|
||||
};
|
||||
return Some(MatchedFlag {
|
||||
name,
|
||||
value: Some(param.value),
|
||||
next_pos: param.next_pos,
|
||||
});
|
||||
} else if char.is_whitespace() {
|
||||
// no value if whitespace
|
||||
return Some(MatchedFlag {
|
||||
name: &substr_to_match[..pos],
|
||||
value: None,
|
||||
next_pos: current_pos + pos + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if eof then no value
|
||||
Some(MatchedFlag {
|
||||
name: substr_to_match,
|
||||
value: None,
|
||||
next_pos: current_pos + substr_to_match.len(),
|
||||
})
|
||||
}
|
||||
174
crates/command_parser/src/token.rs
Normal file
174
crates/command_parser/src/token.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
use std::fmt::{Debug, Display};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::parameter::{Parameter, ParameterValue};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Token {
|
||||
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
||||
Value {
|
||||
name: SmolStr,
|
||||
aliases: Vec<SmolStr>,
|
||||
},
|
||||
|
||||
/// A parameter that must be provided a value
|
||||
Parameter(Parameter),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TokenMatchResult {
|
||||
MatchedValue,
|
||||
MatchedParameter {
|
||||
name: SmolStr,
|
||||
value: ParameterValue,
|
||||
},
|
||||
ParameterMatchError {
|
||||
input: SmolStr,
|
||||
msg: SmolStr,
|
||||
},
|
||||
MissingParameter {
|
||||
name: SmolStr,
|
||||
},
|
||||
}
|
||||
|
||||
// q: why not have a NoMatch variant in TokenMatchResult?
|
||||
// a: because we want to differentiate between no match and match failure (it matched with an error)
|
||||
// "no match" has a different charecteristic because we want to continue matching other tokens...
|
||||
// ...while "match failure" means we should stop matching and return the error
|
||||
// Option fits this better (and it makes some code look a bit nicer)
|
||||
pub type TryMatchResult = Option<TokenMatchResult>;
|
||||
|
||||
impl Token {
|
||||
pub fn try_match(&self, input: Option<&str>) -> TryMatchResult {
|
||||
let input = match input {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
// short circuit on:
|
||||
return match self {
|
||||
// missing paramaters
|
||||
Self::Parameter(param) => Some(
|
||||
param
|
||||
.is_optional()
|
||||
.then(|| TokenMatchResult::MatchedParameter {
|
||||
name: param.name().into(),
|
||||
value: ParameterValue::Null,
|
||||
})
|
||||
.unwrap_or_else(|| TokenMatchResult::MissingParameter {
|
||||
name: param.name().into(),
|
||||
}),
|
||||
),
|
||||
// everything else doesnt match if no input anyway
|
||||
Self::Value { .. } => None,
|
||||
// don't add a _ match here!
|
||||
};
|
||||
}
|
||||
};
|
||||
let input = input.trim();
|
||||
|
||||
// try actually matching stuff
|
||||
match self {
|
||||
Self::Value { name, aliases } => (aliases.iter().chain(std::iter::once(name)))
|
||||
.any(|v| v.eq(input))
|
||||
.then(|| TokenMatchResult::MatchedValue),
|
||||
Self::Parameter(param) => Some(match param.match_value(input) {
|
||||
Ok(matched) => TokenMatchResult::MatchedParameter {
|
||||
name: param.name().into(),
|
||||
value: matched,
|
||||
},
|
||||
Err(err) => TokenMatchResult::ParameterMatchError {
|
||||
input: input.into(),
|
||||
msg: err,
|
||||
},
|
||||
}),
|
||||
// don't add a _ match here!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Value { name, .. } => write!(f, "{name}"),
|
||||
Self::Parameter(param) => write!(f, "{param}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (name, aliases) -> Token::Value
|
||||
impl<const L: usize> From<(&str, [&str; L])> for Token {
|
||||
fn from((name, aliases): (&str, [&str; L])) -> Self {
|
||||
Self::Value {
|
||||
name: name.into(),
|
||||
aliases: aliases.into_iter().map(SmolStr::new).collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// name -> Token::Value
|
||||
impl From<&str> for Token {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::from((value, []))
|
||||
}
|
||||
}
|
||||
|
||||
// parameter -> Token::Parameter
|
||||
impl<P: Into<Parameter>> From<P> for Token {
|
||||
fn from(value: P) -> Self {
|
||||
Self::Parameter(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that produces [`Token`]s.
|
||||
///
|
||||
/// This is more of a convenience type that the [`tokens!`] macro uses in order
|
||||
/// to more easily combine tokens together.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokensIterator {
|
||||
inner: Vec<Token>,
|
||||
}
|
||||
|
||||
impl TokensIterator {
|
||||
pub(crate) fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { inner: tokens }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TokensIterator {
|
||||
type Item = Token;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
(self.inner.len() > 0).then(|| self.inner.remove(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Token>> for TokensIterator {
|
||||
fn from(value: Vec<Token>) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Token>> From<T> for TokensIterator {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(vec![value.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> From<[Token; L]> for TokensIterator {
|
||||
fn from(value: [Token; L]) -> Self {
|
||||
Self::new(value.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> From<[Self; L]> for TokensIterator {
|
||||
fn from(value: [Self; L]) -> Self {
|
||||
Self::new(value.into_iter().map(|t| t.inner).flatten().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! tokens {
|
||||
($($v:expr),+$(,)*) => {
|
||||
$crate::token::TokensIterator::from([$($crate::token::TokensIterator::from($v.clone())),+])
|
||||
};
|
||||
}
|
||||
97
crates/command_parser/src/tree.rs
Normal file
97
crates/command_parser/src/tree.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ordermap::OrderMap;
|
||||
|
||||
use crate::{command::Command, token::Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeBranch {
|
||||
current_command: Option<Arc<Command>>,
|
||||
branches: OrderMap<Token, Arc<TreeBranch>>,
|
||||
}
|
||||
|
||||
impl Default for TreeBranch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_command: None,
|
||||
branches: OrderMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeBranch {
|
||||
pub fn register_command(&mut self, command: Command) {
|
||||
let mut current_branch = self;
|
||||
// iterate over tokens in command
|
||||
for (index, token) in command.tokens.clone().into_iter().enumerate() {
|
||||
// if the token is an optional parameter, register rest of the tokens to a separate branch
|
||||
// this allows optional parameters to work if they are not the last token
|
||||
if matches!(token, Token::Parameter(ref param) if param.is_optional())
|
||||
&& index < command.tokens.len() - 1
|
||||
{
|
||||
let mut new_command = command.clone();
|
||||
new_command.tokens = command.tokens[index + 1..].to_vec();
|
||||
new_command.original = command
|
||||
.original
|
||||
.clone()
|
||||
.or_else(|| Some(Arc::new(command.clone())));
|
||||
|
||||
// if the optional parameter we're skipping is *before* the flag insertion point,
|
||||
// we need to shift the index left by 1 to account for the removed token
|
||||
if new_command.parse_flags_before > index {
|
||||
new_command.parse_flags_before -= 1;
|
||||
}
|
||||
|
||||
current_branch.register_command(new_command);
|
||||
}
|
||||
// recursively get or create a sub-branch for each token
|
||||
current_branch = Arc::make_mut(
|
||||
current_branch
|
||||
.branches
|
||||
.entry(token)
|
||||
.or_insert_with(|| Arc::new(TreeBranch::default())),
|
||||
);
|
||||
}
|
||||
// when we're out of tokens add the command to the last branch
|
||||
current_branch.current_command = Some(Arc::new(command));
|
||||
}
|
||||
|
||||
pub fn command(&self) -> Option<Arc<Command>> {
|
||||
self.current_command.clone()
|
||||
}
|
||||
|
||||
pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> + Clone {
|
||||
self.branches.keys()
|
||||
}
|
||||
|
||||
pub fn possible_commands(&self, max_depth: usize) -> impl Iterator<Item = &Command> {
|
||||
// dusk: i am too lazy to write an iterator for this without using recursion so we box everything
|
||||
fn box_iter<'a>(
|
||||
iter: impl Iterator<Item = &'a Command> + 'a,
|
||||
) -> Box<dyn Iterator<Item = &'a Command> + 'a> {
|
||||
Box::new(iter)
|
||||
}
|
||||
|
||||
if max_depth == 0 {
|
||||
return box_iter(std::iter::empty());
|
||||
}
|
||||
let mut commands = box_iter(std::iter::empty());
|
||||
for branch in self.branches.values() {
|
||||
if let Some(command) = branch.current_command.as_ref() {
|
||||
commands = box_iter(commands.chain(std::iter::once(command.as_ref())));
|
||||
// we dont need to look further if we found a command
|
||||
continue;
|
||||
}
|
||||
commands = box_iter(commands.chain(branch.possible_commands(max_depth - 1)));
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
pub fn get_branch(&self, token: &Token) -> Option<&Arc<Self>> {
|
||||
self.branches.get(token)
|
||||
}
|
||||
|
||||
pub fn branches(&self) -> impl Iterator<Item = (&Token, &Arc<Self>)> {
|
||||
self.branches.iter()
|
||||
}
|
||||
}
|
||||
51
crates/command_parser/tests/parser.rs
Normal file
51
crates/command_parser/tests/parser.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use command_parser::{Tree, command::Command, parameter::*, parse_command, tokens};
|
||||
|
||||
/// this checks if we properly keep track of filtered tokens (eg. branches we failed on)
|
||||
/// when we backtrack. a previous parser bug would cause infinite loops since it did not
|
||||
/// (the parser would "flip-flop" between branches) this is here for reference.
|
||||
#[test]
|
||||
fn test_infinite_loop_repro() {
|
||||
let p1 = Optional(("param1", ParameterKind::OpaqueString));
|
||||
let p2 = Optional(("param2", ParameterKind::OpaqueString));
|
||||
|
||||
let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1");
|
||||
let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2");
|
||||
|
||||
let mut tree = Tree::default();
|
||||
tree.register_command(cmd1);
|
||||
tree.register_command(cmd2);
|
||||
|
||||
let input = "s foo C";
|
||||
// this should fail and not loop
|
||||
let result = parse_command(tree, "pk;".to_string(), input.to_string());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// check if we have params from other branches when we trying to match them and they succeeded
|
||||
/// but then we backtracked, making them invalid. this should no longer happen since we just
|
||||
/// extract params from matched tokens when we match the command, but keeping here just for reference.
|
||||
#[test]
|
||||
fn test_dirty_params() {
|
||||
let p1 = Optional(("param1", ParameterKind::OpaqueString));
|
||||
let p2 = Optional(("param2", ParameterKind::OpaqueString));
|
||||
|
||||
let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1");
|
||||
let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2");
|
||||
|
||||
let mut tree = Tree::default();
|
||||
tree.register_command(cmd1);
|
||||
tree.register_command(cmd2);
|
||||
|
||||
let input = "s foo B";
|
||||
let result = parse_command(tree, "pk;".to_string(), input.to_string()).unwrap();
|
||||
|
||||
println!("params: {:?}", result.parameters);
|
||||
assert!(
|
||||
!result.parameters.contains_key("param1"),
|
||||
"params should not contain 'param1' from failed branch"
|
||||
);
|
||||
assert!(
|
||||
result.parameters.contains_key("param2"),
|
||||
"params should contain 'param2'"
|
||||
);
|
||||
}
|
||||
55
crates/command_parser/tests/ranking.rs
Normal file
55
crates/command_parser/tests/ranking.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use command_parser::{Tree, command::Command, parameter::*, parse_command, tokens};
|
||||
|
||||
#[test]
|
||||
fn test_typoed_command_with_parameter() {
|
||||
let message_token = ("message", ["msg", "messageinfo"]);
|
||||
let author_token = ("author", ["sender", "a"]);
|
||||
|
||||
// message <optional msg ref> author
|
||||
let cmd = Command::new(
|
||||
tokens!(message_token, Optional(MESSAGE_REF), author_token),
|
||||
"message_author",
|
||||
)
|
||||
.help("Shows the author of a proxied message");
|
||||
|
||||
let mut tree = Tree::default();
|
||||
tree.register_command(cmd);
|
||||
|
||||
let input = "message 1 auth";
|
||||
let result = parse_command(tree, "pk;".to_string(), input.to_string());
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("Should have failed to parse"),
|
||||
Err(msg) => {
|
||||
println!("Error: {}", msg);
|
||||
assert!(msg.contains("Perhaps you meant one of the following commands"));
|
||||
assert!(msg.contains("message <target message link/id> author"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typoed_command_with_flags() {
|
||||
let message_token = ("message", ["msg", "messageinfo"]);
|
||||
let author_token = ("author", ["sender", "a"]);
|
||||
|
||||
let cmd = Command::new(tokens!(message_token, author_token), "message_author")
|
||||
.flag(("flag", ["f"]))
|
||||
.flag(("flag2", ["f2"]))
|
||||
.help("Shows the author of a proxied message");
|
||||
|
||||
let mut tree = Tree::default();
|
||||
tree.register_command(cmd);
|
||||
|
||||
let input = "message auth -f -flag2";
|
||||
let result = parse_command(tree, "pk;".to_string(), input.to_string());
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("Should have failed to parse"),
|
||||
Err(msg) => {
|
||||
println!("Error: {}", msg);
|
||||
assert!(msg.contains("Perhaps you meant one of the following commands"));
|
||||
assert!(msg.contains("message author"));
|
||||
}
|
||||
}
|
||||
}
|
||||
23
crates/commands/Cargo.toml
Normal file
23
crates/commands/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "commands"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "commands"
|
||||
|
||||
[[bin]]
|
||||
name = "write_cs_glue"
|
||||
path = "src/write_cs_glue.rs"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
lazy_static = { workspace = true }
|
||||
command_parser = { path = "../command_parser"}
|
||||
command_definitions = { path = "../command_definitions"}
|
||||
uniffi = { version = "0.29" }
|
||||
log = "0.4"
|
||||
simple_logger = "4.3.3"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.29", features = [ "build" ] }
|
||||
3
crates/commands/build.rs
Normal file
3
crates/commands/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
uniffi::generate_scaffolding("src/commands.udl").unwrap();
|
||||
}
|
||||
36
crates/commands/src/commands.udl
Normal file
36
crates/commands/src/commands.udl
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
namespace commands {
|
||||
CommandResult parse_command(string prefix, string input);
|
||||
string get_related_commands(string prefix, string input);
|
||||
};
|
||||
[Enum]
|
||||
interface CommandResult {
|
||||
Ok(ParsedCommand command);
|
||||
Err(string error);
|
||||
};
|
||||
[Enum]
|
||||
interface Parameter {
|
||||
MemberRef(string member);
|
||||
MemberRefs(sequence<string> members);
|
||||
GroupRef(string group);
|
||||
GroupRefs(sequence<string> groups);
|
||||
SystemRef(string system);
|
||||
UserRef(u64 user_id);
|
||||
MessageRef(u64? guild_id, u64? channel_id, u64 message_id);
|
||||
ChannelRef(u64 channel_id);
|
||||
GuildRef(u64 guild_id);
|
||||
MemberPrivacyTarget(string target);
|
||||
GroupPrivacyTarget(string target);
|
||||
SystemPrivacyTarget(string target);
|
||||
PrivacyLevel(string level);
|
||||
OpaqueString(string raw);
|
||||
OpaqueInt(i32 raw);
|
||||
Toggle(boolean toggle);
|
||||
Avatar(string avatar);
|
||||
ProxySwitchAction(string action);
|
||||
Null();
|
||||
};
|
||||
dictionary ParsedCommand {
|
||||
string command_ref;
|
||||
record<string, Parameter> params;
|
||||
record<string, Parameter?> flags;
|
||||
};
|
||||
183
crates/commands/src/lib.rs
Normal file
183
crates/commands/src/lib.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Write,
|
||||
sync::{Arc, Once},
|
||||
};
|
||||
|
||||
use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree};
|
||||
|
||||
uniffi::include_scaffolding!("commands");
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COMMAND_TREE: Arc<Tree> = {
|
||||
let mut tree = Tree::default();
|
||||
|
||||
command_definitions::all().into_iter().for_each(|x| tree.register_command(x));
|
||||
|
||||
Arc::new(tree)
|
||||
};
|
||||
}
|
||||
|
||||
static LOG_INIT: Once = Once::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandResult {
|
||||
Ok { command: ParsedCommand },
|
||||
Err { error: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Parameter {
|
||||
MemberRef {
|
||||
member: String,
|
||||
},
|
||||
MemberRefs {
|
||||
members: Vec<String>,
|
||||
},
|
||||
GroupRef {
|
||||
group: String,
|
||||
},
|
||||
GroupRefs {
|
||||
groups: Vec<String>,
|
||||
},
|
||||
SystemRef {
|
||||
system: String,
|
||||
},
|
||||
UserRef {
|
||||
user_id: u64,
|
||||
},
|
||||
MessageRef {
|
||||
guild_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
message_id: u64,
|
||||
},
|
||||
ChannelRef {
|
||||
channel_id: u64,
|
||||
},
|
||||
GuildRef {
|
||||
guild_id: u64,
|
||||
},
|
||||
MemberPrivacyTarget {
|
||||
target: String,
|
||||
},
|
||||
GroupPrivacyTarget {
|
||||
target: String,
|
||||
},
|
||||
SystemPrivacyTarget {
|
||||
target: String,
|
||||
},
|
||||
PrivacyLevel {
|
||||
level: String,
|
||||
},
|
||||
OpaqueString {
|
||||
raw: String,
|
||||
},
|
||||
OpaqueInt {
|
||||
raw: i32,
|
||||
},
|
||||
Toggle {
|
||||
toggle: bool,
|
||||
},
|
||||
Avatar {
|
||||
avatar: String,
|
||||
},
|
||||
ProxySwitchAction {
|
||||
action: String,
|
||||
},
|
||||
Null,
|
||||
}
|
||||
|
||||
impl From<ParameterValue> for Parameter {
|
||||
fn from(value: ParameterValue) -> Self {
|
||||
match value {
|
||||
ParameterValue::MemberRef(member) => Self::MemberRef { member },
|
||||
ParameterValue::MemberRefs(members) => Self::MemberRefs { members },
|
||||
ParameterValue::GroupRef(group) => Self::GroupRef { group },
|
||||
ParameterValue::GroupRefs(groups) => Self::GroupRefs { groups },
|
||||
ParameterValue::SystemRef(system) => Self::SystemRef { system },
|
||||
ParameterValue::UserRef(user_id) => Self::UserRef { user_id },
|
||||
ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target },
|
||||
ParameterValue::GroupPrivacyTarget(target) => Self::GroupPrivacyTarget { target },
|
||||
ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target },
|
||||
ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level },
|
||||
ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw },
|
||||
ParameterValue::OpaqueInt(raw) => Self::OpaqueInt { raw },
|
||||
ParameterValue::Toggle(toggle) => Self::Toggle { toggle },
|
||||
ParameterValue::Avatar(avatar) => Self::Avatar { avatar },
|
||||
ParameterValue::MessageRef(guild_id, channel_id, message_id) => Self::MessageRef {
|
||||
guild_id,
|
||||
channel_id,
|
||||
message_id,
|
||||
},
|
||||
ParameterValue::ChannelRef(channel_id) => Self::ChannelRef { channel_id },
|
||||
ParameterValue::GuildRef(guild_id) => Self::GuildRef { guild_id },
|
||||
ParameterValue::Null => Self::Null,
|
||||
ParameterValue::ProxySwitchAction(action) => Self::ProxySwitchAction {
|
||||
action: action.as_ref().to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedCommand {
|
||||
pub command_ref: String,
|
||||
pub params: HashMap<String, Parameter>,
|
||||
pub flags: HashMap<String, Option<Parameter>>,
|
||||
}
|
||||
|
||||
pub fn parse_command(prefix: String, input: String) -> CommandResult {
|
||||
LOG_INIT.call_once(|| {
|
||||
if let Err(err) = simple_logger::SimpleLogger::new()
|
||||
.with_level(log::LevelFilter::Info)
|
||||
.env()
|
||||
.init()
|
||||
{
|
||||
eprintln!("cant initialize logger: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
command_parser::parse_command(COMMAND_TREE.clone(), prefix, input).map_or_else(
|
||||
|error| CommandResult::Err { error },
|
||||
|parsed| CommandResult::Ok {
|
||||
command: {
|
||||
let command_ref = parsed.command_def.cb.clone().into();
|
||||
let mut flags = HashMap::with_capacity(parsed.flags.capacity());
|
||||
for (name, value) in parsed.flags {
|
||||
flags.insert(name, value.map(Parameter::from));
|
||||
}
|
||||
let mut params = HashMap::with_capacity(parsed.parameters.capacity());
|
||||
for (name, value) in parsed.parameters {
|
||||
params.insert(name, Parameter::from(value));
|
||||
}
|
||||
ParsedCommand {
|
||||
command_ref,
|
||||
flags,
|
||||
params,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_related_commands(prefix: String, input: String) -> String {
|
||||
let mut s = String::new();
|
||||
for command in command_definitions::all() {
|
||||
if !command.show_in_suggestions {
|
||||
continue;
|
||||
}
|
||||
if command.tokens.first().map_or(false, |token| {
|
||||
token
|
||||
.try_match(Some(&input))
|
||||
.map_or(false, |r| matches!(r, TokenMatchResult::MatchedValue))
|
||||
}) {
|
||||
writeln!(
|
||||
&mut s,
|
||||
"- **{prefix}{command}** - *{help}*",
|
||||
help = command.help
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
51
crates/commands/src/main.rs
Normal file
51
crates/commands/src/main.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#![feature(iter_intersperse)]
|
||||
|
||||
use command_parser::Tree;
|
||||
use commands::COMMAND_TREE;
|
||||
|
||||
fn main() {
|
||||
parse();
|
||||
}
|
||||
|
||||
fn related() {
|
||||
let cmd = std::env::args().nth(1).unwrap();
|
||||
let related = commands::get_related_commands("pk;".to_string(), cmd);
|
||||
println!("Related commands:\n{related}");
|
||||
}
|
||||
|
||||
fn parse() {
|
||||
let cmd = std::env::args()
|
||||
.skip(1)
|
||||
.intersperse(" ".to_string())
|
||||
.collect::<String>();
|
||||
if !cmd.is_empty() {
|
||||
use commands::CommandResult;
|
||||
let parsed = commands::parse_command("pk;".to_string(), cmd);
|
||||
match parsed {
|
||||
CommandResult::Ok { command } => println!("{command:#?}"),
|
||||
CommandResult::Err { error } => println!("{error}"),
|
||||
}
|
||||
} else {
|
||||
for command in command_definitions::all() {
|
||||
println!("{} => {} - {}", command.cb, command, command.help);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tree(tree: &Tree, depth: usize) {
|
||||
println!();
|
||||
for (token, branch) in tree.branches() {
|
||||
for _ in 0..depth {
|
||||
print!(" ");
|
||||
}
|
||||
for _ in 0..depth {
|
||||
print!("-");
|
||||
}
|
||||
print!("> {token:?}");
|
||||
if let Some(command) = branch.command() {
|
||||
println!(": {}", command.cb)
|
||||
} else {
|
||||
print_tree(branch, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
318
crates/commands/src/write_cs_glue.rs
Normal file
318
crates/commands/src/write_cs_glue.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
use std::{collections::HashSet, env, fmt::Write, fs, path::PathBuf, str::FromStr};
|
||||
|
||||
use command_parser::{
|
||||
parameter::{Parameter, ParameterKind},
|
||||
token::Token,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let write_location = env::args()
|
||||
.nth(1)
|
||||
.expect("file location should be provided");
|
||||
let write_location = PathBuf::from_str(&write_location).unwrap();
|
||||
|
||||
let commands = command_definitions::all().collect::<Vec<_>>();
|
||||
|
||||
let mut glue = String::new();
|
||||
|
||||
writeln!(&mut glue, "#nullable enable\n")?;
|
||||
writeln!(&mut glue, "using PluralKit.Core;\n")?;
|
||||
writeln!(&mut glue, "using Myriad.Types;")?;
|
||||
writeln!(&mut glue, "namespace PluralKit.Bot;\n")?;
|
||||
|
||||
let mut commands_seen = HashSet::new();
|
||||
let mut record_fields = String::new();
|
||||
for command in &commands {
|
||||
if commands_seen.contains(&command.cb) {
|
||||
continue;
|
||||
}
|
||||
writeln!(
|
||||
&mut record_fields,
|
||||
r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#,
|
||||
command_name = command_callback_to_name(&command.cb),
|
||||
)?;
|
||||
commands_seen.insert(command.cb.clone());
|
||||
}
|
||||
|
||||
commands_seen.clear();
|
||||
let mut match_branches = String::new();
|
||||
for command in &commands {
|
||||
if commands_seen.contains(&command.cb) {
|
||||
continue;
|
||||
}
|
||||
let mut command_params_init = String::new();
|
||||
let command_params = find_parameters(&command.tokens);
|
||||
for param in &command_params {
|
||||
writeln!(
|
||||
&mut command_params_init,
|
||||
r#"@{fieldName} = await ctx.ParamResolve{extract_fn_name}("{name}"){throw_null},"#,
|
||||
fieldName = param.name().replace("-", "_"),
|
||||
name = param.name(),
|
||||
extract_fn_name = get_param_param_ty(param.kind()),
|
||||
throw_null = param
|
||||
.is_optional()
|
||||
.then(String::new)
|
||||
.unwrap_or(format!(" ?? throw new PKError(\"parameter {} not found but was required, this is a bug in the command parser, for command: {}!\")", param.name(), command.cb)),
|
||||
)?;
|
||||
}
|
||||
let mut command_flags_init = String::new();
|
||||
for flag in &command.flags {
|
||||
if let Some(param) = flag.get_value() {
|
||||
writeln!(
|
||||
&mut command_flags_init,
|
||||
r#"@{fieldName} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#,
|
||||
fieldName = flag.get_name().replace("-", "_"),
|
||||
name = flag.get_name(),
|
||||
extract_fn_name = get_param_param_ty(param.kind()),
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
&mut command_flags_init,
|
||||
r#"@{fieldName} = ctx.Parameters.HasFlag("{name}"),"#,
|
||||
fieldName = flag.get_name().replace("-", "_"),
|
||||
name = flag.get_name(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
write!(
|
||||
&mut match_branches,
|
||||
r#"
|
||||
"{command_callback}" => new {command_name}(
|
||||
new {command_name}Params {{ {command_params_init} }},
|
||||
new {command_name}Flags {{ {command_flags_init} }}
|
||||
),
|
||||
"#,
|
||||
command_name = command_callback_to_name(&command.cb),
|
||||
command_callback = command.cb,
|
||||
)?;
|
||||
commands_seen.insert(command.cb.clone());
|
||||
}
|
||||
write!(
|
||||
&mut glue,
|
||||
r#"
|
||||
public abstract record Commands()
|
||||
{{
|
||||
{record_fields}
|
||||
|
||||
public static async Task<Commands?> FromContext(Context ctx)
|
||||
{{
|
||||
return ctx.Parameters.Callback() switch
|
||||
{{
|
||||
{match_branches}
|
||||
_ => null,
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
commands_seen.clear();
|
||||
for command in &commands {
|
||||
if commands_seen.contains(&command.cb) {
|
||||
continue;
|
||||
}
|
||||
let mut command_params_fields = String::new();
|
||||
let command_params = find_parameters(&command.tokens);
|
||||
for param in &command_params {
|
||||
writeln!(
|
||||
&mut command_params_fields,
|
||||
r#"public required {ty}{nullable} @{name};"#,
|
||||
name = param.name().replace("-", "_"),
|
||||
ty = get_param_ty(param.kind()),
|
||||
nullable = param.is_optional().then_some("?").unwrap_or(""),
|
||||
)?;
|
||||
}
|
||||
let mut command_flags_fields = String::new();
|
||||
for flag in &command.flags {
|
||||
if let Some(param) = flag.get_value() {
|
||||
writeln!(
|
||||
&mut command_flags_fields,
|
||||
r#"public {ty}? @{name};"#,
|
||||
name = flag.get_name().replace("-", "_"),
|
||||
ty = get_param_ty(param.kind()),
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
&mut command_flags_fields,
|
||||
r#"public required bool @{name};"#,
|
||||
name = flag.get_name().replace("-", "_"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let mut command_reply_format = String::new();
|
||||
if command
|
||||
.flags
|
||||
.iter()
|
||||
.any(|flag| flag.get_name() == "plaintext")
|
||||
{
|
||||
writeln!(
|
||||
&mut command_reply_format,
|
||||
r#"if (plaintext) return ReplyFormat.Plaintext;"#,
|
||||
)?;
|
||||
}
|
||||
if command.flags.iter().any(|flag| flag.get_name() == "raw") {
|
||||
writeln!(
|
||||
&mut command_reply_format,
|
||||
r#"if (raw) return ReplyFormat.Raw;"#,
|
||||
)?;
|
||||
}
|
||||
command_reply_format.push_str("return ReplyFormat.Standard;\n");
|
||||
let mut command_list_options = String::new();
|
||||
let mut command_list_options_class = String::new();
|
||||
let list_flags = command_definitions::utils::get_list_flags();
|
||||
if list_flags.iter().all(|flag| command.flags.contains(&flag)) {
|
||||
write!(&mut command_list_options_class, ": IHasListOptions")?;
|
||||
writeln!(
|
||||
&mut command_list_options,
|
||||
r#"
|
||||
public ListOptions GetListOptions(Context ctx, SystemId target)
|
||||
{{
|
||||
var directLookupCtx = ctx.DirectLookupContextFor(target);
|
||||
var lookupCtx = ctx.LookupContextFor(target);
|
||||
|
||||
var p = new ListOptions();
|
||||
p.Type = full ? ListType.Long : ListType.Short;
|
||||
// Search description filter
|
||||
p.SearchDescription = search_description;
|
||||
|
||||
// Sort property
|
||||
if (by_name) p.SortProperty = SortProperty.Name;
|
||||
if (by_display_name) p.SortProperty = SortProperty.DisplayName;
|
||||
if (by_id) p.SortProperty = SortProperty.Hid;
|
||||
if (by_message_count) p.SortProperty = SortProperty.MessageCount;
|
||||
if (by_created) p.SortProperty = SortProperty.CreationDate;
|
||||
if (by_last_fronted) p.SortProperty = SortProperty.LastSwitch;
|
||||
if (by_last_message) p.SortProperty = SortProperty.LastMessage;
|
||||
if (by_birthday) p.SortProperty = SortProperty.Birthdate;
|
||||
if (random) p.SortProperty = SortProperty.Random;
|
||||
|
||||
// Sort reverse
|
||||
p.Reverse = reverse;
|
||||
|
||||
// Privacy filter
|
||||
if (all) p.PrivacyFilter = null;
|
||||
if (private_only) 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 = lookupCtx;
|
||||
|
||||
// Additional fields to include
|
||||
p.IncludeLastSwitch = with_last_switch;
|
||||
p.IncludeLastMessage = with_last_message;
|
||||
p.IncludeMessageCount = with_message_count;
|
||||
p.IncludeCreated = with_created;
|
||||
p.IncludeAvatar = with_avatar;
|
||||
p.IncludePronouns = with_pronouns;
|
||||
p.IncludeDisplayName = with_display_name;
|
||||
p.IncludeBirthday = with_birthday;
|
||||
|
||||
// Always show the sort property (unless short list and 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;
|
||||
}}
|
||||
|
||||
p.AssertIsValid();
|
||||
return p;
|
||||
}}
|
||||
"#,
|
||||
)?;
|
||||
}
|
||||
write!(
|
||||
&mut glue,
|
||||
r#"
|
||||
public class {command_name}Params
|
||||
{{
|
||||
{command_params_fields}
|
||||
}}
|
||||
public class {command_name}Flags {command_list_options_class}
|
||||
{{
|
||||
{command_flags_fields}
|
||||
|
||||
public ReplyFormat GetReplyFormat()
|
||||
{{
|
||||
{command_reply_format}
|
||||
}}
|
||||
|
||||
{command_list_options}
|
||||
}}
|
||||
"#,
|
||||
command_name = command_callback_to_name(&command.cb),
|
||||
)?;
|
||||
commands_seen.insert(command.cb.clone());
|
||||
}
|
||||
fs::write(write_location, glue)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command_callback_to_name(cb: &str) -> String {
|
||||
cb.split("_")
|
||||
.map(|w| w.chars().nth(0).unwrap().to_uppercase().collect::<String>() + &w[1..])
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_param_ty(kind: ParameterKind) -> &'static str {
|
||||
match kind {
|
||||
ParameterKind::OpaqueString => "string",
|
||||
ParameterKind::OpaqueInt => "int",
|
||||
ParameterKind::MemberRef => "PKMember",
|
||||
ParameterKind::MemberRefs => "List<PKMember>",
|
||||
ParameterKind::GroupRef => "PKGroup",
|
||||
ParameterKind::GroupRefs => "List<PKGroup>",
|
||||
ParameterKind::SystemRef => "PKSystem",
|
||||
ParameterKind::UserRef => "User",
|
||||
ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject",
|
||||
ParameterKind::GroupPrivacyTarget => "GroupPrivacySubject",
|
||||
ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject",
|
||||
ParameterKind::PrivacyLevel => "PrivacyLevel",
|
||||
ParameterKind::Toggle => "bool",
|
||||
ParameterKind::Avatar => "ParsedImage",
|
||||
ParameterKind::MessageRef { .. } => "Message.Reference",
|
||||
ParameterKind::ChannelRef => "Channel",
|
||||
ParameterKind::GuildRef => "Guild",
|
||||
ParameterKind::ProxySwitchAction => "SystemConfig.ProxySwitchAction",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_param_param_ty(kind: ParameterKind) -> &'static str {
|
||||
match kind {
|
||||
ParameterKind::OpaqueString => "Opaque",
|
||||
ParameterKind::OpaqueInt => "Number",
|
||||
ParameterKind::MemberRef => "Member",
|
||||
ParameterKind::MemberRefs => "Members",
|
||||
ParameterKind::GroupRef => "Group",
|
||||
ParameterKind::GroupRefs => "Groups",
|
||||
ParameterKind::SystemRef => "System",
|
||||
ParameterKind::UserRef => "User",
|
||||
ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget",
|
||||
ParameterKind::GroupPrivacyTarget => "GroupPrivacyTarget",
|
||||
ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget",
|
||||
ParameterKind::PrivacyLevel => "PrivacyLevel",
|
||||
ParameterKind::Toggle => "Toggle",
|
||||
ParameterKind::Avatar => "Avatar",
|
||||
ParameterKind::MessageRef { .. } => "Message",
|
||||
ParameterKind::ChannelRef => "Channel",
|
||||
ParameterKind::GuildRef => "Guild",
|
||||
ParameterKind::ProxySwitchAction => "ProxySwitchAction",
|
||||
}
|
||||
}
|
||||
|
||||
fn find_parameters(tokens: &[Token]) -> Vec<&Parameter> {
|
||||
let mut result = Vec::new();
|
||||
for token in tokens {
|
||||
match token {
|
||||
Token::Parameter(param) => result.push(param),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
2
crates/commands/uniffi.toml
Normal file
2
crates/commands/uniffi.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[bindings.csharp]
|
||||
cdylib_name = "commands"
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(if_let_guard)]
|
||||
#![feature(duration_constructors)]
|
||||
|
||||
use chrono::Timelike;
|
||||
use discord::gateway::cluster_config;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pk_macros = { path = "../macros" }
|
|||
sentry = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["chrono"] }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true}
|
||||
|
|
|
|||
100
flake.lock
generated
100
flake.lock
generated
|
|
@ -3,16 +3,16 @@
|
|||
"crane": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1727316705,
|
||||
"narHash": "sha256-/mumx8AQ5xFuCJqxCIOFCHTVlxHkMT21idpbgbm/TIE=",
|
||||
"lastModified": 1758758545,
|
||||
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "5b03654ce046b5167e7b0bccbd8244cb56c16f0e",
|
||||
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"ref": "v0.19.0",
|
||||
"ref": "v0.21.1",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
@ -26,11 +26,11 @@
|
|||
"pyproject-nix": "pyproject-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754978539,
|
||||
"narHash": "sha256-nrDovydywSKRbWim9Ynmgj8SBm8LK3DI2WuhIqzOHYI=",
|
||||
"lastModified": 1765953015,
|
||||
"narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "dream2nix",
|
||||
"rev": "fbec3263cb4895ac86ee9506cdc4e6919a1a2214",
|
||||
"rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"revCount": 69,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
||||
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?rev=ff81ac966bb2cae68946d5ed5fc4994f96d0ffec&revCount=69"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
|
@ -74,13 +74,13 @@
|
|||
"locked": {
|
||||
"lastModified": 1681286841,
|
||||
"narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
|
||||
"owner": "yusdacra",
|
||||
"owner": "90-008",
|
||||
"repo": "mk-naked-shell",
|
||||
"rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "yusdacra",
|
||||
"owner": "90-008",
|
||||
"repo": "mk-naked-shell",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
@ -104,26 +104,26 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756016279,
|
||||
"narHash": "sha256-5BhsvOXsoMu4ZNe9HxQSynbbXm2FAZ0TIW5mWKlG5+Q=",
|
||||
"owner": "yusdacra",
|
||||
"lastModified": 1768285363,
|
||||
"narHash": "sha256-n4dqIGCz2+/pyP0jtuTZxFTjuyBkgiKMwtOJrmbipDA=",
|
||||
"owner": "90-008",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "4714a69e6de235cf750e6cc73f6a989cc7867579",
|
||||
"rev": "90432aa96bd7bb603ff710ffa2c02959dc338bd3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "yusdacra",
|
||||
"owner": "90-008",
|
||||
"repo": "nix-cargo-integration",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755829505,
|
||||
"narHash": "sha256-4/Jd+LkQ2ssw8luQVkqVs9spDBVE6h/u/hC/tzngsPo=",
|
||||
"lastModified": 1768302833,
|
||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f937f8ecd1c70efd7e9f90ba13dfb400cf559de4",
|
||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -134,11 +134,11 @@
|
|||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1753579242,
|
||||
"narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=",
|
||||
"lastModified": 1765674936,
|
||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e",
|
||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -152,11 +152,11 @@
|
|||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754487366,
|
||||
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
|
||||
"lastModified": 1768135262,
|
||||
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
|
||||
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -167,11 +167,11 @@
|
|||
},
|
||||
"process-compose": {
|
||||
"locked": {
|
||||
"lastModified": 1749418557,
|
||||
"narHash": "sha256-wJHHckWz4Gvj8HXtM5WVJzSKXAEPvskQANVoRiu2w1w=",
|
||||
"lastModified": 1767863885,
|
||||
"narHash": "sha256-XXekPAxzbv1DmHFo3Elmj/vDnvWc1V0jdDUvM0/Wf7k=",
|
||||
"owner": "Platonic-Systems",
|
||||
"repo": "process-compose-flake",
|
||||
"rev": "91dcc48a6298e47e2441ec76df711f4e38eab94e",
|
||||
"rev": "99bea96cf269cfd235833ebdf645b567069fd398",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -211,11 +211,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752481895,
|
||||
"narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=",
|
||||
"lastModified": 1763017646,
|
||||
"narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=",
|
||||
"owner": "pyproject-nix",
|
||||
"repo": "pyproject.nix",
|
||||
"rev": "16ee295c25107a94e59a7fc7f2e5322851781162",
|
||||
"rev": "47bd6f296502842643078d66128f7b5e5370790c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -234,7 +234,8 @@
|
|||
"process-compose": "process-compose",
|
||||
"services": "services",
|
||||
"systems": "systems",
|
||||
"treefmt": "treefmt"
|
||||
"treefmt": "treefmt",
|
||||
"uniffi-bindgen-cs": "uniffi-bindgen-cs"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
|
|
@ -245,11 +246,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756003222,
|
||||
"narHash": "sha256-lmEMhIIbjt8Wp1EYbNqCojuU9ygyDFv8Tu0X1k8qIMc=",
|
||||
"lastModified": 1768272338,
|
||||
"narHash": "sha256-Tg/kL8eKMpZtceDvBDQYU8zowgpr7ucFRnpP/AtfuRM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "88ceedecde53e809b4bf8b5fd10d181889d9bac7",
|
||||
"rev": "03dda130a8701b08b0347fcaf850a190c53a3c1e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -260,11 +261,11 @@
|
|||
},
|
||||
"services": {
|
||||
"locked": {
|
||||
"lastModified": 1755996515,
|
||||
"narHash": "sha256-1RQQIDhshp1g4PP5teqibcFLfk/ckTDOJRckecAHiU0=",
|
||||
"lastModified": 1765168239,
|
||||
"narHash": "sha256-NZ7H4lbbytPNwe4ZyvovycuS1BMBFwJrptgX7NiF+F0=",
|
||||
"owner": "juspay",
|
||||
"repo": "services-flake",
|
||||
"rev": "e316d6b994fd153f0c35d54bd07d60e53f0ad9a9",
|
||||
"rev": "8b6244f2b310f229568d5cadf7dfcb5ebe6f8bda",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -317,11 +318,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755934250,
|
||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||
"lastModified": 1768158989,
|
||||
"narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||
"rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -329,6 +330,25 @@
|
|||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"uniffi-bindgen-cs": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1759932560,
|
||||
"narHash": "sha256-CnfB7/n1W5hbeC+cniJZthkpWO9kLyog/q5ldL6yS9g=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "66c316454a04c025a88f8cc8af495bfc2b422d2f",
|
||||
"revCount": 181,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/90-008/uniffi-bindgen-cs"
|
||||
},
|
||||
"original": {
|
||||
"ref": "refs/heads/main",
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/90-008/uniffi-bindgen-cs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
|
|||
312
flake.nix
312
flake.nix
|
|
@ -11,11 +11,13 @@
|
|||
# rust
|
||||
d2n.url = "github:nix-community/dream2nix";
|
||||
d2n.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nci.url = "github:yusdacra/nix-cargo-integration";
|
||||
nci.url = "github:90-008/nix-cargo-integration";
|
||||
nci.inputs.parts.follows = "parts";
|
||||
nci.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nci.inputs.dream2nix.follows = "d2n";
|
||||
nci.inputs.treefmt.follows = "treefmt";
|
||||
uniffi-bindgen-cs.url = "git+https://github.com/90-008/uniffi-bindgen-cs?ref=refs/heads/main&submodules=1";
|
||||
uniffi-bindgen-cs.flake = false;
|
||||
# misc
|
||||
treefmt.url = "github:numtide/treefmt-nix";
|
||||
treefmt.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
|
@ -37,210 +39,188 @@
|
|||
self',
|
||||
pkgs,
|
||||
lib,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# this is used as devshell for bot, and in the process-compose processes as environment
|
||||
mkBotEnv =
|
||||
cmd:
|
||||
pkgs.buildFHSEnv {
|
||||
name = "env";
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
coreutils
|
||||
git
|
||||
dotnet-sdk_8
|
||||
gcc
|
||||
omnisharp-roslyn
|
||||
bashInteractive
|
||||
];
|
||||
runScript = cmd;
|
||||
};
|
||||
uniffi-bindgen-cs = config.nci.lib.buildCrate {
|
||||
src = inp.uniffi-bindgen-cs;
|
||||
cratePath = "bindgen";
|
||||
};
|
||||
|
||||
rustOutputs = config.nci.outputs;
|
||||
composeCfg = config.process-compose."dev";
|
||||
|
||||
sourceDotenv = ''
|
||||
# shellcheck disable=SC1091
|
||||
[[ -f ".env" ]] && echo "sourcing .env file..." && set -a && source .env && set +a
|
||||
'';
|
||||
in
|
||||
{
|
||||
# _module.args.pkgs = import inp.nixpkgs {
|
||||
# inherit system;
|
||||
# config.permittedInsecurePackages = [ "dotnet-sdk-6.0.428" ];
|
||||
# };
|
||||
|
||||
treefmt = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs.nixfmt.enable = true;
|
||||
};
|
||||
|
||||
nci.toolchainConfig = ./rust-toolchain.toml;
|
||||
nci.projects."pluralkit-services" = {
|
||||
nci.projects."pk-services" = {
|
||||
path = ./.;
|
||||
export = false;
|
||||
};
|
||||
# nci.crates."gateway" = {
|
||||
# depsDrvConfig.mkDerivation = {
|
||||
# nativeBuildInputs = [ pkgs.protobuf ];
|
||||
# };
|
||||
# drvConfig.mkDerivation = {
|
||||
# nativeBuildInputs = [ pkgs.protobuf ];
|
||||
# };
|
||||
# };
|
||||
nci.crates."commands" = rec {
|
||||
depsDrvConfig.env = {
|
||||
# we don't really need this since the lib is just used to generate the bindings
|
||||
doNotRemoveReferencesToVendorDir = true;
|
||||
};
|
||||
depsDrvConfig.mkDerivation = {
|
||||
# also not really needed
|
||||
dontPatchShebangs = true;
|
||||
};
|
||||
drvConfig = depsDrvConfig;
|
||||
};
|
||||
|
||||
apps = {
|
||||
generate-command-parser-bindings.program = pkgs.writeShellApplication {
|
||||
name = "generate-command-parser-bindings";
|
||||
runtimeInputs = [
|
||||
(config.nci.toolchains.mkBuild pkgs)
|
||||
self'.devShells.services.stdenv.cc
|
||||
pkgs.dotnet-sdk_8
|
||||
pkgs.csharpier
|
||||
pkgs.coreutils
|
||||
uniffi-bindgen-cs
|
||||
];
|
||||
text = ''
|
||||
set -x
|
||||
commandslib="''${1:-}"
|
||||
if [ "$commandslib" == "" ]; then
|
||||
cargo -Z unstable-options build --package commands --lib --release --artifact-dir obj/
|
||||
commandslib="obj/libcommands.so"
|
||||
else
|
||||
cp -f "$commandslib" obj/
|
||||
fi
|
||||
uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}"
|
||||
cargo run --package commands --bin write_cs_glue -- "''${2:-./PluralKit.Bot}"/commandtypes.cs
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: expose other rust packages after it's verified they build and work properly
|
||||
packages = lib.genAttrs ["gateway"] (name: rustOutputs.${name}.packages.release);
|
||||
packages = lib.genAttrs [ "gateway" "commands" ] (name: rustOutputs.${name}.packages.release);
|
||||
# TODO: package the bot itself (dotnet)
|
||||
|
||||
devShells = {
|
||||
services = rustOutputs."pluralkit-services".devShell;
|
||||
bot = (mkBotEnv "bash").env;
|
||||
devShells = rec {
|
||||
services = rustOutputs."pk-services".devShell;
|
||||
bot = pkgs.mkShell {
|
||||
name = "pkbot-devshell";
|
||||
nativeBuildInputs = with pkgs; [
|
||||
coreutils
|
||||
git
|
||||
dotnet-sdk_8
|
||||
gcc
|
||||
omnisharp-roslyn
|
||||
bashInteractive
|
||||
postgresql
|
||||
];
|
||||
};
|
||||
all = (pkgs.mkShell.override { stdenv = services.stdenv; }) {
|
||||
name = "pk-devshell";
|
||||
nativeBuildInputs = bot.nativeBuildInputs ++ services.nativeBuildInputs;
|
||||
shellHook = ''
|
||||
${sourceDotenv}
|
||||
'';
|
||||
};
|
||||
docs = pkgs.mkShellNoCC {
|
||||
buildInputs = with pkgs; [ nodejs yarn ];
|
||||
NODE_OPTIONS = "--openssl-legacy-provider";
|
||||
};
|
||||
};
|
||||
|
||||
process-compose."dev" = let
|
||||
dataDir = ".nix-process-compose";
|
||||
sourceDotenv = ''
|
||||
# shellcheck disable=SC2046
|
||||
[[ -f ".env" ]] && echo "sourcing .env file..." && export $(xargs < .env)
|
||||
'';
|
||||
in {
|
||||
imports = [ inp.services.processComposeModules.default ];
|
||||
process-compose."dev" =
|
||||
let
|
||||
dataDir = ".nix-process-compose";
|
||||
in
|
||||
{
|
||||
imports = [ inp.services.processComposeModules.default ];
|
||||
|
||||
settings.log_location = "${dataDir}/log";
|
||||
settings.log_location = "${dataDir}/log";
|
||||
|
||||
settings.environment = {
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = "1";
|
||||
NODE_OPTIONS = "--openssl-legacy-provider";
|
||||
};
|
||||
settings.environment = {
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = "1";
|
||||
NODE_OPTIONS = "--openssl-legacy-provider";
|
||||
};
|
||||
|
||||
services.redis."redis" = {
|
||||
enable = true;
|
||||
dataDir = "${dataDir}/redis";
|
||||
};
|
||||
services.postgres."postgres" = {
|
||||
enable = true;
|
||||
dataDir = "${dataDir}/postgres";
|
||||
initialScript.before = ''
|
||||
CREATE DATABASE pluralkit;
|
||||
CREATE USER postgres WITH password 'postgres';
|
||||
GRANT ALL PRIVILEGES ON DATABASE pluralkit TO postgres;
|
||||
ALTER DATABASE pluralkit OWNER TO postgres;
|
||||
'';
|
||||
};
|
||||
services.redis."redis" = {
|
||||
enable = true;
|
||||
dataDir = "${dataDir}/redis";
|
||||
};
|
||||
services.postgres."postgres" = {
|
||||
enable = true;
|
||||
dataDir = "${dataDir}/postgres";
|
||||
initialScript.before = ''
|
||||
CREATE DATABASE pluralkit;
|
||||
CREATE USER postgres WITH password 'postgres';
|
||||
GRANT ALL PRIVILEGES ON DATABASE pluralkit TO postgres;
|
||||
ALTER DATABASE pluralkit OWNER TO postgres;
|
||||
'';
|
||||
};
|
||||
|
||||
settings.processes =
|
||||
let
|
||||
procCfg = composeCfg.settings.processes;
|
||||
mkServiceInitProcess =
|
||||
{
|
||||
name,
|
||||
inputs ? [ ],
|
||||
...
|
||||
}:
|
||||
let
|
||||
shell = rustOutputs.${name}.devShell;
|
||||
in
|
||||
{
|
||||
settings.processes =
|
||||
let
|
||||
mkServiceProcess =
|
||||
name: attrs:
|
||||
{
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-${name}";
|
||||
runtimeInputs = [ pkgs.coreutils ];
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
nix develop .#services -c cargo run --package ${name}
|
||||
'';
|
||||
};
|
||||
} // attrs;
|
||||
in
|
||||
{
|
||||
### migrations ###
|
||||
pluralkit-migrate = mkServiceProcess "migrate" {
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
};
|
||||
### bot ###
|
||||
pluralkit-bot = {
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-${name}-init";
|
||||
runtimeInputs =
|
||||
(with pkgs; [
|
||||
coreutils
|
||||
shell.stdenv.cc
|
||||
])
|
||||
++ shell.nativeBuildInputs
|
||||
++ inputs;
|
||||
name = "pluralkit-bot";
|
||||
runtimeInputs = [ pkgs.coreutils ];
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
exec cargo build --bin ${name}
|
||||
${self'.apps.generate-command-parser-bindings.program}
|
||||
nix develop .#bot -c bash -c "dotnet build ./PluralKit.Bot/PluralKit.Bot.csproj -c Release -o obj/ && dotnet obj/PluralKit.Bot.dll"
|
||||
'';
|
||||
};
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
depends_on.redis.condition = "process_healthy";
|
||||
depends_on.pluralkit-gateway.condition = "process_log_ready";
|
||||
depends_on.pluralkit-migrate.condition = "process_completed_successfully";
|
||||
# TODO: add liveness check
|
||||
ready_log_line = "Connected! All is good (probably).";
|
||||
availability.restart = "on_failure";
|
||||
availability.max_restarts = 3;
|
||||
};
|
||||
in
|
||||
{
|
||||
### bot ###
|
||||
pluralkit-bot-init = {
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-bot-init";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.git
|
||||
];
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
exec ${mkBotEnv "dotnet build -c Release -o obj/"}/bin/env
|
||||
'';
|
||||
### gateway ###
|
||||
pluralkit-gateway = mkServiceProcess "gateway" {
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
depends_on.redis.condition = "process_healthy";
|
||||
# configure health checks
|
||||
# TODO: don't assume port?
|
||||
liveness_probe.exec.command = ''${pkgs.curl}/bin/curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | ${pkgs.busybox}/bin/grep "302"'';
|
||||
liveness_probe.period_seconds = 7;
|
||||
# TODO: add actual listening or running line in gateway
|
||||
ready_log_line = "Running ";
|
||||
availability.restart = "on_failure";
|
||||
availability.max_restarts = 3;
|
||||
};
|
||||
# TODO: add the rest of the services
|
||||
};
|
||||
pluralkit-bot = {
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-bot";
|
||||
runtimeInputs = [ pkgs.coreutils ];
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
exec ${mkBotEnv "dotnet obj/PluralKit.Bot.dll"}/bin/env
|
||||
'';
|
||||
};
|
||||
depends_on.pluralkit-bot-init.condition = "process_completed_successfully";
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
depends_on.redis.condition = "process_healthy";
|
||||
depends_on.pluralkit-gateway.condition = "process_healthy";
|
||||
# TODO: add liveness check
|
||||
ready_log_line = "Received Ready";
|
||||
};
|
||||
### migrations ###
|
||||
pluralkit-migrate-init = mkServiceInitProcess {
|
||||
name = "migrate";
|
||||
};
|
||||
pluralkit-migrate = {
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-migrate";
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
exec target/debug/migrate
|
||||
'';
|
||||
};
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
depends_on.pluralkit-migrate-init.condition = "process_completed_successfully";
|
||||
};
|
||||
### gateway ###
|
||||
pluralkit-gateway-init = mkServiceInitProcess {
|
||||
name = "gateway";
|
||||
};
|
||||
pluralkit-gateway = {
|
||||
command = pkgs.writeShellApplication {
|
||||
name = "pluralkit-gateway";
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
curl
|
||||
gnugrep
|
||||
];
|
||||
text = ''
|
||||
${sourceDotenv}
|
||||
set -x
|
||||
exec target/debug/gateway
|
||||
'';
|
||||
};
|
||||
depends_on.postgres.condition = "process_healthy";
|
||||
depends_on.redis.condition = "process_healthy";
|
||||
depends_on.pluralkit-gateway-init.condition = "process_completed_successfully";
|
||||
# configure health checks
|
||||
# TODO: don't assume port?
|
||||
liveness_probe.exec.command = ''curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | grep "302"'';
|
||||
liveness_probe.period_seconds = 5;
|
||||
readiness_probe.exec.command = procCfg.pluralkit-gateway.liveness_probe.exec.command;
|
||||
readiness_probe.period_seconds = 5;
|
||||
readiness_probe.initial_delay_seconds = 3;
|
||||
};
|
||||
# TODO: add the rest of the services
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue