From 2f10b302835fa8dc4e5d0e62c9dcacf2eec360cc Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Dec 2023 15:56:03 +0100 Subject: [PATCH] Sets up rewards api and handling. --- .../Utils}/TaskFactory.cs | 2 +- Framework/Utils/Time.cs | 5 + ProjectPlugins/GethPlugin/EthAddress.cs | 2 +- .../ContinuousTestRunner.cs | 1 + Tests/CodexContinuousTests/SingleTestRun.cs | 1 + Tests/CodexContinuousTests/TestLoop.cs | 1 + Tools/BiblioTech/CommandHandler.cs | 6 ++ Tools/BiblioTech/Configuration.cs | 4 + Tools/BiblioTech/Program.cs | 1 + Tools/BiblioTech/Rewards/GiveRewards.cs | 13 +++ Tools/BiblioTech/Rewards/RewardsApi.cs | 93 +++++++++++++++++++ Tools/BiblioTech/Rewards/RoleController.cs | 82 ++++++++++++++++ Tools/BiblioTech/UserRepo.cs | 52 ++++++----- 13 files changed, 236 insertions(+), 27 deletions(-) rename {Tests/CodexContinuousTests => Framework/Utils}/TaskFactory.cs (97%) create mode 100644 Tools/BiblioTech/Rewards/GiveRewards.cs create mode 100644 Tools/BiblioTech/Rewards/RewardsApi.cs create mode 100644 Tools/BiblioTech/Rewards/RoleController.cs diff --git a/Tests/CodexContinuousTests/TaskFactory.cs b/Framework/Utils/TaskFactory.cs similarity index 97% rename from Tests/CodexContinuousTests/TaskFactory.cs rename to Framework/Utils/TaskFactory.cs index e0be06e..44ff489 100644 --- a/Tests/CodexContinuousTests/TaskFactory.cs +++ b/Framework/Utils/TaskFactory.cs @@ -1,4 +1,4 @@ -namespace ContinuousTests +namespace Utils { public class TaskFactory { diff --git a/Framework/Utils/Time.cs b/Framework/Utils/Time.cs index ca4e115..82a836e 100644 --- a/Framework/Utils/Time.cs +++ b/Framework/Utils/Time.cs @@ -13,6 +13,11 @@ return task.Result; } + public static void Wait(Task task) + { + task.Wait(); + } + public static string FormatDuration(TimeSpan d) { var result = ""; diff --git a/ProjectPlugins/GethPlugin/EthAddress.cs b/ProjectPlugins/GethPlugin/EthAddress.cs index bc660b6..803a1f7 100644 --- a/ProjectPlugins/GethPlugin/EthAddress.cs +++ b/ProjectPlugins/GethPlugin/EthAddress.cs @@ -9,7 +9,7 @@ { public EthAddress(string address) { - Address = address; + Address = address.ToLowerInvariant(); } public string Address { get; } diff --git a/Tests/CodexContinuousTests/ContinuousTestRunner.cs b/Tests/CodexContinuousTests/ContinuousTestRunner.cs index b591d30..c79fc71 100644 --- a/Tests/CodexContinuousTests/ContinuousTestRunner.cs +++ b/Tests/CodexContinuousTests/ContinuousTestRunner.cs @@ -3,6 +3,7 @@ using DistTestCore.Logs; using Logging; using Newtonsoft.Json; using Utils; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tests/CodexContinuousTests/SingleTestRun.cs b/Tests/CodexContinuousTests/SingleTestRun.cs index 39b45e5..d7ccd80 100644 --- a/Tests/CodexContinuousTests/SingleTestRun.cs +++ b/Tests/CodexContinuousTests/SingleTestRun.cs @@ -6,6 +6,7 @@ using CodexPlugin; using DistTestCore.Logs; using Core; using KubernetesWorkflow.Types; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tests/CodexContinuousTests/TestLoop.cs b/Tests/CodexContinuousTests/TestLoop.cs index 7e44f73..46b4e29 100644 --- a/Tests/CodexContinuousTests/TestLoop.cs +++ b/Tests/CodexContinuousTests/TestLoop.cs @@ -1,5 +1,6 @@ using DistTestCore.Logs; using Logging; +using TaskFactory = Utils.TaskFactory; namespace ContinuousTests { diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index 800ac4b..5669636 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -2,6 +2,7 @@ using Discord.WebSocket; using Discord; using Newtonsoft.Json; +using BiblioTech.Rewards; namespace BiblioTech { @@ -25,6 +26,9 @@ namespace BiblioTech Program.AdminChecker.SetGuild(guild); Program.Log.Log($"Initializing for guild: '{guild.Name}'"); + var roleController = new RoleController(guild); + var rewardsApi = new RewardsApi(roleController); + var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray(); if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel"); Program.AdminChecker.SetAdminChannel(adminChannels.First()); @@ -58,6 +62,8 @@ namespace BiblioTech var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); Program.Log.Error(json); } + + rewardsApi.Start(); } private async Task SlashCommandHandler(SocketSlashCommand command) diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index bce62ea..f324d23 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -19,6 +19,10 @@ 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("rewards-channel-name", "ac", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")] + public string RewardsChannelName { get; set; } = ""; + + public string EndpointsPath { get diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 22e68f8..f5c897c 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -1,5 +1,6 @@ using ArgsUniform; using BiblioTech.Commands; +using BiblioTech.Rewards; using Discord; using Discord.WebSocket; using Logging; diff --git a/Tools/BiblioTech/Rewards/GiveRewards.cs b/Tools/BiblioTech/Rewards/GiveRewards.cs new file mode 100644 index 0000000..18c300a --- /dev/null +++ b/Tools/BiblioTech/Rewards/GiveRewards.cs @@ -0,0 +1,13 @@ +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/RewardsApi.cs b/Tools/BiblioTech/Rewards/RewardsApi.cs new file mode 100644 index 0000000..30eca5b --- /dev/null +++ b/Tools/BiblioTech/Rewards/RewardsApi.cs @@ -0,0 +1,93 @@ +using Newtonsoft.Json; +using System.Net; +using TaskFactory = Utils.TaskFactory; + +namespace BiblioTech.Rewards +{ + public interface IDiscordRoleController + { + void GiveRole(ulong roleId, UserData userData); + } + + public class RewardsApi + { + private readonly HttpListener listener = new HttpListener(); + private readonly TaskFactory taskFactory = new TaskFactory(); + private readonly IDiscordRoleController roleController; + private CancellationTokenSource cts = new CancellationTokenSource(); + + public RewardsApi(IDiscordRoleController roleController) + { + this.roleController = roleController; + } + + public void Start() + { + cts = new CancellationTokenSource(); + listener.Prefixes.Add($"http://*:31080/"); + listener.Start(); + taskFactory.Run(ConnectionDispatcher, nameof(ConnectionDispatcher)); + } + + public void Stop() + { + listener.Stop(); + cts.Cancel(); + taskFactory.WaitAll(); + } + + private void ConnectionDispatcher() + { + while (!cts.Token.IsCancellationRequested) + { + var wait = listener.GetContextAsync(); + wait.Wait(cts.Token); + if (wait.IsCompletedSuccessfully) + { + taskFactory.Run(() => + { + var context = wait.Result; + try + { + HandleConnection(context); + } + catch (Exception ex) + { + Program.Log.Error("Exception during HTTP handler: " + ex); + } + // Whatever happens, everything's always OK. + context.Response.StatusCode = 200; + context.Response.OutputStream.Close(); + }, nameof(HandleConnection)); + } + } + } + + private void HandleConnection(HttpListenerContext context) + { + var reader = new StreamReader(context.Request.InputStream); + var content = reader.ReadToEnd(); + + var rewards = JsonConvert.DeserializeObject(content); + if (rewards != null) ProcessRewards(rewards); + } + + private void ProcessRewards(GiveRewards rewards) + { + foreach (var reward in rewards.Rewards) ProcessReward(reward); + } + + private void ProcessReward(Reward reward) + { + foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress); + } + + private void GiveRoleToUser(ulong rewardId, string userAddress) + { + var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress)); + if (userData == null) return; + + roleController.GiveRole(rewardId, userData); + } + } +} diff --git a/Tools/BiblioTech/Rewards/RoleController.cs b/Tools/BiblioTech/Rewards/RoleController.cs new file mode 100644 index 0000000..e437e2e --- /dev/null +++ b/Tools/BiblioTech/Rewards/RoleController.cs @@ -0,0 +1,82 @@ +using Discord.WebSocket; +using Utils; + +namespace BiblioTech.Rewards +{ + public class RoleController : IDiscordRoleController + { + private const string UsernameTag = ""; + private readonly SocketGuild guild; + private readonly SocketTextChannel? rewardsChannel; + + private readonly RoleReward[] roleRewards = new[] + { + new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!") + }; + + public RoleController(SocketGuild guild) + { + this.guild = guild; + + if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName)) + { + rewardsChannel = guild.TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName); + } + } + + public void GiveRole(ulong roleId, UserData userData) + { + var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId); + if (reward == null) return; + + var user = guild.Users.SingleOrDefault(u => u.Id == userData.DiscordId); + if (user == null) return; + + var role = guild.Roles.SingleOrDefault(r => r.Id == roleId); + if (role == null) return; + + + GiveRole(user, role); + SendNotification(reward, userData, user, role); + } + + private void GiveRole(SocketGuildUser user, SocketRole role) + { + try + { + 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) + { + Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName))); + } + } + catch (Exception ex) + { + Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}"); + } + } + } + + public class RoleReward + { + public RoleReward(ulong roleId, string message) + { + RoleId = roleId; + Message = message; + } + + public ulong RoleId { get; } + public string Message { get; } + } +} diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index 23b25d5..0ce7a34 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -96,6 +96,29 @@ namespace BiblioTech return userData.CreateOverview(); } + public UserData? GetUserDataForAddress(EthAddress? address) + { + if (address == null) return null; + + // If this becomes a performance problem, switch to in-memory cached list. + var files = Directory.GetFiles(Program.Config.UserDataPath); + foreach (var file in files) + { + try + { + var user = JsonConvert.DeserializeObject(File.ReadAllText(file))!; + if (user.CurrentAddress != null && + user.CurrentAddress.Address == address.Address) + { + return user; + } + } + catch { } + } + + return null; + } + private bool SetUserAddress(IUser user, EthAddress? address) { if (GetUserDataForAddress(address) != null) @@ -132,34 +155,11 @@ namespace BiblioTech private UserData CreateAndSaveNewUserData(IUser user) { - var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List(), new List()); + var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List(), new List(), true); SaveUserData(newUser); return newUser; } - private UserData? GetUserDataForAddress(EthAddress? address) - { - if (address == null) return null; - - // If this becomes a performance problem, switch to in-memory cached list. - var files = Directory.GetFiles(Program.Config.UserDataPath); - foreach (var file in files) - { - try - { - var user = JsonConvert.DeserializeObject(File.ReadAllText(file))!; - if (user.CurrentAddress != null && - user.CurrentAddress.Address == address.Address) - { - return user; - } - } - catch { } - } - - return null; - } - private void SaveUserData(UserData userData) { var filename = GetFilename(userData); @@ -185,7 +185,7 @@ namespace BiblioTech public class UserData { - public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents) + public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents, bool notificationsEnabled) { DiscordId = discordId; Name = name; @@ -193,6 +193,7 @@ namespace BiblioTech CurrentAddress = currentAddress; AssociateEvents = associateEvents; MintEvents = mintEvents; + NotificationsEnabled = notificationsEnabled; } public ulong DiscordId { get; } @@ -201,6 +202,7 @@ namespace BiblioTech public EthAddress? CurrentAddress { get; set; } public List AssociateEvents { get; } public List MintEvents { get; } + public bool NotificationsEnabled { get; } public string[] CreateOverview() {