Setting up balance-getting command

This commit is contained in:
benbierens 2023-10-20 09:49:23 +02:00
parent 766e2f5c20
commit 991927b95f
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
13 changed files with 278 additions and 291 deletions

View File

@ -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; }
}

View File

@ -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<CommandOption>();
}
}
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; }
}
}

View File

@ -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);
}
}

View File

@ -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<Address>();
}
}

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,54 @@
using CodexPlugin;
using Newtonsoft.Json;
namespace BiblioTech
{
public class DeploymentsFilesMonitor
{
private DateTime lastUpdate = DateTime.MinValue;
private CodexDeployment[] deployments = Array.Empty<CodexDeployment>();
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<CodexDeployment>().ToArray();
}
private CodexDeployment? ProcessFile(string filename)
{
try
{
var lines = string.Join(" ", File.ReadAllLines(filename));
return JsonConvert.DeserializeObject<CodexDeployment>(lines);
}
catch
{
return null;
}
}
private bool ShouldUpdate()
{
return !deployments.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
}
}
}

View File

@ -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<CodexEndpoints>();
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<CodexEndpoints>().ToArray();
}
private CodexEndpoints? ProcessFile(string filename)
{
try
{
var lines = string.Join(" ", File.ReadAllLines(filename));
try
{
return JsonConvert.DeserializeObject<CodexEndpoints>(lines);
}
catch { }
return ConvertToEndpoints(JsonConvert.DeserializeObject<CodexDeployment>(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);
}
}
}

View File

@ -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<string> 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<string> 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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<CodexPlugin.CodexPlugin>();
ProjectPlugin.Load<GethPlugin.GethPlugin>();
ProjectPlugin.Load<CodexContractsPlugin.CodexContractsPlugin>();
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();

View File

@ -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);
}
}
}
}

View File

@ -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<EthAddress?> 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);
}
}
}

View File

@ -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}.");
}
}
}