diff --git a/Framework/DiscordRewards/CheckConfig.cs b/Framework/DiscordRewards/CheckConfig.cs deleted file mode 100644 index 34425cea..00000000 --- a/Framework/DiscordRewards/CheckConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Utils; - -namespace DiscordRewards -{ - public class CheckConfig - { - public CheckType Type { get; set; } - public ulong MinNumberOfHosts { get; set; } - public ByteSize MinSlotSize { get; set; } = 0.Bytes(); - public TimeSpan MinDuration { get; set; } = TimeSpan.Zero; - } - - public enum CheckType - { - Uninitialized, - HostFilledSlot, - HostFinishedSlot, - ClientPostedContract, - ClientStartedContract, - } -} diff --git a/Framework/DiscordRewards/GiveRewardsCommand.cs b/Framework/DiscordRewards/EventsAndErrors.cs similarity index 53% rename from Framework/DiscordRewards/GiveRewardsCommand.cs rename to Framework/DiscordRewards/EventsAndErrors.cs index 3aae088b..53e4b3cc 100644 --- a/Framework/DiscordRewards/GiveRewardsCommand.cs +++ b/Framework/DiscordRewards/EventsAndErrors.cs @@ -1,23 +1,16 @@ namespace DiscordRewards { - public class GiveRewardsCommand + public class EventsAndErrors { - public RewardUsersCommand[] Rewards { get; set; } = Array.Empty(); public ChainEventMessage[] EventsOverview { get; set; } = Array.Empty(); public string[] Errors { get; set; } = Array.Empty(); public bool HasAny() { - return Rewards.Any() || EventsOverview.Any(); + return Errors.Length > 0 || EventsOverview.Length > 0; } } - public class RewardUsersCommand - { - public ulong RewardId { get; set; } - public string[] UserAddresses { get; set; } = Array.Empty(); - } - public class ChainEventMessage { public ulong BlockNumber { get; set; } diff --git a/Framework/DiscordRewards/RewardConfig.cs b/Framework/DiscordRewards/RewardConfig.cs deleted file mode 100644 index dda0dfe1..00000000 --- a/Framework/DiscordRewards/RewardConfig.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace DiscordRewards -{ - public class RewardConfig - { - public const string UsernameTag = ""; - - public RewardConfig(ulong roleId, string message, CheckConfig checkConfig) - { - RoleId = roleId; - Message = message; - CheckConfig = checkConfig; - } - - public ulong RoleId { get; } - public string Message { get; } - public CheckConfig CheckConfig { get; } - } -} diff --git a/Framework/DiscordRewards/RewardRepo.cs b/Framework/DiscordRewards/RewardRepo.cs deleted file mode 100644 index 1c97caf9..00000000 --- a/Framework/DiscordRewards/RewardRepo.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace DiscordRewards -{ - public class RewardRepo - { - private static string Tag => RewardConfig.UsernameTag; - - public RewardConfig[] Rewards { get; } = new RewardConfig[0]; - - // Example configuration, from test server: - //{ - // // Filled any slot - // new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig - // { - // Type = CheckType.HostFilledSlot - // }), - - // // Finished any slot - // new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig - // { - // Type = CheckType.HostFinishedSlot - // }), - - // // Finished a sizable slot - // new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig - // { - // Type = CheckType.HostFinishedSlot, - // MinSlotSize = 10.MB(), - // MinDuration = TimeSpan.FromMinutes(5.0), - // }), - - // // Posted any contract - // new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig - // { - // Type = CheckType.ClientPostedContract - // }), - - // // Started any contract - // new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig - // { - // Type = CheckType.ClientStartedContract - // }), - - // // Started a sizable contract - // new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig - // { - // Type = CheckType.ClientStartedContract, - // MinNumberOfHosts = 4, - // MinSlotSize = 10.MB(), - // MinDuration = TimeSpan.FromMinutes(5.0), - // }) - //}; - } -} diff --git a/Tests/ExperimentalTests/UtilityTests/DiscordBotTests.cs b/Tests/ExperimentalTests/UtilityTests/DiscordBotTests.cs index abebac0c..22819e1b 100644 --- a/Tests/ExperimentalTests/UtilityTests/DiscordBotTests.cs +++ b/Tests/ExperimentalTests/UtilityTests/DiscordBotTests.cs @@ -88,7 +88,7 @@ namespace ExperimentalTests.UtilityTests $"Event '{msg}' did not occure correct number of times."); } - private void OnCommand(string timestamp, GiveRewardsCommand call) + private void OnCommand(string timestamp, EventsAndErrors call) { Log($""); foreach (var e in call.EventsOverview) @@ -276,7 +276,7 @@ namespace ExperimentalTests.UtilityTests monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log"); } - public void Start(Action onCommand) + public void Start(Action onCommand) { monitor.Start(line => ParseLine(line, onCommand)); } @@ -286,14 +286,14 @@ namespace ExperimentalTests.UtilityTests monitor.Stop(); } - private void ParseLine(string line, Action onCommand) + private void ParseLine(string line, Action onCommand) { try { var timestamp = line.Substring(0, 30); var json = line.Substring(31); - var cmd = JsonConvert.DeserializeObject(json); + var cmd = JsonConvert.DeserializeObject(json); if (cmd != null) { onCommand(timestamp, cmd); diff --git a/Tools/BiblioTech/AdminChecker.cs b/Tools/BiblioTech/AdminChecker.cs index 532220de..96f89691 100644 --- a/Tools/BiblioTech/AdminChecker.cs +++ b/Tools/BiblioTech/AdminChecker.cs @@ -1,7 +1,6 @@ using BiblioTech.Options; using Discord; using Discord.WebSocket; -using Org.BouncyCastle.Utilities; namespace BiblioTech { diff --git a/Tools/BiblioTech/CodexChecking/ActiveP2pRoleRemover.cs b/Tools/BiblioTech/CodexChecking/ActiveP2pRoleRemover.cs new file mode 100644 index 00000000..4a448792 --- /dev/null +++ b/Tools/BiblioTech/CodexChecking/ActiveP2pRoleRemover.cs @@ -0,0 +1,66 @@ +using Discord; +using Logging; +using System.Threading.Tasks; + +namespace BiblioTech.CodexChecking +{ + public class ActiveP2pRoleRemover + { + private readonly Configuration config; + private readonly ILog log; + private readonly CheckRepo repo; + + public ActiveP2pRoleRemover(Configuration config, ILog log, CheckRepo repo) + { + this.config = config; + this.log = log; + this.repo = repo; + } + + public void Start() + { + if (config.ActiveP2pRoleDurationMinutes > 0) + { + Task.Run(Worker); + } + } + + private void Worker() + { + var loopDelay = TimeSpan.FromMinutes(config.ActiveP2pRoleDurationMinutes) / 60; + var min = TimeSpan.FromMinutes(10.0); + if (loopDelay < min) loopDelay = min; + + try + { + while (true) + { + Thread.Sleep(loopDelay); + CheckP2pRoleRemoval(); + } + } + catch (Exception ex) + { + log.Error($"Exception in {nameof(ActiveP2pRoleRemover)}: {ex}"); + Environment.Exit(1); + } + } + + private void CheckP2pRoleRemoval() + { + var expiryMoment = DateTime.UtcNow - TimeSpan.FromMinutes(config.ActiveP2pRoleDurationMinutes); + + Program.RoleDriver.IterateRemoveActiveP2pParticipants(p => ShouldRemoveRole(p, expiryMoment)); + } + + private bool ShouldRemoveRole(IUser user, DateTime expiryMoment) + { + var report = repo.GetOrCreate(user.Id); + + if (report.UploadCheck.CompletedUtc > expiryMoment) return false; + if (report.DownloadCheck.CompletedUtc > expiryMoment) return false; + + return true; + } + } +} diff --git a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs index 5f37cfb2..c5b9361a 100644 --- a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs +++ b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs @@ -194,7 +194,6 @@ namespace BiblioTech.CodexChecking { await handler.NowCompleted(userId, checkName); - if (check.CompletedUtc != DateTime.MinValue) return; check.CompletedUtc = DateTime.UtcNow; repo.SaveChanges(); @@ -205,8 +204,7 @@ namespace BiblioTech.CodexChecking { var check = repo.GetOrCreate(userId); - if ( - check.UploadCheck.CompletedUtc != DateTime.MinValue && + if (check.UploadCheck.CompletedUtc != DateTime.MinValue && check.DownloadCheck.CompletedUtc != DateTime.MinValue) { await handler.GiveRoleReward(); diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index b6f2f7ca..8c888ea7 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -4,6 +4,9 @@ using Discord; using Newtonsoft.Json; using BiblioTech.Rewards; using Logging; +using BiblioTech.CodexChecking; +using Nethereum.Model; +using static Org.BouncyCastle.Math.EC.ECCurve; namespace BiblioTech { @@ -11,13 +14,15 @@ namespace BiblioTech { private readonly DiscordSocketClient client; private readonly CustomReplacement replacement; + private readonly ActiveP2pRoleRemover roleRemover; private readonly BaseCommand[] commands; private readonly ILog log; - public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, params BaseCommand[] commands) + public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, ActiveP2pRoleRemover roleRemover, params BaseCommand[] commands) { this.client = client; this.replacement = replacement; + this.roleRemover = roleRemover; this.commands = commands; this.log = log; client.Ready += Client_Ready; @@ -30,10 +35,14 @@ namespace BiblioTech Program.AdminChecker.SetGuild(guild); log.Log($"Initializing for guild: '{guild.Name}'"); - 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()); - Program.RoleDriver = new RoleDriver(client, log, replacement); + var adminChannel = GetChannel(guild, Program.Config.AdminChannelId); + if (adminChannel == null) throw new Exception("No admin message channel"); + var chainEventsChannel = GetChannel(guild, Program.Config.ChainEventsChannelId); + var rewardsChannel = GetChannel(guild, Program.Config.RewardsChannelId); + + Program.AdminChecker.SetAdminChannel(adminChannel); + Program.RoleDriver = new RoleDriver(client, Program.UserRepo, log, rewardsChannel); + Program.EventsSender = new ChainEventsSender(log, replacement, chainEventsChannel); var builders = commands.Select(c => { @@ -65,6 +74,8 @@ namespace BiblioTech { log.Log($"{cmd.Name} ({cmd.Description}) [{DescribOptions(cmd.Options)}]"); } + + roleRemover.Start(); } catch (HttpException exception) { @@ -75,6 +86,12 @@ namespace BiblioTech log.Log("Initialized."); } + private SocketTextChannel? GetChannel(SocketGuild guild, ulong id) + { + if (id == 0) return null; + return guild.TextChannels.SingleOrDefault(c => c.Id == id); + } + private string DescribOptions(IReadOnlyCollection options) { return string.Join(",", options.Select(DescribeOption).ToArray()); diff --git a/Tools/BiblioTech/Commands/CheckResponseHandler.cs b/Tools/BiblioTech/Commands/CheckResponseHandler.cs index 8d20e5bf..ca9dedd4 100644 --- a/Tools/BiblioTech/Commands/CheckResponseHandler.cs +++ b/Tools/BiblioTech/Commands/CheckResponseHandler.cs @@ -56,7 +56,11 @@ namespace BiblioTech.Commands { try { - await Program.RoleDriver.GiveAltruisticRole(user); + await Program.RoleDriver.RunRoleGiver(async r => + { + await r.GiveAltruisticRole(user); + await r.GiveActiveP2pParticipant(user); + }); await context.Followup($"Congratulations! You've been granted the Altruistic Mode role!"); } catch (Exception ex) diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index b732e090..a8187379 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -26,9 +26,6 @@ namespace BiblioTech [Uniform("chain-events-channel-id", "cc", "CHAINEVENTSCHANNELID", false, "ID of the Discord server channel where chain events will be posted.")] public ulong ChainEventsChannelId { get; set; } - [Uniform("altruistic-role-id", "ar", "ALTRUISTICROLE", true, "ID of the Discord server role for Altruistic Mode.")] - public ulong AltruisticRoleId { get; set; } - [Uniform("reward-api-port", "rp", "REWARDAPIPORT", true, "TCP listen port for the reward API.")] public int RewardApiPort { get; set; } = 31080; @@ -47,6 +44,37 @@ namespace BiblioTech [Uniform("codex-endpoint-auth", "cea", "CODEXENDPOINTAUTH", false, "Codex endpoint basic auth. Colon separated username and password. (default: empty, no auth used.)")] public string CodexEndpointAuth { get; set; } = ""; + #region Role Rewards + + /// + /// Awarded when both checkupload and checkdownload have been completed. + /// + [Uniform("altruistic-role-id", "ar", "ALTRUISTICROLE", true, "ID of the Discord server role for Altruistic Mode.")] + public ulong AltruisticRoleId { get; set; } + + /// + /// Awarded as long as either checkupload or checkdownload were completed within the last ActiveP2pRoleDuration minutes. + /// + [Uniform("active-p2p-role-id", "apri", "ACTIVEP2PROLEID", false, "ID of discord server role for active p2p participants.")] + public ulong ActiveP2pParticipantRoleId { get; set; } + + [Uniform("active-p2p-role-duration", "aprd", "ACTIVEP2PROLEDURATION", false, "Duration in minutes for the active p2p participant role from the last successful check command.")] + public int ActiveP2pRoleDurationMinutes { get; set; } + + /// + /// Awarded as long as the user is hosting at least 1 slot. + /// + [Uniform("active-host-role-id", "ahri", "ACTIVEHOSTROLEID", false, "Id of discord server role for active slot hosters.")] + public ulong ActiveHostRoleId { get; set; } + + /// + /// Awarded as long as the user has at least 1 active storage purchase contract. + /// + [Uniform("active-client-role-id", "acri", "ACTIVECLIENTROLEID", false, "Id of discord server role for users with at least 1 active purchase contract.")] + public ulong ActiveClientRoleId { get; set; } + + #endregion + public string EndpointsPath => Path.Combine(DataPath, "endpoints"); public string UserDataPath => Path.Combine(DataPath, "users"); public string ChecksDataPath => Path.Combine(DataPath, "checks"); diff --git a/Tools/BiblioTech/LoggingRoleDriver.cs b/Tools/BiblioTech/LoggingRoleDriver.cs index f5435523..a5631271 100644 --- a/Tools/BiblioTech/LoggingRoleDriver.cs +++ b/Tools/BiblioTech/LoggingRoleDriver.cs @@ -15,18 +15,37 @@ namespace BiblioTech this.log = log; } - public async Task GiveAltruisticRole(IUser user) + public async Task RunRoleGiver(Func action) { await Task.CompletedTask; - - log.Log($"Give altruistic role to {user.Id}"); + await action(new LoggingRoleGiver(log)); } - public async Task GiveRewards(GiveRewardsCommand rewards) + public async Task IterateRemoveActiveP2pParticipants(Func predicate) { await Task.CompletedTask; + } - log.Log(JsonConvert.SerializeObject(rewards, Formatting.None)); + private class LoggingRoleGiver : IRoleGiver + { + private readonly ILog log; + + public LoggingRoleGiver(ILog log) + { + this.log = log; + } + + public async Task GiveActiveP2pParticipant(IUser user) + { + log.Log($"Giving ActiveP2p role to " + user.Id); + await Task.CompletedTask; + } + + public async Task GiveAltruisticRole(IUser user) + { + log.Log($"Giving Altruistic role to " + user.Id); + await Task.CompletedTask; + } } } } diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index af2c9ca3..df1ce6bc 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -5,6 +5,7 @@ using BiblioTech.Rewards; using Discord; using Discord.WebSocket; using Logging; +using Nethereum.Model; namespace BiblioTech { @@ -17,6 +18,7 @@ namespace BiblioTech public static UserRepo UserRepo { get; } = new UserRepo(); public static AdminChecker AdminChecker { get; private set; } = null!; public static IDiscordRoleDriver RoleDriver { get; set; } = null!; + public static ChainEventsSender EventsSender { get; set; } = null!; public static ILog Log { get; private set; } = null!; public static Task Main(string[] args) @@ -88,7 +90,8 @@ namespace BiblioTech var notifyCommand = new NotifyCommand(); var associateCommand = new UserAssociateCommand(notifyCommand); var sprCommand = new SprCommand(); - var handler = new CommandHandler(Log, client, replacement, + var roleRemover = new ActiveP2pRoleRemover(Config, Log, checkRepo); + var handler = new CommandHandler(Log, client, replacement, roleRemover, new GetBalanceCommand(associateCommand), new MintCommand(associateCommand), sprCommand, diff --git a/Tools/BiblioTech/Rewards/RewardContext.cs b/Tools/BiblioTech/Rewards/RewardContext.cs deleted file mode 100644 index d05d5f6d..00000000 --- a/Tools/BiblioTech/Rewards/RewardContext.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Discord.WebSocket; -using Discord; -using DiscordRewards; - -namespace BiblioTech.Rewards -{ - public class RewardContext - { - 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(UserReward[] rewards) - { - foreach (var rewardCommand in rewards) - { - if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId)) - { - var role = roles[rewardCommand.RewardCommand.RewardId]; - await ProcessRewardCommand(role, rewardCommand); - } - else - { - Program.Log.Error($"RoleID not found on guild: {rewardCommand.RewardCommand.RewardId}"); - } - } - } - - private async Task ProcessRewardCommand(RoleReward role, UserReward reward) - { - foreach (var user in reward.Users) - { - await GiveReward(role, user); - } - } - - private async Task GiveReward(RoleReward 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(); - var logMessage = $"Giving reward '{role.SocketRole.Id}' to user '{user.DiscordId}'({user.Name})[" + - $"alreadyHas:{string.Join(",", alreadyHas.Select(a => a.ToString()))}]: "; - - - if (alreadyHas.Any(r => r == role.Reward.RoleId)) - { - logMessage += "Already has role"; - Program.Log.Log(logMessage); - return; - } - - await GiveRole(guildUser, role.SocketRole); - await SendNotification(role, user, guildUser); - await Task.Delay(1000); - logMessage += "Role given. Notification sent."; - Program.Log.Log(logMessage); - } - - 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(RoleReward reward, UserData userData, IGuildUser user) - { - try - { - if (userData.NotificationsEnabled && rewardsChannel != null) - { - var msg = reward.Reward.Message.Replace(RewardConfig.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}"); - } - } - } -} diff --git a/Tools/BiblioTech/Rewards/RewardController.cs b/Tools/BiblioTech/Rewards/RewardController.cs index 3a20a084..7cbed562 100644 --- a/Tools/BiblioTech/Rewards/RewardController.cs +++ b/Tools/BiblioTech/Rewards/RewardController.cs @@ -4,10 +4,20 @@ using Microsoft.AspNetCore.Mvc; namespace BiblioTech.Rewards { + /// + /// We like callbacks in this interface because we're trying to batch role-modifying operations, + /// So that we're not poking the server lots of times very quickly. + /// public interface IDiscordRoleDriver { - Task GiveRewards(GiveRewardsCommand rewards); + Task RunRoleGiver(Func action); + Task IterateRemoveActiveP2pParticipants(Func predicate); + } + + public interface IRoleGiver + { Task GiveAltruisticRole(IUser user); + Task GiveActiveP2pParticipant(IUser user); } [Route("api/[controller]")] @@ -21,11 +31,11 @@ namespace BiblioTech.Rewards } [HttpPost] - public async Task Give(GiveRewardsCommand cmd) + public async Task Give(EventsAndErrors cmd) { try { - await Program.RoleDriver.GiveRewards(cmd); + await Program.EventsSender.ProcessChainEvents(cmd.EventsOverview, cmd.Errors); } catch (Exception ex) { diff --git a/Tools/BiblioTech/Rewards/RoleDriver.cs b/Tools/BiblioTech/Rewards/RoleDriver.cs index 1964da8e..c716c011 100644 --- a/Tools/BiblioTech/Rewards/RoleDriver.cs +++ b/Tools/BiblioTech/Rewards/RoleDriver.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using DiscordRewards; +using k8s.KubeConfigModels; using Logging; using Newtonsoft.Json; using Utils; @@ -10,145 +11,46 @@ namespace BiblioTech.Rewards public class RoleDriver : IDiscordRoleDriver { private readonly DiscordSocketClient client; + private readonly UserRepo userRepo; private readonly ILog log; private readonly SocketTextChannel? rewardsChannel; - private readonly ChainEventsSender eventsSender; - private readonly RewardRepo repo = new RewardRepo(); - public RoleDriver(DiscordSocketClient client, ILog log, CustomReplacement replacement) + public RoleDriver(DiscordSocketClient client, UserRepo userRepo, ILog log, SocketTextChannel? rewardsChannel) { this.client = client; + this.userRepo = userRepo; this.log = log; - rewardsChannel = GetChannel(Program.Config.RewardsChannelId); - eventsSender = new ChainEventsSender(log, replacement, GetChannel(Program.Config.ChainEventsChannelId)); + this.rewardsChannel = rewardsChannel; } - public async Task GiveRewards(GiveRewardsCommand rewards) + public async Task RunRoleGiver(Func action) { - log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'"); + var context = await OpenRoleModifyContext(); + var mapper = new RoleMapper(context); + await action(mapper); + } - if (rewards.Rewards.Any()) + public async Task IterateRemoveActiveP2pParticipants(Func shouldRemove) + { + var context = await OpenRoleModifyContext(); + foreach (var user in context.Users) { - await ProcessRewards(rewards); - } - - await eventsSender.ProcessChainEvents(rewards.EventsOverview, rewards.Errors); - } - - public async Task GiveAltruisticRole(IUser user) - { - var guild = GetGuild(); - var role = guild.Roles.SingleOrDefault(r => r.Id == Program.Config.AltruisticRoleId); - if (role == null) return; - - var guildUser = guild.Users.SingleOrDefault(u => u.Id == user.Id); - if (guildUser == null) return; - - await guildUser.AddRoleAsync(role); - } - - private async Task ProcessRewards(GiveRewardsCommand rewards) - { - try - { - 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); - - await context.ProcessGiveRewardsCommand(LookUpUsers(rewards)); - } - catch (Exception ex) - { - log.Error("Failed to process rewards: " + ex); - } - } - - private SocketTextChannel? GetChannel(ulong id) - { - if (id == 0) return null; - return GetGuild().TextChannels.SingleOrDefault(c => c.Id == id); - } - - private async Task> LoadAllUsers(SocketGuild guild) - { - log.Log("Loading all users.."); - var result = new Dictionary(); - var users = guild.GetUsersAsync(); - await foreach (var ulist in users) - { - foreach (var u in ulist) + if (user.RoleIds.Any(r => r == Program.Config.ActiveP2pParticipantRoleId)) { - result.Add(u.Id, u); - //var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray()); - //log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]"); - } - } - return result; - } - - private Dictionary LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards) - { - var result = new Dictionary(); - foreach (var r in rewards.Rewards) - { - if (!result.ContainsKey(r.RewardId)) - { - var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId); - if (rewardConfig == null) + // This user has the role. Should it be removed? + if (shouldRemove(user)) { - log.Log($"No Reward is configured for id '{r.RewardId}'."); - } - else - { - var socketRole = guild.GetRole(r.RewardId); - if (socketRole == null) - { - log.Log($"Guild Role by id '{r.RewardId}' not found."); - } - else - { - result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig)); - } + await context.RemoveRole(user, Program.Config.ActiveP2pParticipantRoleId); } } } - - return result; } - private UserReward[] LookUpUsers(GiveRewardsCommand rewards) + private async Task OpenRoleModifyContext() { - return rewards.Rewards.Select(LookUpUserData).ToArray(); - } - - private UserReward LookUpUserData(RewardUsersCommand command) - { - return new UserReward(command, - command.UserAddresses - .Select(LookUpUserDataForAddress) - .Where(d => d != null) - .Cast() - .ToArray()); - } - - private UserData? LookUpUserDataForAddress(string address) - { - try - { - var userData = Program.UserRepo.GetUserDataForAddress(new EthAddress(address)); - if (userData != null) log.Log($"User '{userData.Name}' was looked up."); - else log.Log($"Lookup for user was unsuccessful. EthAddress: '{address}'"); - return userData; - } - catch (Exception ex) - { - log.Error("Error during UserData lookup: " + ex); - return null; - } + var context = new RoleModifyContext(GetGuild(), userRepo, log, rewardsChannel); + await context.Initialize(); + return context; } private SocketGuild GetGuild() @@ -163,27 +65,23 @@ namespace BiblioTech.Rewards } } - public class RoleReward + public class RoleMapper : IRoleGiver { - public RoleReward(SocketRole socketRole, RewardConfig reward) + private readonly RoleModifyContext context; + + public RoleMapper(RoleModifyContext context) { - SocketRole = socketRole; - Reward = reward; + this.context = context; } - public SocketRole SocketRole { get; } - public RewardConfig Reward { get; } - } - - public class UserReward - { - public UserReward(RewardUsersCommand rewardCommand, UserData[] users) + public async Task GiveActiveP2pParticipant(IUser user) { - RewardCommand = rewardCommand; - Users = users; + await context.GiveRole(user, Program.Config.ActiveP2pParticipantRoleId); } - public RewardUsersCommand RewardCommand { get; } - public UserData[] Users { get; } + public async Task GiveAltruisticRole(IUser user) + { + await context.GiveRole(user, Program.Config.AltruisticRoleId); + } } } diff --git a/Tools/BiblioTech/Rewards/RoleModifyContext.cs b/Tools/BiblioTech/Rewards/RoleModifyContext.cs new file mode 100644 index 00000000..124d687d --- /dev/null +++ b/Tools/BiblioTech/Rewards/RoleModifyContext.cs @@ -0,0 +1,115 @@ +using Discord.WebSocket; +using Discord; +using DiscordRewards; +using Nethereum.Model; +using Logging; + +namespace BiblioTech.Rewards +{ + public class RoleModifyContext + { + private Dictionary users = new(); + private Dictionary roles = new(); + private readonly SocketGuild guild; + private readonly UserRepo userRepo; + private readonly ILog log; + private readonly SocketTextChannel? rewardsChannel; + + public RoleModifyContext(SocketGuild guild, UserRepo userRepo, ILog log, SocketTextChannel? rewardsChannel) + { + this.guild = guild; + this.userRepo = userRepo; + this.log = log; + this.rewardsChannel = rewardsChannel; + } + + public async Task Initialize() + { + this.users = await LoadAllUsers(guild); + this.roles = LoadAllRoles(guild); + } + + public IGuildUser[] Users => users.Values.ToArray(); + + public async Task GiveRole(IUser user, ulong roleId) + { + var role = GetRole(roleId); + var guildUser = GetUser(user.Id); + if (role == null) return; + if (guildUser == null) return; + + await guildUser.AddRoleAsync(role); + await Program.AdminChecker.SendInAdminChannel($"Added role '{role.Name}' for user <@{user.Id}>."); + + await SendNotification(guildUser, role); + } + + public async Task RemoveRole(IUser user, ulong roleId) + { + var role = GetRole(roleId); + var guildUser = GetUser(user.Id); + if (role == null) return; + if (guildUser == null) return; + + await guildUser.RemoveRoleAsync(role); + await Program.AdminChecker.SendInAdminChannel($"Removed role '{role.Name}' for user <@{user.Id}>."); + } + + private SocketRole? GetRole(ulong roleId) + { + if (roles.ContainsKey(roleId)) return roles[roleId]; + return null; + } + + private IGuildUser? GetUser(ulong userId) + { + if (users.ContainsKey(userId)) return users[userId]; + return null; + } + + private async Task> LoadAllUsers(SocketGuild guild) + { + log.Log("Loading all users.."); + var result = new Dictionary(); + var users = guild.GetUsersAsync(); + await foreach (var ulist in users) + { + foreach (var u in ulist) + { + result.Add(u.Id, u); + } + } + return result; + } + + private Dictionary LoadAllRoles(SocketGuild guild) + { + var result = new Dictionary(); + var roles = guild.Roles.ToArray(); + foreach (var role in roles) + { + result.Add(role.Id, role); + } + return result; + } + + private async Task SendNotification(IGuildUser user, SocketRole role) + { + try + { + var userData = userRepo.GetUserById(user.Id); + if (userData == null) return; + + if (userData.NotificationsEnabled && rewardsChannel != null) + { + var msg = $"<@{user.Id}> has received '{role.Name}'."; + await rewardsChannel.SendMessageAsync(msg); + } + } + catch (Exception ex) + { + log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}"); + } + } + } +} diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index d1a766ab..b6c02500 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -41,6 +41,13 @@ namespace BiblioTech return cache.Values.ToArray(); } + public UserData? GetUserById(ulong id) + { + if (cache.Count == 0) LoadAllUserData(); + if (cache.ContainsKey(id)) return cache[id]; + return null; + } + public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction? eth, Transaction? tokens) { lock (repoLock) diff --git a/Tools/TestNetRewarder/BotClient.cs b/Tools/TestNetRewarder/BotClient.cs index 6a5c5759..c5ddd9a0 100644 --- a/Tools/TestNetRewarder/BotClient.cs +++ b/Tools/TestNetRewarder/BotClient.cs @@ -21,7 +21,7 @@ namespace TestNetRewarder return result == "Pong"; } - public async Task SendRewards(GiveRewardsCommand command) + public async Task SendRewards(EventsAndErrors command) { if (command == null) return false; var result = await HttpPostJson(command); diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index 2b49ef11..7377a7da 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -8,7 +8,6 @@ namespace TestNetRewarder public class Processor : ITimeSegmentHandler { private readonly RequestBuilder builder; - private readonly RewardChecker rewardChecker; private readonly EventsFormatter eventsFormatter; private readonly ChainState chainState; private readonly Configuration config; @@ -24,15 +23,9 @@ namespace TestNetRewarder lastPeriodUpdateUtc = DateTime.UtcNow; builder = new RequestBuilder(); - rewardChecker = new RewardChecker(builder); eventsFormatter = new EventsFormatter(config); - var handler = new ChainStateChangeHandlerMux( - rewardChecker.Handler, - eventsFormatter - ); - - chainState = new ChainState(log, contracts, handler, config.HistoryStartUtc, + chainState = new ChainState(log, contracts, eventsFormatter, config.HistoryStartUtc, doProofPeriodMonitoring: config.ShowProofPeriodReports > 0); } diff --git a/Tools/TestNetRewarder/RequestBuilder.cs b/Tools/TestNetRewarder/RequestBuilder.cs index b1f641b8..adc68032 100644 --- a/Tools/TestNetRewarder/RequestBuilder.cs +++ b/Tools/TestNetRewarder/RequestBuilder.cs @@ -3,38 +3,15 @@ using Utils; namespace TestNetRewarder { - public class RequestBuilder : IRewardGiver + public class RequestBuilder { - private readonly Dictionary> rewards = new Dictionary>(); - - public void Give(RewardConfig reward, EthAddress receiver) + public EventsAndErrors Build(ChainEventMessage[] lines, string[] errors) { - if (rewards.ContainsKey(reward.RoleId)) + return new EventsAndErrors { - rewards[reward.RoleId].Add(receiver); - } - else - { - rewards.Add(reward.RoleId, new List { receiver }); - } - } - - public GiveRewardsCommand Build(ChainEventMessage[] lines, string[] errors) - { - var result = new GiveRewardsCommand - { - Rewards = rewards.Select(p => new RewardUsersCommand - { - RewardId = p.Key, - UserAddresses = p.Value.Select(v => v.Address).ToArray() - }).ToArray(), EventsOverview = lines, Errors = errors }; - - rewards.Clear(); - - return result; } } } diff --git a/Tools/TestNetRewarder/RewardCheck.cs b/Tools/TestNetRewarder/RewardCheck.cs deleted file mode 100644 index 2f947d8d..00000000 --- a/Tools/TestNetRewarder/RewardCheck.cs +++ /dev/null @@ -1,113 +0,0 @@ -using BlockchainUtils; -using CodexContractsPlugin.ChainMonitor; -using DiscordRewards; -using System.Numerics; -using Utils; - -namespace TestNetRewarder -{ - public interface IRewardGiver - { - void Give(RewardConfig reward, EthAddress receiver); - } - - public class RewardCheck : IChainStateChangeHandler - { - private readonly RewardConfig reward; - private readonly IRewardGiver giver; - - public RewardCheck(RewardConfig reward, IRewardGiver giver) - { - this.reward = reward; - this.giver = giver; - } - - public void OnNewRequest(RequestEvent requestEvent) - { - if (MeetsRequirements(CheckType.ClientPostedContract, requestEvent)) - { - GiveReward(reward, requestEvent.Request.Client); - } - } - - public void OnRequestCancelled(RequestEvent requestEvent) - { - } - - public void OnRequestFailed(RequestEvent requestEvent) - { - } - - public void OnRequestFinished(RequestEvent requestEvent) - { - if (MeetsRequirements(CheckType.HostFinishedSlot, requestEvent)) - { - foreach (var host in requestEvent.Request.Hosts.GetHosts()) - { - GiveReward(reward, host); - } - } - } - - public void OnRequestFulfilled(RequestEvent requestEvent) - { - if (MeetsRequirements(CheckType.ClientStartedContract, requestEvent)) - { - GiveReward(reward, requestEvent.Request.Client); - } - } - - public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) - { - if (MeetsRequirements(CheckType.HostFilledSlot, requestEvent)) - { - if (host != null) - { - GiveReward(reward, host); - } - } - } - - public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) - { - } - - public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) - { - } - - public void OnError(string msg) - { - } - - public void OnProofSubmitted(BlockTimeEntry block, string id) - { - } - - private void GiveReward(RewardConfig reward, EthAddress receiver) - { - giver.Give(reward, receiver); - } - - private bool MeetsRequirements(CheckType type, RequestEvent requestEvent) - { - return - reward.CheckConfig.Type == type && - MeetsDurationRequirement(requestEvent.Request) && - MeetsSizeRequirement(requestEvent.Request); - } - - private bool MeetsSizeRequirement(IChainStateRequest r) - { - var slotSize = r.Request.Ask.SlotSize; - ulong min = Convert.ToUInt64(reward.CheckConfig.MinSlotSize.SizeInBytes); - return slotSize >= min; - } - - private bool MeetsDurationRequirement(IChainStateRequest r) - { - var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration); - return duration >= reward.CheckConfig.MinDuration; - } - } -} diff --git a/Tools/TestNetRewarder/RewardChecker.cs b/Tools/TestNetRewarder/RewardChecker.cs deleted file mode 100644 index f8e500cc..00000000 --- a/Tools/TestNetRewarder/RewardChecker.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CodexContractsPlugin.ChainMonitor; -using DiscordRewards; - -namespace TestNetRewarder -{ - public class RewardChecker - { - public RewardChecker(IRewardGiver giver) - { - var repo = new RewardRepo(); - var checks = repo.Rewards.Select(r => new RewardCheck(r, giver)).ToArray(); - Handler = new ChainStateChangeHandlerMux(checks); - } - - public IChainStateChangeHandler Handler { get; } - } -}