bot: split bot namespace, add system card, fix command handling
This commit is contained in:
parent
c36cee6f28
commit
62cde789cb
@ -15,7 +15,7 @@ using Npgsql.TypeHandling;
|
|||||||
using Npgsql.TypeMapping;
|
using Npgsql.TypeMapping;
|
||||||
using NpgsqlTypes;
|
using NpgsqlTypes;
|
||||||
|
|
||||||
namespace PluralKit
|
namespace PluralKit.Bot
|
||||||
{
|
{
|
||||||
class Initialize
|
class Initialize
|
||||||
{
|
{
|
||||||
@ -58,6 +58,7 @@ namespace PluralKit
|
|||||||
.AddSingleton<Bot>()
|
.AddSingleton<Bot>()
|
||||||
|
|
||||||
.AddSingleton<CommandService>()
|
.AddSingleton<CommandService>()
|
||||||
|
.AddSingleton<EmbedService>()
|
||||||
.AddSingleton<LogChannelService>()
|
.AddSingleton<LogChannelService>()
|
||||||
.AddSingleton<ProxyService>()
|
.AddSingleton<ProxyService>()
|
||||||
|
|
||||||
@ -67,7 +68,6 @@ namespace PluralKit
|
|||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Bot
|
class Bot
|
||||||
{
|
{
|
||||||
private IServiceProvider _services;
|
private IServiceProvider _services;
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Discord.Commands;
|
using Discord.Commands;
|
||||||
|
|
||||||
namespace PluralKit.Commands
|
namespace PluralKit.Bot.Commands
|
||||||
{
|
{
|
||||||
[Group("system")]
|
[Group("system")]
|
||||||
public class SystemCommands : ContextParameterModuleBase<PKSystem>
|
public class SystemCommands : ContextParameterModuleBase<PKSystem>
|
||||||
@ -11,24 +11,36 @@ namespace PluralKit.Commands
|
|||||||
public override string Prefix => "system";
|
public override string Prefix => "system";
|
||||||
public SystemStore Systems {get; set;}
|
public SystemStore Systems {get; set;}
|
||||||
public MemberStore Members {get; set;}
|
public MemberStore Members {get; set;}
|
||||||
|
public EmbedService EmbedService {get; set;}
|
||||||
|
|
||||||
private RuntimeResult NO_SYSTEM_ERROR => PKResult.Error($"You do not have a system registered with PluralKit. To create one, type `pk;system new`. If you already have a system registered on another account, type `pk;link {Context.User.Mention}` from that account to link it here.");
|
private RuntimeResult NO_SYSTEM_ERROR => PKResult.Error($"You do not have a system registered with PluralKit. To create one, type `pk;system new`. If you already have a system registered on another account, type `pk;link {Context.User.Mention}` from that account to link it here.");
|
||||||
private RuntimeResult OTHER_SYSTEM_CONTEXT_ERROR => PKResult.Error("You can only run this command on your own system.");
|
private RuntimeResult OTHER_SYSTEM_CONTEXT_ERROR => PKResult.Error("You can only run this command on your own system.");
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public async Task<RuntimeResult> Query(PKSystem system = null) {
|
||||||
|
if (system == null) system = Context.SenderSystem;
|
||||||
|
if (system == null) return NO_SYSTEM_ERROR;
|
||||||
|
|
||||||
|
await Context.Channel.SendMessageAsync(embed: await EmbedService.CreateEmbed(system));
|
||||||
|
return PKResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
[Command("new")]
|
[Command("new")]
|
||||||
public async Task<RuntimeResult> New([Remainder] string systemName = null)
|
public async Task<RuntimeResult> New([Remainder] string systemName = null)
|
||||||
{
|
{
|
||||||
if (Context.ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
||||||
if (Context.SenderSystem != null) return PKResult.Error("You already have a system registered with PluralKit. To view it, type `pk;system`. If you'd like to delete your system and start anew, type `pk;system delete`, or if you'd like to unlink this account from it, type `pk;unlink.");
|
if (Context.SenderSystem != null) return PKResult.Error("You already have a system registered with PluralKit. To view it, type `pk;system`. If you'd like to delete your system and start anew, type `pk;system delete`, or if you'd like to unlink this account from it, type `pk;unlink.");
|
||||||
|
|
||||||
var system = await Systems.Create(systemName);
|
var system = await Systems.Create(systemName);
|
||||||
|
await Systems.Link(system, Context.User.Id);
|
||||||
|
|
||||||
await ReplyAsync("Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
await ReplyAsync("Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now.");
|
||||||
return PKResult.Success();
|
return PKResult.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("name")]
|
[Command("name")]
|
||||||
public async Task<RuntimeResult> Name([Remainder] string newSystemName = null) {
|
public async Task<RuntimeResult> Name([Remainder] string newSystemName = null) {
|
||||||
if (Context.ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
||||||
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
||||||
if (newSystemName != null && newSystemName.Length > 250) return PKResult.Error($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)");
|
if (newSystemName != null && newSystemName.Length > 250) return PKResult.Error($"Your chosen system name is too long. ({newSystemName.Length} > 250 characters)");
|
||||||
|
|
||||||
@ -39,7 +51,7 @@ namespace PluralKit.Commands
|
|||||||
|
|
||||||
[Command("description")]
|
[Command("description")]
|
||||||
public async Task<RuntimeResult> Description([Remainder] string newDescription = null) {
|
public async Task<RuntimeResult> Description([Remainder] string newDescription = null) {
|
||||||
if (Context.ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
||||||
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
||||||
if (newDescription != null && newDescription.Length > 1000) return PKResult.Error($"Your chosen description is too long. ({newDescription.Length} > 250 characters)");
|
if (newDescription != null && newDescription.Length > 1000) return PKResult.Error($"Your chosen description is too long. ({newDescription.Length} > 250 characters)");
|
||||||
|
|
||||||
@ -50,7 +62,7 @@ namespace PluralKit.Commands
|
|||||||
|
|
||||||
[Command("tag")]
|
[Command("tag")]
|
||||||
public async Task<RuntimeResult> Tag([Remainder] string newTag = null) {
|
public async Task<RuntimeResult> Tag([Remainder] string newTag = null) {
|
||||||
if (Context.ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
if (ContextEntity != null) return OTHER_SYSTEM_CONTEXT_ERROR;
|
||||||
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
if (Context.SenderSystem == null) return NO_SYSTEM_ERROR;
|
||||||
|
|
||||||
Context.SenderSystem.Tag = newTag;
|
Context.SenderSystem.Tag = newTag;
|
35
PluralKit/Bot/Services/EmbedService.cs
Normal file
35
PluralKit/Bot/Services/EmbedService.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace PluralKit.Bot {
|
||||||
|
public class EmbedService {
|
||||||
|
private SystemStore _systems;
|
||||||
|
private IDiscordClient _client;
|
||||||
|
|
||||||
|
public EmbedService(SystemStore systems, IDiscordClient client)
|
||||||
|
{
|
||||||
|
this._systems = systems;
|
||||||
|
this._client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Embed> CreateEmbed(PKSystem system) {
|
||||||
|
var accounts = await _systems.GetLinkedAccountIds(system);
|
||||||
|
|
||||||
|
// Fetch/render info for all accounts simultaneously
|
||||||
|
var users = await Task.WhenAll(accounts.Select(async uid => (await _client.GetUserAsync(uid)).NameAndMention() ?? $"(deleted account {uid})"));
|
||||||
|
|
||||||
|
var eb = new EmbedBuilder()
|
||||||
|
.WithColor(Color.Blue)
|
||||||
|
.WithTitle(system.Name ?? null)
|
||||||
|
.WithDescription(system.Description?.Truncate(1024))
|
||||||
|
.WithThumbnailUrl(system.AvatarUrl ?? null)
|
||||||
|
.WithFooter($"System ID: {system.Hid}");
|
||||||
|
|
||||||
|
eb.AddField("Linked accounts", string.Join(", ", users));
|
||||||
|
eb.AddField("Members", $"(see `pk;system {system.Id} list` or `pk;system {system.Hid} list full`)");
|
||||||
|
// TODO: fronter
|
||||||
|
return eb.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Discord;
|
using Discord;
|
||||||
|
|
||||||
namespace PluralKit {
|
namespace PluralKit.Bot {
|
||||||
class ServerDefinition {
|
class ServerDefinition {
|
||||||
public ulong Id;
|
public ulong Id;
|
||||||
public ulong LogChannel;
|
public ulong LogChannel;
|
@ -11,7 +11,7 @@ using Discord.Rest;
|
|||||||
using Discord.Webhook;
|
using Discord.Webhook;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace PluralKit
|
namespace PluralKit.Bot
|
||||||
{
|
{
|
||||||
class ProxyDatabaseResult
|
class ProxyDatabaseResult
|
||||||
{
|
{
|
171
PluralKit/Bot/Utils.cs
Normal file
171
PluralKit/Bot/Utils.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Commands;
|
||||||
|
using Discord.Commands.Builders;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace PluralKit.Bot
|
||||||
|
{
|
||||||
|
public static class Utils {
|
||||||
|
public static string NameAndMention(this IUser user) {
|
||||||
|
return $"{user.Username}#{user.Discriminator} ({user.Mention})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UlongEncodeAsLongHandler : SqlMapper.TypeHandler<ulong>
|
||||||
|
{
|
||||||
|
public override ulong Parse(object value)
|
||||||
|
{
|
||||||
|
// Cast to long to unbox, then to ulong (???)
|
||||||
|
return (ulong)(long)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetValue(IDbDataParameter parameter, ulong value)
|
||||||
|
{
|
||||||
|
parameter.Value = (long)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PKSystemTypeReader : TypeReader
|
||||||
|
{
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var client = services.GetService<IDiscordClient>();
|
||||||
|
var conn = services.GetService<IDbConnection>();
|
||||||
|
|
||||||
|
// 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, conn);
|
||||||
|
|
||||||
|
// Then, try mention parsing.
|
||||||
|
if (MentionUtils.TryParseUser(input, out var idFromMention)) return await FindSystemByAccountHelper(idFromMention, client, conn);
|
||||||
|
|
||||||
|
// Finally, try HID parsing
|
||||||
|
var res = await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where hid = @Hid", new { Hid = 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, IDbConnection conn)
|
||||||
|
{
|
||||||
|
var foundByAccountId = await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from accounts, systems where accounts.system = system.id and accounts.id = @Id", new { Id = 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 conn = services.GetService(typeof(IDbConnection)) as IDbConnection;
|
||||||
|
|
||||||
|
// 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 conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where system = @System and lower(name) = lower(@Name)", new { System = ctx.SenderSystem.Id, Name = 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 conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where hid = @Hid", new { Hid = input });
|
||||||
|
if (foundByHid != null) return TypeReaderResult.FromSuccess(foundByHid);
|
||||||
|
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Member not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subclass of ICommandContext with PK-specific additional fields and functionality
|
||||||
|
public class PKCommandContext : SocketCommandContext, ICommandContext
|
||||||
|
{
|
||||||
|
public IDbConnection Connection { get; }
|
||||||
|
public PKSystem SenderSystem { get; }
|
||||||
|
|
||||||
|
private object _entity;
|
||||||
|
|
||||||
|
public PKCommandContext(DiscordSocketClient client, SocketUserMessage msg, IDbConnection connection, PKSystem system) : base(client, msg)
|
||||||
|
{
|
||||||
|
Connection = connection;
|
||||||
|
SenderSystem = system;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
var res = await ReadContextParameterAsync(param[0] as string);
|
||||||
|
pkCtx.SetContextEntity(res);
|
||||||
|
|
||||||
|
await commandService.ExecuteAsync(pkCtx, Prefix + " " + param[1] as string, services);
|
||||||
|
}, (cb) => {
|
||||||
|
cb.WithPriority(-9999);
|
||||||
|
cb.AddPrecondition(new ContextParameterFallbackPreconditionAttribute());
|
||||||
|
cb.AddParameter<string>("contextValue", (pb) => pb.WithDefault(""));
|
||||||
|
cb.AddParameter<string>("rest", (pb) => pb.WithDefault("").WithIsRemainder(true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContextParameterFallbackPreconditionAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
public ContextParameterFallbackPreconditionAttribute()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (context.GetType().Name != "ContextualContext`1") {
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
} else {
|
||||||
|
return PreconditionResult.FromError("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PKResult : RuntimeResult
|
||||||
|
{
|
||||||
|
public PKResult(CommandError? error, string reason) : base(error, reason)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RuntimeResult Error(string reason) => new PKResult(CommandError.Unsuccessful, reason);
|
||||||
|
public static RuntimeResult Success(string reason = null) => new PKResult(null, reason);
|
||||||
|
}
|
||||||
|
}
|
@ -16,10 +16,14 @@ namespace PluralKit {
|
|||||||
|
|
||||||
public async Task<PKSystem> Create(string systemName = null) {
|
public async Task<PKSystem> Create(string systemName = null) {
|
||||||
// TODO: handle HID collision case
|
// TODO: handle HID collision case
|
||||||
var hid = HidUtils.GenerateHid();
|
var hid = Utils.GenerateHid();
|
||||||
return await conn.QuerySingleAsync<PKSystem>("insert into systems (hid, name) values (@Hid, @Name) returning *", new { Hid = hid, Name = systemName });
|
return await conn.QuerySingleAsync<PKSystem>("insert into systems (hid, name) values (@Hid, @Name) returning *", new { Hid = hid, Name = systemName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Link(PKSystem system, ulong accountId) {
|
||||||
|
await conn.ExecuteAsync("insert into accounts (uid, system) values (@Id, @SystemId)", new { Id = accountId, SystemId = system.Id });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<PKSystem> GetByAccount(ulong accountId) {
|
public async Task<PKSystem> GetByAccount(ulong accountId) {
|
||||||
return await conn.QuerySingleAsync<PKSystem>("select systems.* from systems, accounts where accounts.system = system.id and accounts.uid = @Id", new { Id = accountId });
|
return await conn.QuerySingleAsync<PKSystem>("select systems.* from systems, accounts where accounts.system = system.id and accounts.uid = @Id", new { Id = accountId });
|
||||||
}
|
}
|
||||||
@ -39,6 +43,11 @@ namespace PluralKit {
|
|||||||
public async Task Delete(PKSystem system) {
|
public async Task Delete(PKSystem system) {
|
||||||
await conn.DeleteAsync(system);
|
await conn.DeleteAsync(system);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ulong>> GetLinkedAccountIds(PKSystem system)
|
||||||
|
{
|
||||||
|
return await conn.QueryAsync<ulong>("select uid from accounts where system = @Id", new { Id = system.Id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MemberStore {
|
public class MemberStore {
|
||||||
@ -50,7 +59,7 @@ namespace PluralKit {
|
|||||||
|
|
||||||
public async Task<PKMember> Create(PKSystem system, string name) {
|
public async Task<PKMember> Create(PKSystem system, string name) {
|
||||||
// TODO: handle collision
|
// TODO: handle collision
|
||||||
var hid = HidUtils.GenerateHid();
|
var hid = Utils.GenerateHid();
|
||||||
return await conn.QuerySingleAsync("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new {
|
return await conn.QuerySingleAsync("insert into members (hid, system, name) values (@Hid, @SystemId, @Name) returning *", new {
|
||||||
Hid = hid,
|
Hid = hid,
|
||||||
SystemID = system.Id,
|
SystemID = system.Id,
|
||||||
|
@ -10,146 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace PluralKit
|
namespace PluralKit
|
||||||
{
|
{
|
||||||
class UlongEncodeAsLongHandler : SqlMapper.TypeHandler<ulong>
|
public static class Utils
|
||||||
{
|
|
||||||
public override ulong Parse(object value)
|
|
||||||
{
|
|
||||||
// Cast to long to unbox, then to ulong (???)
|
|
||||||
return (ulong)(long)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetValue(IDbDataParameter parameter, ulong value)
|
|
||||||
{
|
|
||||||
parameter.Value = (long)value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PKSystemTypeReader : TypeReader
|
|
||||||
{
|
|
||||||
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
|
||||||
{
|
|
||||||
var client = services.GetService<IDiscordClient>();
|
|
||||||
var conn = services.GetService<IDbConnection>();
|
|
||||||
|
|
||||||
// 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, conn);
|
|
||||||
|
|
||||||
// Then, try mention parsing.
|
|
||||||
if (MentionUtils.TryParseUser(input, out var idFromMention)) return await FindSystemByAccountHelper(idFromMention, client, conn);
|
|
||||||
|
|
||||||
// Finally, try HID parsing
|
|
||||||
var res = await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from systems where hid = @Hid", new { Hid = 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, IDbConnection conn)
|
|
||||||
{
|
|
||||||
var foundByAccountId = await conn.QuerySingleOrDefaultAsync<PKSystem>("select * from accounts, systems where accounts.system = system.id and accounts.id = @Id", new { Id = 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 conn = services.GetService(typeof(IDbConnection)) as IDbConnection;
|
|
||||||
|
|
||||||
// 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 conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where system = @System and lower(name) = lower(@Name)", new { System = ctx.SenderSystem.Id, Name = 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 conn.QuerySingleOrDefaultAsync<PKMember>("select * from members where hid = @Hid", new { Hid = input });
|
|
||||||
if (foundByHid != null) return TypeReaderResult.FromSuccess(foundByHid);
|
|
||||||
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Member not found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subclass of ICommandContext with PK-specific additional fields and functionality
|
|
||||||
public class PKCommandContext : SocketCommandContext, ICommandContext
|
|
||||||
{
|
|
||||||
public IDbConnection Connection { get; }
|
|
||||||
public PKSystem SenderSystem { get; }
|
|
||||||
|
|
||||||
public PKCommandContext(DiscordSocketClient client, SocketUserMessage msg, IDbConnection connection, PKSystem system) : base(client, msg)
|
|
||||||
{
|
|
||||||
Connection = connection;
|
|
||||||
SenderSystem = system;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ContextualContext<T> : PKCommandContext
|
|
||||||
{
|
|
||||||
public T ContextEntity { get; internal set; }
|
|
||||||
|
|
||||||
public ContextualContext(PKCommandContext ctx, T contextEntity): base(ctx.Client, ctx.Message, ctx.Connection, ctx.SenderSystem)
|
|
||||||
{
|
|
||||||
this.ContextEntity = contextEntity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class ContextParameterModuleBase<T> : ModuleBase<ContextualContext<T>>
|
|
||||||
{
|
|
||||||
public IServiceProvider _services { get; set; }
|
|
||||||
public CommandService _commands { get; set; }
|
|
||||||
|
|
||||||
public abstract string Prefix { get; }
|
|
||||||
public abstract Task<T> ReadContextParameterAsync(string value);
|
|
||||||
|
|
||||||
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;
|
|
||||||
var res = await ReadContextParameterAsync(param[0] as string);
|
|
||||||
await commandService.ExecuteAsync(new ContextualContext<T>(pkCtx, res), Prefix + " " + param[1] as string, services);
|
|
||||||
}, (cb) => {
|
|
||||||
cb.WithPriority(-9999);
|
|
||||||
cb.AddPrecondition(new ContextParameterFallbackPreconditionAttribute());
|
|
||||||
cb.AddParameter<string>("contextValue", (pb) => pb.WithDefault(""));
|
|
||||||
cb.AddParameter<string>("rest", (pb) => pb.WithDefault("").WithIsRemainder(true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ContextParameterFallbackPreconditionAttribute : PreconditionAttribute
|
|
||||||
{
|
|
||||||
public ContextParameterFallbackPreconditionAttribute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
|
||||||
{
|
|
||||||
if (context.GetType().Name != "ContextualContext`1") {
|
|
||||||
return PreconditionResult.FromSuccess();
|
|
||||||
} else {
|
|
||||||
return PreconditionResult.FromError("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HidUtils
|
|
||||||
{
|
{
|
||||||
public static string GenerateHid()
|
public static string GenerateHid()
|
||||||
{
|
{
|
||||||
@ -162,15 +23,10 @@ namespace PluralKit
|
|||||||
}
|
}
|
||||||
return hid;
|
return hid;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public class PKResult : RuntimeResult
|
public static string Truncate(this string str, int maxLength, string ellipsis = "...") {
|
||||||
{
|
if (str.Length < maxLength) return str;
|
||||||
public PKResult(CommandError? error, string reason) : base(error, reason)
|
return str.Substring(0, maxLength - ellipsis.Length) + ellipsis;
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RuntimeResult Error(string reason) => new PKResult(CommandError.Unsuccessful, reason);
|
|
||||||
public static RuntimeResult Success(string reason = null) => new PKResult(null, reason);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user