Merge branch 'app/discord-bot'

This commit is contained in:
benbierens 2023-10-23 10:32:23 +02:00
commit cc2e8d5992
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
29 changed files with 921 additions and 5 deletions

View File

@ -1,28 +1,32 @@
using GethPlugin; using CodexContractsPlugin;
using GethPlugin;
using KubernetesWorkflow; using KubernetesWorkflow;
namespace CodexPlugin namespace CodexPlugin
{ {
public class CodexDeployment 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; CodexContainers = codexContainers;
GethDeployment = gethDeployment; GethDeployment = gethDeployment;
CodexContractsDeployment = codexContractsDeployment;
PrometheusContainer = prometheusContainer; PrometheusContainer = prometheusContainer;
Metadata = metadata; Metadata = metadata;
} }
public RunningContainer[] CodexContainers { get; } public RunningContainer[] CodexContainers { get; }
public GethDeployment GethDeployment { get; } public GethDeployment GethDeployment { get; }
public CodexContractsDeployment CodexContractsDeployment { get; }
public RunningContainer? PrometheusContainer { get; } public RunningContainer? PrometheusContainer { get; }
public DeploymentMetadata Metadata { get; } public DeploymentMetadata Metadata { get; }
} }
public class DeploymentMetadata public class DeploymentMetadata
{ {
public DeploymentMetadata(DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN) public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
{ {
Name = name;
StartUtc = startUtc; StartUtc = startUtc;
FinishedUtc = finishedUtc; FinishedUtc = finishedUtc;
KubeNamespace = kubeNamespace; KubeNamespace = kubeNamespace;
@ -39,6 +43,7 @@ namespace CodexPlugin
BlockMN = blockMN; BlockMN = blockMN;
} }
public string Name { get; }
public DateTime StartUtc { get; } public DateTime StartUtc { get; }
public DateTime FinishedUtc { get; } public DateTime FinishedUtc { get; }
public string KubeNamespace { get; } public string KubeNamespace { get; }

View File

@ -13,5 +13,10 @@
} }
public string Address { get; } public string Address { get; }
public override string ToString()
{
return Address;
}
} }
} }

View File

@ -28,7 +28,7 @@
public override string ToString() public override string ToString()
{ {
return $"{Wei} Wei"; return $"{Eth} Eth";
} }
} }

View File

@ -92,6 +92,7 @@ namespace ContinuousTests
{ {
var testDuration = Time.FormatDuration(DateTime.UtcNow - startTime); var testDuration = Time.FormatDuration(DateTime.UtcNow - startTime);
var testData = FormatTestRuns(testLoops); var testData = FormatTestRuns(testLoops);
overviewLog.Log("Total duration: " + testDuration);
if (config.TargetDurationSeconds > 0) if (config.TargetDurationSeconds > 0)
{ {

View File

@ -9,6 +9,7 @@ cd ../../Tools/CodexNetDeployer
for i in $( seq 0 $replication) for i in $( seq 0 $replication)
do do
dotnet run \ dotnet run \
--deploy-name=codex-continuous-$name-$i \
--kube-config=/opt/kubeconfig.yaml \ --kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-continuous-$name-tests-$i \ --kube-namespace=codex-continuous-$name-tests-$i \
--deploy-file=codex-deployment-$name-$i.json \ --deploy-file=codex-deployment-$name-$i.json \

View File

@ -0,0 +1,35 @@
using Discord.WebSocket;
namespace BiblioTech
{
public class AdminChecker
{
private SocketGuild guild = null!;
private ulong[] adminIds = Array.Empty<ulong>();
private DateTime lastUpdate = DateTime.MinValue;
public void SetGuild(SocketGuild guild)
{
this.guild = guild;
}
public bool IsUserAdmin(ulong userId)
{
if (ShouldUpdate()) UpdateAdminIds();
return adminIds.Contains(userId);
}
private bool ShouldUpdate()
{
return !adminIds.Any() || (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
}
private void UpdateAdminIds()
{
lastUpdate = DateTime.UtcNow;
var adminRole = guild.Roles.Single(r => r.Name == Program.Config.AdminRoleName);
adminIds = adminRole.Members.Select(m => m.Id).ToArray();
}
}
}

View File

@ -0,0 +1,66 @@
using Discord.WebSocket;
using Discord;
using BiblioTech.Commands;
namespace BiblioTech
{
public abstract class BaseCommand
{
public abstract string Name { get; }
public abstract string StartingMessage { 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;
try
{
await command.RespondAsync(StartingMessage);
await Invoke(command);
}
catch (Exception ex)
{
await command.FollowupAsync("Something failed while trying to do that...");
Console.WriteLine(ex);
}
}
protected abstract Task Invoke(SocketSlashCommand command);
protected bool IsSenderAdmin(SocketSlashCommand command)
{
return Program.AdminChecker.IsUserAdmin(command.User.Id);
}
protected ulong GetUserId(UserOption userOption, SocketSlashCommand command)
{
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; }
}
}

View File

@ -0,0 +1,45 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech
{
public abstract class BaseNetCommand : BaseCommand
{
private readonly DeploymentsFilesMonitor monitor;
private readonly CoreInterface ci;
public BaseNetCommand(DeploymentsFilesMonitor monitor, CoreInterface ci)
{
this.monitor = monitor;
this.ci = ci;
}
protected override async Task Invoke(SocketSlashCommand command)
{
var deployments = monitor.GetDeployments();
if (deployments.Length == 0)
{
await 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!");
return;
}
var codexDeployment = deployments.Single();
var gethDeployment = codexDeployment.GethDeployment;
var contractsDeployment = codexDeployment.CodexContractsDeployment;
var gethNode = ci.WrapGethDeployment(gethDeployment);
var contracts = ci.WrapCodexContractsDeployment(contractsDeployment);
await Execute(command, gethNode, contracts);
}
protected abstract Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts);
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,63 @@
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);
Program.AdminChecker.SetGuild(guild);
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: option.IsRequired);
}
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,35 @@
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

@ -0,0 +1,38 @@
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

@ -0,0 +1,43 @@
using Discord.WebSocket;
using GethPlugin;
using Nethereum.Util;
namespace BiblioTech.Commands
{
public class EthAddressOption : CommandOption
{
public EthAddressOption()
: base(name: "ethaddress",
description: "Ethereum address starting with '0x'.",
type: Discord.ApplicationCommandOptionType.String,
isRequired: true)
{
}
public async Task<EthAddress?> Parse(SocketSlashCommand command)
{
var ethOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
if (ethOptionData == null)
{
await 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.");
return null;
}
if (!AddressUtil.Current.IsValidAddressLength(ethAddressStr) ||
!AddressUtil.Current.IsValidEthereumAddressHexFormat(ethAddressStr) ||
!AddressUtil.Current.IsChecksumAddress(ethAddressStr))
{
await command.FollowupAsync("EthAddress is not valid.");
return null;
}
return new EthAddress(ethAddressStr);
}
}
}

View File

@ -0,0 +1,42 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
{
public class GetBalanceCommand : BaseNetCommand
{
private readonly UserAssociateCommand userAssociateCommand;
private readonly UserOption optionalUser = new UserOption(
description: "If set, get balance for another user. (Optional, admin-only)",
isRequired: false);
public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(monitor, ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "balance";
public override string StartingMessage => "Fetching balance...";
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)
{
var userId = GetUserId(optionalUser, command);
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.");
return;
}
var eth = gethNode.GetEthBalance(addr);
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
await command.FollowupAsync($"{command.User.Username} has {eth} and {testTokens}.");
}
}
}

View File

@ -0,0 +1,85 @@
using CodexContractsPlugin;
using Core;
using Discord.WebSocket;
using GethPlugin;
namespace BiblioTech.Commands
{
public class MintCommand : BaseNetCommand
{
private readonly Ether defaultEthToSend = 10.Eth();
private readonly TestToken defaultTestTokensToMint = 1024.TestTokens();
private readonly UserOption optionalUser = new UserOption(
description: "If set, mint tokens for this user. (Optional, admin-only)",
isRequired: false);
private readonly UserAssociateCommand userAssociateCommand;
public MintCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand)
: base(monitor, ci)
{
this.userAssociateCommand = userAssociateCommand;
}
public override string Name => "mint";
public override string StartingMessage => "Minting some tokens...";
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)
{
var userId = GetUserId(optionalUser, command);
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.");
return;
}
var report = new List<string>();
var sentEth = ProcessEth(gethNode, addr, report);
var mintedTokens = ProcessTokens(gethNode, contracts, addr, report);
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
await command.FollowupAsync(string.Join(Environment.NewLine, report));
}
private TestToken ProcessTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr, List<string> report)
{
if (ShouldMintTestTokens(gethNode, contracts, addr))
{
contracts.MintTestTokens(gethNode, addr, defaultTestTokensToMint);
report.Add($"Minted {defaultTestTokensToMint}.");
return defaultTestTokensToMint;
}
report.Add("TestToken balance over threshold.");
return 0.TestTokens();
}
private Ether ProcessEth(IGethNode gethNode, EthAddress addr, List<string> report)
{
if (ShouldSendEth(gethNode, addr))
{
gethNode.SendEth(addr, defaultEthToSend);
report.Add($"Sent {defaultEthToSend}.");
return defaultEthToSend;
}
report.Add("Eth balance is over threshold.");
return 0.Eth();
}
private bool ShouldMintTestTokens(IGethNode gethNode, ICodexContracts contracts, EthAddress addr)
{
var testTokens = contracts.GetTestTokenBalance(gethNode, addr);
return testTokens.Amount < 64m;
}
private bool ShouldSendEth(IGethNode gethNode, EthAddress addr)
{
var eth = gethNode.GetEthBalance(addr);
return eth.Eth < 1.0m;
}
}
}

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,34 @@
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class UserAssociateCommand : BaseCommand
{
private readonly EthAddressOption ethOption = new EthAddressOption();
private readonly UserOption optionalUser = new UserOption(
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
isRequired: false);
public override string Name => "set";
public override string StartingMessage => "hold on...";
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)
{
var userId = GetUserId(optionalUser, command);
var data = await ethOption.Parse(command);
if (data == null) return;
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(userId);
if (currentAddress != null && !IsSenderAdmin(command))
{
await 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!");
}
}
}

View File

@ -0,0 +1,22 @@
using Discord;
using Discord.WebSocket;
namespace BiblioTech.Commands
{
public class UserOption : CommandOption
{
public UserOption(string description, bool isRequired)
: base("user", description, ApplicationCommandOptionType.User, isRequired)
{
}
public ulong? GetOptionUserId(SocketSlashCommand command)
{
var userOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name);
if (userOptionData == null) return null;
var user = userOptionData.Value as IUser;
if (user == null) return null;
return user.Id;
}
}
}

View File

@ -0,0 +1,22 @@
using ArgsUniform;
namespace BiblioTech
{
public class Configuration
{
[Uniform("token", "t", "TOKEN", true, "Discord Application Token")]
public string ApplicationToken { get; set; } = string.Empty;
[Uniform("server-name", "sn", "SERVERNAME", true, "Name of the Discord server")]
public string ServerName { get; set; } = string.Empty;
[Uniform("endpoints", "e", "ENDPOINTS", false, "Path where endpoint JSONs are located. Also accepts codex-deployment JSONs.")]
public string EndpointsPath { get; set; } = "endpoints";
[Uniform("userdata", "u", "USERDATA", false, "Path where user data files will be saved.")]
public string UserDataPath { get; set; } = "userdata";
[Uniform("admin-role", "a", "ADMINROLE", true, "Name of the Discord server admin role")]
public string AdminRoleName { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,51 @@
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

@ -0,0 +1,79 @@
using ArgsUniform;
using BiblioTech.Commands;
using Core;
using Discord;
using Discord.WebSocket;
using Logging;
namespace BiblioTech
{
public class Program
{
private DiscordSocketClient client = null!;
public static Configuration Config { get; private set; } = null!;
public static DeploymentsFilesMonitor DeploymentFilesMonitor { get; } = new DeploymentsFilesMonitor();
public static UserRepo UserRepo { get; } = new UserRepo();
public static AdminChecker AdminChecker { get; } = new AdminChecker();
public static Task Main(string[] args)
{
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
Config = uniformArgs.Parse();
if (!Directory.Exists(Config.UserDataPath))
{
Directory.CreateDirectory(Config.UserDataPath);
}
return new Program().MainAsync();
}
public async Task MainAsync()
{
Console.WriteLine("Starting Codex Discord Bot...");
client = new DiscordSocketClient();
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,
operationTimeout: TimeSpan.FromMinutes(5),
retryDelay: TimeSpan.FromSeconds(10),
kubernetesNamespace: "not-applicable"), "datafiles");
var monitor = new DeploymentsFilesMonitor();
var ci = entryPoint.CreateInterface();
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)
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
Console.WriteLine("Running...");
await Task.Delay(-1);
}
private static void PrintHelp()
{
Console.WriteLine("BiblioTech - Codex Discord Bot");
}
private Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,165 @@
using CodexContractsPlugin;
using GethPlugin;
using Newtonsoft.Json;
namespace BiblioTech
{
public class UserRepo
{
private readonly object repoLock = new object();
public void AssociateUserWithAddress(ulong discordId, EthAddress address)
{
lock (repoLock)
{
SetUserAddress(discordId, address);
}
}
public void ClearUserAssociatedAddress(ulong discordId)
{
lock (repoLock)
{
SetUserAddress(discordId, null);
}
}
public void AddMintEventForUser(ulong discordId, EthAddress usedAddress, Ether eth, TestToken tokens)
{
lock (repoLock)
{
var user = GetOrCreate(discordId);
user.MintEvents.Add(new UserMintEvent(DateTime.UtcNow, usedAddress, eth, tokens));
SaveUser(user);
}
}
public EthAddress? GetCurrentAddressForUser(ulong discordId)
{
lock (repoLock)
{
return GetOrCreate(discordId).CurrentAddress;
}
}
public string[] GetInteractionReport(ulong discordId)
{
var result = new List<string>();
lock (repoLock)
{
var filename = GetFilename(discordId);
if (!File.Exists(filename))
{
result.Add("User has not joined the test net.");
}
else
{
var user = JsonConvert.DeserializeObject<User>(File.ReadAllText(filename));
if (user == null)
{
result.Add("Failed to load user records.");
}
else
{
result.Add("User joined on " + user.CreatedUtc.ToString("o"));
result.Add("Current address: " + user.CurrentAddress);
foreach (var ae in user.AssociateEvents)
{
result.Add($"{ae.Utc.ToString("o")} - Address set to: {ae.NewAddress}");
}
foreach (var me in user.MintEvents)
{
result.Add($"{me.Utc.ToString("o")} - Minted {me.EthReceived} and {me.TestTokensMinted} to {me.UsedAddress}.");
}
}
}
}
return result.ToArray();
}
private void SetUserAddress(ulong discordId, EthAddress? address)
{
var user = GetOrCreate(discordId);
user.CurrentAddress = address;
user.AssociateEvents.Add(new UserAssociateAddressEvent(DateTime.UtcNow, address));
SaveUser(user);
}
private User GetOrCreate(ulong discordId)
{
var filename = GetFilename(discordId);
if (!File.Exists(filename))
{
return CreateAndSaveNewUser(discordId);
}
return JsonConvert.DeserializeObject<User>(File.ReadAllText(filename))!;
}
private User CreateAndSaveNewUser(ulong discordId)
{
var newUser = new User(discordId, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
SaveUser(newUser);
return newUser;
}
private void SaveUser(User user)
{
var filename = GetFilename(user.DiscordId);
if (File.Exists(filename)) File.Delete(filename);
File.WriteAllText(filename, JsonConvert.SerializeObject(user));
}
private static string GetFilename(ulong discordId)
{
return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json");
}
}
public class User
{
public User(ulong discordId, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
{
DiscordId = discordId;
CreatedUtc = createdUtc;
CurrentAddress = currentAddress;
AssociateEvents = associateEvents;
MintEvents = mintEvents;
}
public ulong DiscordId { get; }
public DateTime CreatedUtc { get; }
public EthAddress? CurrentAddress { get; set; }
public List<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
}
public class UserAssociateAddressEvent
{
public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress)
{
Utc = utc;
NewAddress = newAddress;
}
public DateTime Utc { get; }
public EthAddress? NewAddress { get; }
}
public class UserMintEvent
{
public UserMintEvent(DateTime utc, EthAddress usedAddress, Ether ethReceived, TestToken testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Ether EthReceived { get; }
public TestToken TestTokensMinted { get; }
}
}

View File

@ -0,0 +1,2 @@
docker build -f docker/Dockerfile -t thatbenbierens/codex-discordbot:initial ../..
docker push thatbenbierens/codex-discordbot:initial

View File

@ -0,0 +1,7 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0
WORKDIR app
COPY ./Tools/BiblioTech ./Tools/BiblioTech
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
CMD ["dotnet", "run", "--project", "Tools/BiblioTech"]

View File

@ -0,0 +1,7 @@
services:
bibliotech-discordbot:
image: thatbenbierens/codex-discordbot:initial
environment:
- TOKEN=tokenplz
- SERVERNAME=ThatBen's server
- ADMINROLE=adminers

View File

@ -8,6 +8,9 @@ namespace CodexNetDeployer
public const int SecondsIn1Day = 24 * 60 * 60; public const int SecondsIn1Day = 24 * 60 * 60;
public const int TenMinutes = 10 * 60; public const int TenMinutes = 10 * 60;
[Uniform("deploy-name", "nm", "DEPLOYNAME", false, "Name of the deployment. (optional)")]
public string DeploymentName { get; set; } = "unnamed";
[Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")] [Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null"; public string KubeConfigFile { get; set; } = "null";

View File

@ -79,7 +79,7 @@ namespace CodexNetDeployer
CheckContainerRestarts(startResults); CheckContainerRestarts(startResults);
var codexContainers = startResults.Select(s => s.CodexNode.Container).ToArray(); var codexContainers = startResults.Select(s => s.CodexNode.Container).ToArray();
return new CodexDeployment(codexContainers, gethDeployment, metricsService, CreateMetadata(startUtc)); return new CodexDeployment(codexContainers, gethDeployment, contractsDeployment, metricsService, CreateMetadata(startUtc));
} }
private EntryPoint CreateEntryPoint(ILog log) private EntryPoint CreateEntryPoint(ILog log)
@ -151,6 +151,7 @@ namespace CodexNetDeployer
private DeploymentMetadata CreateMetadata(DateTime startUtc) private DeploymentMetadata CreateMetadata(DateTime startUtc)
{ {
return new DeploymentMetadata( return new DeploymentMetadata(
name: config.DeploymentName,
startUtc: startUtc, startUtc: startUtc,
finishedUtc: DateTime.UtcNow, finishedUtc: DateTime.UtcNow,
kubeNamespace: config.KubeNamespace, kubeNamespace: config.KubeNamespace,

View File

@ -1,4 +1,5 @@
dotnet run \ dotnet run \
--deploy-name=codex-continuous-test-deployment \
--kube-config=/opt/kubeconfig.yaml \ --kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-continuous-tests \ --kube-namespace=codex-continuous-tests \
--deploy-file=codex-deployment.json \ --deploy-file=codex-deployment.json \

View File

@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistTestCore", "Tests\DistT
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexNetDeployer", "Tools\CodexNetDeployer\CodexNetDeployer.csproj", "{3417D508-E2F4-4974-8988-BB124046D9E2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexNetDeployer", "Tools\CodexNetDeployer\CodexNetDeployer.csproj", "{3417D508-E2F4-4974-8988-BB124046D9E2}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BiblioTech", "Tools\BiblioTech\BiblioTech.csproj", "{078ABA6D-A04E-4F62-A44C-EA66F1B66548}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -113,6 +115,10 @@ Global
{3417D508-E2F4-4974-8988-BB124046D9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {3417D508-E2F4-4974-8988-BB124046D9E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.Build.0 = Release|Any CPU {3417D508-E2F4-4974-8988-BB124046D9E2}.Release|Any CPU.Build.0 = Release|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Debug|Any CPU.Build.0 = Debug|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.ActiveCfg = Release|Any CPU
{078ABA6D-A04E-4F62-A44C-EA66F1B66548}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -134,6 +140,7 @@ Global
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64} = {88C2A621-8A98-4D07-8625-7900FC8EF89E} {562EC700-6984-4C9A-83BF-3BF4E3EB1A64} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{E849B7BA-FDCC-4CFF-998F-845ED2F1BF40} = {88C2A621-8A98-4D07-8625-7900FC8EF89E} {E849B7BA-FDCC-4CFF-998F-845ED2F1BF40} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{3417D508-E2F4-4974-8988-BB124046D9E2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} {3417D508-E2F4-4974-8988-BB124046D9E2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{078ABA6D-A04E-4F62-A44C-EA66F1B66548} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}