From 1b7c11b849bc4ce2cd9b1469832e77bf5f454503 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 22 Jan 2024 10:27:07 +0100 Subject: [PATCH] Setting up rewards --- GethConnector/GethConnector.cs | 39 ++++ GethConnector/GethConnector.csproj | 14 ++ GethConnector/GethInput.cs | 52 ++++++ TestNetRewarder/BotClient.cs | 49 +++++ TestNetRewarder/Configuration.cs | 30 +++ TestNetRewarder/Program.cs | 83 +++++++++ TestNetRewarder/TestNetRewarder.csproj | 16 ++ TestNetRewarder/TimeSegmenter.cs | 51 +++++ Tools/BiblioTech/BaseGethCommand.cs | 77 +------- Tools/BiblioTech/BiblioTech.csproj | 1 + Tools/BiblioTech/Commands/NotifyCommand.cs | 24 +++ .../Commands/UserAssociateCommand.cs | 13 +- Tools/BiblioTech/Options/BoolOption.cs | 23 +++ Tools/BiblioTech/Program.cs | 6 +- Tools/BiblioTech/Rewards/GiveRewards.cs | 13 -- .../BiblioTech/Rewards/GiveRewardsCommand.cs | 18 ++ Tools/BiblioTech/Rewards/RewardsApi.cs | 50 +++-- Tools/BiblioTech/Rewards/RewardsRepo.cs | 48 +++++ Tools/BiblioTech/Rewards/RoleController.cs | 176 +++++++++++++----- Tools/BiblioTech/UserRepo.cs | 18 +- cs-codex-dist-testing.sln | 14 ++ 21 files changed, 665 insertions(+), 150 deletions(-) create mode 100644 GethConnector/GethConnector.cs create mode 100644 GethConnector/GethConnector.csproj create mode 100644 GethConnector/GethInput.cs create mode 100644 TestNetRewarder/BotClient.cs create mode 100644 TestNetRewarder/Configuration.cs create mode 100644 TestNetRewarder/Program.cs create mode 100644 TestNetRewarder/TestNetRewarder.csproj create mode 100644 TestNetRewarder/TimeSegmenter.cs create mode 100644 Tools/BiblioTech/Commands/NotifyCommand.cs create mode 100644 Tools/BiblioTech/Options/BoolOption.cs delete mode 100644 Tools/BiblioTech/Rewards/GiveRewards.cs create mode 100644 Tools/BiblioTech/Rewards/GiveRewardsCommand.cs create mode 100644 Tools/BiblioTech/Rewards/RewardsRepo.cs diff --git a/GethConnector/GethConnector.cs b/GethConnector/GethConnector.cs new file mode 100644 index 0000000..c7c7687 --- /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 0000000..b1bdda3 --- /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 0000000..e38af8a --- /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 0000000..6907fae --- /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 0000000..cb9f95d --- /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 0000000..a5dce56 --- /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 0000000..75b2ee1 --- /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 0000000..3608c79 --- /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 ecbdfec..4906995 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 9c32ad4..c941a71 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 0000000..3e0bd15 --- /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 c23718c..81e4646 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 0000000..6318ee1 --- /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 22e68f8..7d6d690 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 18c300a..0000000 --- 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 0000000..494285b --- /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 657112a..17d4362 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 0000000..bbff34a --- /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 0aedfb6..63274fd 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 0ce7a34..01afbe9 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 6b879f1..f5074ed 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}