Refactor command system
This commit is contained in:
parent
0ec522ca0a
commit
1988b29fbc
@ -12,6 +12,10 @@ using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using PluralKit.Bot.Commands;
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
using Sentry;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
@ -84,23 +88,18 @@ namespace PluralKit.Bot
|
||||
ExclusiveBulkDelete = true
|
||||
}))
|
||||
.AddSingleton<Bot>()
|
||||
.AddTransient<CommandTree>()
|
||||
|
||||
.AddTransient<SystemCommands>()
|
||||
.AddTransient<MemberCommands>()
|
||||
.AddTransient<SwitchCommands>()
|
||||
.AddTransient<LinkCommands>()
|
||||
.AddTransient<APICommands>()
|
||||
.AddTransient<ImportExportCommands>()
|
||||
.AddTransient<HelpCommands>()
|
||||
.AddTransient<ModCommands>()
|
||||
.AddTransient<MiscCommands>()
|
||||
|
||||
.AddSingleton(_ => new CommandService(new CommandServiceConfig
|
||||
{
|
||||
CaseSensitiveCommands = false,
|
||||
QuotationMarkAliasMap = new Dictionary<char, char>
|
||||
{
|
||||
{'"', '"'},
|
||||
{'\'', '\''},
|
||||
{'‘', '’'},
|
||||
{'“', '”'},
|
||||
{'„', '‟'},
|
||||
},
|
||||
// We're already asyncing stuff by forking off at the client event handlers
|
||||
// So adding an additional layer of forking is pointless
|
||||
// and leads to the service scope being disposed of prematurely
|
||||
DefaultRunMode = RunMode.Sync
|
||||
}))
|
||||
.AddTransient<EmbedService>()
|
||||
.AddTransient<ProxyService>()
|
||||
.AddTransient<LogChannelService>()
|
||||
@ -133,29 +132,22 @@ namespace PluralKit.Bot
|
||||
{
|
||||
private IServiceProvider _services;
|
||||
private DiscordShardedClient _client;
|
||||
private CommandService _commands;
|
||||
private Timer _updateTimer;
|
||||
private IMetrics _metrics;
|
||||
private PeriodicStatCollector _collector;
|
||||
private ILogger _logger;
|
||||
|
||||
public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
|
||||
public Bot(IServiceProvider services, IDiscordClient client, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
|
||||
{
|
||||
_services = services;
|
||||
_client = client as DiscordShardedClient;
|
||||
_commands = commands;
|
||||
_metrics = metrics;
|
||||
_collector = collector;
|
||||
_logger = logger.ForContext<Bot>();
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
public Task Init()
|
||||
{
|
||||
_commands.AddTypeReader<PKSystem>(new PKSystemTypeReader());
|
||||
_commands.AddTypeReader<PKMember>(new PKMemberTypeReader());
|
||||
_commands.CommandExecuted += CommandExecuted;
|
||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
|
||||
|
||||
_client.ShardReady += ShardReady;
|
||||
_client.Log += FrameworkLog;
|
||||
|
||||
@ -171,6 +163,8 @@ namespace PluralKit.Bot
|
||||
_client.ReactionAdded += (msg, channel, reaction) => HandleEvent(s => s.AddReactionAddedBreadcrumb(msg, channel, reaction), eh => eh.HandleReactionAdded(msg, channel, reaction));
|
||||
_client.MessageDeleted += (msg, channel) => HandleEvent(s => s.AddMessageDeleteBreadcrumb(msg, channel), eh => eh.HandleMessageDeleted(msg, channel));
|
||||
_client.MessagesBulkDeleted += (msgs, channel) => HandleEvent(s => s.AddMessageBulkDeleteBreadcrumb(msgs, channel), eh => eh.HandleMessagesBulkDelete(msgs, channel));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task FrameworkLog(LogMessage msg)
|
||||
@ -224,39 +218,39 @@ namespace PluralKit.Bot
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CommandExecuted(Optional<CommandInfo> cmd, ICommandContext ctx, IResult _result)
|
||||
{
|
||||
var svc = ((PKCommandContext) ctx).ServiceProvider;
|
||||
var id = svc.GetService<EventIdProvider>();
|
||||
|
||||
_metrics.Measure.Meter.Mark(BotMetrics.CommandsRun);
|
||||
|
||||
// TODO: refactor this entire block, it's fugly.
|
||||
if (!_result.IsSuccess) {
|
||||
if (_result.Error == CommandError.Unsuccessful || _result.Error == CommandError.Exception) {
|
||||
// If this is a PKError (ie. thrown deliberately), show user facing message
|
||||
// If not, log as error
|
||||
var exception = (_result as ExecuteResult?)?.Exception;
|
||||
if (exception is PKError) {
|
||||
await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {exception.Message}");
|
||||
} else if (exception is TimeoutException) {
|
||||
await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} Operation timed out. Try being faster next time :)");
|
||||
} else if (_result is PreconditionResult)
|
||||
{
|
||||
await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}");
|
||||
} else
|
||||
{
|
||||
await ctx.Message.Channel.SendMessageAsync(
|
||||
$"{Emojis.Error} Internal error occurred. Please join the support server (<https://discord.gg/PczBt78>), and send the developer this ID: `{id.EventId}`.");
|
||||
HandleRuntimeError((_result as ExecuteResult?)?.Exception, svc);
|
||||
}
|
||||
} else if ((_result.Error == CommandError.BadArgCount || _result.Error == CommandError.MultipleMatches) && cmd.IsSpecified) {
|
||||
await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}\n**Usage: **pk;{cmd.Value.Remarks}");
|
||||
} else if (_result.Error == CommandError.UnknownCommand || _result.Error == CommandError.UnmetPrecondition || _result.Error == CommandError.ObjectNotFound) {
|
||||
await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// private async Task CommandExecuted(Optional<CommandInfo> cmd, ICommandContext ctx, IResult _result)
|
||||
// {
|
||||
// var svc = ((PKCommandContext) ctx).ServiceProvider;
|
||||
// var id = svc.GetService<EventIdProvider>();
|
||||
//
|
||||
// _metrics.Measure.Meter.Mark(BotMetrics.CommandsRun);
|
||||
//
|
||||
// // TODO: refactor this entire block, it's fugly.
|
||||
// if (!_result.IsSuccess) {
|
||||
// if (_result.Error == CommandError.Unsuccessful || _result.Error == CommandError.Exception) {
|
||||
// // If this is a PKError (ie. thrown deliberately), show user facing message
|
||||
// // If not, log as error
|
||||
// var exception = (_result as ExecuteResult?)?.Exception;
|
||||
// if (exception is PKError) {
|
||||
// await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {exception.Message}");
|
||||
// } else if (exception is TimeoutException) {
|
||||
// await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} Operation timed out. Try being faster next time :)");
|
||||
// } else if (_result is PreconditionResult)
|
||||
// {
|
||||
// await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}");
|
||||
// } else
|
||||
// {
|
||||
// await ctx.Message.Channel.SendMessageAsync(
|
||||
// $"{Emojis.Error} Internal error occurred. Please join the support server (<https://discord.gg/PczBt78>), and send the developer this ID: `{id.EventId}`.");
|
||||
// HandleRuntimeError((_result as ExecuteResult?)?.Exception, svc);
|
||||
// }
|
||||
// } else if ((_result.Error == CommandError.BadArgCount || _result.Error == CommandError.MultipleMatches) && cmd.IsSpecified) {
|
||||
// await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}\n**Usage: **pk;{cmd.Value.Remarks}");
|
||||
// } else if (_result.Error == CommandError.UnknownCommand || _result.Error == CommandError.UnmetPrecondition || _result.Error == CommandError.ObjectNotFound) {
|
||||
// await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private Task HandleEvent(Action<Scope> breadcrumbFactory, Func<PKEventHandler, Task> handler)
|
||||
{
|
||||
@ -309,47 +303,51 @@ namespace PluralKit.Bot
|
||||
}
|
||||
|
||||
class PKEventHandler {
|
||||
private CommandService _commands;
|
||||
private ProxyService _proxy;
|
||||
private ILogger _logger;
|
||||
private IMetrics _metrics;
|
||||
private DiscordShardedClient _client;
|
||||
private DbConnectionFactory _connectionFactory;
|
||||
private IServiceProvider _services;
|
||||
private CommandTree _tree;
|
||||
|
||||
public PKEventHandler(CommandService commands, ProxyService proxy, ILogger logger, IMetrics metrics, IDiscordClient client, DbConnectionFactory connectionFactory, IServiceProvider services)
|
||||
public PKEventHandler(ProxyService proxy, ILogger logger, IMetrics metrics, IDiscordClient client, DbConnectionFactory connectionFactory, IServiceProvider services, CommandTree tree)
|
||||
{
|
||||
_commands = commands;
|
||||
_proxy = proxy;
|
||||
_logger = logger;
|
||||
_metrics = metrics;
|
||||
_client = (DiscordShardedClient) client;
|
||||
_connectionFactory = connectionFactory;
|
||||
_services = services;
|
||||
_tree = tree;
|
||||
}
|
||||
|
||||
public async Task HandleMessage(SocketMessage msg)
|
||||
public async Task HandleMessage(SocketMessage arg)
|
||||
{
|
||||
RegisterMessageMetrics(msg);
|
||||
RegisterMessageMetrics(arg);
|
||||
|
||||
// Ignore system messages (member joined, message pinned, etc)
|
||||
var arg = msg as SocketUserMessage;
|
||||
if (arg == null) return;
|
||||
var msg = arg as SocketUserMessage;
|
||||
if (msg == null) return;
|
||||
|
||||
// Ignore bot messages
|
||||
if (arg.Author.IsBot || arg.Author.IsWebhook) return;
|
||||
if (msg.Author.IsBot || msg.Author.IsWebhook) return;
|
||||
|
||||
int argPos = 0;
|
||||
int argPos = -1;
|
||||
// Check if message starts with the command prefix
|
||||
if (arg.HasStringPrefix("pk;", ref argPos, StringComparison.OrdinalIgnoreCase) ||
|
||||
arg.HasStringPrefix("pk!", ref argPos, StringComparison.OrdinalIgnoreCase) ||
|
||||
arg.HasMentionPrefix(_client.CurrentUser, ref argPos))
|
||||
if (msg.Content.StartsWith("pk;")) argPos = 3;
|
||||
else if (msg.Content.StartsWith("pk!")) argPos = 3;
|
||||
else if (Utils.HasMentionPrefix(msg.Content, ref argPos, out var id)) // Set argPos to the proper value
|
||||
if (id != _client.CurrentUser.Id) // But undo it if it's someone else's ping
|
||||
argPos = -1;
|
||||
|
||||
if (argPos > -1)
|
||||
{
|
||||
_logger.Debug("Parsing command {Command} from message {Channel}-{Message}", msg.Content, msg.Channel.Id, msg.Id);
|
||||
|
||||
// Essentially move the argPos pointer by however much whitespace is at the start of the post-argPos string
|
||||
var trimStartLengthDiff = arg.Content.Substring(argPos).Length -
|
||||
arg.Content.Substring(argPos).TrimStart().Length;
|
||||
var trimStartLengthDiff = msg.Content.Substring(argPos).Length -
|
||||
msg.Content.Substring(argPos).TrimStart().Length;
|
||||
argPos += trimStartLengthDiff;
|
||||
|
||||
// If it does, fetch the sender's system (because most commands need that) into the context,
|
||||
@ -359,21 +357,20 @@ namespace PluralKit.Bot
|
||||
using (var conn = await _connectionFactory.Obtain())
|
||||
system = await conn.QueryFirstOrDefaultAsync<PKSystem>(
|
||||
"select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system",
|
||||
new {Id = arg.Author.Id});
|
||||
new {Id = msg.Author.Id});
|
||||
|
||||
await _commands.ExecuteAsync(new PKCommandContext(_client, arg, system, _services), argPos,
|
||||
_services);
|
||||
await _tree.ExecuteCommand(new Context(_services, msg, argPos, system));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not, try proxying anyway
|
||||
try
|
||||
{
|
||||
await _proxy.HandleMessageAsync(arg);
|
||||
await _proxy.HandleMessageAsync(msg);
|
||||
}
|
||||
catch (PKError e)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync($"{Emojis.Error} {e.Message}");
|
||||
await arg.Channel.SendMessageAsync($"{Emojis.Error} {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
PluralKit.Bot/CommandSystem/Command.cs
Normal file
16
PluralKit.Bot/CommandSystem/Command.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
{
|
||||
public class Command
|
||||
{
|
||||
public string Key { get; }
|
||||
public string Usage { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public Command(string key, string usage, string description)
|
||||
{
|
||||
Key = key;
|
||||
Usage = usage;
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
}
|
19
PluralKit.Bot/CommandSystem/CommandGroup.cs
Normal file
19
PluralKit.Bot/CommandSystem/CommandGroup.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
{
|
||||
public class CommandGroup
|
||||
{
|
||||
public string Key { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public ICollection<Command> Children { get; }
|
||||
|
||||
public CommandGroup(string key, string description, ICollection<Command> children)
|
||||
{
|
||||
Key = key;
|
||||
Description = description;
|
||||
Children = children;
|
||||
}
|
||||
}
|
||||
}
|
242
PluralKit.Bot/CommandSystem/Context.cs
Normal file
242
PluralKit.Bot/CommandSystem/Context.cs
Normal file
@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
{
|
||||
public class Context
|
||||
{
|
||||
private IServiceProvider _provider;
|
||||
|
||||
private readonly DiscordShardedClient _client;
|
||||
private readonly SocketUserMessage _message;
|
||||
private readonly Parameters _parameters;
|
||||
|
||||
private readonly SystemStore _systems;
|
||||
private readonly MemberStore _members;
|
||||
private readonly PKSystem _senderSystem;
|
||||
|
||||
private Command _currentCommand;
|
||||
|
||||
public Context(IServiceProvider provider, SocketUserMessage message, int commandParseOffset,
|
||||
PKSystem senderSystem)
|
||||
{
|
||||
_client = provider.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
|
||||
_message = message;
|
||||
_systems = provider.GetRequiredService<SystemStore>();
|
||||
_members = provider.GetRequiredService<MemberStore>();
|
||||
_senderSystem = senderSystem;
|
||||
_provider = provider;
|
||||
_parameters = new Parameters(message.Content.Substring(commandParseOffset));
|
||||
}
|
||||
|
||||
public IUser Author => _message.Author;
|
||||
public IMessageChannel Channel => _message.Channel;
|
||||
public IUserMessage Message => _message;
|
||||
public IGuild Guild => (_message.Channel as ITextChannel)?.Guild;
|
||||
public DiscordSocketClient Shard => _client.GetShardFor(Guild);
|
||||
public DiscordShardedClient Client => _client;
|
||||
public PKSystem System => _senderSystem;
|
||||
|
||||
public string PopArgument() => _parameters.Pop();
|
||||
public string PeekArgument() => _parameters.Peek();
|
||||
public string Remainder() => _parameters.Remainder();
|
||||
public string RemainderOrNull() => Remainder().Trim().Length == 0 ? null : Remainder();
|
||||
public bool HasNext() => RemainderOrNull() != null;
|
||||
public string FullCommand => _parameters.FullCommand;
|
||||
|
||||
public Task<IUserMessage> Reply(string text = null, Embed embed = null) =>
|
||||
Channel.SendMessageAsync(text, embed: embed);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||
/// </summary>
|
||||
public bool Match(params string[] potentialMatches)
|
||||
{
|
||||
foreach (var match in potentialMatches)
|
||||
{
|
||||
if (PeekArgument().Equals(match, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
PopArgument();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)
|
||||
{
|
||||
_currentCommand = commandDef;
|
||||
|
||||
try
|
||||
{
|
||||
await handler(_provider.GetRequiredService<T>());
|
||||
}
|
||||
catch (PKSyntaxError e)
|
||||
{
|
||||
await Reply($"{Emojis.Error} {e.Message}\n**Command usage:** pk;{commandDef.Usage}");
|
||||
}
|
||||
catch (PKError e)
|
||||
{
|
||||
await Reply($"{Emojis.Error} {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IUser> MatchUser()
|
||||
{
|
||||
var text = PeekArgument();
|
||||
if (MentionUtils.TryParseUser(text, out var id))
|
||||
return await Shard.Rest.GetUserAsync(id); // TODO: this should properly fetch
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<PKSystem> PeekSystem() => MatchSystemInner();
|
||||
|
||||
public async Task<PKSystem> MatchSystem()
|
||||
{
|
||||
var system = await MatchSystemInner();
|
||||
if (system != null) PopArgument();
|
||||
return system;
|
||||
}
|
||||
|
||||
private async Task<PKSystem> MatchSystemInner()
|
||||
{
|
||||
var input = 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>)
|
||||
// - A system hid
|
||||
|
||||
// Direct IDs and mentions are both handled by the below method:
|
||||
if (input.TryParseMention(out var id))
|
||||
return await _systems.GetByAccount(id);
|
||||
|
||||
// Finally, try HID parsing
|
||||
var system = await _systems.GetByHid(input);
|
||||
return system;
|
||||
}
|
||||
|
||||
public async Task<PKMember> PeekMember()
|
||||
{
|
||||
var input = PeekArgument();
|
||||
|
||||
// Member references can have one or two forms, depending on
|
||||
// whether you're in a system or not:
|
||||
// - A member hid
|
||||
// - A textual name of a member *in your own system*
|
||||
|
||||
// First, try member HID parsing:
|
||||
if (await _members.GetByHid(input) is PKMember memberByHid)
|
||||
return memberByHid;
|
||||
|
||||
// Then, if we have a system, try finding by member name in system
|
||||
if (_senderSystem != null && await _members.GetByName(_senderSystem, input) is PKMember memberByName)
|
||||
return memberByName;
|
||||
|
||||
// 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 async Task<PKMember> MatchMember()
|
||||
{
|
||||
// First, peek a member
|
||||
var member = await PeekMember();
|
||||
|
||||
// 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) PopArgument();
|
||||
|
||||
// Finally, we return the member value.
|
||||
return member;
|
||||
}
|
||||
|
||||
public string CreateMemberNotFoundError(string input)
|
||||
{
|
||||
// TODO: does this belong here?
|
||||
if (input.Length == 5)
|
||||
{
|
||||
if (_senderSystem != null)
|
||||
return $"Member with ID or name `{input}` not found.";
|
||||
return $"Member with ID `{input}` not found."; // Accounts without systems can't query by name
|
||||
}
|
||||
|
||||
if (_senderSystem != null)
|
||||
return $"Member with name `{input}` not found. Note that a member ID is 5 characters long.";
|
||||
return $"Member not found. Note that a member ID is 5 characters long.";
|
||||
}
|
||||
|
||||
public Context CheckSystem()
|
||||
{
|
||||
if (_senderSystem == null)
|
||||
throw Errors.NoSystemError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Context CheckNoSystem()
|
||||
{
|
||||
if (_senderSystem != null)
|
||||
throw Errors.ExistingSystemError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Context CheckOwnMember(PKMember member)
|
||||
{
|
||||
if (member.System != _senderSystem.Id)
|
||||
throw Errors.NotOwnMemberError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GuildPermissions GetGuildPermissions(IUser user)
|
||||
{
|
||||
if (Channel is SocketGuildChannel gc)
|
||||
return gc.GetUser(user.Id).GuildPermissions;
|
||||
return GuildPermissions.None;
|
||||
}
|
||||
|
||||
public ChannelPermissions GetChannelPermissions(IUser user)
|
||||
{
|
||||
if (Channel is SocketGuildChannel gc)
|
||||
return gc.GetUser(user.Id).GetPermissions(gc);
|
||||
return ChannelPermissions.DM;
|
||||
}
|
||||
|
||||
public Context CheckAuthorPermission(GuildPermission permission, string permissionName)
|
||||
{
|
||||
if (!GetGuildPermissions(Author).Has(permission))
|
||||
throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command.");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Context CheckAuthorPermission(ChannelPermission permission, string permissionName)
|
||||
{
|
||||
if (!GetChannelPermissions(Author).Has(permission))
|
||||
throw new PKError($"You must have the \"{permissionName}\" permission in this server to use this command.");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Context CheckGuildContext()
|
||||
{
|
||||
if (Channel is IGuildChannel) return this;
|
||||
throw new PKError("This command can not be run in a DM.");
|
||||
}
|
||||
|
||||
public ITextChannel MatchChannel()
|
||||
{
|
||||
if (!MentionUtils.TryParseChannel(PeekArgument(), out var channel)) return null;
|
||||
if (!(_client.GetChannel(channel) is ITextChannel textChannel)) return null;
|
||||
|
||||
PopArgument();
|
||||
return textChannel;
|
||||
}
|
||||
}
|
||||
}
|
77
PluralKit.Bot/CommandSystem/Parameters.cs
Normal file
77
PluralKit.Bot/CommandSystem/Parameters.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PluralKit.Bot.CommandSystem
|
||||
{
|
||||
public class Parameters
|
||||
{
|
||||
private static readonly Dictionary<char, char> _quotePairs = new Dictionary<char, char>()
|
||||
{
|
||||
{'\'', '\''}, {'"', '"'}
|
||||
};
|
||||
|
||||
private readonly string _cmd;
|
||||
private int _ptr;
|
||||
|
||||
public Parameters(string cmd)
|
||||
{
|
||||
_cmd = cmd;
|
||||
_ptr = 0;
|
||||
}
|
||||
|
||||
public string Pop()
|
||||
{
|
||||
var positions = NextWordPosition();
|
||||
if (positions == null) return "";
|
||||
|
||||
var (start, end, advance) = positions.Value;
|
||||
_ptr = end + advance;
|
||||
return _cmd.Substring(start, end - start).Trim();
|
||||
}
|
||||
|
||||
public string Peek()
|
||||
{
|
||||
var positions = NextWordPosition();
|
||||
if (positions == null) return "";
|
||||
|
||||
var (start, end, _) = positions.Value;
|
||||
return _cmd.Substring(start, end - start).Trim();
|
||||
}
|
||||
|
||||
public string Remainder() => _cmd.Substring(_ptr).Trim();
|
||||
public string FullCommand => _cmd;
|
||||
|
||||
// Returns tuple of (startpos, endpos, advanceafter)
|
||||
private ValueTuple<int, int, int>? NextWordPosition()
|
||||
{
|
||||
// Is this the end of the string?
|
||||
if (_cmd.Length <= _ptr) return null;
|
||||
|
||||
// Is this a quoted word?
|
||||
if (_quotePairs.ContainsKey(_cmd[_ptr]))
|
||||
{
|
||||
// This is a quoted word, find corresponding end quote and return span
|
||||
var endQuote = _quotePairs[_cmd[_ptr]];
|
||||
var endQuotePosition = _cmd.IndexOf(endQuote, _ptr + 1);
|
||||
|
||||
// Position after the end quote should be a space (or EOL)
|
||||
// Otherwise treat it as a standard word that's not quoted
|
||||
if (_cmd.Length == endQuotePosition + 1 || _cmd[endQuotePosition + 1] == ' ')
|
||||
{
|
||||
if (endQuotePosition == -1)
|
||||
{
|
||||
// This is an unterminated quoted word, just return the entire word including the start quote
|
||||
// TODO: should we do something else here?
|
||||
return (_ptr, _cmd.Length, 0);
|
||||
}
|
||||
|
||||
return (_ptr + 1, endQuotePosition, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Not a quoted word, just find the next space and return as appropriate
|
||||
var wordEnd = _cmd.IndexOf(' ', _ptr + 1);
|
||||
return wordEnd != -1 ? (_ptr, wordEnd, 1) : (_ptr, _cmd.Length, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +1,67 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("token")]
|
||||
public class APICommands: ModuleBase<PKCommandContext>
|
||||
public class APICommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
private SystemStore _systems;
|
||||
public APICommands(SystemStore systems)
|
||||
{
|
||||
_systems = systems;
|
||||
}
|
||||
|
||||
[Command]
|
||||
[MustHaveSystem]
|
||||
[Remarks("token")]
|
||||
public async Task GetToken()
|
||||
public async Task GetToken(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Get or make a token
|
||||
var token = Context.SenderSystem.Token ?? await MakeAndSetNewToken();
|
||||
var token = ctx.System.Token ?? await MakeAndSetNewToken(ctx.System);
|
||||
|
||||
// If we're not already in a DM, reply with a reminder to check
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
|
||||
// DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile)
|
||||
await Context.User.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:");
|
||||
await Context.User.SendMessageAsync(token);
|
||||
await ctx.Author.SendMessageAsync($"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:");
|
||||
await ctx.Author.SendMessageAsync(token);
|
||||
}
|
||||
|
||||
private async Task<string> MakeAndSetNewToken()
|
||||
private async Task<string> MakeAndSetNewToken(PKSystem system)
|
||||
{
|
||||
Context.SenderSystem.Token = PluralKit.Utils.GenerateToken();
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
return Context.SenderSystem.Token;
|
||||
system.Token = PluralKit.Utils.GenerateToken();
|
||||
await _systems.Save(system);
|
||||
return system.Token;
|
||||
}
|
||||
|
||||
[Command("refresh")]
|
||||
[MustHaveSystem]
|
||||
[Alias("expire", "invalidate", "update", "new")]
|
||||
[Remarks("token refresh")]
|
||||
public async Task RefreshToken()
|
||||
public async Task RefreshToken(Context ctx)
|
||||
{
|
||||
if (Context.SenderSystem.Token == null)
|
||||
ctx.CheckSystem();
|
||||
|
||||
if (ctx.System.Token == null)
|
||||
{
|
||||
// If we don't have a token, call the other method instead
|
||||
// This does pretty much the same thing, except words the messages more appropriately for that :)
|
||||
await GetToken();
|
||||
await GetToken(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a new token from scratch
|
||||
var token = await MakeAndSetNewToken();
|
||||
var token = await MakeAndSetNewToken(ctx.System);
|
||||
|
||||
// If we're not already in a DM, reply with a reminder to check
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
|
||||
// DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile)
|
||||
await Context.User.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:");
|
||||
await Context.User.SendMessageAsync(token);
|
||||
await ctx.Author.SendMessageAsync($"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:");
|
||||
await ctx.Author.SendMessageAsync(token);
|
||||
}
|
||||
}
|
||||
}
|
285
PluralKit.Bot/Commands/CommandTree.cs
Normal file
285
PluralKit.Bot/Commands/CommandTree.cs
Normal file
@ -0,0 +1,285 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Discord;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class CommandTree
|
||||
{
|
||||
public static Command SystemInfo = new Command("system", "system [system]", "uwu");
|
||||
public static Command SystemNew = new Command("system new", "system new [name]", "uwu");
|
||||
public static Command SystemRename = new Command("system name", "system rename [name]", "uwu");
|
||||
public static Command SystemDesc = new Command("system description", "system description [description]", "uwu");
|
||||
public static Command SystemTag = new Command("system tag", "system tag [tag]", "uwu");
|
||||
public static Command SystemAvatar = new Command("system avatar", "system avatar [url|@mention]", "uwu");
|
||||
public static Command SystemDelete = new Command("system delete", "system delete", "uwu");
|
||||
public static Command SystemTimezone = new Command("system timezone", "system timezone [timezone]", "uwu");
|
||||
public static Command SystemList = new Command("system list", "system list [full]", "uwu");
|
||||
public static Command SystemFronter = new Command("system fronter", "system fronter", "uwu");
|
||||
public static Command SystemFrontHistory = new Command("system fronthistory", "system fronthistory", "uwu");
|
||||
public static Command SystemFrontPercent = new Command("system frontpercent", "system frontpercent [timespan]", "uwu");
|
||||
public static Command MemberInfo = new Command("member", "member <member>", "uwu");
|
||||
public static Command MemberNew = new Command("member new", "member new <name>", "uwu");
|
||||
public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "uwu");
|
||||
public static Command MemberDesc = new Command("member description", "member <member> description [description]", "uwu");
|
||||
public static Command MemberPronouns = new Command("member pronouns", "member <member> pronouns [pronouns]", "uwu");
|
||||
public static Command MemberColor = new Command("member color", "member <member> color [color]", "uwu");
|
||||
public static Command MemberBirthday = new Command("member birthday", "member <member> birthday [birthday]", "uwu");
|
||||
public static Command MemberProxy = new Command("member proxy", "member <member> proxy [example proxy]", "uwu");
|
||||
public static Command MemberDelete = new Command("member delete", "member <member> delete", "uwu");
|
||||
public static Command MemberAvatar = new Command("member avatar", "member <member> avatar [url|@mention]", "uwu");
|
||||
public static Command MemberDisplayName = new Command("member displayname", "member <member> displayname [display name]", "uwu");
|
||||
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "uwu");
|
||||
public static Command SwitchOut = new Command("switch out", "switch out", "uwu");
|
||||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "uwu");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete", "uwu");
|
||||
public static Command Link = new Command("link", "link <account>", "uwu");
|
||||
public static Command Unlink = new Command("unlink", "unlink [account]", "uwu");
|
||||
public static Command TokenGet = new Command("token", "token", "uwu");
|
||||
public static Command TokenRefresh = new Command("token refresh", "token refresh", "uwu");
|
||||
public static Command Import = new Command("import", "import [fileurl]", "uwu");
|
||||
public static Command Export = new Command("export", "export", "uwu");
|
||||
public static Command HelpCommandList = new Command("commands", "commands", "uwu");
|
||||
public static Command HelpProxy = new Command("help proxy", "help proxy", "uwu");
|
||||
public static Command Help = new Command("help", "help", "uwu");
|
||||
public static Command Message = new Command("message", "message <id|link>", "uwu");
|
||||
public static Command Log = new Command("log", "log <channel>", "uwu");
|
||||
public static Command Invite = new Command("invite", "invite", "uwu");
|
||||
public static Command PermCheck = new Command("permcheck", "permcheck <guild>", "uwu");
|
||||
|
||||
private IDiscordClient _client;
|
||||
|
||||
public CommandTree(IDiscordClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public Task ExecuteCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("system", "s"))
|
||||
return HandleSystemCommand(ctx);
|
||||
if (ctx.Match("member", "m"))
|
||||
return HandleMemberCommand(ctx);
|
||||
if (ctx.Match("switch", "sw"))
|
||||
return HandleSwitchCommand(ctx);
|
||||
if (ctx.Match("link"))
|
||||
return ctx.Execute<LinkCommands>(Link, m => m.LinkSystem(ctx));
|
||||
if (ctx.Match("unlink"))
|
||||
return ctx.Execute<LinkCommands>(Unlink, m => m.UnlinkAccount(ctx));
|
||||
if (ctx.Match("token"))
|
||||
if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen"))
|
||||
return ctx.Execute<APICommands>(TokenRefresh, m => m.RefreshToken(ctx));
|
||||
else
|
||||
return ctx.Execute<APICommands>(TokenGet, m => m.GetToken(ctx));
|
||||
if (ctx.Match("import"))
|
||||
return ctx.Execute<ImportExportCommands>(Import, m => m.Import(ctx));
|
||||
if (ctx.Match("export"))
|
||||
return ctx.Execute<ImportExportCommands>(Export, m => m.Export(ctx));
|
||||
if (ctx.Match("help"))
|
||||
if (ctx.Match("commands"))
|
||||
return ctx.Execute<HelpCommands>(HelpCommandList, m => m.CommandList(ctx));
|
||||
else if (ctx.Match("proxy"))
|
||||
return ctx.Execute<HelpCommands>(HelpProxy, m => m.HelpProxy(ctx));
|
||||
else return ctx.Execute<HelpCommands>(Help, m => m.HelpRoot(ctx));
|
||||
if (ctx.Match("commands"))
|
||||
return ctx.Execute<HelpCommands>(HelpCommandList, m => m.CommandList(ctx));
|
||||
if (ctx.Match("message", "msg"))
|
||||
return ctx.Execute<ModCommands>(Message, m => m.GetMessage(ctx));
|
||||
if (ctx.Match("log"))
|
||||
return ctx.Execute<ModCommands>(Log, m => m.SetLogChannel(ctx));
|
||||
if (ctx.Match("invite")) return ctx.Execute<MiscCommands>(Invite, m => m.Invite(ctx));
|
||||
if (ctx.Match("mn")) return ctx.Execute<MiscCommands>(null, m => m.Mn(ctx));
|
||||
if (ctx.Match("fire")) return ctx.Execute<MiscCommands>(null, m => m.Fire(ctx));
|
||||
if (ctx.Match("thunder")) return ctx.Execute<MiscCommands>(null, m => m.Thunder(ctx));
|
||||
if (ctx.Match("freeze")) return ctx.Execute<MiscCommands>(null, m => m.Freeze(ctx));
|
||||
if (ctx.Match("starstorm")) return ctx.Execute<MiscCommands>(null, m => m.Starstorm(ctx));
|
||||
if (ctx.Match("stats")) return ctx.Execute<MiscCommands>(null, m => m.Stats(ctx));
|
||||
if (ctx.Match("permcheck"))
|
||||
return ctx.Execute<MiscCommands>(PermCheck, m => m.PermCheckGuild(ctx));
|
||||
|
||||
ctx.Reply(
|
||||
$"{Emojis.Error} Unknown command `{ctx.PeekArgument().Sanitize()}`. For a list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleSystemCommand(Context ctx)
|
||||
{
|
||||
// If we have no parameters, default to self-target
|
||||
if (!ctx.HasNext())
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
|
||||
|
||||
// First, we match own-system-only commands (ie. no target system parameter)
|
||||
else if (ctx.Match("new", "create", "make", "add", "register", "init"))
|
||||
await ctx.Execute<SystemCommands>(SystemNew, m => m.New(ctx));
|
||||
else if (ctx.Match("name", "rename", "changename"))
|
||||
await ctx.Execute<SystemCommands>(SystemRename, m => m.Name(ctx));
|
||||
else if (ctx.Match("tag"))
|
||||
await ctx.Execute<SystemCommands>(SystemTag, m => m.Tag(ctx));
|
||||
else if (ctx.Match("description", "desc", "bio"))
|
||||
await ctx.Execute<SystemCommands>(SystemDesc, m => m.Description(ctx));
|
||||
else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp"))
|
||||
await ctx.Execute<SystemCommands>(SystemAvatar, m => m.SystemAvatar(ctx));
|
||||
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
|
||||
await ctx.Execute<SystemCommands>(SystemDelete, m => m.Delete(ctx));
|
||||
else if (ctx.Match("timezone", "tz"))
|
||||
await ctx.Execute<SystemCommands>(SystemTimezone, m => m.SystemTimezone(ctx));
|
||||
else if (ctx.Match("list", "l", "members"))
|
||||
{
|
||||
if (ctx.Match("f", "full", "big", "details", "long"))
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberLongList(ctx, ctx.System));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, ctx.System));
|
||||
}
|
||||
else if (ctx.Match("f", "front", "fronter", "fronters"))
|
||||
{
|
||||
if (ctx.Match("h", "history"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||
else if (ctx.Match("p", "percent", "%"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, ctx.System));
|
||||
}
|
||||
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, ctx.System));
|
||||
else
|
||||
await HandleSystemCommandTargeted(ctx);
|
||||
}
|
||||
|
||||
private async Task HandleSystemCommandTargeted(Context ctx)
|
||||
{
|
||||
// Commands that have a system target (eg. pk;system <system> fronthistory)
|
||||
var target = await ctx.MatchSystem();
|
||||
if (target == null)
|
||||
{
|
||||
var list = CreatePotentialCommandList(SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\nPerhaps you meant to use one of the following commands?\n{list}");
|
||||
}
|
||||
else if (ctx.Match("list", "l", "members"))
|
||||
{
|
||||
if (ctx.Match("f", "full", "big", "details", "long"))
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberLongList(ctx, target));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemList, m => m.MemberShortList(ctx, target));
|
||||
}
|
||||
else if (ctx.Match("f", "front", "fronter", "fronters"))
|
||||
{
|
||||
if (ctx.Match("h", "history"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
|
||||
else if (ctx.Match("p", "percent", "%"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
|
||||
else
|
||||
await ctx.Execute<SystemCommands>(SystemFronter, m => m.SystemFronter(ctx, target));
|
||||
}
|
||||
else if (ctx.Match("fh", "fronthistory", "history", "switches"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target));
|
||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, target));
|
||||
else if (ctx.Match("info", "view", "show"))
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
|
||||
else if (!ctx.HasNext())
|
||||
await ctx.Execute<SystemCommands>(SystemInfo, m => m.Query(ctx, target));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent,
|
||||
SystemInfo);
|
||||
}
|
||||
|
||||
private async Task HandleMemberCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("new", "n", "add", "create", "register"))
|
||||
await ctx.Execute<MemberCommands>(MemberNew, m => m.NewMember(ctx));
|
||||
else if (await ctx.MatchMember() is PKMember target)
|
||||
await HandleMemberCommandTargeted(ctx, target);
|
||||
else if (!ctx.HasNext())
|
||||
await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, MemberDesc, MemberPronouns,
|
||||
MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar);
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} {ctx.CreateMemberNotFoundError(ctx.PopArgument())}");
|
||||
}
|
||||
|
||||
private async Task HandleMemberCommandTargeted(Context ctx, PKMember target)
|
||||
{
|
||||
// Commands that have a member target (eg. pk;member <member> delete)
|
||||
if (ctx.Match("rename", "name", "changename", "setname"))
|
||||
await ctx.Execute<MemberCommands>(MemberRename, m => m.RenameMember(ctx, target));
|
||||
else if (ctx.Match("description", "info", "bio", "text", "desc"))
|
||||
await ctx.Execute<MemberCommands>(MemberDesc, m => m.MemberDescription(ctx, target));
|
||||
else if (ctx.Match("pronouns", "pronoun"))
|
||||
await ctx.Execute<MemberCommands>(MemberPronouns, m => m.MemberPronouns(ctx, target));
|
||||
else if (ctx.Match("color", "colour"))
|
||||
await ctx.Execute<MemberCommands>(MemberColor, m => m.MemberColor(ctx, target));
|
||||
else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate"))
|
||||
await ctx.Execute<MemberCommands>(MemberBirthday, m => m.MemberBirthday(ctx, target));
|
||||
else if (ctx.Match("proxy", "tags", "proxytags", "brackets"))
|
||||
await ctx.Execute<MemberCommands>(MemberProxy, m => m.MemberProxy(ctx, target));
|
||||
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
|
||||
await ctx.Execute<MemberCommands>(MemberDelete, m => m.MemberDelete(ctx, target));
|
||||
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
|
||||
await ctx.Execute<MemberCommands>(MemberAvatar, m => m.MemberAvatar(ctx, target));
|
||||
else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname"))
|
||||
await ctx.Execute<MemberCommands>(MemberDisplayName, m => m.MemberDisplayName(ctx, target));
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
|
||||
}
|
||||
|
||||
private async Task HandleSwitchCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("out"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchOut, m => m.SwitchOut(ctx));
|
||||
else if (ctx.Match("move", "shift", "offset"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchMove, m => m.SwitchMove(ctx));
|
||||
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
|
||||
await ctx.Execute<SwitchCommands>(SwitchDelete, m => m.SwitchDelete(ctx));
|
||||
else if (ctx.HasNext()) // there are following arguments
|
||||
await ctx.Execute<SwitchCommands>(Switch, m => m.Switch(ctx));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchDelete, SystemFronter, SystemFrontHistory);
|
||||
}
|
||||
|
||||
private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands)
|
||||
{
|
||||
var commandListStr = CreatePotentialCommandList(potentialCommands);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} Unknown command `pk;{ctx.FullCommand}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)
|
||||
{
|
||||
var commandListStr = CreatePotentialCommandList(potentialCommands);
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private static string CreatePotentialCommandList(params Command[] potentialCommands)
|
||||
{
|
||||
return string.Join("\n", potentialCommands.Select(cmd => $"- `pk;{cmd.Usage}`"));
|
||||
}
|
||||
|
||||
private async Task<string> CreateSystemNotFoundError(Context ctx)
|
||||
{
|
||||
var input = ctx.PopArgument();
|
||||
if (input.TryParseMention(out var id))
|
||||
{
|
||||
// Try to resolve the user ID to find the associated account,
|
||||
// so we can print their username.
|
||||
var user = await _client.GetUserAsync(id);
|
||||
|
||||
// Print descriptive errors based on whether we found the user or not.
|
||||
if (user == null)
|
||||
return $"Account with ID `{id}` not found.";
|
||||
return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered.";
|
||||
}
|
||||
|
||||
return $"System with ID `{input}` not found.";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +1,27 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class HelpCommands: ModuleBase<PKCommandContext>
|
||||
public class HelpCommands
|
||||
{
|
||||
[Group("help")]
|
||||
public class HelpGroup: ModuleBase<PKCommandContext>
|
||||
public async Task HelpProxy(Context ctx)
|
||||
{
|
||||
[Command("proxy")]
|
||||
[Priority(1)]
|
||||
[Remarks("help proxy")]
|
||||
public async Task HelpProxy()
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
"The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying");
|
||||
}
|
||||
|
||||
[Command("member")]
|
||||
[Priority(1)]
|
||||
[Remarks("help member")]
|
||||
public async Task HelpMember()
|
||||
public async Task HelpMember(Context ctx)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
"The member help page has been moved! See the website: https://pluralkit.me/guide#member-management");
|
||||
}
|
||||
|
||||
[Command]
|
||||
[Remarks("help")]
|
||||
public async Task HelpRoot([Remainder] string _ignored = null)
|
||||
public async Task HelpRoot(Context ctx)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(embed: new EmbedBuilder()
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.WithTitle("PluralKit")
|
||||
.WithDescription("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.")
|
||||
.AddField("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.")
|
||||
@ -44,13 +34,10 @@ namespace PluralKit.Bot.Commands
|
||||
.WithColor(Color.Blue)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
[Command("commands")]
|
||||
[Remarks("commands")]
|
||||
public async Task CommandList()
|
||||
public async Task CommandList(Context ctx)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
"The command list has been moved! See the website: https://pluralkit.me/commands");
|
||||
}
|
||||
}
|
||||
|
@ -5,24 +5,27 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.Net;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class ImportExportCommands : ModuleBase<PKCommandContext>
|
||||
public class ImportExportCommands
|
||||
{
|
||||
public DataFileService DataFiles { get; set; }
|
||||
private DataFileService _dataFiles;
|
||||
public ImportExportCommands(DataFileService dataFiles)
|
||||
{
|
||||
_dataFiles = dataFiles;
|
||||
}
|
||||
|
||||
[Command("import")]
|
||||
[Remarks("import [fileurl]")]
|
||||
public async Task Import([Remainder] string url = null)
|
||||
public async Task Import(Context ctx)
|
||||
{
|
||||
if (url == null) url = Context.Message.Attachments.FirstOrDefault()?.Url;
|
||||
var url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url;
|
||||
if (url == null) throw Errors.NoImportFilePassed;
|
||||
|
||||
await Context.BusyIndicator(async () =>
|
||||
await ctx.BusyIndicator(async () =>
|
||||
{
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
@ -74,8 +77,8 @@ namespace PluralKit.Bot.Commands
|
||||
issueStr +=
|
||||
"\n- PluralKit does not support per-member system tags. Since you had multiple members with distinct tags, tags will not be imported. You can set your system tag using the `pk;system tag <tag>` command later.";
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync($"{issueStr}\n\nDo you want to proceed with the import?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
var msg = await ctx.Reply($"{issueStr}\n\nDo you want to proceed with the import?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
}
|
||||
|
||||
data = res.System;
|
||||
@ -89,37 +92,36 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
if (!data.Valid) throw Errors.InvalidImportFile;
|
||||
|
||||
if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(Context.User.Id))
|
||||
if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id))
|
||||
{
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled;
|
||||
}
|
||||
|
||||
// If passed system is null, it'll create a new one
|
||||
// (and that's okay!)
|
||||
var result = await DataFiles.ImportSystem(data, Context.SenderSystem, Context.User.Id);
|
||||
var result = await _dataFiles.ImportSystem(data, ctx.System, ctx.Author.Id);
|
||||
|
||||
if (Context.SenderSystem == null)
|
||||
if (ctx.System != null)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} PluralKit has created a system for you based on the given file. Your system ID is `{result.System.Hid}`. Type `pk;system` for more information.");
|
||||
await ctx.Reply($"{Emojis.Success} PluralKit has created a system for you based on the given file. Your system ID is `{result.System.Hid}`. Type `pk;system` for more information.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Updated {result.ModifiedNames.Count} members, created {result.AddedNames.Count} members. Type `pk;system list` to check!");
|
||||
await ctx.Reply($"{Emojis.Success} Updated {result.ModifiedNames.Count} members, created {result.AddedNames.Count} members. Type `pk;system list` to check!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Command("export")]
|
||||
[Remarks("export")]
|
||||
[MustHaveSystem]
|
||||
public async Task Export()
|
||||
public async Task Export(Context ctx)
|
||||
{
|
||||
var json = await Context.BusyIndicator(async () =>
|
||||
ctx.CheckSystem();
|
||||
|
||||
var json = await ctx.BusyIndicator(async () =>
|
||||
{
|
||||
// Make the actual data file
|
||||
var data = await DataFiles.ExportSystem(Context.SenderSystem);
|
||||
var data = await _dataFiles.ExportSystem(ctx.System);
|
||||
return JsonConvert.SerializeObject(data, Formatting.None);
|
||||
});
|
||||
|
||||
@ -129,16 +131,16 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
try
|
||||
{
|
||||
await Context.User.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!");
|
||||
await ctx.Author.SendFileAsync(stream, "system.json", $"{Emojis.Success} Here you go!");
|
||||
|
||||
// If the original message wasn't posted in DMs, send a public reminder
|
||||
if (!(Context.Channel is IDMChannel))
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Check your DMs!");
|
||||
if (!(ctx.Channel is IDMChannel))
|
||||
await ctx.Reply($"{Emojis.Success} Check your DMs!");
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
// If user has DMs closed, tell 'em to open them
|
||||
await Context.Channel.SendMessageAsync(
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Error} Could not send the data file in your DMs. Do you have DMs closed?");
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,57 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class LinkCommands: ModuleBase<PKCommandContext>
|
||||
public class LinkCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
private SystemStore _systems;
|
||||
|
||||
|
||||
[Command("link")]
|
||||
[Remarks("link <account>")]
|
||||
[MustHaveSystem]
|
||||
public async Task LinkSystem(IUser account)
|
||||
public LinkCommands(SystemStore systems)
|
||||
{
|
||||
var accountIds = await Systems.GetLinkedAccountIds(Context.SenderSystem);
|
||||
if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked;
|
||||
|
||||
var existingAccount = await Systems.GetByAccount(account.Id);
|
||||
if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount);
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
$"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.");
|
||||
if (!await Context.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled;
|
||||
await Systems.Link(Context.SenderSystem, account.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Account linked to system.");
|
||||
_systems = systems;
|
||||
}
|
||||
|
||||
[Command("unlink")]
|
||||
[Remarks("unlink [account]")]
|
||||
[MustHaveSystem]
|
||||
public async Task UnlinkAccount(IUser account = null)
|
||||
public async Task LinkSystem(Context ctx)
|
||||
{
|
||||
if (account == null) account = Context.User;
|
||||
ctx.CheckSystem();
|
||||
|
||||
var accountIds = (await Systems.GetLinkedAccountIds(Context.SenderSystem)).ToList();
|
||||
var account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
|
||||
var accountIds = await _systems.GetLinkedAccountIds(ctx.System);
|
||||
if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked;
|
||||
|
||||
var existingAccount = await _systems.GetByAccount(account.Id);
|
||||
if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount);
|
||||
|
||||
var msg = await ctx.Reply($"{account.Mention}, please confirm the link by clicking the {Emojis.Success} reaction on this message.");
|
||||
if (!await ctx.PromptYesNo(msg, user: account)) throw Errors.MemberLinkCancelled;
|
||||
await _systems.Link(ctx.System, account.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Account linked to system.");
|
||||
}
|
||||
|
||||
public async Task UnlinkAccount(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
IUser account;
|
||||
if (!ctx.HasNext())
|
||||
account = ctx.Author;
|
||||
else
|
||||
account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
|
||||
|
||||
var accountIds = (await _systems.GetLinkedAccountIds(ctx.System)).ToList();
|
||||
if (!accountIds.Contains(account.Id)) throw Errors.AccountNotLinked;
|
||||
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
|
||||
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
var msg = await ctx.Reply(
|
||||
$"Are you sure you want to unlink {account.Mention} from your system?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled;
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled;
|
||||
|
||||
await Systems.Unlink(Context.SenderSystem, account.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Account unlinked.");
|
||||
await _systems.Unlink(ctx.System, account.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Account unlinked.");
|
||||
}
|
||||
}
|
||||
}
|
@ -2,167 +2,170 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("member")]
|
||||
[Alias("m")]
|
||||
public class MemberCommands : ContextParameterModuleBase<PKMember>
|
||||
public class MemberCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
public MemberStore Members { get; set; }
|
||||
public EmbedService Embeds { get; set; }
|
||||
private SystemStore _systems;
|
||||
private MemberStore _members;
|
||||
private EmbedService _embeds;
|
||||
|
||||
public ProxyCacheService ProxyCache { get; set; }
|
||||
private ProxyCacheService _proxyCache;
|
||||
|
||||
public override string Prefix => "member";
|
||||
public override string ContextNoun => "member";
|
||||
public MemberCommands(SystemStore systems, MemberStore members, EmbedService embeds, ProxyCacheService proxyCache)
|
||||
{
|
||||
_systems = systems;
|
||||
_members = members;
|
||||
_embeds = embeds;
|
||||
_proxyCache = proxyCache;
|
||||
}
|
||||
|
||||
public async Task NewMember(Context ctx) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
|
||||
|
||||
[Command("new")]
|
||||
[Alias("n", "add", "create", "register")]
|
||||
[Remarks("member new <name>")]
|
||||
[MustHaveSystem]
|
||||
public async Task NewMember([Remainder] string memberName) {
|
||||
// Hard name length cap
|
||||
if (memberName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(memberName.Length);
|
||||
|
||||
// Warn if member name will be unproxyable (with/without tag)
|
||||
if (memberName.Length > Context.SenderSystem.MaxMemberNameLength) {
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Member name too long ({memberName.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later, or set a member display name)");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
if (memberName.Length > ctx.System.MaxMemberNameLength) {
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} Member name too long ({memberName.Length} > {ctx.System.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to create it anyway? (You can change the name later, or set a member display name)");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
}
|
||||
|
||||
// Warn if there's already a member by this name
|
||||
var existingMember = await Members.GetByName(Context.SenderSystem, memberName);
|
||||
var existingMember = await _members.GetByName(ctx.System, memberName);
|
||||
if (existingMember != null) {
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.Sanitize()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.Sanitize()}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled.");
|
||||
}
|
||||
|
||||
// Create the member
|
||||
var member = await Members.Create(Context.SenderSystem, memberName);
|
||||
var member = await _members.Create(ctx.System, memberName);
|
||||
|
||||
// Send confirmation and space hint
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member \"{memberName.Sanitize()}\" (`{member.Hid}`) registered! See the user guide for commands for editing this member: https://pluralkit.me/guide#member-management");
|
||||
if (memberName.Contains(" ")) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`).");
|
||||
await ctx.Reply($"{Emojis.Success} Member \"{memberName.Sanitize()}\" (`{member.Hid}`) registered! See the user guide for commands for editing this member: https://pluralkit.me/guide#member-management");
|
||||
if (memberName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`).");
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("rename")]
|
||||
[Alias("name", "changename", "setname")]
|
||||
[Remarks("member <member> rename <newname>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task RenameMember([Remainder] string newName) {
|
||||
public async Task RenameMember(Context ctx, PKMember target) {
|
||||
// TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean?
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var newName = ctx.RemainderOrNull();
|
||||
|
||||
// Hard name length cap
|
||||
if (newName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(newName.Length);
|
||||
|
||||
// Warn if member name will be unproxyable (with/without tag), only if member doesn't have a display name
|
||||
if (ContextEntity.DisplayName == null && newName.Length > Context.SenderSystem.MaxMemberNameLength) {
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} New member name too long ({newName.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to change it anyway? (You can set a member display name instead)");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
if (target.DisplayName == null && newName.Length > ctx.System.MaxMemberNameLength) {
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} New member name too long ({newName.Length} > {ctx.System.MaxMemberNameLength} characters), this member will be unproxyable. Do you want to change it anyway? (You can set a member display name instead)");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
}
|
||||
|
||||
// Warn if there's already a member by this name
|
||||
var existingMember = await Members.GetByName(Context.SenderSystem, newName);
|
||||
var existingMember = await _members.GetByName(ctx.System, newName);
|
||||
if (existingMember != null) {
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.Sanitize()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.Name.Sanitize()}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled.");
|
||||
}
|
||||
|
||||
// Rename the member
|
||||
ContextEntity.Name = newName;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Name = newName;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member renamed.");
|
||||
if (newName.Contains(" ")) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it.");
|
||||
if (ContextEntity.DisplayName != null) await Context.Channel.SendMessageAsync($"{Emojis.Note} Note that this member has a display name set (`{ContextEntity.DisplayName}`), and will be proxied using that name instead.");
|
||||
await ctx.Reply($"{Emojis.Success} Member renamed.");
|
||||
if (newName.Contains(" ")) await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it.");
|
||||
if (target.DisplayName != null) await ctx.Reply($"{Emojis.Note} Note that this member has a display name set (`{target.DisplayName}`), and will be proxied using that name instead.");
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("description")]
|
||||
[Alias("info", "bio", "text", "desc")]
|
||||
[Remarks("member <member> description <description>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDescription([Remainder] string description = null) {
|
||||
public async Task MemberDescription(Context ctx, PKMember target) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var description = ctx.RemainderOrNull();
|
||||
if (description.IsLongerThan(Limits.MaxDescriptionLength)) throw Errors.DescriptionTooLongError(description.Length);
|
||||
|
||||
ContextEntity.Description = description;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Description = description;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member description {(description == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
[Command("pronouns")]
|
||||
[Alias("pronoun")]
|
||||
[Remarks("member <member> pronouns <pronouns>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberPronouns([Remainder] string pronouns = null) {
|
||||
public async Task MemberPronouns(Context ctx, PKMember target) {
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var pronouns = ctx.RemainderOrNull();
|
||||
if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) throw Errors.MemberPronounsTooLongError(pronouns.Length);
|
||||
|
||||
ContextEntity.Pronouns = pronouns;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Pronouns = pronouns;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member pronouns {(pronouns == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
[Command("color")]
|
||||
[Alias("colour")]
|
||||
[Remarks("member <member> color <color>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberColor([Remainder] string color = null)
|
||||
public async Task MemberColor(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var color = ctx.RemainderOrNull();
|
||||
if (color != null)
|
||||
{
|
||||
if (color.StartsWith("#")) color = color.Substring(1);
|
||||
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
|
||||
}
|
||||
|
||||
ContextEntity.Color = color;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Color = color;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member color {(color == null ? "cleared" : "changed")}.");
|
||||
}
|
||||
|
||||
[Command("birthday")]
|
||||
[Alias("birthdate", "bday", "cakeday", "bdate")]
|
||||
[Remarks("member <member> birthday <birthday>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberBirthday([Remainder] string birthday = null)
|
||||
public async Task MemberBirthday(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
LocalDate? date = null;
|
||||
var birthday = ctx.RemainderOrNull();
|
||||
if (birthday != null)
|
||||
{
|
||||
date = PluralKit.Utils.ParseDate(birthday, true);
|
||||
if (date == null) throw Errors.BirthdayParseError(birthday);
|
||||
}
|
||||
|
||||
ContextEntity.Birthday = date;
|
||||
await Members.Save(ContextEntity);
|
||||
target.Birthday = date;
|
||||
await _members.Save(target);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {ContextEntity.BirthdayString}")}.");
|
||||
await ctx.Reply($"{Emojis.Success} Member birthdate {(date == null ? "cleared" : $"changed to {target.BirthdayString}")}.");
|
||||
}
|
||||
|
||||
[Command("proxy")]
|
||||
[Alias("proxy", "tags", "proxytags", "brackets")]
|
||||
[Remarks("member <member> proxy <proxy tags>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberProxy([Remainder] string exampleProxy = null)
|
||||
public async Task MemberProxy(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
// Handling the clear case in an if here to keep the body dedented
|
||||
var exampleProxy = ctx.RemainderOrNull();
|
||||
if (exampleProxy == null)
|
||||
{
|
||||
// Just reset and send OK message
|
||||
ContextEntity.Prefix = null;
|
||||
ContextEntity.Suffix = null;
|
||||
await Members.Save(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member proxy tags cleared.");
|
||||
target.Prefix = null;
|
||||
target.Suffix = null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member proxy tags cleared.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -172,75 +175,83 @@ namespace PluralKit.Bot.Commands
|
||||
if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText;
|
||||
|
||||
// If the prefix/suffix is empty, use "null" instead (for DB)
|
||||
ContextEntity.Prefix = prefixAndSuffix[0].Length > 0 ? prefixAndSuffix[0] : null;
|
||||
ContextEntity.Suffix = prefixAndSuffix[1].Length > 0 ? prefixAndSuffix[1] : null;
|
||||
await Members.Save(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member proxy tags changed to `{ContextEntity.ProxyString.Sanitize()}`. Try proxying now!");
|
||||
target.Prefix = prefixAndSuffix[0].Length > 0 ? prefixAndSuffix[0] : null;
|
||||
target.Suffix = prefixAndSuffix[1].Length > 0 ? prefixAndSuffix[1] : null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member proxy tags changed to `{target.ProxyString.Sanitize()}`. Try proxying now!");
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Alias("remove", "destroy", "erase", "yeet")]
|
||||
[Remarks("member <member> delete")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDelete()
|
||||
public async Task MemberDelete(Context ctx, PKMember target)
|
||||
{
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Warn} Are you sure you want to delete \"{ContextEntity.Name.Sanitize()}\"? If so, reply to this message with the member's ID (`{ContextEntity.Hid}`). __***This cannot be undone!***__");
|
||||
if (!await Context.ConfirmWithReply(ContextEntity.Hid)) throw Errors.MemberDeleteCancelled;
|
||||
await Members.Delete(ContextEntity);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member deleted.");
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.Name.Sanitize()}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
|
||||
await _members.Delete(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member deleted.");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("member <member> avatar <avatar url>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberAvatarByMention(IUser member)
|
||||
public async Task MemberAvatar(Context ctx, PKMember target)
|
||||
{
|
||||
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
ContextEntity.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await Members.Save(ContextEntity);
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(ContextEntity.AvatarUrl).Build();
|
||||
await Context.Channel.SendMessageAsync(
|
||||
$"{Emojis.Success} Member avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the webhook's avatar will need to be re-set.", embed: embed);
|
||||
if (await ctx.MatchUser() is IUser user)
|
||||
{
|
||||
if (user.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
target.AvatarUrl = user.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _members.Save(target);
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(target.AvatarUrl).Build();
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Member avatar changed to {user.Username}'s avatar! {Emojis.Warn} Please note that if {user.Username} changes their avatar, the webhook's avatar will need to be re-set.", embed: embed);
|
||||
|
||||
}
|
||||
else if (ctx.RemainderOrNull() is string url)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(url);
|
||||
target.AvatarUrl = url;
|
||||
await _members.Save(target);
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(url).Build();
|
||||
await ctx.Reply($"{Emojis.Success} Member avatar changed.", embed: embed);
|
||||
}
|
||||
else if (ctx.Message.Attachments.FirstOrDefault() is Attachment attachment)
|
||||
{
|
||||
await Utils.VerifyAvatarOrThrow(attachment.Url);
|
||||
target.AvatarUrl = attachment.Url;
|
||||
await _members.Save(target);
|
||||
|
||||
await ctx.Reply($"{Emojis.Success} Member avatar changed to attached image. Please note that if you delete the message containing the attachment, the avatar will stop working.");
|
||||
}
|
||||
else
|
||||
{
|
||||
target.AvatarUrl = null;
|
||||
await _members.Save(target);
|
||||
await ctx.Reply($"{Emojis.Success} Member avatar cleared.");
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("member <member> avatar <avatar url>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberAvatar([Remainder] string avatarUrl = null)
|
||||
{
|
||||
string url = avatarUrl ?? Context.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await Context.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
|
||||
ContextEntity.AvatarUrl = url;
|
||||
await Members.Save(ContextEntity);
|
||||
|
||||
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Member avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("displayname")]
|
||||
[Alias("nick", "nickname", "displayname")]
|
||||
[Remarks("member <member> displayname <displayname>")]
|
||||
[MustPassOwnMember]
|
||||
public async Task MemberDisplayName([Remainder] string newDisplayName = null)
|
||||
public async Task MemberDisplayName(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
var newDisplayName = ctx.RemainderOrNull();
|
||||
// Refuse if proxy name will be unproxyable (with/without tag)
|
||||
if (newDisplayName != null && newDisplayName.Length > Context.SenderSystem.MaxMemberNameLength)
|
||||
throw Errors.DisplayNameTooLong(newDisplayName, Context.SenderSystem.MaxMemberNameLength);
|
||||
if (newDisplayName != null && newDisplayName.Length > ctx.System.MaxMemberNameLength)
|
||||
throw Errors.DisplayNameTooLong(newDisplayName, ctx.System.MaxMemberNameLength);
|
||||
|
||||
ContextEntity.DisplayName = newDisplayName;
|
||||
await Members.Save(ContextEntity);
|
||||
target.DisplayName = newDisplayName;
|
||||
await _members.Save(target);
|
||||
|
||||
var successStr = $"{Emojis.Success} ";
|
||||
if (newDisplayName != null)
|
||||
@ -253,30 +264,21 @@ namespace PluralKit.Bot.Commands
|
||||
successStr += $"Member display name cleared. ";
|
||||
|
||||
// If we're removing display name and the *real* name will be unproxyable, warn.
|
||||
if (ContextEntity.Name.Length > Context.SenderSystem.MaxMemberNameLength)
|
||||
if (target.Name.Length > ctx.System.MaxMemberNameLength)
|
||||
successStr +=
|
||||
$" {Emojis.Warn} This member's actual name is too long ({ContextEntity.Name.Length} > {Context.SenderSystem.MaxMemberNameLength} characters), and thus cannot be proxied.";
|
||||
$" {Emojis.Warn} This member's actual name is too long ({target.Name.Length} > {ctx.System.MaxMemberNameLength} characters), and thus cannot be proxied.";
|
||||
else
|
||||
successStr += $"This member will now be proxied using their member name `{ContextEntity.Name}.";
|
||||
successStr += $"This member will now be proxied using their member name `{target.Name}.";
|
||||
}
|
||||
await Context.Channel.SendMessageAsync(successStr);
|
||||
await ctx.Reply(successStr);
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command]
|
||||
[Alias("view", "show", "info")]
|
||||
[Remarks("member <member>")]
|
||||
public async Task ViewMember(PKMember member)
|
||||
public async Task ViewMember(Context ctx, PKMember target)
|
||||
{
|
||||
var system = await Systems.GetById(member.System);
|
||||
await Context.Channel.SendMessageAsync(embed: await Embeds.CreateMemberEmbed(system, member));
|
||||
}
|
||||
|
||||
public override async Task<PKMember> ReadContextParameterAsync(string value)
|
||||
{
|
||||
var res = await new PKMemberTypeReader().ReadAsync(Context, value, _services);
|
||||
return res.IsSuccess ? res.BestMatch as PKMember : null;
|
||||
var system = await _systems.GetById(target.System);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using App.Metrics;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Humanizer;
|
||||
|
||||
namespace PluralKit.Bot.Commands {
|
||||
public class MiscCommands: ModuleBase<PKCommandContext> {
|
||||
public BotConfig BotConfig { get; set; }
|
||||
public IMetrics Metrics { get; set; }
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
[Command("invite")]
|
||||
[Alias("inv")]
|
||||
[Remarks("invite")]
|
||||
public async Task Invite()
|
||||
namespace PluralKit.Bot.Commands {
|
||||
public class MiscCommands
|
||||
{
|
||||
var clientId = BotConfig.ClientId ?? (await Context.Client.GetApplicationInfoAsync()).Id;
|
||||
private BotConfig _botConfig;
|
||||
private IMetrics _metrics;
|
||||
|
||||
public MiscCommands(BotConfig botConfig, IMetrics metrics)
|
||||
{
|
||||
_botConfig = botConfig;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public async Task Invite(Context ctx)
|
||||
{
|
||||
var clientId = _botConfig.ClientId ?? (await ctx.Client.GetApplicationInfoAsync()).Id;
|
||||
var permissions = new GuildPermissions(
|
||||
addReactions: true,
|
||||
attachFiles: true,
|
||||
@ -29,36 +33,36 @@ namespace PluralKit.Bot.Commands {
|
||||
);
|
||||
|
||||
var invite = $"https://discordapp.com/oauth2/authorize?client_id={clientId}&scope=bot&permissions={permissions.RawValue}";
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
|
||||
await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
|
||||
}
|
||||
|
||||
[Command("mn")] public Task Mn() => Context.Channel.SendMessageAsync("Gotta catch 'em all!");
|
||||
[Command("fire")] public Task Fire() => Context.Channel.SendMessageAsync("*A giant lightning bolt promptly erupts into a pillar of fire as it hits your opponent.*");
|
||||
[Command("thunder")] public Task Thunder() => Context.Channel.SendMessageAsync("*A giant ball of lightning is conjured and fired directly at your opponent, vanquishing them.*");
|
||||
[Command("freeze")] public Task Freeze() => Context.Channel.SendMessageAsync("*A giant crystal ball of ice is charged and hurled toward your opponent, bursting open and freezing them solid on contact.*");
|
||||
[Command("starstorm")] public Task Starstorm() => Context.Channel.SendMessageAsync("*Vibrant colours burst forth from the sky as meteors rain down upon your opponent.*");
|
||||
public Task Mn(Context ctx) => ctx.Reply("Gotta catch 'em all!");
|
||||
public Task Fire(Context ctx) => ctx.Reply("*A giant lightning bolt promptly erupts into a pillar of fire as it hits your opponent.*");
|
||||
public Task Thunder(Context ctx) => ctx.Reply("*A giant ball of lightning is conjured and fired directly at your opponent, vanquishing them.*");
|
||||
public Task Freeze(Context ctx) => ctx.Reply("*A giant crystal ball of ice is charged and hurled toward your opponent, bursting open and freezing them solid on contact.*");
|
||||
public Task Starstorm(Context ctx) => ctx.Reply("*Vibrant colours burst forth from the sky as meteors rain down upon your opponent.*");
|
||||
|
||||
[Command("stats")]
|
||||
public async Task Stats()
|
||||
public async Task Stats(Context ctx)
|
||||
{
|
||||
var messagesReceived = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name).Value;
|
||||
var messagesProxied = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name).Value;
|
||||
var messagesReceived = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name).Value;
|
||||
var messagesProxied = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name).Value;
|
||||
|
||||
var commandsRun = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name).Value;
|
||||
var commandsRun = _metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name).Value;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: new EmbedBuilder()
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.AddField("Messages processed", $"{messagesReceived.OneMinuteRate:F1}/s ({messagesReceived.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.AddField("Messages proxied", $"{messagesProxied.OneMinuteRate:F1}/s ({messagesProxied.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.AddField("Commands executed", $"{commandsRun.OneMinuteRate:F1}/s ({commandsRun.FifteenMinuteRate:F1}/s over 15m)")
|
||||
.Build());
|
||||
}
|
||||
|
||||
[Command("permcheck")]
|
||||
[Summary("permcheck [guild]")]
|
||||
public async Task PermCheckGuild(ulong guildId)
|
||||
public async Task PermCheckGuild(Context ctx)
|
||||
{
|
||||
var guildIdStr = ctx.PopArgument() ?? throw new PKSyntaxError("You must pass a server ID.");
|
||||
if (!ulong.TryParse(guildIdStr, out var guildId)) throw new PKSyntaxError($"Could not parse `{guildIdStr}` as an ID.");
|
||||
|
||||
// TODO: will this call break for sharding if you try to request a guild on a different bot instance?
|
||||
var guild = Context.Client.GetGuild(guildId) as IGuild;
|
||||
var guild = ctx.Client.GetGuild(guildId) as IGuild;
|
||||
if (guild == null)
|
||||
throw Errors.GuildNotFound(guildId);
|
||||
|
||||
@ -118,13 +122,7 @@ namespace PluralKit.Bot.Commands {
|
||||
}
|
||||
|
||||
// Send! :)
|
||||
await Context.Channel.SendMessageAsync(embed: eb.Build());
|
||||
}
|
||||
|
||||
[Command("permcheck")]
|
||||
[Summary("permcheck [guild]")]
|
||||
[RequireContext(ContextType.Guild, ErrorMessage =
|
||||
"When running this command in DMs, you must pass a guild ID.")]
|
||||
public Task PermCheckGuild() => PermCheckGuild(Context.Guild.Id);
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +1,56 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
public class ModCommands: ModuleBase<PKCommandContext>
|
||||
public class ModCommands
|
||||
{
|
||||
public LogChannelService LogChannels { get; set; }
|
||||
public MessageStore Messages { get; set; }
|
||||
private LogChannelService _logChannels;
|
||||
private MessageStore _messages;
|
||||
|
||||
public EmbedService Embeds { get; set; }
|
||||
private EmbedService _embeds;
|
||||
|
||||
[Command("log")]
|
||||
[Remarks("log <channel>")]
|
||||
[RequireUserPermission(GuildPermission.ManageGuild, ErrorMessage = "You must have the Manage Server permission to use this command.")]
|
||||
[RequireContext(ContextType.Guild, ErrorMessage = "This command can not be run in a DM.")]
|
||||
public async Task SetLogChannel(ITextChannel channel = null)
|
||||
public ModCommands(LogChannelService logChannels, MessageStore messages, EmbedService embeds)
|
||||
{
|
||||
await LogChannels.SetLogChannel(Context.Guild, channel);
|
||||
_logChannels = logChannels;
|
||||
_messages = messages;
|
||||
_embeds = embeds;
|
||||
}
|
||||
|
||||
public async Task SetLogChannel(Context ctx)
|
||||
{
|
||||
ctx.CheckAuthorPermission(GuildPermission.ManageGuild, "Manage Server").CheckGuildContext();
|
||||
|
||||
ITextChannel channel = null;
|
||||
if (ctx.HasNext())
|
||||
channel = ctx.MatchChannel() ?? throw new PKSyntaxError("You must pass a #channel to set.");
|
||||
|
||||
await _logChannels.SetLogChannel(ctx.Guild, channel);
|
||||
|
||||
if (channel != null)
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Proxy logging channel set to #{channel.Name.Sanitize()}.");
|
||||
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name.Sanitize()}.");
|
||||
else
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Proxy logging channel cleared.");
|
||||
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
|
||||
}
|
||||
|
||||
[Command("message")]
|
||||
[Remarks("message <messageid>")]
|
||||
[Alias("msg")]
|
||||
public async Task GetMessage(ulong messageId)
|
||||
public async Task GetMessage(Context ctx)
|
||||
{
|
||||
var message = await Messages.Get(messageId);
|
||||
var word = ctx.PopArgument() ?? throw new PKSyntaxError("You must pass a message ID or link.");
|
||||
|
||||
ulong messageId;
|
||||
if (ulong.TryParse(word, out var id))
|
||||
messageId = id;
|
||||
else if (Regex.Match(word, "https://discordapp.com/channels/\\d+/(\\d+)") is Match match && match.Success)
|
||||
messageId = ulong.Parse(match.Groups[1].Value);
|
||||
else throw new PKSyntaxError($"Could not parse `{word}` as a message ID or link.");
|
||||
|
||||
var message = await _messages.Get(messageId);
|
||||
if (message == null) throw Errors.MessageNotFound(messageId);
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await Embeds.CreateMessageInfoEmbed(message));
|
||||
}
|
||||
|
||||
[Command("message")]
|
||||
[Remarks("message <messageid>")]
|
||||
[Alias("msg")]
|
||||
public async Task GetMessage(IMessage msg) => await GetMessage(msg.Id);
|
||||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));
|
||||
}
|
||||
}
|
||||
}
|
@ -2,75 +2,91 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
using NodaTime;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("switch")]
|
||||
[Alias("sw")]
|
||||
public class SwitchCommands: ModuleBase<PKCommandContext>
|
||||
public class SwitchCommands
|
||||
{
|
||||
public SystemStore Systems { get; set; }
|
||||
public SwitchStore Switches { get; set; }
|
||||
private SwitchStore _switches;
|
||||
|
||||
[Command]
|
||||
[Remarks("switch <member> [member...]")]
|
||||
[MustHaveSystem]
|
||||
public async Task Switch(params PKMember[] members) => await DoSwitchCommand(members);
|
||||
|
||||
[Command("out")]
|
||||
[Alias("none")]
|
||||
[Remarks("switch out")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchOut() => await DoSwitchCommand(new PKMember[] { });
|
||||
|
||||
private async Task DoSwitchCommand(ICollection<PKMember> members)
|
||||
public SwitchCommands(SwitchStore switches)
|
||||
{
|
||||
// Make sure all the members *are actually in the system*
|
||||
// PKMember parameters won't let this happen if they resolve by name
|
||||
// but they can if they resolve with ID
|
||||
if (members.Any(m => m.System != Context.SenderSystem.Id)) throw Errors.SwitchMemberNotInSystem;
|
||||
_switches = switches;
|
||||
}
|
||||
|
||||
public async Task Switch(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
var members = new List<PKMember>();
|
||||
|
||||
// Loop through all the given arguments
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
// and attempt to match a member
|
||||
var member = await ctx.MatchMember();
|
||||
if (member == null)
|
||||
// if we can't, big error. Every member name must be valid.
|
||||
throw new PKError(ctx.CreateMemberNotFoundError(ctx.PopArgument()));
|
||||
|
||||
ctx.CheckOwnMember(member); // Ensure they're in our own system
|
||||
members.Add(member); // Then add to the final output list
|
||||
}
|
||||
|
||||
// Finally, do the actual switch
|
||||
await DoSwitchCommand(ctx, members);
|
||||
}
|
||||
public async Task SwitchOut(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Switch with no members = switch-out
|
||||
await DoSwitchCommand(ctx, new PKMember[] { });
|
||||
}
|
||||
|
||||
private async Task DoSwitchCommand(Context ctx, ICollection<PKMember> members)
|
||||
{
|
||||
// Make sure there are no dupes in the list
|
||||
// We do this by checking if removing duplicate member IDs results in a list of different length
|
||||
if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers;
|
||||
|
||||
// Find the last switch and its members if applicable
|
||||
var lastSwitch = await Switches.GetLatestSwitch(Context.SenderSystem);
|
||||
var lastSwitch = await _switches.GetLatestSwitch(ctx.System);
|
||||
if (lastSwitch != null)
|
||||
{
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastSwitch);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastSwitch);
|
||||
// Make sure the requested switch isn't identical to the last one
|
||||
if (lastSwitchMembers.Select(m => m.Id).SequenceEqual(members.Select(m => m.Id)))
|
||||
throw Errors.SameSwitch(members);
|
||||
}
|
||||
|
||||
await Switches.RegisterSwitch(Context.SenderSystem, members);
|
||||
await _switches.RegisterSwitch(ctx.System, members);
|
||||
|
||||
if (members.Count == 0)
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch-out registered.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch-out registered.");
|
||||
else
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.Name)).Sanitize()}.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.Name)).Sanitize()}.");
|
||||
}
|
||||
|
||||
[Command("move")]
|
||||
[Alias("shift")]
|
||||
[Remarks("switch move <date/time>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchMove([Remainder] string str)
|
||||
public async Task SwitchMove(Context ctx)
|
||||
{
|
||||
var tz = TzdbDateTimeZoneSource.Default.ForId(Context.SenderSystem.UiTz ?? "UTC");
|
||||
ctx.CheckSystem();
|
||||
|
||||
var result = PluralKit.Utils.ParseDateTime(str, true, tz);
|
||||
if (result == null) throw Errors.InvalidDateTime(str);
|
||||
var timeToMove = ctx.RemainderOrNull() ?? throw new PKSyntaxError("Must pass a date or time to move the switch to.");
|
||||
var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.System.UiTz ?? "UTC");
|
||||
|
||||
var result = PluralKit.Utils.ParseDateTime(timeToMove, true, tz);
|
||||
if (result == null) throw Errors.InvalidDateTime(timeToMove);
|
||||
|
||||
var time = result.Value;
|
||||
if (time.ToInstant() > SystemClock.Instance.GetCurrentInstant()) throw Errors.SwitchTimeInFuture;
|
||||
|
||||
// Fetch the last two switches for the system to do bounds checking on
|
||||
var lastTwoSwitches = (await Switches.GetSwitches(Context.SenderSystem, 2)).ToArray();
|
||||
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
|
||||
|
||||
// If we don't have a switch to move, don't bother
|
||||
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
|
||||
@ -84,55 +100,53 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
// Now we can actually do the move, yay!
|
||||
// But, we do a prompt to confirm.
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
|
||||
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(Context.SenderSystem.Zone));
|
||||
var lastSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(lastTwoSwitches[0].Timestamp.InZone(ctx.System.Zone));
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
var newSwitchTimeStr = Formats.ZonedDateTimeFormat.Format(time);
|
||||
var newSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - time.ToInstant());
|
||||
|
||||
// yeet
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr.Sanitize()}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled;
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr.Sanitize()}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?");
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled;
|
||||
|
||||
// aaaand *now* we do the move
|
||||
await Switches.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch moved.");
|
||||
await _switches.MoveSwitch(lastTwoSwitches[0], time.ToInstant());
|
||||
await ctx.Reply($"{Emojis.Success} Switch moved.");
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Remarks("switch delete")]
|
||||
[Alias("remove", "erase", "cancel", "yeet")]
|
||||
[MustHaveSystem]
|
||||
public async Task SwitchDelete()
|
||||
public async Task SwitchDelete(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
// Fetch the last two switches for the system to do bounds checking on
|
||||
var lastTwoSwitches = (await Switches.GetSwitches(Context.SenderSystem, 2)).ToArray();
|
||||
var lastTwoSwitches = (await _switches.GetSwitches(ctx.System, 2)).ToArray();
|
||||
if (lastTwoSwitches.Length == 0) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
var lastSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[0]);
|
||||
var lastSwitchMemberStr = string.Join(", ", lastSwitchMembers.Select(m => m.Name));
|
||||
var lastSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp);
|
||||
|
||||
IUserMessage msg;
|
||||
if (lastTwoSwitches.Length == 1)
|
||||
{
|
||||
msg = await Context.Channel.SendMessageAsync(
|
||||
msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.Sanitize()}, {lastSwitchDeltaStr} ago). You have no other switches logged. Is this okay?");
|
||||
}
|
||||
else
|
||||
{
|
||||
var secondSwitchMembers = await Switches.GetSwitchMembers(lastTwoSwitches[1]);
|
||||
var secondSwitchMembers = await _switches.GetSwitchMembers(lastTwoSwitches[1]);
|
||||
var secondSwitchMemberStr = string.Join(", ", secondSwitchMembers.Select(m => m.Name));
|
||||
var secondSwitchDeltaStr = Formats.DurationFormat.Format(SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[1].Timestamp);
|
||||
msg = await Context.Channel.SendMessageAsync(
|
||||
msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr.Sanitize()}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr.Sanitize()} ({secondSwitchDeltaStr} ago). Is this okay?");
|
||||
}
|
||||
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled;
|
||||
await Switches.DeleteSwitch(lastTwoSwitches[0]);
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled;
|
||||
await _switches.DeleteSwitch(lastTwoSwitches[0]);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Switch deleted.");
|
||||
await ctx.Reply($"{Emojis.Success} Switch deleted.");
|
||||
}
|
||||
}
|
||||
}
|
@ -2,168 +2,149 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Humanizer;
|
||||
using NodaTime;
|
||||
using NodaTime.Text;
|
||||
using NodaTime.TimeZones;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot.Commands
|
||||
{
|
||||
[Group("system")]
|
||||
[Alias("s")]
|
||||
public class SystemCommands : ContextParameterModuleBase<PKSystem>
|
||||
public class SystemCommands
|
||||
{
|
||||
public override string Prefix => "system";
|
||||
public override string ContextNoun => "system";
|
||||
private SystemStore _systems;
|
||||
private MemberStore _members;
|
||||
|
||||
public SystemStore Systems {get; set;}
|
||||
public MemberStore Members {get; set;}
|
||||
private SwitchStore _switches;
|
||||
private EmbedService _embeds;
|
||||
|
||||
public SwitchStore Switches {get; set;}
|
||||
public EmbedService EmbedService {get; set;}
|
||||
private ProxyCacheService _proxyCache;
|
||||
|
||||
public ProxyCacheService ProxyCache { get; set; }
|
||||
public SystemCommands(SystemStore systems, MemberStore members, SwitchStore switches, EmbedService embeds, ProxyCacheService proxyCache)
|
||||
{
|
||||
_systems = systems;
|
||||
_members = members;
|
||||
_switches = switches;
|
||||
_embeds = embeds;
|
||||
_proxyCache = proxyCache;
|
||||
}
|
||||
|
||||
|
||||
[Command]
|
||||
[Remarks("system <name>")]
|
||||
public async Task Query(PKSystem system = null) {
|
||||
if (system == null) system = Context.SenderSystem;
|
||||
public async Task Query(Context ctx, PKSystem system) {
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateSystemEmbed(system));
|
||||
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system));
|
||||
}
|
||||
|
||||
[Command("new")]
|
||||
[Alias("register", "create", "init", "add", "make")]
|
||||
[Remarks("system new <name>")]
|
||||
public async Task New([Remainder] string systemName = null)
|
||||
public async Task New(Context ctx)
|
||||
{
|
||||
if (ContextEntity != null) throw Errors.NotOwnSystemError;
|
||||
if (Context.SenderSystem != null) throw Errors.ExistingSystemError;
|
||||
ctx.CheckNoSystem();
|
||||
|
||||
var system = await Systems.Create(systemName);
|
||||
await Systems.Link(system, Context.User.Id);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
||||
var system = await _systems.Create(ctx.RemainderOrNull());
|
||||
await _systems.Link(system, ctx.Author.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
||||
}
|
||||
|
||||
[Command("name")]
|
||||
[Alias("rename", "changename")]
|
||||
[Remarks("system name <name>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Name([Remainder] string newSystemName = null) {
|
||||
public async Task Name(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var newSystemName = ctx.RemainderOrNull();
|
||||
if (newSystemName != null && newSystemName.Length > Limits.MaxSystemNameLength) throw Errors.SystemNameTooLongError(newSystemName.Length);
|
||||
|
||||
Context.SenderSystem.Name = newSystemName;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}.");
|
||||
ctx.System.Name = newSystemName;
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System name {(newSystemName != null ? "changed" : "cleared")}.");
|
||||
}
|
||||
|
||||
[Command("description")]
|
||||
[Alias("desc")]
|
||||
[Remarks("system description <description>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Description([Remainder] string newDescription = null) {
|
||||
public async Task Description(Context ctx) {
|
||||
ctx.CheckSystem();
|
||||
|
||||
var newDescription = ctx.RemainderOrNull();
|
||||
if (newDescription != null && newDescription.Length > Limits.MaxDescriptionLength) throw Errors.DescriptionTooLongError(newDescription.Length);
|
||||
|
||||
Context.SenderSystem.Description = newDescription;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}.");
|
||||
ctx.System.Description = newDescription;
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System description {(newDescription != null ? "changed" : "cleared")}.");
|
||||
}
|
||||
|
||||
[Command("tag")]
|
||||
[Remarks("system tag <tag>")]
|
||||
[MustHaveSystem]
|
||||
public async Task Tag([Remainder] string newTag = null) {
|
||||
public async Task Tag(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
Context.SenderSystem.Tag = newTag;
|
||||
var newTag = ctx.RemainderOrNull();
|
||||
ctx.System.Tag = newTag;
|
||||
|
||||
if (newTag != null)
|
||||
{
|
||||
if (newTag.Length > Limits.MaxSystemTagLength) throw Errors.SystemNameTooLongError(newTag.Length);
|
||||
|
||||
// Check unproxyable messages *after* changing the tag (so it's seen in the method) but *before* we save to DB (so we can cancel)
|
||||
var unproxyableMembers = await Members.GetUnproxyableMembers(Context.SenderSystem);
|
||||
var unproxyableMembers = await _members.GetUnproxyableMembers(ctx.System);
|
||||
if (unproxyableMembers.Count > 0)
|
||||
{
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
var msg = await ctx.Reply(
|
||||
$"{Emojis.Warn} Changing your system tag to '{newTag}' will result in the following members being unproxyable, since the tag would bring their name over {Limits.MaxProxyNameLength} characters:\n**{string.Join(", ", unproxyableMembers.Select((m) => m.Name))}**\nDo you want to continue anyway?");
|
||||
if (!await Context.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
|
||||
if (!await ctx.PromptYesNo(msg)) throw new PKError("Tag change cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}.");
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System tag {(newTag != null ? "changed" : "cleared")}.");
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("system avatar <avatar url>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemAvatar(IUser member)
|
||||
public async Task SystemAvatar(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var member = await ctx.MatchUser();
|
||||
if (member != null)
|
||||
{
|
||||
if (member.AvatarId == null) throw Errors.UserHasNoAvatar;
|
||||
Context.SenderSystem.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
ctx.System.AvatarUrl = member.GetAvatarUrl(ImageFormat.Png, size: 256);
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
var embed = new EmbedBuilder().WithImageUrl(Context.SenderSystem.AvatarUrl).Build();
|
||||
await Context.Channel.SendMessageAsync(
|
||||
var embed = new EmbedBuilder().WithImageUrl(ctx.System.AvatarUrl).Build();
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} System avatar changed to {member.Username}'s avatar! {Emojis.Warn} Please note that if {member.Username} changes their avatar, the system's avatar will need to be re-set.", embed: embed);
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
}
|
||||
|
||||
[Command("avatar")]
|
||||
[Alias("profile", "picture", "icon", "image", "pic", "pfp")]
|
||||
[Remarks("system avatar <avatar url>")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemAvatar([Remainder] string avatarUrl = null)
|
||||
else
|
||||
{
|
||||
string url = avatarUrl ?? Context.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await Context.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
string url = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.ProxyUrl;
|
||||
if (url != null) await ctx.BusyIndicator(() => Utils.VerifyAvatarOrThrow(url));
|
||||
|
||||
Context.SenderSystem.AvatarUrl = url;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
ctx.System.AvatarUrl = url;
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
var embed = url != null ? new EmbedBuilder().WithImageUrl(url).Build() : null;
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await ctx.Reply($"{Emojis.Success} System avatar {(url == null ? "cleared" : "changed")}.", embed: embed);
|
||||
}
|
||||
|
||||
[Command("delete")]
|
||||
[Alias("remove", "destroy", "erase", "yeet")]
|
||||
[Remarks("system delete")]
|
||||
[MustHaveSystem]
|
||||
public async Task Delete() {
|
||||
var msg = await Context.Channel.SendMessageAsync($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{Context.SenderSystem.Hid}`).\n**Note: this action is permanent.**");
|
||||
var reply = await Context.AwaitMessage(Context.Channel, Context.User, timeout: TimeSpan.FromMinutes(1));
|
||||
if (reply.Content != Context.SenderSystem.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{Context.SenderSystem.Hid}`) *verbatim*.");
|
||||
|
||||
await Systems.Delete(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System deleted.");
|
||||
|
||||
await ProxyCache.InvalidateResultsForSystem(Context.SenderSystem);
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
[Group("list")]
|
||||
[Alias("l", "members")]
|
||||
public class SystemListCommands: ModuleBase<PKCommandContext> {
|
||||
public MemberStore Members { get; set; }
|
||||
public async Task Delete(Context ctx) {
|
||||
ctx.CheckSystem();
|
||||
|
||||
[Command]
|
||||
[Remarks("system [system] list")]
|
||||
public async Task MemberShortList() {
|
||||
var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem;
|
||||
var msg = await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{ctx.System.Hid}`).\n**Note: this action is permanent.**");
|
||||
var reply = await ctx.AwaitMessage(ctx.Channel, ctx.Author, timeout: TimeSpan.FromMinutes(1));
|
||||
if (reply.Content != ctx.System.Hid) throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{ctx.System.Hid}`) *verbatim*.");
|
||||
|
||||
await _systems.Delete(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System deleted.");
|
||||
|
||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||
}
|
||||
|
||||
public async Task MemberShortList(Context ctx, PKSystem system) {
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var members = await Members.GetBySystem(system);
|
||||
var members = await _members.GetBySystem(system);
|
||||
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||
await Context.Paginate<PKMember>(
|
||||
await ctx.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name).ToList(),
|
||||
25,
|
||||
embedTitle,
|
||||
@ -174,16 +155,12 @@ namespace PluralKit.Bot.Commands
|
||||
);
|
||||
}
|
||||
|
||||
[Command("full")]
|
||||
[Alias("big", "details", "long")]
|
||||
[Remarks("system [system] list full")]
|
||||
public async Task MemberLongList() {
|
||||
var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem;
|
||||
public async Task MemberLongList(Context ctx, PKSystem system) {
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var members = await Members.GetBySystem(system);
|
||||
var members = await _members.GetBySystem(system);
|
||||
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||
await Context.Paginate<PKMember>(
|
||||
await ctx.Paginate<PKMember>(
|
||||
members.OrderBy(m => m.Name).ToList(),
|
||||
5,
|
||||
embedTitle,
|
||||
@ -199,82 +176,70 @@ namespace PluralKit.Bot.Commands
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("fronter")]
|
||||
[Alias("f", "front", "fronters")]
|
||||
[Remarks("system [system] fronter")]
|
||||
public async Task SystemFronter()
|
||||
public async Task SystemFronter(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var sw = await Switches.GetLatestSwitch(system);
|
||||
var sw = await _switches.GetLatestSwitch(system);
|
||||
if (sw == null) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFronterEmbed(sw, system.Zone));
|
||||
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone));
|
||||
}
|
||||
|
||||
[Command("fronthistory")]
|
||||
[Alias("fh", "history", "switches")]
|
||||
[Remarks("system [system] fronthistory")]
|
||||
public async Task SystemFrontHistory()
|
||||
public async Task SystemFrontHistory(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
var sws = (await Switches.GetSwitches(system, 10)).ToList();
|
||||
var sws = (await _switches.GetSwitches(system, 10)).ToList();
|
||||
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
|
||||
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontHistoryEmbed(sws, system.Zone));
|
||||
await ctx.Reply(embed: await _embeds.CreateFrontHistoryEmbed(sws, system.Zone));
|
||||
}
|
||||
|
||||
[Command("frontpercent")]
|
||||
[Alias("frontbreakdown", "frontpercent", "front%", "fp")]
|
||||
[Remarks("system [system] frontpercent [duration]")]
|
||||
public async Task SystemFrontPercent([Remainder] string durationStr = "30d")
|
||||
public async Task SystemFrontPercent(Context ctx, PKSystem system)
|
||||
{
|
||||
var system = ContextEntity ?? Context.SenderSystem;
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
|
||||
string durationStr = ctx.RemainderOrNull() ?? "30d";
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var rangeStart = PluralKit.Utils.ParseDateTime(durationStr);
|
||||
if (rangeStart == null) throw Errors.InvalidDateTime(durationStr);
|
||||
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
|
||||
|
||||
var frontpercent = await Switches.GetPerMemberSwitchDuration(system, rangeStart.Value.ToInstant(), now);
|
||||
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateFrontPercentEmbed(frontpercent, system.Zone));
|
||||
var frontpercent = await _switches.GetPerMemberSwitchDuration(system, rangeStart.Value.ToInstant(), now);
|
||||
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system.Zone));
|
||||
}
|
||||
|
||||
[Command("timezone")]
|
||||
[Alias("tz")]
|
||||
[Remarks("system timezone [timezone]")]
|
||||
[MustHaveSystem]
|
||||
public async Task SystemTimezone([Remainder] string zoneStr = null)
|
||||
public async Task SystemTimezone(Context ctx)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
|
||||
var zoneStr = ctx.RemainderOrNull();
|
||||
if (zoneStr == null)
|
||||
{
|
||||
Context.SenderSystem.UiTz = "UTC";
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
await Context.Channel.SendMessageAsync($"{Emojis.Success} System time zone cleared.");
|
||||
ctx.System.UiTz = "UTC";
|
||||
await _systems.Save(ctx.System);
|
||||
await ctx.Reply($"{Emojis.Success} System time zone cleared.");
|
||||
return;
|
||||
}
|
||||
|
||||
var zone = await FindTimeZone(zoneStr);
|
||||
var zone = await FindTimeZone(ctx, zoneStr);
|
||||
if (zone == null) throw Errors.InvalidTimeZone(zoneStr);
|
||||
|
||||
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
|
||||
var msg = await Context.Channel.SendMessageAsync(
|
||||
var msg = await ctx.Reply(
|
||||
$"This will change the system time zone to {zone.Id}. The current time is {Formats.ZonedDateTimeFormat.Format(currentTime)}. Is this correct?");
|
||||
if (!await Context.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
|
||||
Context.SenderSystem.UiTz = zone.Id;
|
||||
await Systems.Save(Context.SenderSystem);
|
||||
if (!await ctx.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled;
|
||||
ctx.System.UiTz = zone.Id;
|
||||
await _systems.Save(ctx.System);
|
||||
|
||||
await Context.Channel.SendMessageAsync($"System time zone changed to {zone.Id}.");
|
||||
await ctx.Reply($"System time zone changed to {zone.Id}.");
|
||||
}
|
||||
|
||||
public async Task<DateTimeZone> FindTimeZone(string zoneStr) {
|
||||
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
|
||||
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||
|
||||
@ -322,7 +287,7 @@ namespace PluralKit.Bot.Commands
|
||||
return matchingZones.First();
|
||||
|
||||
// Otherwise, prompt and return!
|
||||
return await Context.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
|
||||
return await ctx.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
|
||||
z =>
|
||||
{
|
||||
if (TzdbDateTimeZoneSource.Default.Aliases.Contains(z.Id))
|
||||
@ -331,11 +296,5 @@ namespace PluralKit.Bot.Commands
|
||||
return $"**{z.Id}**";
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<PKSystem> ReadContextParameterAsync(string value)
|
||||
{
|
||||
var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services);
|
||||
return res.IsSuccess ? res.BestMatch as PKSystem : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,15 +6,17 @@ using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
|
||||
using PluralKit.Bot.CommandSystem;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
public static class ContextUtils {
|
||||
public static async Task<bool> PromptYesNo(this ICommandContext ctx, IUserMessage message, IUser user = null, TimeSpan? timeout = null) {
|
||||
public static async Task<bool> PromptYesNo(this Context ctx, IUserMessage message, IUser user = null, TimeSpan? timeout = null) {
|
||||
await message.AddReactionsAsync(new[] {new Emoji(Emojis.Success), new Emoji(Emojis.Error)});
|
||||
var reaction = await ctx.AwaitReaction(message, user ?? ctx.User, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout ?? TimeSpan.FromMinutes(1));
|
||||
var reaction = await ctx.AwaitReaction(message, user ?? ctx.Author, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout ?? TimeSpan.FromMinutes(1));
|
||||
return reaction.Emote.Name == Emojis.Success;
|
||||
}
|
||||
|
||||
public static async Task<SocketReaction> AwaitReaction(this ICommandContext ctx, IUserMessage message, IUser user = null, Func<SocketReaction, bool> predicate = null, TimeSpan? timeout = null) {
|
||||
public static async Task<SocketReaction> AwaitReaction(this Context ctx, IUserMessage message, IUser user = null, Func<SocketReaction, bool> predicate = null, TimeSpan? timeout = null) {
|
||||
var tcs = new TaskCompletionSource<SocketReaction>();
|
||||
Task Inner(Cacheable<IUserMessage, ulong> _message, ISocketMessageChannel _channel, SocketReaction reaction) {
|
||||
if (message.Id != _message.Id) return Task.CompletedTask; // Ignore reactions for different messages
|
||||
@ -24,38 +26,38 @@ namespace PluralKit.Bot {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
(ctx.Client as BaseSocketClient).ReactionAdded += Inner;
|
||||
((BaseSocketClient) ctx.Shard).ReactionAdded += Inner;
|
||||
try {
|
||||
return await (tcs.Task.TimeoutAfter(timeout));
|
||||
} finally {
|
||||
(ctx.Client as BaseSocketClient).ReactionAdded -= Inner;
|
||||
((BaseSocketClient) ctx.Shard).ReactionAdded -= Inner;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IUserMessage> AwaitMessage(this ICommandContext ctx, IMessageChannel channel, IUser user = null, Func<SocketMessage, bool> predicate = null, TimeSpan? timeout = null) {
|
||||
public static async Task<IUserMessage> AwaitMessage(this Context ctx, IMessageChannel channel, IUser user = null, Func<SocketMessage, bool> predicate = null, TimeSpan? timeout = null) {
|
||||
var tcs = new TaskCompletionSource<IUserMessage>();
|
||||
Task Inner(SocketMessage msg) {
|
||||
if (channel != msg.Channel) return Task.CompletedTask; // Ignore messages in a different channel
|
||||
if (user != null && user != msg.Author) return Task.CompletedTask; // Ignore messages from other users
|
||||
if (predicate != null && !predicate.Invoke(msg)) return Task.CompletedTask; // Check predicate
|
||||
|
||||
(ctx.Client as BaseSocketClient).MessageReceived -= Inner;
|
||||
((BaseSocketClient) ctx.Shard).MessageReceived -= Inner;
|
||||
tcs.SetResult(msg as IUserMessage);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
(ctx.Client as BaseSocketClient).MessageReceived += Inner;
|
||||
((BaseSocketClient) ctx.Shard).MessageReceived += Inner;
|
||||
return await (tcs.Task.TimeoutAfter(timeout));
|
||||
}
|
||||
|
||||
public static async Task<bool> ConfirmWithReply(this ICommandContext ctx, string expectedReply)
|
||||
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply)
|
||||
{
|
||||
var msg = await ctx.AwaitMessage(ctx.Channel, ctx.User, timeout: TimeSpan.FromMinutes(1));
|
||||
var msg = await ctx.AwaitMessage(ctx.Channel, ctx.Author, timeout: TimeSpan.FromMinutes(1));
|
||||
return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static async Task Paginate<T>(this ICommandContext ctx, ICollection<T> items, int itemsPerPage, string title, Action<EmbedBuilder, IEnumerable<T>> renderer) {
|
||||
public static async Task Paginate<T>(this Context ctx, ICollection<T> items, int itemsPerPage, string title, Action<EmbedBuilder, IEnumerable<T>> renderer) {
|
||||
// TODO: make this generic enough we can use it in Choose<T> below
|
||||
|
||||
var pageCount = (items.Count / itemsPerPage) + 1;
|
||||
@ -74,7 +76,7 @@ namespace PluralKit.Bot {
|
||||
try {
|
||||
var currentPage = 0;
|
||||
while (true) {
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.User, timeout: TimeSpan.FromMinutes(5));
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.Author, timeout: TimeSpan.FromMinutes(5));
|
||||
|
||||
// Increment/decrement page counter based on which reaction was clicked
|
||||
if (reaction.Emote.Name == "\u23EA") currentPage = 0; // <<
|
||||
@ -97,10 +99,10 @@ namespace PluralKit.Bot {
|
||||
}
|
||||
|
||||
if (await ctx.HasPermission(ChannelPermission.ManageMessages)) await msg.RemoveAllReactionsAsync();
|
||||
else await msg.RemoveReactionsAsync(ctx.Client.CurrentUser, botEmojis);
|
||||
else await msg.RemoveReactionsAsync(ctx.Shard.CurrentUser, botEmojis);
|
||||
}
|
||||
|
||||
public static async Task<T> Choose<T>(this ICommandContext ctx, string description, IList<T> items, Func<T, string> display = null)
|
||||
public static async Task<T> Choose<T>(this Context ctx, string description, IList<T> items, Func<T, string> display = null)
|
||||
{
|
||||
// Generate a list of :regional_indicator_?: emoji surrogate pairs (starting at codepoint 0x1F1E6)
|
||||
// We just do 7 (ABCDEFG), this amount is arbitrary (although sending a lot of emojis takes a while)
|
||||
@ -143,7 +145,7 @@ namespace PluralKit.Bot {
|
||||
while (true)
|
||||
{
|
||||
// Wait for a reaction
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.User);
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.Author);
|
||||
|
||||
// If it's a movement reaction, inc/dec the page index
|
||||
if (reaction.Emote.Name == "\u2B05") currPage -= 1; // <
|
||||
@ -160,7 +162,7 @@ namespace PluralKit.Bot {
|
||||
if (idx < items.Count) return items[idx];
|
||||
}
|
||||
|
||||
var __ = msg.RemoveReactionAsync(reaction.Emote, ctx.User); // don't care about awaiting
|
||||
var __ = msg.RemoveReactionAsync(reaction.Emote, ctx.Author); // don't care about awaiting
|
||||
await msg.ModifyAsync(mp => mp.Content = $"**[Page {currPage + 1}/{pageCount}]**\n{description}\n{MakeOptionList(currPage)}");
|
||||
}
|
||||
}
|
||||
@ -177,12 +179,12 @@ namespace PluralKit.Bot {
|
||||
var _ = AddEmojis();
|
||||
|
||||
// Then wait for a reaction and return whichever one we found
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.User,rx => indicators.Contains(rx.Emote.Name));
|
||||
var reaction = await ctx.AwaitReaction(msg, ctx.Author,rx => indicators.Contains(rx.Emote.Name));
|
||||
return items[Array.IndexOf(indicators, reaction.Emote.Name)];
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ChannelPermissions> Permissions(this ICommandContext ctx) {
|
||||
public static async Task<ChannelPermissions> Permissions(this Context ctx) {
|
||||
if (ctx.Channel is IGuildChannel) {
|
||||
var gu = await ctx.Guild.GetCurrentUserAsync();
|
||||
return gu.GetPermissions(ctx.Channel as IGuildChannel);
|
||||
@ -190,9 +192,9 @@ namespace PluralKit.Bot {
|
||||
return ChannelPermissions.DM;
|
||||
}
|
||||
|
||||
public static async Task<bool> HasPermission(this ICommandContext ctx, ChannelPermission permission) => (await Permissions(ctx)).Has(permission);
|
||||
public static async Task<bool> HasPermission(this Context ctx, ChannelPermission permission) => (await Permissions(ctx)).Has(permission);
|
||||
|
||||
public static async Task BusyIndicator(this ICommandContext ctx, Func<Task> f, string emoji = "\u23f3" /* hourglass */)
|
||||
public static async Task BusyIndicator(this Context ctx, Func<Task> f, string emoji = "\u23f3" /* hourglass */)
|
||||
{
|
||||
await ctx.BusyIndicator<object>(async () =>
|
||||
{
|
||||
@ -201,7 +203,7 @@ namespace PluralKit.Bot {
|
||||
}, emoji);
|
||||
}
|
||||
|
||||
public static async Task<T> BusyIndicator<T>(this ICommandContext ctx, Func<Task<T>> f, string emoji = "\u23f3" /* hourglass */)
|
||||
public static async Task<T> BusyIndicator<T>(this Context ctx, Func<Task<T>> f, string emoji = "\u23f3" /* hourglass */)
|
||||
{
|
||||
var task = f();
|
||||
|
||||
@ -215,7 +217,7 @@ namespace PluralKit.Bot {
|
||||
}
|
||||
finally
|
||||
{
|
||||
var _ = ctx.Message.RemoveReactionAsync(new Emoji(emoji), ctx.Client.CurrentUser);
|
||||
var _ = ctx.Message.RemoveReactionAsync(new Emoji(emoji), ctx.Shard.CurrentUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using PluralKit.Core;
|
||||
namespace PluralKit.Bot {
|
||||
public static class Errors {
|
||||
// TODO: is returning constructed errors and throwing them at call site a good idea, or should these be methods that insta-throw instead?
|
||||
// or should we just like... go back to inlining them? at least for the one-time-use commands
|
||||
|
||||
public static PKError NotOwnSystemError => new PKError($"You can only run this command on your own system.");
|
||||
public static PKError NotOwnMemberError => new PKError($"You can only run this command on your own member.");
|
||||
@ -38,7 +39,7 @@ namespace PluralKit.Bot {
|
||||
public static PKError AccountAlreadyLinked => new PKError("That account is already linked to your system.");
|
||||
public static PKError AccountNotLinked => new PKError("That account isn't linked to your system.");
|
||||
public static PKError AccountInOtherSystem(PKSystem system) => new PKError($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`).");
|
||||
public static PKError UnlinkingLastAccount => new PKError("Since this is the only account linked to this system, you cannot unlink it (as that would leave your system account-less).");
|
||||
public static PKError UnlinkingLastAccount => new PKError("Since this is the only account linked to this system, you cannot unlink it (as that would leave your system account-less). If you would like to delete your system, use `pk;system delete`.");
|
||||
public static PKError MemberLinkCancelled => new PKError("Member link cancelled.");
|
||||
public static PKError MemberUnlinkCancelled => new PKError("Member unlink cancelled.");
|
||||
|
||||
|
@ -10,11 +10,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net.Commands" Version="2.1.1" />
|
||||
<PackageReference Include="Discord.Net.Webhook" Version="2.1.1" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="2.1.1" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry" Version="2.0.0-beta2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0006" />
|
||||
</ItemGroup>
|
||||
|
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace PluralKit.Bot {
|
||||
class MustHaveSystem : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||
{
|
||||
var c = context as PKCommandContext;
|
||||
if (c == null) return Task.FromResult(PreconditionResult.FromError("Must be called on a PKCommandContext (should never happen!)")) ;
|
||||
if (c.SenderSystem == null) return Task.FromResult(PreconditionResult.FromError(Errors.NoSystemError));
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
}
|
||||
}
|
||||
|
||||
class MustPassOwnMember : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||
{
|
||||
// OK when:
|
||||
// - Sender has a system
|
||||
// - Sender passes a member as a context parameter
|
||||
// - Sender owns said member
|
||||
|
||||
var c = context as PKCommandContext;
|
||||
if (c.SenderSystem == null) return Task.FromResult(PreconditionResult.FromError(Errors.NoSystemError));
|
||||
if (c.GetContextEntity<PKMember>() == null) return Task.FromResult(PreconditionResult.FromError(Errors.MissingMemberError));
|
||||
if (c.GetContextEntity<PKMember>().System != c.SenderSystem.Id) return Task.FromResult(PreconditionResult.FromError(Errors.NotOwnMemberError));
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ namespace PluralKit.Bot
|
||||
// eg. @Ske [text] => [@Ske text]
|
||||
int matchStartPosition = 0;
|
||||
string leadingMention = null;
|
||||
if (Utils.HasMentionPrefix(message, ref matchStartPosition))
|
||||
if (Utils.HasMentionPrefix(message, ref matchStartPosition, out _))
|
||||
{
|
||||
leadingMention = message.Substring(0, matchStartPosition);
|
||||
message = message.Substring(matchStartPosition);
|
||||
|
@ -5,10 +5,7 @@ using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.Commands.Builders;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using PluralKit.Core;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
@ -68,18 +65,27 @@ namespace PluralKit.Bot
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasMentionPrefix(string content, ref int argPos)
|
||||
public static bool HasMentionPrefix(string content, ref int argPos, out ulong mentionId)
|
||||
{
|
||||
mentionId = 0;
|
||||
|
||||
// Roughly ported from Discord.Commands.MessageExtensions.HasMentionPrefix
|
||||
if (string.IsNullOrEmpty(content) || content.Length <= 3 || (content[0] != '<' || content[1] != '@'))
|
||||
return false;
|
||||
int num = content.IndexOf('>');
|
||||
if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !MentionUtils.TryParseUser(content.Substring(0, num + 1), out _))
|
||||
if (num == -1 || content.Length < num + 2 || content[num + 1] != ' ' || !MentionUtils.TryParseUser(content.Substring(0, num + 1), out mentionId))
|
||||
return false;
|
||||
argPos = num + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseMention(this string potentialMention, out ulong id)
|
||||
{
|
||||
if (ulong.TryParse(potentialMention, out id)) return true;
|
||||
if (MentionUtils.TryParseUser(potentialMention, out id)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string Sanitize(this string input) =>
|
||||
Regex.Replace(Regex.Replace(input, "<@[!&]?(\\d{17,19})>", "<\\@$1>"), "@(everyone|here)", "@\u200B$1");
|
||||
|
||||
@ -113,136 +119,9 @@ namespace PluralKit.Bot
|
||||
(await PermissionsIn(channel)).Has(permission);
|
||||
}
|
||||
|
||||
class PKSystemTypeReader : TypeReader
|
||||
{
|
||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||
{
|
||||
var client = services.GetService<IDiscordClient>();
|
||||
var systems = services.GetService<SystemStore>();
|
||||
|
||||
// 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>)
|
||||
// - A system hid
|
||||
|
||||
// First, try direct user ID parsing
|
||||
if (ulong.TryParse(input, out var idFromNumber)) return await FindSystemByAccountHelper(idFromNumber, client, systems);
|
||||
|
||||
// Then, try mention parsing.
|
||||
if (MentionUtils.TryParseUser(input, out var idFromMention)) return await FindSystemByAccountHelper(idFromMention, client, systems);
|
||||
|
||||
// Finally, try HID parsing
|
||||
var res = await systems.GetByHid(input);
|
||||
if (res != null) return TypeReaderResult.FromSuccess(res);
|
||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, $"System with ID `{input}` not found.");
|
||||
}
|
||||
|
||||
async Task<TypeReaderResult> FindSystemByAccountHelper(ulong id, IDiscordClient client, SystemStore systems)
|
||||
{
|
||||
var foundByAccountId = await systems.GetByAccount(id);
|
||||
if (foundByAccountId != null) return TypeReaderResult.FromSuccess(foundByAccountId);
|
||||
|
||||
// We didn't find any, so we try to resolve the user ID to find the associated account,
|
||||
// so we can print their username.
|
||||
var user = await client.GetUserAsync(id);
|
||||
|
||||
// Return descriptive errors based on whether we found the user or not.
|
||||
if (user == null) return TypeReaderResult.FromError(CommandError.ObjectNotFound, $"System or account with ID `{id}` not found.");
|
||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, $"Account **{user.Username}#{user.Discriminator}** not found.");
|
||||
}
|
||||
}
|
||||
|
||||
class PKMemberTypeReader : TypeReader
|
||||
{
|
||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||
{
|
||||
var members = services.GetRequiredService<MemberStore>();
|
||||
|
||||
// If the sender of the command is in a system themselves,
|
||||
// then try searching by the member's name
|
||||
if (context is PKCommandContext ctx && ctx.SenderSystem != null)
|
||||
{
|
||||
var foundByName = await members.GetByName(ctx.SenderSystem, input);
|
||||
if (foundByName != null) return TypeReaderResult.FromSuccess(foundByName);
|
||||
}
|
||||
|
||||
// Otherwise, if sender isn't in a system, or no member found by that name,
|
||||
// do a standard by-hid search.
|
||||
var foundByHid = await members.GetByHid(input);
|
||||
if (foundByHid != null) return TypeReaderResult.FromSuccess(foundByHid);
|
||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, $"Member with ID `{input}` not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Subclass of ICommandContext with PK-specific additional fields and functionality
|
||||
public class PKCommandContext : ShardedCommandContext
|
||||
{
|
||||
public PKSystem SenderSystem { get; }
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
|
||||
private object _entity;
|
||||
|
||||
public PKCommandContext(DiscordShardedClient client, SocketUserMessage msg, PKSystem system, IServiceProvider serviceProvider) : base(client, msg)
|
||||
{
|
||||
SenderSystem = system;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public T GetContextEntity<T>() where T: class {
|
||||
return _entity as T;
|
||||
}
|
||||
|
||||
public void SetContextEntity(object entity) {
|
||||
_entity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ContextParameterModuleBase<T> : ModuleBase<PKCommandContext> where T: class
|
||||
{
|
||||
public IServiceProvider _services { get; set; }
|
||||
public CommandService _commands { get; set; }
|
||||
|
||||
public abstract string Prefix { get; }
|
||||
public abstract string ContextNoun { get; }
|
||||
public abstract Task<T> ReadContextParameterAsync(string value);
|
||||
|
||||
public T ContextEntity => Context.GetContextEntity<T>();
|
||||
|
||||
protected override void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) {
|
||||
// We create a catch-all command that intercepts the first argument, tries to parse it as
|
||||
// the context parameter, then runs the command service AGAIN with that given in a wrapped
|
||||
// context, with the context argument removed so it delegates to the subcommand executor
|
||||
builder.AddCommand("", async (ctx, param, services, info) => {
|
||||
var pkCtx = ctx as PKCommandContext;
|
||||
pkCtx.SetContextEntity(param[0] as T);
|
||||
|
||||
await commandService.ExecuteAsync(pkCtx, Prefix + " " + param[1] as string, services);
|
||||
}, (cb) => {
|
||||
cb.WithPriority(-9999);
|
||||
cb.AddPrecondition(new MustNotHaveContextPrecondition());
|
||||
cb.AddParameter<T>("contextValue", (pb) => pb.WithDefault(""));
|
||||
cb.AddParameter<string>("rest", (pb) => pb.WithDefault("").WithIsRemainder(true));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class MustNotHaveContextPrecondition : PreconditionAttribute
|
||||
{
|
||||
public MustNotHaveContextPrecondition()
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||
{
|
||||
// This stops the "delegating command" we define above from being called multiple times
|
||||
// If we've already added a context object to the context, then we'll return with the same
|
||||
// error you get when there's an invalid command - it's like it didn't exist
|
||||
// This makes sure the user gets the proper error, instead of the command trying to parse things weirdly
|
||||
if ((context as PKCommandContext)?.GetContextEntity<object>() == null) return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
return Task.FromResult(PreconditionResult.FromError(command.Module.Service.Search("<unknown>")));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception class representing user-facing errors caused when parsing and executing commands.
|
||||
/// </summary>
|
||||
public class PKError : Exception
|
||||
{
|
||||
public PKError(string message) : base(message)
|
||||
@ -250,6 +129,10 @@ namespace PluralKit.Bot
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subclass of <see cref="PKError"/> that represent command syntax errors, meaning they'll have their command
|
||||
/// usages printed in the message.
|
||||
/// </summary>
|
||||
public class PKSyntaxError : PKError
|
||||
{
|
||||
public PKSyntaxError(string message) : base(message)
|
||||
|
Loading…
x
Reference in New Issue
Block a user