cleanup of admin commands

This commit is contained in:
benbierens 2023-10-24 15:25:45 +02:00
parent 5aff8c6f6d
commit a0461a446e
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
16 changed files with 297 additions and 211 deletions

View File

@ -1,6 +1,5 @@
using Discord.WebSocket;
using Discord;
using BiblioTech.Commands;
using BiblioTech.Options;
namespace BiblioTech
{
@ -24,7 +23,7 @@ namespace BiblioTech
try
{
await command.RespondAsync(StartingMessage);
await Invoke(command);
await Invoke(new CommandContext(command, command.Data.Options));
}
catch (Exception ex)
{
@ -33,43 +32,18 @@ namespace BiblioTech
}
}
protected abstract Task Invoke(SocketSlashCommand command);
protected abstract Task Invoke(CommandContext context);
protected bool IsSenderAdmin(SocketSlashCommand command)
{
return Program.AdminChecker.IsUserAdmin(command.User.Id);
}
protected ulong GetUserId(UserOption userOption, SocketSlashCommand command)
protected ulong GetUserId(UserOption userOption, CommandContext context)
{
var targetUser = userOption.GetOptionUserId(command);
if (IsSenderAdmin(command) && targetUser != null) return targetUser.Value;
return command.User.Id;
}
}
public class CommandOption
{
public CommandOption(string name, string description, ApplicationCommandOptionType type, bool isRequired)
{
Name = name;
Description = description;
Type = type;
IsRequired = isRequired;
}
public string Name { get; }
public string Description { get; }
public ApplicationCommandOptionType Type { get; }
public bool IsRequired { get; }
public virtual SlashCommandOptionBuilder Build()
{
return new SlashCommandOptionBuilder()
.WithName(Name)
.WithDescription(Description)
.WithType(Type)
.WithRequired(IsRequired);
var targetUser = userOption.GetOptionUserId(context);
if (IsSenderAdmin(context.Command) && targetUser != null) return targetUser.Value;
return context.Command.User.Id;
}
}
}

View File

@ -1,6 +1,6 @@
using CodexContractsPlugin;
using BiblioTech.Options;
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech
@ -16,17 +16,17 @@ namespace BiblioTech
this.ci = ci;
}
protected override async Task Invoke(SocketSlashCommand command)
protected override async Task Invoke(CommandContext context)
{
var deployments = monitor.GetDeployments();
if (deployments.Length == 0)
{
await command.FollowupAsync("No deployments are currently available.");
await context.Command.FollowupAsync("No deployments are currently available.");
return;
}
if (deployments.Length > 1)
{
await command.FollowupAsync("Multiple deployments are online. I don't know which one to pick!");
await context.Command.FollowupAsync("Multiple deployments are online. I don't know which one to pick!");
return;
}
@ -37,9 +37,9 @@ namespace BiblioTech
var gethNode = ci.WrapGethDeployment(gethDeployment);
var contracts = ci.WrapCodexContractsDeployment(contractsDeployment);
await Execute(command, gethNode, contracts);
await Execute(context, gethNode, contracts);
}
protected abstract Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts);
protected abstract Task Execute(CommandContext context, IGethNode gethNode, ICodexContracts contracts);
}
}

View File

@ -1,46 +1,156 @@
using Discord;
using Discord.WebSocket;
using BiblioTech.Options;
using CodexPlugin;
namespace BiblioTech.Commands
{
public class SubCommandOption : CommandOption
{
private readonly CommandOption[] options;
public SubCommandOption(string name, string description, params CommandOption[] options)
: base(name, description, type: ApplicationCommandOptionType.SubCommand, isRequired: false)
{
this.options = options;
}
public override SlashCommandOptionBuilder Build()
{
var builder = base.Build();
foreach (var option in options)
{
builder.AddOption(option.Build());
}
return builder;
}
}
public class AdminCommand : BaseCommand
{
private readonly ClearUserAssociationCommand clearCommand;
private readonly ReportCommand reportCommand;
private readonly DeployListCommand deployListCommand;
private readonly DeployUploadCommand deployUploadCommand;
public override string Name => "admin";
public override string StartingMessage => "...";
public override string Description => "Admins only.";
private readonly SubCommandOption aaa = new SubCommandOption("aaa", "does AAA", new EthAddressOption());
private readonly SubCommandOption bbb = new SubCommandOption("bbb", "does BBB", new UserOption("a user", true));
public AdminCommand(DeploymentsFilesMonitor monitor)
{
clearCommand = new ClearUserAssociationCommand();
reportCommand = new ReportCommand();
deployListCommand = new DeployListCommand(monitor);
deployUploadCommand = new DeployUploadCommand(monitor);
}
public override CommandOption[] Options => new CommandOption[]
{
aaa, bbb
clearCommand,
reportCommand,
deployListCommand,
deployUploadCommand,
};
protected override Task Invoke(SocketSlashCommand command)
protected override async Task Invoke(CommandContext context)
{
return Task.CompletedTask;
if (!IsSenderAdmin(context.Command))
{
await context.Command.FollowupAsync("You're not an admin.");
return;
}
await clearCommand.CommandHandler(context);
await reportCommand.CommandHandler(context);
await deployListCommand.CommandHandler(context);
await deployUploadCommand.CommandHandler(context);
}
public class ClearUserAssociationCommand : SubCommandOption
{
private readonly UserOption user = new UserOption("User to clear Eth address for.", true);
public ClearUserAssociationCommand()
: base("clear", "Admin only. Clears current Eth address for a user, allowing them to set a new one.")
{
}
public override CommandOption[] Options => new[] { user };
protected override async Task onSubCommand(CommandContext context)
{
var userId = user.GetOptionUserId(context);
if (userId == null)
{
await context.Command.FollowupAsync("Failed to get user ID");
return;
}
Program.UserRepo.ClearUserAssociatedAddress(userId.Value);
await context.Command.FollowupAsync("Done.");
}
}
public class ReportCommand : SubCommandOption
{
private readonly UserOption user = new UserOption(
description: "User to report history for.",
isRequired: true);
public ReportCommand()
: base("report", "Admin only. Reports bot-interaction history for a user.")
{
}
public override CommandOption[] Options => new[] { user };
protected override async Task onSubCommand(CommandContext context)
{
var userId = user.GetOptionUserId(context);
if (userId == null)
{
await context.Command.FollowupAsync("Failed to get user ID");
return;
}
var report = Program.UserRepo.GetInteractionReport(userId.Value);
await context.Command.FollowupAsync(string.Join(Environment.NewLine, report));
}
}
public class DeployListCommand : SubCommandOption
{
private readonly DeploymentsFilesMonitor monitor;
public DeployListCommand(DeploymentsFilesMonitor monitor)
: base("list", "Lists current deployments.")
{
this.monitor = monitor;
}
protected override async Task onSubCommand(CommandContext context)
{
var deployments = monitor.GetDeployments();
if (!deployments.Any())
{
await context.Command.FollowupAsync("No deployments available.");
return;
}
await context.Command.FollowupAsync($"Deployments: {string.Join(", ", deployments.Select(FormatDeployment))}");
}
private string FormatDeployment(CodexDeployment deployment)
{
var m = deployment.Metadata;
return $"{m.Name} ({m.StartUtc.ToString("o")})";
}
}
public class DeployUploadCommand : SubCommandOption
{
private readonly DeploymentsFilesMonitor monitor;
private readonly FileAttachementOption fileOption = new FileAttachementOption(
name: "json",
description: "Codex-deployment json to add.",
isRequired: true);
public DeployUploadCommand(DeploymentsFilesMonitor monitor)
: base("add", "Upload a new deployment JSON file.")
{
this.monitor = monitor;
}
public override CommandOption[] Options => new[] { fileOption };
protected override async Task onSubCommand(CommandContext context)
{
var file = await fileOption.Parse(context);
if (file == null) return;
await context.Command.FollowupAsync("Received: " + file.Size);
// todo pass to monitor, add to folder.
}
}
}
}

View File

@ -1,35 +0,0 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class ClearUserAssociationCommand : BaseCommand
{
private readonly UserOption user = new UserOption(
description: "User to clear Eth address for.",
isRequired: true);
public override string Name => "clear";
public override string StartingMessage => "Hold on...";
public override string Description => "Admin only. Clears current Eth address for a user, allowing them to set a new one.";
public override CommandOption[] Options => new[] { user };
protected override async Task Invoke(SocketSlashCommand command)
{
if (!IsSenderAdmin(command))
{
await command.FollowupAsync("You're not an admin.");
return;
}
var userId = user.GetOptionUserId(command);
if (userId == null)
{
await command.FollowupAsync("Failed to get user ID");
return;
}
Program.UserRepo.ClearUserAssociatedAddress(userId.Value);
await command.FollowupAsync("Done."); ;
}
}
}

View File

@ -1,38 +0,0 @@
using CodexPlugin;
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class DeploymentsCommand : BaseCommand
{
private readonly DeploymentsFilesMonitor monitor;
public DeploymentsCommand(DeploymentsFilesMonitor monitor)
{
this.monitor = monitor;
}
public override string Name => "deployments";
public override string StartingMessage => "Fetching deployments information...";
public override string Description => "Lists active TestNet deployments";
protected override async Task Invoke(SocketSlashCommand command)
{
var deployments = monitor.GetDeployments();
if (!deployments.Any())
{
await command.FollowupAsync("No deployments available.");
return;
}
await command.FollowupAsync($"Deployments: {string.Join(", ", deployments.Select(FormatDeployment))}");
}
private string FormatDeployment(CodexDeployment deployment)
{
var m = deployment.Metadata;
return $"{m.Name} ({m.StartUtc.ToString("o")})";
}
}
}

View File

@ -1,6 +1,6 @@
using CodexContractsPlugin;
using BiblioTech.Options;
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
@ -23,20 +23,20 @@ namespace BiblioTech.Commands
public override string Description => "Shows Eth and TestToken balance of an eth address.";
public override CommandOption[] Options => new[] { optionalUser };
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
protected override async Task Execute(CommandContext context, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserId(optionalUser, command);
var userId = GetUserId(optionalUser, context);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
await context.Command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
return;
}
var eth = gethNode.GetEthBalance(addr);
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
await command.FollowupAsync($"{command.User.Username} has {eth} and {testTokens}.");
await context.Command.FollowupAsync($"{context.Command.User.Username} has {eth} and {testTokens}.");
}
}
}

View File

@ -1,6 +1,6 @@
using CodexContractsPlugin;
using BiblioTech.Options;
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
@ -25,13 +25,13 @@ namespace BiblioTech.Commands
public override string Description => "Mint some TestTokens and send some Eth to the user if their balance is low.";
public override CommandOption[] Options => new[] { optionalUser };
protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts)
protected override async Task Execute(CommandContext context, IGethNode gethNode, ICodexContracts contracts)
{
var userId = GetUserId(optionalUser, command);
var userId = GetUserId(optionalUser, context);
var addr = Program.UserRepo.GetCurrentAddressForUser(userId);
if (addr == null)
{
await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
await context.Command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
return;
}
@ -42,7 +42,7 @@ namespace BiblioTech.Commands
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
await command.FollowupAsync(string.Join(Environment.NewLine, report));
await context.Command.FollowupAsync(string.Join(Environment.NewLine, report));
}
private TestToken ProcessTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr, List<string> report)

View File

@ -1,35 +0,0 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class ReportHistoryCommand : BaseCommand
{
private readonly UserOption user = new UserOption(
description: "User to report history for.",
isRequired: true);
public override string Name => "report";
public override string StartingMessage => "Getting that data...";
public override string Description => "Admin only. Reports bot-interaction history for a user.";
public override CommandOption[] Options => new[] { user };
protected override async Task Invoke(SocketSlashCommand command)
{
if (!IsSenderAdmin(command))
{
await command.FollowupAsync("You're not an admin.");
return;
}
var userId = user.GetOptionUserId(command);
if (userId == null)
{
await command.FollowupAsync("Failed to get user ID");
return;
}
var report = Program.UserRepo.GetInteractionReport(userId.Value);
await command.FollowupAsync(string.Join(Environment.NewLine, report));
}
}
}

View File

@ -1,4 +1,4 @@
using Discord.WebSocket;
using BiblioTech.Options;
namespace BiblioTech.Commands
{
@ -14,21 +14,21 @@ namespace BiblioTech.Commands
public override string Description => "Associates a Discord user with an Ethereum address.";
public override CommandOption[] Options => new CommandOption[] { ethOption, optionalUser };
protected override async Task Invoke(SocketSlashCommand command)
protected override async Task Invoke(CommandContext context)
{
var userId = GetUserId(optionalUser, command);
var data = await ethOption.Parse(command);
var userId = GetUserId(optionalUser, context);
var data = await ethOption.Parse(context);
if (data == null) return;
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(userId);
if (currentAddress != null && !IsSenderAdmin(command))
if (currentAddress != null && !IsSenderAdmin(context.Command))
{
await command.FollowupAsync($"You've already set your Ethereum address to {currentAddress}.");
await context.Command.FollowupAsync($"You've already set your Ethereum address to {currentAddress}.");
return;
}
Program.UserRepo.AssociateUserWithAddress(userId, data);
await command.FollowupAsync("Done! Thank you for joining the test net!");
await context.Command.FollowupAsync("Done! Thank you for joining the test net!");
}
}
}

View File

@ -0,0 +1,16 @@
using Discord.WebSocket;
namespace BiblioTech.Options
{
public class CommandContext
{
public CommandContext(SocketSlashCommand command, IReadOnlyCollection<SocketSlashCommandDataOption> options)
{
Command = command;
Options = options;
}
public SocketSlashCommand Command { get; }
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; }
}
}

View File

@ -0,0 +1,29 @@
using Discord;
namespace BiblioTech.Options
{
public abstract class CommandOption
{
public CommandOption(string name, string description, ApplicationCommandOptionType type, bool isRequired)
{
Name = name;
Description = description;
Type = type;
IsRequired = isRequired;
}
public string Name { get; }
public string Description { get; }
public ApplicationCommandOptionType Type { get; }
public bool IsRequired { get; }
public virtual SlashCommandOptionBuilder Build()
{
return new SlashCommandOptionBuilder()
.WithName(Name)
.WithDescription(Description)
.WithType(Type)
.WithRequired(IsRequired);
}
}
}

View File

@ -1,8 +1,7 @@
using Discord.WebSocket;
using GethPlugin;
using GethPlugin;
using Nethereum.Util;
namespace BiblioTech.Commands
namespace BiblioTech.Options
{
public class EthAddressOption : CommandOption
{
@ -14,18 +13,18 @@ namespace BiblioTech.Commands
{
}
public async Task<EthAddress?> Parse(SocketSlashCommand command)
public async Task<EthAddress?> Parse(CommandContext context)
{
var ethOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
var ethOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (ethOptionData == null)
{
await command.FollowupAsync("EthAddress option not received.");
await context.Command.FollowupAsync("EthAddress option not received.");
return null;
}
var ethAddressStr = ethOptionData.Value as string;
if (string.IsNullOrEmpty(ethAddressStr))
{
await command.FollowupAsync("EthAddress is null or empty.");
await context.Command.FollowupAsync("EthAddress is null or empty.");
return null;
}
@ -33,7 +32,7 @@ namespace BiblioTech.Commands
!AddressUtil.Current.IsValidEthereumAddressHexFormat(ethAddressStr) ||
!AddressUtil.Current.IsChecksumAddress(ethAddressStr))
{
await command.FollowupAsync("EthAddress is not valid.");
await context.Command.FollowupAsync("EthAddress is not valid.");
return null;
}

View File

@ -0,0 +1,30 @@
using Discord;
namespace BiblioTech.Options
{
public class FileAttachementOption : CommandOption
{
public FileAttachementOption(string name, string description, bool isRequired)
: base(name, description, type: ApplicationCommandOptionType.Attachment, isRequired)
{
}
public async Task<IAttachment?> Parse(CommandContext context)
{
var fileOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (fileOptionData == null)
{
await context.Command.FollowupAsync("Attachement option not received.");
return null;
}
var attachement = fileOptionData.Value as IAttachment;
if (attachement == null)
{
await context.Command.FollowupAsync("Attachement is null or empty.");
return null;
}
return attachement;
}
}
}

View File

@ -0,0 +1,40 @@
using Discord;
namespace BiblioTech.Options
{
public abstract class SubCommandOption : CommandOption
{
public SubCommandOption(string name, string description)
: base(name, description, type: ApplicationCommandOptionType.SubCommand, isRequired: false)
{
}
public override SlashCommandOptionBuilder Build()
{
var builder = base.Build();
foreach (var option in Options)
{
builder.AddOption(option.Build());
}
return builder;
}
public async Task CommandHandler(CommandContext context)
{
var mine = context.Options.SingleOrDefault(o => o.Name == Name);
if (mine == null) return;
await onSubCommand(new CommandContext(context.Command, mine.Options));
}
public virtual CommandOption[] Options
{
get
{
return Array.Empty<CommandOption>();
}
}
protected abstract Task onSubCommand(CommandContext context);
}
}

View File

@ -1,7 +1,6 @@
using Discord;
using Discord.WebSocket;
namespace BiblioTech.Commands
namespace BiblioTech.Options
{
public class UserOption : CommandOption
{
@ -10,9 +9,9 @@ namespace BiblioTech.Commands
{
}
public ulong? GetOptionUserId(SocketSlashCommand command)
public ulong? GetOptionUserId(CommandContext context)
{
var userOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
var userOptionData = context.Options.SingleOrDefault(o => o.Name == Name);
if (userOptionData == null) return null;
var user = userOptionData.Value as IUser;
if (user == null) return null;

View File

@ -51,13 +51,10 @@ namespace BiblioTech
var associateCommand = new UserAssociateCommand();
var handler = new CommandHandler(client,
//new ClearUserAssociationCommand(),
new GetBalanceCommand(monitor, ci, associateCommand),
new MintCommand(monitor, ci, associateCommand),
//new ReportHistoryCommand(),
associateCommand,
//new DeploymentsCommand(monitor),
new AdminCommand()
new AdminCommand(monitor)
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);