diff --git a/ProjectPlugins/CodexPlugin/CodexDeployment.cs b/ProjectPlugins/CodexPlugin/CodexDeployment.cs index b44c6d2..f63d8bb 100644 --- a/ProjectPlugins/CodexPlugin/CodexDeployment.cs +++ b/ProjectPlugins/CodexPlugin/CodexDeployment.cs @@ -1,20 +1,23 @@ -using GethPlugin; +using CodexContractsPlugin; +using GethPlugin; using KubernetesWorkflow; namespace CodexPlugin { public class CodexDeployment { - public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata) + public CodexDeployment(RunningContainer[] codexContainers, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainer? prometheusContainer, DeploymentMetadata metadata) { CodexContainers = codexContainers; GethDeployment = gethDeployment; + CodexContractsDeployment = codexContractsDeployment; PrometheusContainer = prometheusContainer; Metadata = metadata; } public RunningContainer[] CodexContainers { get; } public GethDeployment GethDeployment { get; } + public CodexContractsDeployment CodexContractsDeployment { get; } public RunningContainer? PrometheusContainer { get; } public DeploymentMetadata Metadata { get; } } diff --git a/Tools/BiblioTech/BaseCommand.cs b/Tools/BiblioTech/BaseCommand.cs new file mode 100644 index 0000000..93ca84b --- /dev/null +++ b/Tools/BiblioTech/BaseCommand.cs @@ -0,0 +1,40 @@ +using Discord.WebSocket; +using Discord; + +namespace BiblioTech +{ + public abstract class BaseCommand + { + public abstract string Name { get; } + public abstract string Description { get; } + public virtual CommandOption[] Options + { + get + { + return Array.Empty(); + } + } + + public async Task SlashCommandHandler(SocketSlashCommand command) + { + if (command.CommandName != Name) return; + await Invoke(command); + } + + protected abstract Task Invoke(SocketSlashCommand command); + } + + public class CommandOption + { + public CommandOption(string name, string description, ApplicationCommandOptionType type) + { + Name = name; + Description = description; + Type = type; + } + + public string Name { get; } + public string Description { get; } + public ApplicationCommandOptionType Type { get; } + } +} diff --git a/Tools/BiblioTech/BaseNetCommand.cs b/Tools/BiblioTech/BaseNetCommand.cs new file mode 100644 index 0000000..73f0d09 --- /dev/null +++ b/Tools/BiblioTech/BaseNetCommand.cs @@ -0,0 +1,34 @@ +using CodexPlugin; +using Discord.WebSocket; + +namespace BiblioTech +{ + public abstract class BaseNetCommand : BaseCommand + { + private readonly DeploymentsFilesMonitor monitor; + + public BaseNetCommand(DeploymentsFilesMonitor monitor) + { + this.monitor = monitor; + } + + protected override async Task Invoke(SocketSlashCommand command) + { + var deployments = monitor.GetDeployments(); + if (deployments.Length == 0) + { + await command.RespondAsync("No deployments are currently available."); + return; + } + if (deployments.Length > 1) + { + await command.RespondAsync("Multiple deployments are online. I don't know which one to pick!"); + return; + } + + await Execute(command, deployments.Single()); + } + + protected abstract Task Execute(SocketSlashCommand command, CodexDeployment codexDeployment); + } +} diff --git a/Tools/BiblioTech/CodexEndpoints.cs b/Tools/BiblioTech/CodexEndpoints.cs deleted file mode 100644 index 706c27b..0000000 --- a/Tools/BiblioTech/CodexEndpoints.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Utils; - -namespace BiblioTech -{ - public class CodexEndpoints - { - public string Name { get; set; } = string.Empty; - public Address[] Addresses { get; set; } = Array.Empty
(); - } -} diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs new file mode 100644 index 0000000..963de2a --- /dev/null +++ b/Tools/BiblioTech/CommandHandler.cs @@ -0,0 +1,62 @@ +using Discord.Net; +using Discord.WebSocket; +using Discord; +using Newtonsoft.Json; + +namespace BiblioTech +{ + public class CommandHandler + { + private readonly DiscordSocketClient client; + private readonly BaseCommand[] commands; + + public CommandHandler(DiscordSocketClient client, params BaseCommand[] commands) + { + this.client = client; + this.commands = commands; + + client.Ready += Client_Ready; + client.SlashCommandExecuted += SlashCommandHandler; + } + + private async Task Client_Ready() + { + var guild = client.Guilds.Single(g => g.Name == Program.Config.ServerName); + + var builders = commands.Select(c => + { + var builder = new SlashCommandBuilder() + .WithName(c.Name) + .WithDescription(c.Description); + + foreach (var option in c.Options) + { + builder.AddOption(option.Name, option.Type, option.Description, isRequired: true); + } + + return builder; + }); + + try + { + foreach (var builder in builders) + { + await guild.CreateApplicationCommandAsync(builder.Build()); + } + } + catch (HttpException exception) + { + var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); + Console.WriteLine(json); + } + } + + private async Task SlashCommandHandler(SocketSlashCommand command) + { + foreach (var cmd in commands) + { + await cmd.SlashCommandHandler(command); + } + } + } +} diff --git a/Tools/BiblioTech/DeploymentsFilesMonitor.cs b/Tools/BiblioTech/DeploymentsFilesMonitor.cs new file mode 100644 index 0000000..ac44e58 --- /dev/null +++ b/Tools/BiblioTech/DeploymentsFilesMonitor.cs @@ -0,0 +1,54 @@ +using CodexPlugin; +using Newtonsoft.Json; + +namespace BiblioTech +{ + public class DeploymentsFilesMonitor + { + private DateTime lastUpdate = DateTime.MinValue; + private CodexDeployment[] deployments = Array.Empty(); + + public CodexDeployment[] GetDeployments() + { + if (ShouldUpdate()) + { + UpdateDeployments(); + } + + return deployments; + } + + private void UpdateDeployments() + { + lastUpdate = DateTime.UtcNow; + var path = Program.Config.EndpointsPath; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + File.WriteAllText(Path.Combine(path, "readme.txt"), "Place codex-deployment.json here."); + return; + } + + var files = Directory.GetFiles(path); + deployments = files.Select(ProcessFile).Where(d => d != null).Cast().ToArray(); + } + + private CodexDeployment? ProcessFile(string filename) + { + try + { + var lines = string.Join(" ", File.ReadAllLines(filename)); + return JsonConvert.DeserializeObject(lines); + } + catch + { + return null; + } + } + + private bool ShouldUpdate() + { + return !deployments.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10); + } + } +} diff --git a/Tools/BiblioTech/EndpointsFilesMonitor.cs b/Tools/BiblioTech/EndpointsFilesMonitor.cs deleted file mode 100644 index 9136740..0000000 --- a/Tools/BiblioTech/EndpointsFilesMonitor.cs +++ /dev/null @@ -1,78 +0,0 @@ -using CodexPlugin; -using KubernetesWorkflow; -using Newtonsoft.Json; -using Utils; - -namespace BiblioTech -{ - public class EndpointsFilesMonitor - { - private DateTime lastUpdate = DateTime.MinValue; - private CodexEndpoints[] endpoints = Array.Empty(); - - public CodexEndpoints[] GetEndpoints() - { - if (ShouldUpdate()) - { - UpdateEndpoints(); - } - - return endpoints; - } - - private void UpdateEndpoints() - { - lastUpdate = DateTime.UtcNow; - var path = Program.Config.EndpointsPath; - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - File.WriteAllText(Path.Combine(path, "readme.txt"), "Place codex-deployment.json or codex-endpoints.json here."); - return; - } - - var files = Directory.GetFiles(path); - endpoints = files.Select(ProcessFile).Where(d => d != null).Cast().ToArray(); - } - - private CodexEndpoints? ProcessFile(string filename) - { - try - { - var lines = string.Join(" ", File.ReadAllLines(filename)); - try - { - return JsonConvert.DeserializeObject(lines); - } - catch { } - - return ConvertToEndpoints(JsonConvert.DeserializeObject(lines)); - } - catch - { - return null; - } - } - - private CodexEndpoints? ConvertToEndpoints(CodexDeployment? codexDeployment) - { - if (codexDeployment == null) return null; - - return new CodexEndpoints - { - Name = "codex-deployment-" + codexDeployment.Metadata.StartUtc.ToString("o"), - Addresses = codexDeployment.CodexContainers.Select(ConvertToAddress).ToArray() - }; - } - - private Address ConvertToAddress(RunningContainer rc) - { - return rc.ClusterExternalAddress; - } - - private bool ShouldUpdate() - { - return !endpoints.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10); - } - } -} diff --git a/Tools/BiblioTech/EndpointsMonitor.cs b/Tools/BiblioTech/EndpointsMonitor.cs deleted file mode 100644 index 27e61bd..0000000 --- a/Tools/BiblioTech/EndpointsMonitor.cs +++ /dev/null @@ -1,81 +0,0 @@ -using CodexPlugin; -using Core; -using KubernetesWorkflow; - -namespace BiblioTech -{ - public class EndpointsMonitor - { - private readonly EndpointsFilesMonitor fileMonitor; - private readonly EntryPoint entryPoint; - private DateTime lastUpdate = DateTime.MinValue; - private string report = string.Empty; - - public EndpointsMonitor(EndpointsFilesMonitor fileMonitor, EntryPoint entryPoint) - { - this.fileMonitor = fileMonitor; - this.entryPoint = entryPoint; - } - - public async Task GetReport() - { - if (ShouldUpdate()) - { - await UpdateReport(); - } - - return report; - } - - private async Task UpdateReport() - { - lastUpdate = DateTime.UtcNow; - - var endpoints = fileMonitor.GetEndpoints(); - if (!endpoints.Any()) - { - report = "There are no networks currently online."; - } - else - { - var nl = Environment.NewLine; - report = $"There are {endpoints.Length} networks online." + nl; - for (var i = 0; i < endpoints.Length; i++) - { - var e = endpoints[i]; - report += $" [{i} - {e.Name}] = {await GetStatusMessage(e)}"; - } - } - } - - private async Task GetStatusMessage(CodexEndpoints e) - { - var success = 0; - foreach (var addr in e.Addresses) - { - await Task.Run(() => - { - // this is definitely not going to work: - var rc = new RunningContainer(null!, null!, null!, "", addr, addr); - - var access = new CodexAccess(entryPoint.Tools, rc, null!); - var debugInfo = access.GetDebugInfo(); - - if (!string.IsNullOrEmpty(debugInfo.id)) - { - success++; - } - }); - } - - return $"{success} / {e.Addresses.Length} online."; - // todo: analyze returned peerIDs to say something about - // the number of none-test-net managed nodes seen on the network. - } - - private bool ShouldUpdate() - { - return string.IsNullOrEmpty(report) || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10); - } - } -} diff --git a/Tools/BiblioTech/HelloWorldCommand.cs b/Tools/BiblioTech/HelloWorldCommand.cs deleted file mode 100644 index 8659acf..0000000 --- a/Tools/BiblioTech/HelloWorldCommand.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Discord; -using Discord.Net; -using Discord.WebSocket; -using Newtonsoft.Json; - -namespace BiblioTech -{ - public class HelloWorldCommand - { - private readonly DiscordSocketClient client; - - public HelloWorldCommand(DiscordSocketClient client) - { - this.client = client; - - client.Ready += Client_Ready; - client.SlashCommandExecuted += SlashCommandHandler; - } - - private async Task SlashCommandHandler(SocketSlashCommand command) - { - var cheeseOption = command.Data.Options.SingleOrDefault(o => o.Name == "cheese"); - var numberOption = command.Data.Options.SingleOrDefault(o => o.Name == "numberofthings"); - await command.RespondAsync($"Dear {command.User.Username}, You executed {command.Data.Name} with cheese: {cheeseOption.Value} and number: {numberOption.Value}"); - } - - private async Task Client_Ready() - { - // Let's build a guild command! We're going to need a guild so lets just put that in a variable. - var guild = client.Guilds.Single(g => g.Name == "ThatBen's server"); - - // Next, lets create our slash command builder. This is like the embed builder but for slash commands. - var guildCommand = new SlashCommandBuilder() - .WithName("do-thing") - .WithDescription("This command does the thing!") - .AddOption("cheese", ApplicationCommandOptionType.Boolean, "whether you like cheese", isRequired: true) - .AddOption("numberofthings", ApplicationCommandOptionType.Number, "count them please", isRequired: true) - ; - - //// Let's do our global command - //var globalCommand = new SlashCommandBuilder(); - //globalCommand.WithName("first-global-command"); - //globalCommand.WithDescription("This is my first global slash command"); - - try - { - // Now that we have our builder, we can call the CreateApplicationCommandAsync method to make our slash command. - await guild.CreateApplicationCommandAsync(guildCommand.Build()); - - // With global commands we don't need the guild. - //await client.CreateGlobalApplicationCommandAsync(globalCommand.Build()); - // Using the ready event is a simple implementation for the sake of the example. Suitable for testing and development. - // For a production bot, it is recommended to only run the CreateGlobalApplicationCommandAsync() once for each command. - } - catch (ApplicationCommandException exception) - { - // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. - var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); - - // You can send this error somewhere or just print it to the console, for this example we're just going to print it. - Console.WriteLine(json); - } - } - } -} diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index e063f12..fc4e693 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -1,4 +1,5 @@ using ArgsUniform; +using BiblioTech.TokenCommands; using Core; using Discord; using Discord.WebSocket; @@ -11,7 +12,7 @@ namespace BiblioTech private DiscordSocketClient client = null!; public static Configuration Config { get; private set; } = null!; - public static EndpointsFilesMonitor DeploymentFilesMonitor { get; } = new EndpointsFilesMonitor(); + public static DeploymentsFilesMonitor DeploymentFilesMonitor { get; } = new DeploymentsFilesMonitor(); public static Task Main(string[] args) { @@ -28,17 +29,19 @@ namespace BiblioTech client.Log += Log; ProjectPlugin.Load(); + ProjectPlugin.Load(); + ProjectPlugin.Load(); + var entryPoint = new EntryPoint(new ConsoleLog(), new KubernetesWorkflow.Configuration( kubeConfigFile: null, // todo: readonly file operationTimeout: TimeSpan.FromMinutes(5), retryDelay: TimeSpan.FromSeconds(10), kubernetesNamespace: "not-applicable"), "datafiles"); - var fileMonitor = new EndpointsFilesMonitor(); - var monitor = new EndpointsMonitor(fileMonitor, entryPoint); + var monitor = new DeploymentsFilesMonitor(); - var statusCommand = new StatusCommand(client, monitor); - //var helloWorld = new HelloWorldCommand(client); Example for how to do arguments. + var handler = new CommandHandler(client, + new GetBalanceCommand(monitor)); await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); await client.StartAsync(); diff --git a/Tools/BiblioTech/StatusCommand.cs b/Tools/BiblioTech/StatusCommand.cs deleted file mode 100644 index 9d081c0..0000000 --- a/Tools/BiblioTech/StatusCommand.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord.Net; -using Discord.WebSocket; -using Discord; -using Newtonsoft.Json; -using Core; - -namespace BiblioTech -{ - public class StatusCommand - { - private const string CommandName = "status"; - private readonly DiscordSocketClient client; - private readonly EndpointsMonitor monitor; - - public StatusCommand(DiscordSocketClient client, EndpointsMonitor monitor) - { - this.client = client; - this.monitor = monitor; - - client.Ready += Client_Ready; - client.SlashCommandExecuted += SlashCommandHandler; - } - - private async Task SlashCommandHandler(SocketSlashCommand command) - { - if (command.CommandName != CommandName) return; - - await command.RespondAsync(await monitor.GetReport()); - } - - private async Task Client_Ready() - { - var guild = client.Guilds.Single(g => g.Name == Program.Config.ServerName); - - var guildCommand = new SlashCommandBuilder() - .WithName(CommandName) - .WithDescription("Display status of test net."); - - try - { - await guild.CreateApplicationCommandAsync(guildCommand.Build()); - } - catch (HttpException exception) - { - var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); - Console.WriteLine(json); - } - } - } -} diff --git a/Tools/BiblioTech/TokenCommands/EthAddressOption.cs b/Tools/BiblioTech/TokenCommands/EthAddressOption.cs new file mode 100644 index 0000000..5df010b --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/EthAddressOption.cs @@ -0,0 +1,34 @@ +using Discord.WebSocket; +using GethPlugin; + +namespace BiblioTech.TokenCommands +{ + public class EthAddressOption : CommandOption + { + public EthAddressOption() + : base(name: "EthAddress", + description: "Ethereum address starting with '0x'.", + type: Discord.ApplicationCommandOptionType.String) + { + } + + public async Task Parse(SocketSlashCommand command) + { + var ethOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name); + if (ethOptionData == null) + { + await command.RespondAsync("EthAddress option not received."); + return null; + } + var ethAddressStr = ethOptionData.Value as string; + if (string.IsNullOrEmpty(ethAddressStr)) + { + // todo, validate that it is an eth address. + await command.RespondAsync("EthAddress is null or invalid."); + return null; + } + + return new EthAddress(ethAddressStr); + } + } +} diff --git a/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs b/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs new file mode 100644 index 0000000..b1a2aaf --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs @@ -0,0 +1,41 @@ +using CodexContractsPlugin; +using CodexPlugin; +using Core; +using Discord.WebSocket; +using GethPlugin; + +namespace BiblioTech.TokenCommands +{ + public class GetBalanceCommand : BaseNetCommand + { + private readonly EthAddressOption ethOption = new EthAddressOption(); + private readonly CoreInterface ci; + + public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci) + : base(monitor) + { + this.ci = ci; + } + + public override string Name => "balance"; + public override string Description => "Shows Eth and TestToken balance of an eth address."; + public override CommandOption[] Options => new[] { ethOption }; + + protected override async Task Execute(SocketSlashCommand command, CodexDeployment codexDeployment) + { + var addr = await ethOption.Parse(command); + if (addr == null) return; + + var gethDeployment = codexDeployment.GethDeployment; + var contractsDeployment = codexDeployment.CodexContractsDeployment; + + var gethNode = ci.WrapGethDeployment(gethDeployment); + var contracts = ci.WrapCodexContractsDeployment(contractsDeployment); + + var eth = gethNode.GetEthBalance(addr); + var testTokens = contracts.GetTestTokenBalance(gethNode, addr); + + await command.RespondAsync($"Address '{addr.Address}' has {eth} and {testTokens}."); + } + } +}