diff --git a/GethConnector/GethConnector.cs b/GethConnector/GethConnector.cs
new file mode 100644
index 00000000..c7c7687c
--- /dev/null
+++ b/GethConnector/GethConnector.cs
@@ -0,0 +1,39 @@
+using CodexContractsPlugin;
+using GethPlugin;
+using Logging;
+
+namespace GethConnector
+{
+ public class GethConnector
+ {
+ public IGethNode GethNode { get; }
+ public ICodexContracts CodexContracts { get; }
+
+ public static GethConnector? Initialize(ILog log)
+ {
+ if (!string.IsNullOrEmpty(GethInput.LoadError))
+ {
+ var msg = "Geth input incorrect: " + GethInput.LoadError;
+ log.Error(msg);
+ return null;
+ }
+
+ 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);
+
+ return new GethConnector(gethNode, contracts);
+ }
+
+ private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
+ {
+ GethNode = gethNode;
+ CodexContracts = codexContracts;
+ }
+ }
+}
diff --git a/GethConnector/GethConnector.csproj b/GethConnector/GethConnector.csproj
new file mode 100644
index 00000000..b1bdda3f
--- /dev/null
+++ b/GethConnector/GethConnector.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/GethConnector/GethInput.cs b/GethConnector/GethInput.cs
new file mode 100644
index 00000000..e38af8a4
--- /dev/null
+++ b/GethConnector/GethInput.cs
@@ -0,0 +1,52 @@
+namespace GethConnector
+{
+ public static class GethInput
+ {
+ 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";
+
+ static GethInput()
+ {
+ var error = new List();
+ 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!;
+ }
+ }
+
+ 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;
+
+ private static string? GetEnvVar(List error, string name)
+ {
+ var result = Environment.GetEnvironmentVariable(name);
+ if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
+ return result;
+ }
+ }
+}
diff --git a/TestNetRewarder/BotClient.cs b/TestNetRewarder/BotClient.cs
new file mode 100644
index 00000000..6907fae9
--- /dev/null
+++ b/TestNetRewarder/BotClient.cs
@@ -0,0 +1,49 @@
+using BiblioTech.Rewards;
+using Logging;
+using Newtonsoft.Json;
+
+namespace TestNetRewarder
+{
+ public class BotClient
+ {
+ private readonly Configuration configuration;
+ private readonly ILog log;
+
+ public BotClient(Configuration configuration, ILog log)
+ {
+ this.configuration = configuration;
+ this.log = log;
+ }
+
+ public async Task IsOnline()
+ {
+ return await HttpPost("Ping") == "Ping";
+ }
+
+ public async Task SendRewards(GiveRewardsCommand command)
+ {
+ if (command == null || command.Rewards == null || !command.Rewards.Any()) return;
+ await HttpPost(JsonConvert.SerializeObject(command));
+ }
+
+ private async Task HttpPost(string content)
+ {
+ try
+ {
+ var client = new HttpClient();
+ var response = await client.PostAsync(GetUrl(), new StringContent(content));
+ return await response.Content.ReadAsStringAsync();
+ }
+ catch (Exception ex)
+ {
+ log.Error(ex.ToString());
+ return string.Empty;
+ }
+ }
+
+ private string GetUrl()
+ {
+ return $"{configuration.DiscordHost}:{configuration.DiscordPort}";
+ }
+ }
+}
diff --git a/TestNetRewarder/Configuration.cs b/TestNetRewarder/Configuration.cs
new file mode 100644
index 00000000..cb9f95dd
--- /dev/null
+++ b/TestNetRewarder/Configuration.cs
@@ -0,0 +1,30 @@
+using ArgsUniform;
+
+namespace TestNetRewarder
+{
+ public class Configuration
+ {
+ [Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
+ public string DataPath { get; set; } = "datapath";
+
+ [Uniform("discordbot-host", "dh", "DISCORDBOTHOST", true, "http address of the discord bot.")]
+ public string DiscordHost { get; set; } = "host";
+
+ [Uniform("discordbot-port", "dp", "DISCORDBOTPORT", true, "port number of the discord bot reward API. (31080 by default)")]
+ public int DiscordPort { get; set; } = 31080;
+
+ [Uniform("interval-minutes", "im", "INTERVALMINUTES", false, "time in minutes between reward updates. (default 15)")]
+ public int Interval { get; set; } = 15;
+
+ [Uniform("check-history", "ch", "CHECKHISTORY", false, "if not 0, Unix epoc timestamp of a moment in history on which processing should begin. (default 0)")]
+ public int CheckHistoryTimestamp { get; set; } = 0;
+
+ public string LogPath
+ {
+ get
+ {
+ return Path.Combine(DataPath, "logs");
+ }
+ }
+ }
+}
diff --git a/TestNetRewarder/Program.cs b/TestNetRewarder/Program.cs
new file mode 100644
index 00000000..a5dce567
--- /dev/null
+++ b/TestNetRewarder/Program.cs
@@ -0,0 +1,83 @@
+using ArgsUniform;
+using GethConnector;
+using Logging;
+using Utils;
+
+namespace TestNetRewarder
+{
+ public class Program
+ {
+ public static Configuration Config { get; private set; } = null!;
+ public static ILog Log { get; private set; } = null!;
+ public static CancellationToken CancellationToken { get; private set; }
+
+ public static Task Main(string[] args)
+ {
+ var cts = new CancellationTokenSource();
+ CancellationToken = cts.Token;
+ Console.CancelKeyPress += (sender, args) => cts.Cancel();
+
+ var uniformArgs = new ArgsUniform(PrintHelp, args);
+ Config = uniformArgs.Parse(true);
+
+ Log = new LogSplitter(
+ new FileLog(Path.Combine(Config.LogPath, "testnetrewarder")),
+ new ConsoleLog()
+ );
+
+ EnsurePath(Config.DataPath);
+ EnsurePath(Config.LogPath);
+
+ return new Program().MainAsync();
+ }
+
+ public async Task MainAsync()
+ {
+ Log.Log("Starting TestNet Rewarder...");
+ var segmenter = new TimeSegmenter(Log, Config);
+
+ while (!CancellationToken.IsCancellationRequested)
+ {
+ await segmenter.WaitForNextSegment(ProcessTimeSegment);
+ await Task.Delay(1000, CancellationToken);
+ }
+ }
+
+ private async Task ProcessTimeSegment(TimeRange range)
+ {
+ try
+ {
+ var connector = GethConnector.GethConnector.Initialize(Log);
+ if (connector == null) return;
+
+ var newRequests = connector.CodexContracts.GetStorageRequests(range);
+ foreach (var request in newRequests)
+ {
+ for (ulong i = 0; i < request.Ask.Slots; i++)
+ {
+ var host = connector.CodexContracts.GetSlotHost(request, i);
+ }
+ }
+ var newSlotsFilled = connector.CodexContracts.GetSlotFilledEvents(range);
+ var newSlotsFreed = connector.CodexContracts.GetSlotFreedEvents(range);
+
+ // can we get them all?
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Exception processing time segment: " + ex);
+ }
+ }
+
+ private static void PrintHelp()
+ {
+ Log.Log("TestNet Rewarder");
+ }
+
+ private static void EnsurePath(string path)
+ {
+ if (Directory.Exists(path)) return;
+ Directory.CreateDirectory(path);
+ }
+ }
+}
diff --git a/TestNetRewarder/TestNetRewarder.csproj b/TestNetRewarder/TestNetRewarder.csproj
new file mode 100644
index 00000000..75b2ee19
--- /dev/null
+++ b/TestNetRewarder/TestNetRewarder.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/TestNetRewarder/TimeSegmenter.cs b/TestNetRewarder/TimeSegmenter.cs
new file mode 100644
index 00000000..3608c792
--- /dev/null
+++ b/TestNetRewarder/TimeSegmenter.cs
@@ -0,0 +1,51 @@
+using Logging;
+using Utils;
+
+namespace TestNetRewarder
+{
+ public class TimeSegmenter
+ {
+ private readonly ILog log;
+ private readonly TimeSpan segmentSize;
+ private DateTime start;
+
+ public TimeSegmenter(ILog log, Configuration configuration)
+ {
+ this.log = log;
+
+ if (configuration.Interval < 0) configuration.Interval = 15;
+ segmentSize = TimeSpan.FromSeconds(configuration.Interval);
+ if (configuration.CheckHistoryTimestamp != 0)
+ {
+ start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime;
+ }
+ else
+ {
+ start = DateTime.UtcNow - segmentSize;
+ }
+
+ log.Log("Starting time segments at " + start);
+ log.Log("Segment size: " + Time.FormatDuration(segmentSize));
+ }
+
+ public async Task WaitForNextSegment(Func onSegment)
+ {
+ var now = DateTime.UtcNow;
+ var end = start + segmentSize;
+ if (end > now)
+ {
+ // Wait for the entire time segment to be in the past.
+ var delay = (end - now).Add(TimeSpan.FromSeconds(3));
+ await Task.Delay(delay, Program.CancellationToken);
+ }
+
+ if (Program.CancellationToken.IsCancellationRequested) return;
+
+ log.Log($"Time segment {start} to {end}");
+ var range = new TimeRange(start, end);
+ start = end;
+
+ await onSegment(range);
+ }
+ }
+}
diff --git a/Tools/BiblioTech/BaseGethCommand.cs b/Tools/BiblioTech/BaseGethCommand.cs
index ecbdfecf..49069953 100644
--- a/Tools/BiblioTech/BaseGethCommand.cs
+++ b/Tools/BiblioTech/BaseGethCommand.cs
@@ -1,87 +1,18 @@
using BiblioTech.Options;
using CodexContractsPlugin;
using GethPlugin;
-using Logging;
namespace BiblioTech
{
- public static class GethInput
- {
- 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";
-
- static GethInput()
- {
- var error = new List();
- 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!;
- }
- }
-
- 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;
-
- private static string? GetEnvVar(List 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)
{
- if (!string.IsNullOrEmpty(GethInput.LoadError))
- {
- var msg = "Geth input incorrect: " + GethInput.LoadError;
- Program.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 gethConnector = GethConnector.GethConnector.Initialize(Program.Log);
- var contractsDeployment = new CodexContractsDeployment(
- marketplaceAddress: GethInput.MarketplaceAddress,
- abi: GethInput.ABI,
- tokenAddress: GethInput.TokenAddress
- );
-
- var gethNode = new CustomGethNode(Program.Log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
- var contracts = new CodexContractsAccess(Program.Log, gethNode, contractsDeployment);
+ if (gethConnector == null) return;
+ var gethNode = gethConnector.GethNode;
+ var contracts = gethConnector.CodexContracts;
if (!contracts.IsDeployed())
{
diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj
index 9c32ad43..c941a712 100644
--- a/Tools/BiblioTech/BiblioTech.csproj
+++ b/Tools/BiblioTech/BiblioTech.csproj
@@ -10,6 +10,7 @@
+
diff --git a/Tools/BiblioTech/Commands/NotifyCommand.cs b/Tools/BiblioTech/Commands/NotifyCommand.cs
new file mode 100644
index 00000000..3e0bd152
--- /dev/null
+++ b/Tools/BiblioTech/Commands/NotifyCommand.cs
@@ -0,0 +1,24 @@
+using BiblioTech.Options;
+
+namespace BiblioTech.Commands
+{
+ public class NotifyCommand : BaseCommand
+ {
+ private readonly BoolOption boolOption = new BoolOption(name: "enabled", description: "Controls whether the bot will @-mention you.", isRequired: false);
+
+ public override string Name => "notify";
+ public override string StartingMessage => RandomBusyMessage.Get();
+ public override string Description => "Enable or disable notifications from the bot.";
+ public override CommandOption[] Options => new CommandOption[] { boolOption };
+
+ protected override async Task Invoke(CommandContext context)
+ {
+ var user = context.Command.User;
+ var enabled = await boolOption.Parse(context);
+ if (enabled == null) return;
+
+ Program.UserRepo.SetUserNotificationPreference(user, enabled.Value);
+ await context.Followup("Done!");
+ }
+ }
+}
diff --git a/Tools/BiblioTech/Commands/UserAssociateCommand.cs b/Tools/BiblioTech/Commands/UserAssociateCommand.cs
index c23718ce..81e46467 100644
--- a/Tools/BiblioTech/Commands/UserAssociateCommand.cs
+++ b/Tools/BiblioTech/Commands/UserAssociateCommand.cs
@@ -4,6 +4,12 @@ namespace BiblioTech.Commands
{
public class UserAssociateCommand : BaseCommand
{
+ public UserAssociateCommand(NotifyCommand notifyCommand)
+ {
+ this.notifyCommand = notifyCommand;
+ }
+
+ private readonly NotifyCommand notifyCommand;
private readonly EthAddressOption ethOption = new EthAddressOption(isRequired: false);
private readonly UserOption optionalUser = new UserOption(
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
@@ -30,7 +36,12 @@ namespace BiblioTech.Commands
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
if (result)
{
- await context.Followup("Done! Thank you for joining the test net!");
+ await context.Followup(new string[]
+ {
+ "Done! Thank you for joining the test net!",
+ "By default, the bot will @-mention you with test-net reward related notifications.",
+ $"You can enable/disable this behavior with the '/{notifyCommand.Name}' command."
+ });
}
else
{
diff --git a/Tools/BiblioTech/Options/BoolOption.cs b/Tools/BiblioTech/Options/BoolOption.cs
new file mode 100644
index 00000000..6318ee16
--- /dev/null
+++ b/Tools/BiblioTech/Options/BoolOption.cs
@@ -0,0 +1,23 @@
+using Discord;
+
+namespace BiblioTech.Options
+{
+ public class BoolOption : CommandOption
+ {
+ public BoolOption(string name, string description, bool isRequired)
+ : base(name, description, type: ApplicationCommandOptionType.Boolean, isRequired)
+ {
+ }
+
+ public async Task Parse(CommandContext context)
+ {
+ var bData = context.Options.SingleOrDefault(o => o.Name == Name);
+ if (bData == null || !(bData.Value is bool))
+ {
+ await context.Followup("Bool option not received.");
+ return null;
+ }
+ return (bool) bData.Value;
+ }
+ }
+}
diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs
index 22e68f8b..7d6d6900 100644
--- a/Tools/BiblioTech/Program.cs
+++ b/Tools/BiblioTech/Program.cs
@@ -21,7 +21,7 @@ namespace BiblioTech
Config = uniformArgs.Parse();
Log = new LogSplitter(
- new FileLog(Path.Combine(Config.LogPath, "discordbot.log")),
+ new FileLog(Path.Combine(Config.LogPath, "discordbot")),
new ConsoleLog()
);
@@ -38,13 +38,15 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
- var associateCommand = new UserAssociateCommand();
+ var notifyCommand = new NotifyCommand();
+ var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(client,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
+ notifyCommand,
new AdminCommand(sprCommand)
);
diff --git a/Tools/BiblioTech/Rewards/GiveRewards.cs b/Tools/BiblioTech/Rewards/GiveRewards.cs
deleted file mode 100644
index 18c300ac..00000000
--- a/Tools/BiblioTech/Rewards/GiveRewards.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace BiblioTech.Rewards
-{
- public class GiveRewards
- {
- public Reward[] Rewards { get; set; } = Array.Empty();
- }
-
- public class Reward
- {
- public ulong RewardId { get; set; }
- public string[] UserAddresses { get; set; } = Array.Empty();
- }
-}
diff --git a/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs b/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs
new file mode 100644
index 00000000..494285bd
--- /dev/null
+++ b/Tools/BiblioTech/Rewards/GiveRewardsCommand.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace BiblioTech.Rewards
+{
+ public class GiveRewardsCommand
+ {
+ public RewardUsersCommand[] Rewards { get; set; } = Array.Empty();
+ }
+
+ public class RewardUsersCommand
+ {
+ public ulong RewardId { get; set; }
+ public string[] UserAddresses { get; set; } = Array.Empty();
+
+ [JsonIgnore]
+ public UserData[] Users { get; set; } = Array.Empty();
+ }
+}
diff --git a/Tools/BiblioTech/Rewards/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs
index 657112a9..17d4362d 100644
--- a/Tools/BiblioTech/Rewards/RewardsApi.cs
+++ b/Tools/BiblioTech/Rewards/RewardsApi.cs
@@ -6,7 +6,7 @@ namespace BiblioTech.Rewards
{
public interface IDiscordRoleController
{
- void GiveRole(ulong roleId, UserData userData);
+ Task GiveRewards(GiveRewardsCommand rewards);
}
public class RewardsApi
@@ -49,7 +49,7 @@ namespace BiblioTech.Rewards
var context = wait.Result;
try
{
- HandleConnection(context);
+ HandleConnection(context).Wait();
}
catch (Exception ex)
{
@@ -63,32 +63,50 @@ namespace BiblioTech.Rewards
}
}
- private void HandleConnection(HttpListenerContext context)
+ private async Task HandleConnection(HttpListenerContext context)
{
- var reader = new StreamReader(context.Request.InputStream);
+ using var reader = new StreamReader(context.Request.InputStream);
var content = reader.ReadToEnd();
- var rewards = JsonConvert.DeserializeObject(content);
- if (rewards != null) ProcessRewards(rewards);
+ if (content == "Ping")
+ {
+ using var writer = new StreamWriter(context.Response.OutputStream);
+ writer.Write("Pong");
+ return;
+ }
+
+ if (!content.StartsWith("{")) return;
+ var rewards = JsonConvert.DeserializeObject(content);
+ if (rewards != null)
+ {
+ AttachUsers(rewards);
+ await ProcessRewards(rewards);
+ }
}
- private void ProcessRewards(GiveRewards rewards)
+ private void AttachUsers(GiveRewardsCommand rewards)
{
- Program.Log.Log("Processing: " + JsonConvert.SerializeObject(rewards));
- foreach (var reward in rewards.Rewards) ProcessReward(reward);
+ foreach (var reward in rewards.Rewards)
+ {
+ reward.Users = reward.UserAddresses.Select(GetUserFromAddress).Where(u => u != null).Cast().ToArray();
+ }
}
- private void ProcessReward(Reward reward)
+ private UserData? GetUserFromAddress(string address)
{
- foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress);
+ try
+ {
+ return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
+ }
+ catch
+ {
+ return null;
+ }
}
- private void GiveRoleToUser(ulong rewardId, string userAddress)
+ private async Task ProcessRewards(GiveRewardsCommand rewards)
{
- var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress));
- if (userData == null) { Program.Log.Log("no userdata"); return; }
-
- roleController.GiveRole(rewardId, userData);
+ await roleController.GiveRewards(rewards);
}
}
}
diff --git a/Tools/BiblioTech/Rewards/RewardsRepo.cs b/Tools/BiblioTech/Rewards/RewardsRepo.cs
new file mode 100644
index 00000000..bbff34a8
--- /dev/null
+++ b/Tools/BiblioTech/Rewards/RewardsRepo.cs
@@ -0,0 +1,48 @@
+namespace BiblioTech.Rewards
+{
+ public class RewardsRepo
+ {
+ private static string Tag => RoleController.UsernameTag;
+
+ public RoleRewardConfig[] Rewards { get; }
+
+ public RewardsRepo()
+ {
+ Rewards = new[]
+ {
+ // Join reward
+ new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Hosting:
+ //// Filled any slot:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Finished any slot:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Finished a min-256MB min-8h slot:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Finished a min-64GB min-24h slot:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Oops:
+ //// Missed a storage proof:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Clienting:
+ //// Posted any contract:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Posted any contract that reached started state:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Started a contract with min-4 hosts, min-256MB per host, min-8h duration:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+
+ //// Started a contract with min-4 hosts, min-64GB per host, min-24h duration:
+ //new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
+ };
+ }
+ }
+}
diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs
index 0aedfb68..63274fd2 100644
--- a/Tools/BiblioTech/Rewards/RoleController.cs
+++ b/Tools/BiblioTech/Rewards/RoleController.cs
@@ -1,18 +1,14 @@
-using Discord.WebSocket;
-using Utils;
+using Discord;
+using Discord.WebSocket;
namespace BiblioTech.Rewards
{
public class RoleController : IDiscordRoleController
{
- private const string UsernameTag = "";
+ public const string UsernameTag = "";
private readonly DiscordSocketClient client;
private readonly SocketTextChannel? rewardsChannel;
-
- private readonly RoleReward[] roleRewards = new[]
- {
- new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!")
- };
+ private readonly RewardsRepo repo = new RewardsRepo();
public RoleController(DiscordSocketClient client)
{
@@ -24,52 +20,62 @@ namespace BiblioTech.Rewards
}
}
- public void GiveRole(ulong roleId, UserData userData)
+ public async Task GiveRewards(GiveRewardsCommand rewards)
{
- var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId);
- if (reward == null) { Program.Log.Log("no reward"); return; };
-
var guild = GetGuild();
+ // We load all role and user information first,
+ // so we don't ask the server for the same info multiple times.
+ var context = new RewardContext(
+ await LoadAllUsers(guild),
+ LookUpAllRoles(guild, rewards),
+ rewardsChannel);
- var user = guild.GetUser(userData.DiscordId);
- if (user == null) { Program.Log.Log("no user"); return; };
-
- var role = guild.GetRole(roleId);
- if (role == null) { Program.Log.Log("no role"); return; };
-
- Program.Log.Log($"User has roles: {string.Join(",", user.Roles.Select(r => r.Name + "=" + r.Id))}");
- if (user.Roles.Any(r => r.Id == role.Id)) { Program.Log.Log("already has"); return; };
-
- GiveRole(user, role);
- SendNotification(reward, userData, user, role);
+ await context.ProcessGiveRewardsCommand(rewards);
}
- private void GiveRole(SocketGuildUser user, SocketRole role)
+ private async Task> LoadAllUsers(SocketGuild guild)
{
- try
+ var result = new Dictionary();
+ var users = guild.GetUsersAsync();
+ await foreach (var ulist in users)
{
- Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}");
- Time.Wait(user.AddRoleAsync(role));
- }
- catch (Exception ex)
- {
- Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
- }
- }
-
- private void SendNotification(RoleReward reward, UserData userData, SocketGuildUser user, SocketRole role)
- {
- try
- {
- if (userData.NotificationsEnabled && rewardsChannel != null)
+ foreach (var u in ulist)
{
- Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName)));
+ result.Add(u.Id, u);
}
}
- catch (Exception ex)
+ return result;
+ }
+
+ private Dictionary LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
+ {
+ var result = new Dictionary();
+ foreach (var r in rewards.Rewards)
{
- Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}");
+ var role = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
+ if (role == null)
+ {
+ Program.Log.Log($"No RoleReward is configured for reward with id '{r.RewardId}'.");
+ }
+ else
+ {
+ if (role.SocketRole == null)
+ {
+ var socketRole = guild.GetRole(r.RewardId);
+ if (socketRole == null)
+ {
+ Program.Log.Log($"Guild Role by id '{r.RewardId}' not found.");
+ }
+ else
+ {
+ role.SocketRole = socketRole;
+ }
+ }
+ result.Add(role.RoleId, role);
+ }
}
+
+ return result;
}
private SocketGuild GetGuild()
@@ -78,9 +84,90 @@ namespace BiblioTech.Rewards
}
}
- public class RoleReward
+ public class RewardContext
{
- public RoleReward(ulong roleId, string message)
+ private readonly Dictionary users;
+ private readonly Dictionary roles;
+ private readonly SocketTextChannel? rewardsChannel;
+
+ public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel)
+ {
+ this.users = users;
+ this.roles = roles;
+ this.rewardsChannel = rewardsChannel;
+ }
+
+ public async Task ProcessGiveRewardsCommand(GiveRewardsCommand rewards)
+ {
+ foreach (var rewardCommand in rewards.Rewards)
+ {
+ if (roles.ContainsKey(rewardCommand.RewardId))
+ {
+ var role = roles[rewardCommand.RewardId];
+ await ProcessRewardCommand(role, rewardCommand);
+ }
+ }
+ }
+
+ private async Task ProcessRewardCommand(RoleRewardConfig role, RewardUsersCommand reward)
+ {
+ foreach (var user in reward.Users)
+ {
+ await GiveReward(role, user);
+ }
+ }
+
+ private async Task GiveReward(RoleRewardConfig role, UserData user)
+ {
+ if (!users.ContainsKey(user.DiscordId))
+ {
+ Program.Log.Log($"User by id '{user.DiscordId}' not found.");
+ return;
+ }
+
+ var guildUser = users[user.DiscordId];
+
+ var alreadyHas = guildUser.RoleIds.ToArray();
+ if (alreadyHas.Any(r => r == role.RoleId)) return;
+
+ await GiveRole(guildUser, role.SocketRole!);
+ await SendNotification(role, user, guildUser);
+ await Task.Delay(1000);
+ }
+
+ private async Task GiveRole(IGuildUser user, SocketRole role)
+ {
+ try
+ {
+ Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}");
+ await user.AddRoleAsync(role);
+ }
+ catch (Exception ex)
+ {
+ Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
+ }
+ }
+
+ private async Task SendNotification(RoleRewardConfig reward, UserData userData, IGuildUser user)
+ {
+ try
+ {
+ if (userData.NotificationsEnabled && rewardsChannel != null)
+ {
+ var msg = reward.Message.Replace(RoleController.UsernameTag, $"<@{user.Id}>");
+ await rewardsChannel.SendMessageAsync(msg);
+ }
+ }
+ catch (Exception ex)
+ {
+ Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole!.Name}': {ex}");
+ }
+ }
+ }
+
+ public class RoleRewardConfig
+ {
+ public RoleRewardConfig(ulong roleId, string message)
{
RoleId = roleId;
Message = message;
@@ -88,5 +175,6 @@ namespace BiblioTech.Rewards
public ulong RoleId { get; }
public string Message { get; }
+ public SocketRole? SocketRole { get; set; }
}
}
diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs
index 0ce7a348..01afbe90 100644
--- a/Tools/BiblioTech/UserRepo.cs
+++ b/Tools/BiblioTech/UserRepo.cs
@@ -25,6 +25,14 @@ namespace BiblioTech
}
}
+ public void SetUserNotificationPreference(IUser user, bool enableNotifications)
+ {
+ lock (repoLock)
+ {
+ SetUserNotification(user, enableNotifications);
+ }
+ }
+
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction? eth, Transaction? tokens)
{
lock (repoLock)
@@ -133,6 +141,14 @@ namespace BiblioTech
return true;
}
+ private void SetUserNotification(IUser user, bool notifyEnabled)
+ {
+ var userData = GetUserData(user);
+ if (userData == null) return;
+ userData.NotificationsEnabled = notifyEnabled;
+ SaveUserData(userData);
+ }
+
private UserData? GetUserData(IUser user)
{
var filename = GetFilename(user);
@@ -202,7 +218,7 @@ namespace BiblioTech
public EthAddress? CurrentAddress { get; set; }
public List AssociateEvents { get; }
public List MintEvents { get; }
- public bool NotificationsEnabled { get; }
+ public bool NotificationsEnabled { get; set; }
public string[] CreateOverview()
{
diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln
index 6b879f19..f5074ed4 100644
--- a/cs-codex-dist-testing.sln
+++ b/cs-codex-dist-testing.sln
@@ -53,6 +53,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAndRunPlugin", "Proje
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Tests\FrameworkTests\FrameworkTests.csproj", "{25E72004-4D71-4D1E-A193-FC125D12FF96}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestNetRewarder", "TestNetRewarder\TestNetRewarder.csproj", "{27B56A82-E8CE-4B50-9746-D574BAD646A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GethConnector", "GethConnector\GethConnector.csproj", "{04F2D26E-0768-4F93-9A1A-834089646B56}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -143,6 +147,14 @@ Global
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -169,6 +181,8 @@ Global
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
{25E72004-4D71-4D1E-A193-FC125D12FF96} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
+ {27B56A82-E8CE-4B50-9746-D574BAD646A2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
+ {04F2D26E-0768-4F93-9A1A-834089646B56} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}