diff --git a/Tools/BiblioTech/BaseCommand.cs b/Tools/BiblioTech/BaseCommand.cs index b65c416..57c6658 100644 --- a/Tools/BiblioTech/BaseCommand.cs +++ b/Tools/BiblioTech/BaseCommand.cs @@ -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; } } } diff --git a/Tools/BiblioTech/BaseNetCommand.cs b/Tools/BiblioTech/BaseNetCommand.cs index 83976a7..7294e05 100644 --- a/Tools/BiblioTech/BaseNetCommand.cs +++ b/Tools/BiblioTech/BaseNetCommand.cs @@ -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); } } diff --git a/Tools/BiblioTech/Commands/AdminCommand.cs b/Tools/BiblioTech/Commands/AdminCommand.cs index fc1dc2e..171b755 100644 --- a/Tools/BiblioTech/Commands/AdminCommand.cs +++ b/Tools/BiblioTech/Commands/AdminCommand.cs @@ -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. + } } } } diff --git a/Tools/BiblioTech/Commands/ClearUserAssociationCommand.cs b/Tools/BiblioTech/Commands/ClearUserAssociationCommand.cs deleted file mode 100644 index cc49829..0000000 --- a/Tools/BiblioTech/Commands/ClearUserAssociationCommand.cs +++ /dev/null @@ -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."); ; - } - } -} diff --git a/Tools/BiblioTech/Commands/DeploymentsCommand.cs b/Tools/BiblioTech/Commands/DeploymentsCommand.cs deleted file mode 100644 index 50dc95f..0000000 --- a/Tools/BiblioTech/Commands/DeploymentsCommand.cs +++ /dev/null @@ -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")})"; - } - } -} diff --git a/Tools/BiblioTech/Commands/GetBalanceCommand.cs b/Tools/BiblioTech/Commands/GetBalanceCommand.cs index e51ab11..c1fdf07 100644 --- a/Tools/BiblioTech/Commands/GetBalanceCommand.cs +++ b/Tools/BiblioTech/Commands/GetBalanceCommand.cs @@ -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}."); } } } diff --git a/Tools/BiblioTech/Commands/MintCommand.cs b/Tools/BiblioTech/Commands/MintCommand.cs index 31a5f8e..0483c84 100644 --- a/Tools/BiblioTech/Commands/MintCommand.cs +++ b/Tools/BiblioTech/Commands/MintCommand.cs @@ -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 report) diff --git a/Tools/BiblioTech/Commands/ReportHistoryCommand.cs b/Tools/BiblioTech/Commands/ReportHistoryCommand.cs deleted file mode 100644 index ccad6f7..0000000 --- a/Tools/BiblioTech/Commands/ReportHistoryCommand.cs +++ /dev/null @@ -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)); - } - } -} diff --git a/Tools/BiblioTech/Commands/UserAssociateCommand.cs b/Tools/BiblioTech/Commands/UserAssociateCommand.cs index 8ab25f2..93411b0 100644 --- a/Tools/BiblioTech/Commands/UserAssociateCommand.cs +++ b/Tools/BiblioTech/Commands/UserAssociateCommand.cs @@ -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!"); } } } diff --git a/Tools/BiblioTech/Options/CommandContext.cs b/Tools/BiblioTech/Options/CommandContext.cs new file mode 100644 index 0000000..5d37087 --- /dev/null +++ b/Tools/BiblioTech/Options/CommandContext.cs @@ -0,0 +1,16 @@ +using Discord.WebSocket; + +namespace BiblioTech.Options +{ + public class CommandContext + { + public CommandContext(SocketSlashCommand command, IReadOnlyCollection options) + { + Command = command; + Options = options; + } + + public SocketSlashCommand Command { get; } + public IReadOnlyCollection Options { get; } + } +} diff --git a/Tools/BiblioTech/Options/CommandOption.cs b/Tools/BiblioTech/Options/CommandOption.cs new file mode 100644 index 0000000..7784801 --- /dev/null +++ b/Tools/BiblioTech/Options/CommandOption.cs @@ -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); + } + } +} diff --git a/Tools/BiblioTech/Commands/EthAddressOption.cs b/Tools/BiblioTech/Options/EthAddressOption.cs similarity index 67% rename from Tools/BiblioTech/Commands/EthAddressOption.cs rename to Tools/BiblioTech/Options/EthAddressOption.cs index 6996c79..8e2173d 100644 --- a/Tools/BiblioTech/Commands/EthAddressOption.cs +++ b/Tools/BiblioTech/Options/EthAddressOption.cs @@ -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 Parse(SocketSlashCommand command) + public async Task 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; } diff --git a/Tools/BiblioTech/Options/FileAttachementOption.cs b/Tools/BiblioTech/Options/FileAttachementOption.cs new file mode 100644 index 0000000..d759811 --- /dev/null +++ b/Tools/BiblioTech/Options/FileAttachementOption.cs @@ -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 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; + } + } +} diff --git a/Tools/BiblioTech/Options/SubCommandOption.cs b/Tools/BiblioTech/Options/SubCommandOption.cs new file mode 100644 index 0000000..8635f98 --- /dev/null +++ b/Tools/BiblioTech/Options/SubCommandOption.cs @@ -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(); + } + } + + protected abstract Task onSubCommand(CommandContext context); + } +} diff --git a/Tools/BiblioTech/Commands/UserOption.cs b/Tools/BiblioTech/Options/UserOption.cs similarity index 67% rename from Tools/BiblioTech/Commands/UserOption.cs rename to Tools/BiblioTech/Options/UserOption.cs index e61558d..5ac869b 100644 --- a/Tools/BiblioTech/Commands/UserOption.cs +++ b/Tools/BiblioTech/Options/UserOption.cs @@ -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; diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index faccb3d..27c7b2d 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -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);