mirror of
https://github.com/codex-storage/cs-codex-dist-tests.git
synced 2025-02-03 07:43:52 +00:00
Merge branch 'feature/public-testnet-deploying'
This commit is contained in:
commit
b143136590
@ -84,6 +84,31 @@ namespace KubernetesWorkflow
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] GetUsedExternalPorts()
|
||||
{
|
||||
return client.Run(c =>
|
||||
{
|
||||
var result = new List<int>();
|
||||
|
||||
var services = c.ListServiceForAllNamespaces();
|
||||
var nodePorts = services.Items.Where(s => s.Spec.Type == "NodePort").ToArray();
|
||||
if (!nodePorts.Any()) return result.ToArray();
|
||||
|
||||
foreach (var service in nodePorts)
|
||||
{
|
||||
foreach (var port in service.Spec.Ports)
|
||||
{
|
||||
if (port.NodePort.HasValue)
|
||||
{
|
||||
result.Add(port.NodePort.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteAllNamespacesStartingWith(string prefix)
|
||||
{
|
||||
log.Debug();
|
||||
@ -704,17 +729,17 @@ namespace KubernetesWorkflow
|
||||
|
||||
private RunningService? CreateInternalService(ContainerRecipe[] recipes)
|
||||
{
|
||||
return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int");
|
||||
return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int", false);
|
||||
}
|
||||
|
||||
private RunningService? CreateExternalService(ContainerRecipe[] recipes)
|
||||
{
|
||||
return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext");
|
||||
return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext", true);
|
||||
}
|
||||
|
||||
private RunningService? CreateService(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix)
|
||||
private RunningService? CreateService(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix, bool isNodePort)
|
||||
{
|
||||
var ports = CreateServicePorts(recipes, portSelector);
|
||||
var ports = CreateServicePorts(recipes, portSelector, isNodePort);
|
||||
if (!ports.Any()) return null;
|
||||
|
||||
var serviceSpec = new V1Service
|
||||
@ -788,7 +813,7 @@ namespace KubernetesWorkflow
|
||||
};
|
||||
}
|
||||
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector)
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, bool isNodePort)
|
||||
{
|
||||
var result = new List<V1ServicePort>();
|
||||
foreach (var recipe in recipes)
|
||||
@ -796,29 +821,33 @@ namespace KubernetesWorkflow
|
||||
var ports = portSelector(recipe);
|
||||
foreach (var port in ports)
|
||||
{
|
||||
result.AddRange(CreateServicePorts(recipe, port));
|
||||
result.AddRange(CreateServicePorts(recipe, port, isNodePort));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort)
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort, bool isNodePort)
|
||||
{
|
||||
var result = new List<V1ServicePort>();
|
||||
if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP");
|
||||
if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP");
|
||||
if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP", isNodePort);
|
||||
if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP", isNodePort);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol)
|
||||
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort)
|
||||
{
|
||||
result.Add(new V1ServicePort
|
||||
var p = new V1ServicePort
|
||||
{
|
||||
Name = GetNameForPort(recipe, port),
|
||||
Protocol = protocol,
|
||||
Port = port.Number,
|
||||
TargetPort = GetNameForPort(recipe, port)
|
||||
});
|
||||
};
|
||||
|
||||
if (isNodePort) p.NodePort = port.Number;
|
||||
|
||||
result.Add(p);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -56,17 +56,17 @@ namespace KubernetesWorkflow.Recipe
|
||||
|
||||
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(tag, protocol));
|
||||
return AddExposedPort(factory.CreateExternalPort(tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(number, tag, protocol));
|
||||
return AddExposedPort(factory.CreateExternalPort(number, tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
var p = factory.CreatePort(tag, protocol);
|
||||
var p = factory.CreateInternalPort(tag, protocol);
|
||||
internalPorts.Add(p);
|
||||
return p;
|
||||
}
|
||||
|
@ -5,16 +5,36 @@ namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class RecipeComponentFactory
|
||||
{
|
||||
private NumberSource portNumberSource = new NumberSource(8080);
|
||||
private readonly NumberSource internalNumberSource = new NumberSource(8080);
|
||||
private static readonly NumberSource externalNumberSource = new NumberSource(30000);
|
||||
private static int[] usedExternalPorts = Array.Empty<int>();
|
||||
|
||||
public Port CreatePort(int number, string tag, PortProtocol protocol)
|
||||
public void Update(K8sController controller)
|
||||
{
|
||||
usedExternalPorts = controller.GetUsedExternalPorts();
|
||||
}
|
||||
|
||||
public Port CreateInternalPort(string tag, PortProtocol protocol)
|
||||
{
|
||||
return new Port(internalNumberSource.GetNextNumber(), tag, protocol);
|
||||
}
|
||||
|
||||
public Port CreateExternalPort(int number, string tag, PortProtocol protocol)
|
||||
{
|
||||
if (usedExternalPorts.Contains(number)) throw new Exception($"External port number {number} is already in use by the cluster.");
|
||||
return new Port(number, tag, protocol);
|
||||
}
|
||||
|
||||
public Port CreatePort(string tag, PortProtocol protocol)
|
||||
public Port CreateExternalPort(string tag, PortProtocol protocol)
|
||||
{
|
||||
return new Port(portNumberSource.GetNextNumber(), tag, protocol);
|
||||
while (true)
|
||||
{
|
||||
var number = externalNumberSource.GetNextNumber();
|
||||
if (!usedExternalPorts.Contains(number))
|
||||
{
|
||||
return new Port(number, tag, protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EnvVar CreateEnvVar(string name, int value)
|
||||
|
@ -54,12 +54,19 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
return K8s(controller =>
|
||||
{
|
||||
componentFactory.Update(controller);
|
||||
|
||||
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
|
||||
var startResult = controller.BringOnline(recipes, location);
|
||||
var containers = CreateContainers(startResult, recipes, startupConfig);
|
||||
|
||||
var rc = new RunningContainers(startupConfig, startResult, containers);
|
||||
cluster.Configuration.Hooks.OnContainersStarted(rc);
|
||||
|
||||
if (startResult.ExternalService != null)
|
||||
{
|
||||
componentFactory.Update(controller);
|
||||
}
|
||||
return rc;
|
||||
});
|
||||
}
|
||||
@ -222,7 +229,7 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex));
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -238,7 +245,7 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex));
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,9 @@ namespace Logging
|
||||
|
||||
public virtual void Error(string message)
|
||||
{
|
||||
Log($"[ERROR] {message}");
|
||||
var msg = $"[ERROR] {message}";
|
||||
Console.WriteLine(msg);
|
||||
Log(msg);
|
||||
}
|
||||
|
||||
public virtual void AddStringReplace(string from, string to)
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
public override void Error(string message)
|
||||
{
|
||||
Console.WriteLine("Error: " + message);
|
||||
base.Error(message);
|
||||
}
|
||||
|
||||
|
@ -19,12 +19,24 @@ namespace CodexDiscordBotPlugin
|
||||
AddEnvVar("SERVERNAME", config.ServerName);
|
||||
AddEnvVar("ADMINROLE", config.AdminRoleName);
|
||||
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName);
|
||||
AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml");
|
||||
AddEnvVar("KUBENAMESPACE", config.KubeNamespace);
|
||||
|
||||
var gethInfo = config.GethInfo;
|
||||
AddEnvVar("GETH_HOST", gethInfo.Host);
|
||||
AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString());
|
||||
AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey);
|
||||
AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
|
||||
|
||||
if (!string.IsNullOrEmpty(config.DataPath))
|
||||
{
|
||||
AddEnvVar("DATAPATH", config.DataPath);
|
||||
AddVolume(config.DataPath, 1.GB());
|
||||
}
|
||||
|
||||
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,15 @@
|
||||
{
|
||||
public class DiscordBotStartupConfig
|
||||
{
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName)
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo)
|
||||
{
|
||||
Name = name;
|
||||
Token = token;
|
||||
ServerName = serverName;
|
||||
AdminRoleName = adminRoleName;
|
||||
AdminChannelName = adminChannelName;
|
||||
KubeNamespace = kubeNamespace;
|
||||
GethInfo = gethInfo;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
@ -16,6 +18,28 @@
|
||||
public string ServerName { get; }
|
||||
public string AdminRoleName { get; }
|
||||
public string AdminChannelName { get; }
|
||||
public string KubeNamespace { get; }
|
||||
public DiscordBotGethInfo GethInfo { get; }
|
||||
public string? DataPath { get; set; }
|
||||
}
|
||||
|
||||
public class DiscordBotGethInfo
|
||||
{
|
||||
public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
PrivKey = privKey;
|
||||
MarketplaceAddress = marketplaceAddress;
|
||||
TokenAddress = tokenAddress;
|
||||
Abi = abi;
|
||||
}
|
||||
|
||||
public string Host { get; }
|
||||
public int Port { get; }
|
||||
public string PrivKey { get; }
|
||||
public string MarketplaceAddress { get; }
|
||||
public string TokenAddress { get; }
|
||||
public string Abi { get; }
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ namespace CodexPlugin
|
||||
public class CodexDebugResponse
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public string[] addrs { get; set; } = new string[0];
|
||||
public string[] addrs { get; set; } = Array.Empty<string>();
|
||||
public string repo { get; set; } = string.Empty;
|
||||
public string spr { get; set; } = string.Empty;
|
||||
public string[] announceAddresses { get; set; } = Array.Empty<string>();
|
||||
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
|
||||
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
|
||||
public CodexDebugVersionResponse codex { get; set; } = new();
|
||||
|
@ -48,8 +48,9 @@ namespace CodexPlugin
|
||||
|
||||
if (config.PublicTestNet != null)
|
||||
{
|
||||
AddEnvVar("CODEX_NAT", config.PublicTestNet.PublicNatIP);
|
||||
// This makes the node announce itself to its public IP address.
|
||||
AddEnvVar("NAT_IP_AUTO", "false");
|
||||
AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -133,7 +133,7 @@ namespace CodexPlugin
|
||||
|
||||
private IEnumerable<string> DescribeArgs()
|
||||
{
|
||||
if (PublicTestNet != null) yield return $"<!>Public TestNet at {PublicTestNet.PublicNatIP}:{PublicTestNet.PublicListenPort}<!>";
|
||||
if (PublicTestNet != null) yield return $"<!>Public TestNet with listenPort: {PublicTestNet.PublicListenPort}<!>";
|
||||
yield return $"LogLevel={LogLevelWithTopics()}";
|
||||
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
|
||||
if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}";
|
||||
|
@ -65,7 +65,6 @@ namespace CodexPlugin
|
||||
|
||||
public class CodexTestNetConfig
|
||||
{
|
||||
public string PublicNatIP { get; set; } = string.Empty;
|
||||
public int PublicDiscoveryPort { get; set; }
|
||||
public int PublicListenPort { get; set; }
|
||||
}
|
||||
|
@ -32,8 +32,11 @@ namespace GethPlugin
|
||||
|
||||
private string CreateArgs(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsMiner) AddEnvVar("ENABLE_MINER", "1");
|
||||
UnlockAccounts(0, 1);
|
||||
if (config.IsMiner)
|
||||
{
|
||||
AddEnvVar("ENABLE_MINER", "1");
|
||||
UnlockAccounts(0, 1);
|
||||
}
|
||||
|
||||
var httpPort = CreateApiPort(config, tag: HttpPortTag);
|
||||
var discovery = CreateDiscoveryPort(config);
|
||||
@ -41,16 +44,20 @@ namespace GethPlugin
|
||||
var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag);
|
||||
var wsPort = CreateP2pPort(config, tag: WsPortTag);
|
||||
|
||||
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {GetTestNetArgs(config)} {defaultArgs}";
|
||||
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {defaultArgs}";
|
||||
|
||||
if (config.BootstrapNode != null)
|
||||
{
|
||||
var bootPubKey = config.BootstrapNode.PublicKey;
|
||||
var bootIp = config.BootstrapNode.IpAddress;
|
||||
var bootPort = config.BootstrapNode.Port;
|
||||
var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}";
|
||||
var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort}";
|
||||
args += bootstrapArg;
|
||||
}
|
||||
if (config.IsPublicTestNet != null)
|
||||
{
|
||||
AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address);
|
||||
}
|
||||
|
||||
return args + $" --authrpc.port {authRpc.Number} --ws --ws.addr 0.0.0.0 --ws.port {wsPort.Number}";
|
||||
}
|
||||
@ -58,20 +65,13 @@ namespace GethPlugin
|
||||
private void UnlockAccounts(int startIndex, int numberOfAccounts)
|
||||
{
|
||||
if (startIndex < 0) throw new ArgumentException();
|
||||
if (numberOfAccounts < 1) throw new ArgumentException();
|
||||
if (numberOfAccounts < 0) throw new ArgumentException();
|
||||
if (startIndex + numberOfAccounts > 1000) throw new ArgumentException("Out of accounts!");
|
||||
|
||||
AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString());
|
||||
AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.ToString());
|
||||
}
|
||||
|
||||
private string GetTestNetArgs(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsPublicTestNet == null) return string.Empty;
|
||||
|
||||
return $"--nat=extip:{config.IsPublicTestNet.PublicIp}";
|
||||
}
|
||||
|
||||
private Port CreateDiscoveryPort(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag);
|
||||
|
@ -19,13 +19,14 @@ namespace GethPlugin
|
||||
void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
decimal? GetSyncedBlockNumber();
|
||||
bool IsContractAvailable(string abi, string contractAddress);
|
||||
GethBootstrapNode GetBootstrapRecord();
|
||||
}
|
||||
|
||||
public class GethNode : IGethNode
|
||||
public class DeploymentGethNode : BaseGethNode, IGethNode
|
||||
{
|
||||
private readonly ILog log;
|
||||
|
||||
public GethNode(ILog log, GethDeployment startResult)
|
||||
public DeploymentGethNode(ILog log, GethDeployment startResult)
|
||||
{
|
||||
this.log = log;
|
||||
StartResult = startResult;
|
||||
@ -34,6 +35,59 @@ namespace GethPlugin
|
||||
public GethDeployment StartResult { get; }
|
||||
public RunningContainer Container => StartResult.Container;
|
||||
|
||||
public GethBootstrapNode GetBootstrapRecord()
|
||||
{
|
||||
var address = StartResult.Container.GetInternalAddress(GethContainerRecipe.ListenPortTag);
|
||||
|
||||
return new GethBootstrapNode(
|
||||
publicKey: StartResult.PubKey,
|
||||
ipAddress: address.Host.Replace("http://", ""),
|
||||
port: address.Port
|
||||
);
|
||||
}
|
||||
|
||||
protected override NethereumInteraction StartInteraction()
|
||||
{
|
||||
var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag);
|
||||
var account = StartResult.Account;
|
||||
|
||||
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomGethNode : BaseGethNode, IGethNode
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly string gethHost;
|
||||
private readonly int gethPort;
|
||||
private readonly string privateKey;
|
||||
|
||||
public GethDeployment StartResult => throw new NotImplementedException();
|
||||
public RunningContainer Container => throw new NotImplementedException();
|
||||
|
||||
public CustomGethNode(ILog log, string gethHost, int gethPort, string privateKey)
|
||||
{
|
||||
this.log = log;
|
||||
this.gethHost = gethHost;
|
||||
this.gethPort = gethPort;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public GethBootstrapNode GetBootstrapRecord()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override NethereumInteraction StartInteraction()
|
||||
{
|
||||
var creator = new NethereumInteractionCreator(log, gethHost, gethPort, privateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseGethNode
|
||||
{
|
||||
public Ether GetEthBalance()
|
||||
{
|
||||
return StartInteraction().GetEthBalance().Eth();
|
||||
@ -69,15 +123,6 @@ namespace GethPlugin
|
||||
StartInteraction().SendTransaction(contractAddress, function);
|
||||
}
|
||||
|
||||
private NethereumInteraction StartInteraction()
|
||||
{
|
||||
var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag);
|
||||
var account = StartResult.Account;
|
||||
|
||||
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
|
||||
public decimal? GetSyncedBlockNumber()
|
||||
{
|
||||
return StartInteraction().GetSyncedBlockNumber();
|
||||
@ -87,5 +132,7 @@ namespace GethPlugin
|
||||
{
|
||||
return StartInteraction().IsContractAvailable(abi, contractAddress);
|
||||
}
|
||||
|
||||
protected abstract NethereumInteraction StartInteraction();
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ namespace GethPlugin
|
||||
public IGethNode WrapGethContainer(GethDeployment startResult)
|
||||
{
|
||||
startResult = SerializeGate.Gate(startResult);
|
||||
return new GethNode(tools.GetLog(), startResult);
|
||||
return new DeploymentGethNode(tools.GetLog(), startResult);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
|
@ -3,6 +3,7 @@
|
||||
public interface IGethSetup
|
||||
{
|
||||
IGethSetup IsMiner();
|
||||
IGethSetup WithBootstrapNode(IGethNode node);
|
||||
IGethSetup WithBootstrapNode(GethBootstrapNode node);
|
||||
IGethSetup WithName(string name);
|
||||
IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig);
|
||||
@ -15,6 +16,11 @@
|
||||
public string? NameOverride { get; private set; }
|
||||
public GethTestNetConfig? IsPublicTestNet { get; private set; }
|
||||
|
||||
public IGethSetup WithBootstrapNode(IGethNode node)
|
||||
{
|
||||
return WithBootstrapNode(node.GetBootstrapRecord());
|
||||
}
|
||||
|
||||
public IGethSetup WithBootstrapNode(GethBootstrapNode node)
|
||||
{
|
||||
BootstrapNode = node;
|
||||
@ -42,14 +48,12 @@
|
||||
|
||||
public class GethTestNetConfig
|
||||
{
|
||||
public GethTestNetConfig(string publicIp, int discoveryPort, int listenPort)
|
||||
public GethTestNetConfig(int discoveryPort, int listenPort)
|
||||
{
|
||||
PublicIp = publicIp;
|
||||
DiscoveryPort = discoveryPort;
|
||||
ListenPort = listenPort;
|
||||
}
|
||||
|
||||
public string PublicIp { get; }
|
||||
public int DiscoveryPort { get; }
|
||||
public int ListenPort { get; }
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ namespace MetricsPlugin
|
||||
|
||||
public RunningContainers CollectMetricsFor(IMetricsScrapeTarget[] targets)
|
||||
{
|
||||
if (!targets.Any()) throw new ArgumentException(nameof(targets) + " must not be empty.");
|
||||
|
||||
Log($"Starting metrics server for {targets.Length} targets...");
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets)));
|
||||
|
@ -97,5 +97,22 @@ namespace CodexTests.BasicTests
|
||||
|
||||
CheckLogForErrors(seller, buyer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GethBootstrapTest()
|
||||
{
|
||||
var boot = Ci.StartGethNode(s => s.WithName("boot").IsMiner());
|
||||
var disconnected = Ci.StartGethNode(s => s.WithName("disconnected"));
|
||||
var follow = Ci.StartGethNode(s => s.WithBootstrapNode(boot).WithName("follow"));
|
||||
|
||||
Thread.Sleep(12000);
|
||||
|
||||
var bootN = boot.GetSyncedBlockNumber();
|
||||
var discN = disconnected.GetSyncedBlockNumber();
|
||||
var followN = follow.GetSyncedBlockNumber();
|
||||
|
||||
Assert.That(bootN, Is.EqualTo(followN));
|
||||
Assert.That(discN, Is.LessThan(bootN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public abstract class BaseCodexCommand : BaseDeploymentCommand
|
||||
{
|
||||
private readonly CoreInterface ci;
|
||||
|
||||
public BaseCodexCommand(CoreInterface ci)
|
||||
{
|
||||
this.ci = ci;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment)
|
||||
{
|
||||
var codexContainers = codexDeployment.CodexInstances.Select(c => c.Containers).ToArray();
|
||||
|
||||
var group = ci.WrapCodexContainers(codexContainers);
|
||||
|
||||
await Execute(context, group);
|
||||
}
|
||||
|
||||
protected abstract Task Execute(CommandContext context, ICodexNodeGroup codexGroup);
|
||||
}
|
||||
}
|
@ -9,13 +9,7 @@ namespace BiblioTech
|
||||
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 virtual CommandOption[] Options => Array.Empty<CommandOption>();
|
||||
|
||||
public async Task SlashCommandHandler(SocketSlashCommand command)
|
||||
{
|
||||
@ -29,7 +23,15 @@ namespace BiblioTech
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true);
|
||||
if (IsInAdminChannel(command))
|
||||
{
|
||||
var msg = "Failed with exception: " + ex;
|
||||
await command.FollowupAsync(msg.Substring(0, Math.Min(1900, msg.Length)));
|
||||
}
|
||||
else
|
||||
{
|
||||
await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true);
|
||||
}
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexPlugin;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public abstract class BaseDeploymentCommand : BaseCommand
|
||||
{
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
var proceed = await OnInvoke(context);
|
||||
if (!proceed) return;
|
||||
|
||||
var deployments = Program.DeploymentFilesMonitor.GetDeployments();
|
||||
if (deployments.Length == 0)
|
||||
{
|
||||
await context.Followup("No deployments are currently available.");
|
||||
return;
|
||||
}
|
||||
if (deployments.Length > 1)
|
||||
{
|
||||
await context.Followup("Multiple deployments are online. I don't know which one to pick!");
|
||||
return;
|
||||
}
|
||||
|
||||
var codexDeployment = deployments.Single();
|
||||
await ExecuteDeploymentCommand(context, codexDeployment);
|
||||
}
|
||||
|
||||
protected abstract Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment);
|
||||
|
||||
protected virtual Task<bool> OnInvoke(CommandContext context)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +1,89 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexContractsPlugin;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public abstract class BaseGethCommand : BaseDeploymentCommand
|
||||
public static class GethInput
|
||||
{
|
||||
private readonly CoreInterface ci;
|
||||
private const string GethHostVar = "GETH_HOST";
|
||||
private const string GethPortVar = "GETH_HTTP_PORT";
|
||||
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
|
||||
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
|
||||
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
|
||||
private const string AbiVar = "CODEXCONTRACTS_ABI";
|
||||
|
||||
public BaseGethCommand(CoreInterface ci)
|
||||
static GethInput()
|
||||
{
|
||||
this.ci = ci;
|
||||
var error = new List<string>();
|
||||
var gethHost = GetEnvVar(error, GethHostVar);
|
||||
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
|
||||
var privateKey = GetEnvVar(error, GethPrivKeyVar);
|
||||
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
|
||||
var tokenAddress = GetEnvVar(error, TokenAddressVar);
|
||||
var abi = GetEnvVar(error, AbiVar);
|
||||
|
||||
if (error.Any())
|
||||
{
|
||||
LoadError = string.Join(", ", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
GethHost = gethHost!;
|
||||
GethPort = gethPort;
|
||||
PrivateKey = privateKey!;
|
||||
MarketplaceAddress = marketplaceAddress!;
|
||||
TokenAddress = tokenAddress!;
|
||||
ABI = abi!;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteDeploymentCommand(CommandContext context, CodexDeployment codexDeployment)
|
||||
{
|
||||
var gethDeployment = codexDeployment.GethDeployment;
|
||||
var contractsDeployment = codexDeployment.CodexContractsDeployment;
|
||||
public static string GethHost { get; } = string.Empty;
|
||||
public static int GethPort { get; }
|
||||
public static string PrivateKey { get; } = string.Empty;
|
||||
public static string MarketplaceAddress { get; } = string.Empty;
|
||||
public static string TokenAddress { get; } = string.Empty;
|
||||
public static string ABI { get; } = string.Empty;
|
||||
public static string LoadError { get; } = string.Empty;
|
||||
|
||||
var gethNode = ci.WrapGethDeployment(gethDeployment);
|
||||
var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment);
|
||||
private static string? GetEnvVar(List<string> error, string name)
|
||||
{
|
||||
var result = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseGethCommand : BaseCommand
|
||||
{
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
var log = new ConsoleLog();
|
||||
|
||||
if (!string.IsNullOrEmpty(GethInput.LoadError))
|
||||
{
|
||||
var msg = "Geth input incorrect: " + GethInput.LoadError;
|
||||
log.Error(msg);
|
||||
if (IsInAdminChannel(context.Command))
|
||||
{
|
||||
await context.Followup(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.Followup("I'm sorry, there seems to be a configuration error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var contractsDeployment = new CodexContractsDeployment(
|
||||
marketplaceAddress: GethInput.MarketplaceAddress,
|
||||
abi: GethInput.ABI,
|
||||
tokenAddress: GethInput.TokenAddress
|
||||
);
|
||||
|
||||
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
|
||||
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
|
||||
|
||||
await Execute(context, gethNode, contracts);
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
@ -9,17 +6,16 @@ namespace BiblioTech.Commands
|
||||
{
|
||||
private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand();
|
||||
private readonly ReportCommand reportCommand = new ReportCommand();
|
||||
private readonly DeployListCommand deployListCommand = new DeployListCommand();
|
||||
private readonly DeployUploadCommand deployUploadCommand = new DeployUploadCommand();
|
||||
private readonly DeployRemoveCommand deployRemoveCommand = new DeployRemoveCommand();
|
||||
private readonly WhoIsCommand whoIsCommand = new WhoIsCommand();
|
||||
private readonly NetInfoCommand netInfoCommand;
|
||||
private readonly DebugPeerCommand debugPeerCommand;
|
||||
private readonly AddSprCommand addSprCommand;
|
||||
private readonly ClearSprsCommand clearSprsCommand;
|
||||
private readonly GetSprCommand getSprCommand;
|
||||
|
||||
public AdminCommand(CoreInterface ci)
|
||||
public AdminCommand(SprCommand sprCommand)
|
||||
{
|
||||
netInfoCommand = new NetInfoCommand(ci);
|
||||
debugPeerCommand = new DebugPeerCommand(ci);
|
||||
addSprCommand = new AddSprCommand(sprCommand);
|
||||
clearSprsCommand = new ClearSprsCommand(sprCommand);
|
||||
getSprCommand = new GetSprCommand(sprCommand);
|
||||
}
|
||||
|
||||
public override string Name => "admin";
|
||||
@ -30,12 +26,10 @@ namespace BiblioTech.Commands
|
||||
{
|
||||
clearCommand,
|
||||
reportCommand,
|
||||
deployListCommand,
|
||||
deployUploadCommand,
|
||||
deployRemoveCommand,
|
||||
whoIsCommand,
|
||||
netInfoCommand,
|
||||
debugPeerCommand
|
||||
addSprCommand,
|
||||
clearSprsCommand,
|
||||
getSprCommand
|
||||
};
|
||||
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
@ -54,12 +48,10 @@ namespace BiblioTech.Commands
|
||||
|
||||
await clearCommand.CommandHandler(context);
|
||||
await reportCommand.CommandHandler(context);
|
||||
await deployListCommand.CommandHandler(context);
|
||||
await deployUploadCommand.CommandHandler(context);
|
||||
await deployRemoveCommand.CommandHandler(context);
|
||||
await whoIsCommand.CommandHandler(context);
|
||||
await netInfoCommand.CommandHandler(context);
|
||||
await debugPeerCommand.CommandHandler(context);
|
||||
await addSprCommand.CommandHandler(context);
|
||||
await clearSprsCommand.CommandHandler(context);
|
||||
await getSprCommand.CommandHandler(context);
|
||||
}
|
||||
|
||||
public class ClearUserAssociationCommand : SubCommandOption
|
||||
@ -109,108 +101,8 @@ namespace BiblioTech.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var report = string.Join(Environment.NewLine, Program.UserRepo.GetInteractionReport(user));
|
||||
if (report.Length > 1900)
|
||||
{
|
||||
var filename = $"user-{user.Username}.log";
|
||||
await context.FollowupWithAttachement(filename, report);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.Followup(report);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DeployListCommand : SubCommandOption
|
||||
{
|
||||
public DeployListCommand()
|
||||
: base("list", "Lists current deployments.")
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var deployments = Program.DeploymentFilesMonitor.GetDeployments();
|
||||
|
||||
//todo shows old deployments
|
||||
|
||||
if (!deployments.Any())
|
||||
{
|
||||
await context.Followup("No deployments available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var nl = Environment.NewLine;
|
||||
await context.Followup($"Deployments:{nl}{string.Join(nl, 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 FileAttachementOption fileOption = new FileAttachementOption(
|
||||
name: "json",
|
||||
description: "Codex-deployment json to add.",
|
||||
isRequired: true);
|
||||
|
||||
public DeployUploadCommand()
|
||||
: base("add", "Upload a new deployment JSON file.")
|
||||
{
|
||||
}
|
||||
|
||||
public override CommandOption[] Options => new[] { fileOption };
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var file = await fileOption.Parse(context);
|
||||
if (file == null) return;
|
||||
|
||||
var result = await Program.DeploymentFilesMonitor.DownloadDeployment(file);
|
||||
if (result)
|
||||
{
|
||||
await context.Followup("Success!");
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.Followup("That didn't work.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DeployRemoveCommand : SubCommandOption
|
||||
{
|
||||
private readonly StringOption stringOption = new StringOption(
|
||||
name: "name",
|
||||
description: "Name of deployment to remove.",
|
||||
isRequired: true);
|
||||
|
||||
public DeployRemoveCommand()
|
||||
: base("remove", "Removes a deployment file.")
|
||||
{
|
||||
}
|
||||
|
||||
public override CommandOption[] Options => new[] { stringOption };
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var str = await stringOption.Parse(context);
|
||||
if (string.IsNullOrEmpty(str)) return;
|
||||
|
||||
var result = Program.DeploymentFilesMonitor.DeleteDeployment(str);
|
||||
if (result)
|
||||
{
|
||||
await context.Followup("Success!");
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.Followup("That didn't work.");
|
||||
}
|
||||
var report = Program.UserRepo.GetInteractionReport(user);
|
||||
await context.Followup(report);
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,118 +139,76 @@ namespace BiblioTech.Commands
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AdminDeploymentCommand : SubCommandOption
|
||||
public class AddSprCommand : SubCommandOption
|
||||
{
|
||||
private readonly CoreInterface ci;
|
||||
private readonly SprCommand sprCommand;
|
||||
private readonly StringOption stringOption = new StringOption("spr", "Codex SPR", true);
|
||||
|
||||
public AdminDeploymentCommand(CoreInterface ci, string name, string description)
|
||||
: base(name, description)
|
||||
public AddSprCommand(SprCommand sprCommand)
|
||||
: base(name: "addspr",
|
||||
description: "Adds a Codex SPR, to be given to users with '/boot'.")
|
||||
{
|
||||
this.ci = ci;
|
||||
this.sprCommand = sprCommand;
|
||||
}
|
||||
|
||||
protected async Task OnDeployment(CommandContext context, Func<ICodexNodeGroup, string, Task> action)
|
||||
{
|
||||
var deployment = Program.DeploymentFilesMonitor.GetDeployments().SingleOrDefault();
|
||||
if (deployment == null)
|
||||
{
|
||||
await context.Followup("No deployment found.");
|
||||
return;
|
||||
}
|
||||
public override CommandOption[] Options => new[] { stringOption };
|
||||
|
||||
try
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var spr = await stringOption.Parse(context);
|
||||
|
||||
if (!string.IsNullOrEmpty(spr) )
|
||||
{
|
||||
var group = ci.WrapCodexContainers(deployment.CodexInstances.Select(i => i.Containers).ToArray());
|
||||
await action(group, deployment.Metadata.Name);
|
||||
sprCommand.Add(spr);
|
||||
await context.Followup("A-OK!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
await context.Followup("Failed to wrap nodes with exception: " + ex);
|
||||
await context.Followup("SPR is null or empty.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NetInfoCommand : AdminDeploymentCommand
|
||||
public class ClearSprsCommand : SubCommandOption
|
||||
{
|
||||
public NetInfoCommand(CoreInterface ci)
|
||||
: base(ci, name: "netinfo",
|
||||
description: "Fetches info endpoints of codex nodes.")
|
||||
private readonly SprCommand sprCommand;
|
||||
private readonly StringOption stringOption = new StringOption("areyousure", "set to 'true' if you are.", true);
|
||||
|
||||
public ClearSprsCommand(SprCommand sprCommand)
|
||||
: base(name: "clearsprs",
|
||||
description: "Clears all Codex SPRs in the bot. Users won't be able to use '/boot' till new ones are added.")
|
||||
{
|
||||
this.sprCommand = sprCommand;
|
||||
}
|
||||
|
||||
public override CommandOption[] Options => new[] { stringOption };
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var areyousure = await stringOption.Parse(context);
|
||||
|
||||
if (areyousure != "true") return;
|
||||
|
||||
sprCommand.Clear();
|
||||
await context.Followup("Cleared all SPRs.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GetSprCommand: SubCommandOption
|
||||
{
|
||||
private readonly SprCommand sprCommand;
|
||||
|
||||
public GetSprCommand(SprCommand sprCommand)
|
||||
: base(name: "getsprs",
|
||||
description: "Shows all Codex SPRs in the bot.")
|
||||
{
|
||||
this.sprCommand = sprCommand;
|
||||
}
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
await OnDeployment(context, async (group, name) =>
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
var content = new List<string>
|
||||
{
|
||||
$"{DateTime.UtcNow.ToString("o")} - {group.Count()} Codex nodes."
|
||||
};
|
||||
|
||||
foreach (var node in group)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = node.GetDebugInfo();
|
||||
var json = JsonConvert.SerializeObject(info, Formatting.Indented);
|
||||
var jsonInsert = $"{nl}```{nl}{json}{nl}```{nl}";
|
||||
content.Add($"Node '{node.GetName()}' responded with {jsonInsert}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
content.Add($"Node '{node.GetName()}' failed to respond with exception: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
var filename = $"netinfo-{NoWhitespaces(name)}.log";
|
||||
await context.FollowupWithAttachement(filename, string.Join(nl, content.ToArray()));
|
||||
});
|
||||
await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'")));
|
||||
}
|
||||
}
|
||||
|
||||
public class DebugPeerCommand : AdminDeploymentCommand
|
||||
{
|
||||
private readonly StringOption peerIdOption = new StringOption("peerid", "id of peer to try and reach.", true);
|
||||
|
||||
public DebugPeerCommand(CoreInterface ci)
|
||||
: base(ci, name: "debugpeer",
|
||||
description: "Calls debug/peer on each codex node.")
|
||||
{
|
||||
}
|
||||
|
||||
public override CommandOption[] Options => new[] { peerIdOption };
|
||||
|
||||
protected override async Task onSubCommand(CommandContext context)
|
||||
{
|
||||
var peerId = await peerIdOption.Parse(context);
|
||||
if (string.IsNullOrEmpty(peerId)) return;
|
||||
|
||||
await OnDeployment(context, async (group, name) =>
|
||||
{
|
||||
await context.Followup($"Calling debug/peer for '{peerId}' on {group.Count()} Codex nodes.");
|
||||
foreach (var node in group)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = node.GetDebugPeer(peerId);
|
||||
var nl = Environment.NewLine;
|
||||
var json = JsonConvert.SerializeObject(info, Formatting.Indented);
|
||||
var jsonInsert = $"{nl}```{nl}{json}{nl}```{nl}";
|
||||
await context.Followup($"Node '{node.GetName()}' responded with {jsonInsert}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await context.Followup($"Node '{node.GetName()}' failed to respond with exception: " + ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static string NoWhitespaces(string s)
|
||||
{
|
||||
return s.Replace(" ", "-");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexContractsPlugin;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
|
||||
@ -12,8 +13,7 @@ namespace BiblioTech.Commands
|
||||
description: "If set, get balance for another user. (Optional, admin-only)",
|
||||
isRequired: false);
|
||||
|
||||
public GetBalanceCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand)
|
||||
: base(ci)
|
||||
public GetBalanceCommand(UserAssociateCommand userAssociateCommand)
|
||||
{
|
||||
this.userAssociateCommand = userAssociateCommand;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexContractsPlugin;
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
@ -14,8 +13,7 @@ namespace BiblioTech.Commands
|
||||
isRequired: false);
|
||||
private readonly UserAssociateCommand userAssociateCommand;
|
||||
|
||||
public MintCommand(CoreInterface ci, UserAssociateCommand userAssociateCommand)
|
||||
: base(ci)
|
||||
public MintCommand(UserAssociateCommand userAssociateCommand)
|
||||
{
|
||||
this.userAssociateCommand = userAssociateCommand;
|
||||
}
|
||||
|
@ -1,61 +1,48 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class SprCommand : BaseCodexCommand
|
||||
public class SprCommand : BaseCommand
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
private readonly List<string> sprCache = new List<string>();
|
||||
private DateTime lastUpdate = DateTime.MinValue;
|
||||
|
||||
public SprCommand(CoreInterface ci) : base(ci)
|
||||
{
|
||||
}
|
||||
private readonly List<string> knownSprs = new List<string>();
|
||||
|
||||
public override string Name => "boot";
|
||||
public override string StartingMessage => RandomBusyMessage.Get();
|
||||
public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)";
|
||||
|
||||
protected override async Task<bool> OnInvoke(CommandContext context)
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
if (ShouldUpdate())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await ReplyWithRandomSpr(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override async Task Execute(CommandContext context, ICodexNodeGroup codexGroup)
|
||||
public void Add(string spr)
|
||||
{
|
||||
lastUpdate = DateTime.UtcNow;
|
||||
sprCache.Clear();
|
||||
if (knownSprs.Contains(spr)) return;
|
||||
knownSprs.Add(spr);
|
||||
}
|
||||
|
||||
var infos = codexGroup.Select(c => c.GetDebugInfo()).ToArray();
|
||||
sprCache.AddRange(infos.Select(i => i.spr));
|
||||
public void Clear()
|
||||
{
|
||||
knownSprs.Clear();
|
||||
}
|
||||
|
||||
await ReplyWithRandomSpr(context);
|
||||
public string[] Get()
|
||||
{
|
||||
return knownSprs.ToArray();
|
||||
}
|
||||
|
||||
private async Task ReplyWithRandomSpr(CommandContext context)
|
||||
{
|
||||
if (!sprCache.Any())
|
||||
if (!knownSprs.Any())
|
||||
{
|
||||
await context.Followup("I'm sorry, no SPRs are available... :c");
|
||||
return;
|
||||
}
|
||||
|
||||
var i = random.Next(0, sprCache.Count);
|
||||
var spr = sprCache[i];
|
||||
await context.Followup($"Your SPR: '{spr}'");
|
||||
}
|
||||
|
||||
private bool ShouldUpdate()
|
||||
{
|
||||
return (DateTime.UtcNow - lastUpdate) > TimeSpan.FromMinutes(10);
|
||||
var i = random.Next(0, knownSprs.Count);
|
||||
var spr = knownSprs[i];
|
||||
await context.Followup($"Your SPR: `{spr}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ namespace BiblioTech.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
// private commands
|
||||
|
||||
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
|
||||
if (result)
|
||||
{
|
||||
|
@ -19,6 +19,12 @@ namespace BiblioTech
|
||||
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")]
|
||||
public string AdminChannelName { get; set; } = "admin-channel";
|
||||
|
||||
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use a Kubeconfig with read-only access.")]
|
||||
public string KubeConfigFile { get; set; } = "null";
|
||||
|
||||
[Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace.")]
|
||||
public string KubeNamespace { get; set; } = string.Empty;
|
||||
|
||||
public string EndpointsPath
|
||||
{
|
||||
get
|
||||
|
@ -1,89 +0,0 @@
|
||||
using CodexPlugin;
|
||||
using Discord;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public class DeploymentsFilesMonitor
|
||||
{
|
||||
private readonly List<CodexDeployment> deployments = new List<CodexDeployment>();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
LoadDeployments();
|
||||
}
|
||||
|
||||
public CodexDeployment[] GetDeployments()
|
||||
{
|
||||
return deployments.ToArray();
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadDeployment(IAttachment file)
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
var response = await http.GetAsync(file.Url);
|
||||
var str = await response.Content.ReadAsStringAsync();
|
||||
if (string.IsNullOrEmpty(str)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var deploy = JsonConvert.DeserializeObject<CodexDeployment>(str);
|
||||
if (deploy != null)
|
||||
{
|
||||
var targetFile = Path.Combine(Program.Config.EndpointsPath, Guid.NewGuid().ToString().ToLowerInvariant() + ".json");
|
||||
File.WriteAllText(targetFile, str);
|
||||
deployments.Add(deploy);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool DeleteDeployment(string deploymentName)
|
||||
{
|
||||
var path = Program.Config.EndpointsPath;
|
||||
if (!Directory.Exists(path)) return false;
|
||||
var files = Directory.GetFiles(path);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var deploy = ProcessFile(file);
|
||||
if (deploy != null && deploy.Metadata.Name == deploymentName)
|
||||
{
|
||||
File.Delete(file);
|
||||
deployments.Remove(deploy);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void LoadDeployments()
|
||||
{
|
||||
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.AddRange(files.Select(ProcessFile).Where(d => d != null).Cast<CodexDeployment>());
|
||||
}
|
||||
|
||||
private CodexDeployment? ProcessFile(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = string.Join(" ", File.ReadAllLines(filename));
|
||||
return JsonConvert.DeserializeObject<CodexDeployment>(lines);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,30 +4,99 @@ namespace BiblioTech.Options
|
||||
{
|
||||
public class CommandContext
|
||||
{
|
||||
private const string AttachmentFolder = "attachments";
|
||||
|
||||
public CommandContext(SocketSlashCommand command, IReadOnlyCollection<SocketSlashCommandDataOption> options)
|
||||
{
|
||||
Command = command;
|
||||
Options = options;
|
||||
|
||||
var attachmentPath = Path.Combine(Program.Config.DataPath, AttachmentFolder);
|
||||
if (Directory.Exists(attachmentPath))
|
||||
{
|
||||
Directory.CreateDirectory(attachmentPath);
|
||||
}
|
||||
}
|
||||
|
||||
public SocketSlashCommand Command { get; }
|
||||
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; }
|
||||
|
||||
public async Task Followup(string message)
|
||||
public async Task Followup(string line)
|
||||
{
|
||||
await Command.ModifyOriginalResponseAsync(m =>
|
||||
{
|
||||
m.Content = message;
|
||||
});
|
||||
var array = line.Split(Environment.NewLine);
|
||||
await Followup(array);
|
||||
}
|
||||
|
||||
public async Task FollowupWithAttachement(string filename, string content)
|
||||
public async Task Followup(string[] lines)
|
||||
{
|
||||
using var fileStream = new MemoryStream();
|
||||
using var streamWriter = new StreamWriter(fileStream);
|
||||
await streamWriter.WriteAsync(content);
|
||||
var chunker = new LineChunker(lines);
|
||||
var chunks = chunker.GetChunks();
|
||||
if (!chunks.Any()) return;
|
||||
|
||||
await Command.FollowupWithFileAsync(fileStream, filename);
|
||||
// First chunk is a modification of the original message.
|
||||
// Everything after that, we must create a new message.
|
||||
var first = chunks.First();
|
||||
chunks.RemoveAt(0);
|
||||
|
||||
await Command.ModifyOriginalResponseAsync(m =>
|
||||
{
|
||||
m.Content = FormatChunk(first);
|
||||
});
|
||||
|
||||
foreach (var remaining in chunks)
|
||||
{
|
||||
await Command.FollowupAsync(FormatChunk(remaining));
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatChunk(string[] chunk)
|
||||
{
|
||||
return string.Join(Environment.NewLine, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
public class LineChunker
|
||||
{
|
||||
private readonly List<string> input;
|
||||
private readonly int maxCharacters;
|
||||
|
||||
public LineChunker(string[] input, int maxCharacters = 1950)
|
||||
{
|
||||
this.input = input.ToList();
|
||||
this.maxCharacters = maxCharacters;
|
||||
}
|
||||
|
||||
public List<string[]> GetChunks()
|
||||
{
|
||||
var result = new List<string[]>();
|
||||
while (input.Any())
|
||||
{
|
||||
result.Add(GetChunk());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string[] GetChunk()
|
||||
{
|
||||
var totalLength = 0;
|
||||
var result = new List<string>();
|
||||
|
||||
while (input.Any())
|
||||
{
|
||||
var nextLine = input[0];
|
||||
var nextLength = totalLength + nextLine.Length;
|
||||
if (nextLength > maxCharacters)
|
||||
{
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
input.RemoveAt(0);
|
||||
result.Add(nextLine);
|
||||
totalLength += nextLine.Length;
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
using ArgsUniform;
|
||||
using BiblioTech.Commands;
|
||||
using Core;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Logging;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
@ -12,7 +10,6 @@ namespace BiblioTech
|
||||
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();
|
||||
|
||||
@ -21,8 +18,6 @@ namespace BiblioTech
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
Config = uniformArgs.Parse();
|
||||
|
||||
DeploymentFilesMonitor.Initialize();
|
||||
|
||||
EnsurePath(Config.DataPath);
|
||||
EnsurePath(Config.UserDataPath);
|
||||
EnsurePath(Config.EndpointsPath);
|
||||
@ -36,25 +31,14 @@ namespace BiblioTech
|
||||
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 ci = entryPoint.CreateInterface();
|
||||
|
||||
var associateCommand = new UserAssociateCommand();
|
||||
var sprCommand = new SprCommand();
|
||||
var handler = new CommandHandler(client,
|
||||
new GetBalanceCommand(ci, associateCommand),
|
||||
new MintCommand(ci, associateCommand),
|
||||
new SprCommand(ci),
|
||||
new GetBalanceCommand(associateCommand),
|
||||
new MintCommand(associateCommand),
|
||||
sprCommand,
|
||||
associateCommand,
|
||||
new AdminCommand(ci)
|
||||
new AdminCommand(sprCommand)
|
||||
);
|
||||
|
||||
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Discord;
|
||||
using GethPlugin;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
@ -76,17 +75,17 @@ namespace BiblioTech
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public string GetUserReport(IUser user)
|
||||
public string[] GetUserReport(IUser user)
|
||||
{
|
||||
var userData = GetUserData(user);
|
||||
if (userData == null) return "User has not joined the test net.";
|
||||
if (userData == null) return new[] { "User has not joined the test net." };
|
||||
return userData.CreateOverview();
|
||||
}
|
||||
|
||||
public string GetUserReport(EthAddress ethAddress)
|
||||
public string[] GetUserReport(EthAddress ethAddress)
|
||||
{
|
||||
var userData = GetUserDataForAddress(ethAddress);
|
||||
if (userData == null) return "No user is using this eth address.";
|
||||
if (userData == null) return new[] { "No user is using this eth address." };
|
||||
return userData.CreateOverview();
|
||||
}
|
||||
|
||||
@ -196,14 +195,15 @@ namespace BiblioTech
|
||||
public List<UserAssociateAddressEvent> AssociateEvents { get; }
|
||||
public List<UserMintEvent> MintEvents { get; }
|
||||
|
||||
public string CreateOverview()
|
||||
public string[] CreateOverview()
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
return
|
||||
$"name: '{Name}' - id:{DiscordId}{nl}" +
|
||||
$"joined: {CreatedUtc.ToString("o")}{nl}" +
|
||||
$"current address: {CurrentAddress}{nl}" +
|
||||
$"{AssociateEvents.Count + MintEvents.Count} total bot events.";
|
||||
return new[]
|
||||
{
|
||||
$"name: '{Name}' - id:{DiscordId}",
|
||||
$"joined: {CreatedUtc.ToString("o")}",
|
||||
$"current address: {CurrentAddress}",
|
||||
$"{AssociateEvents.Count + MintEvents.Count} total bot events."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,6 @@ namespace CodexNetDeployer
|
||||
|
||||
return new CodexTestNetConfig
|
||||
{
|
||||
PublicNatIP = config.PublicIP,
|
||||
PublicDiscoveryPort = Convert.ToInt32(discPort),
|
||||
PublicListenPort = Convert.ToInt32(listenPort)
|
||||
};
|
||||
|
@ -86,18 +86,12 @@ namespace CodexNetDeployer
|
||||
[Uniform("public-testnet", "ptn", "PUBLICTESTNET", false, "If true, deployment is created for public exposure. Default is false.")]
|
||||
public bool IsPublicTestNet { get; set; } = false;
|
||||
|
||||
[Uniform("public-ip", "pip", "PUBLICIP", false, "Required if public-testnet is true. Public IP address used by nodes for network annoucements.")]
|
||||
public string PublicIP { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("public-discports", "pdps", "PUBLICDISCPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for discovery. Number must match number of nodes.")]
|
||||
public string PublicDiscPorts { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("public-listenports", "plps", "PUBLICLISTENPORTS", false, "Required if public-testnet is true. Comma-separated port numbers used for listening. Number must match number of nodes.")]
|
||||
public string PublicListenPorts { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("public-gethip", "pgdp", "PUBLICGETHIP", false, "Required if public-testnet is true. Geth's public IP address.")]
|
||||
public string PublicGethIP { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("public-gethdiscport", "pgdp", "PUBLICGETHDISCPORT", false, "Required if public-testnet is true. Single port number used for Geth's public discovery port.")]
|
||||
public int PublicGethDiscPort { get; set; }
|
||||
|
||||
@ -150,9 +144,11 @@ namespace CodexNetDeployer
|
||||
|
||||
if (IsPublicTestNet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PublicIP)) errors.Add("Public IP required when deploying public testnet.");
|
||||
if (PublicDiscPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public discovery-ports provided does not match number of codex nodes.");
|
||||
if (PublicListenPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public listen-ports provided does not match number of codex nodes.");
|
||||
if (NumberOfCodexNodes > 0)
|
||||
{
|
||||
if (PublicDiscPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public discovery-ports provided does not match number of codex nodes.");
|
||||
if (PublicListenPorts.Split(",").Length != NumberOfCodexNodes) errors.Add("Number of public listen-ports provided does not match number of codex nodes.");
|
||||
}
|
||||
if (PublicGethDiscPort == 0) errors.Add("Geth public discovery port is not set.");
|
||||
if (PublicGethListenPort == 0) errors.Add("Geth public listen port is not set.");
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ namespace CodexNetDeployer
|
||||
|
||||
var codexInstances = CreateCodexInstances(startResults);
|
||||
|
||||
var discordBotContainer = DeployDiscordBot(ci);
|
||||
var discordBotContainer = DeployDiscordBot(ci, gethDeployment, contractsDeployment);
|
||||
|
||||
return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc));
|
||||
}
|
||||
@ -113,7 +113,6 @@ namespace CodexNetDeployer
|
||||
if (config.IsPublicTestNet)
|
||||
{
|
||||
s.AsPublicTestNet(new GethTestNetConfig(
|
||||
publicIp: config.PublicGethIP,
|
||||
discoveryPort: config.PublicGethDiscPort,
|
||||
listenPort: config.PublicGethListenPort
|
||||
));
|
||||
@ -121,17 +120,29 @@ namespace CodexNetDeployer
|
||||
});
|
||||
}
|
||||
|
||||
private RunningContainers? DeployDiscordBot(CoreInterface ci)
|
||||
private RunningContainers? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment, CodexContractsDeployment contractsDeployment)
|
||||
{
|
||||
if (!config.DeployDiscordBot) return null;
|
||||
Log("Deploying Discord bot...");
|
||||
|
||||
var addr = gethDeployment.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag);
|
||||
var info = new DiscordBotGethInfo(
|
||||
host: addr.Host,
|
||||
port: addr.Port,
|
||||
privKey: gethDeployment.Account.PrivateKey,
|
||||
marketplaceAddress: contractsDeployment.MarketplaceAddress,
|
||||
tokenAddress: contractsDeployment.TokenAddress,
|
||||
abi: contractsDeployment.Abi
|
||||
);
|
||||
|
||||
var rc = ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
|
||||
name: "discordbot-" + config.DeploymentName,
|
||||
token: config.DiscordBotToken,
|
||||
serverName: config.DiscordBotServerName,
|
||||
adminRoleName: config.DiscordBotAdminRoleName,
|
||||
adminChannelName: config.DiscordBotAdminChannelName)
|
||||
adminChannelName: config.DiscordBotAdminChannelName,
|
||||
kubeNamespace: config.KubeNamespace,
|
||||
gethInfo: info)
|
||||
{
|
||||
DataPath = config.DiscordBotDataPath
|
||||
});
|
||||
@ -142,7 +153,7 @@ namespace CodexNetDeployer
|
||||
|
||||
private RunningContainers? StartMetricsService(CoreInterface ci, List<CodexNodeStartResult> startResults)
|
||||
{
|
||||
if (!config.MetricsScraper) return null;
|
||||
if (!config.MetricsScraper || !startResults.Any()) return null;
|
||||
|
||||
Log("Starting metrics service...");
|
||||
|
||||
@ -176,7 +187,7 @@ namespace CodexNetDeployer
|
||||
|
||||
private void CheckPeerConnectivity(List<CodexNodeStartResult> codexContainers)
|
||||
{
|
||||
if (!config.CheckPeerConnection) return;
|
||||
if (!config.CheckPeerConnection || !codexContainers.Any()) return;
|
||||
|
||||
Log("Starting peer connectivity check for deployed nodes...");
|
||||
peerConnectivityChecker.CheckConnectivity(codexContainers);
|
||||
|
@ -16,12 +16,10 @@ dotnet run \
|
||||
--check-connect=0 \
|
||||
\
|
||||
--public-testnet=1 \
|
||||
--public-ip=1.2.3.4 \
|
||||
--public-discports=20010,20020,20030 \
|
||||
--public-listenports=20011,20021,20031 \
|
||||
--public-gethip=1.2.3.5 \
|
||||
--public-gethdiscport=20040 \
|
||||
--public-gethlistenport=20041 \
|
||||
--public-discports=30010,30020,30030 \
|
||||
--public-listenports=30011,30021,30031 \
|
||||
--public-gethdiscport=30040 \
|
||||
--public-gethlistenport=30041 \
|
||||
\
|
||||
--discord-bot=1 \
|
||||
--dbot-token=tokenhere \
|
||||
|
Loading…
x
Reference in New Issue
Block a user