mirror of
https://github.com/logos-storage/logos-storage-nim-cs-dist-tests.git
synced 2026-01-07 07:53:05 +00:00
Cleans up a lot of old reward system code. Adds periodic role check for p2p participant role
This commit is contained in:
parent
112c1f37c1
commit
9e4e56205a
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,16 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class GiveRewardsCommand
|
||||
public class EventsAndErrors
|
||||
{
|
||||
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
|
||||
public ChainEventMessage[] EventsOverview { get; set; } = Array.Empty<ChainEventMessage>();
|
||||
public string[] Errors { get; set; } = Array.Empty<string>();
|
||||
|
||||
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<string>();
|
||||
}
|
||||
|
||||
public class ChainEventMessage
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
@ -1,18 +0,0 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardConfig
|
||||
{
|
||||
public const string UsernameTag = "<USER>";
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
// })
|
||||
//};
|
||||
}
|
||||
}
|
||||
@ -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($"<API call {timestamp}>");
|
||||
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<string, GiveRewardsCommand> onCommand)
|
||||
public void Start(Action<string, EventsAndErrors> onCommand)
|
||||
{
|
||||
monitor.Start(line => ParseLine(line, onCommand));
|
||||
}
|
||||
@ -286,14 +286,14 @@ namespace ExperimentalTests.UtilityTests
|
||||
monitor.Stop();
|
||||
}
|
||||
|
||||
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
|
||||
private void ParseLine(string line, Action<string, EventsAndErrors> onCommand)
|
||||
{
|
||||
try
|
||||
{
|
||||
var timestamp = line.Substring(0, 30);
|
||||
var json = line.Substring(31);
|
||||
|
||||
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
|
||||
var cmd = JsonConvert.DeserializeObject<EventsAndErrors>(json);
|
||||
if (cmd != null)
|
||||
{
|
||||
onCommand(timestamp, cmd);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using BiblioTech.Options;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
|
||||
66
Tools/BiblioTech/CodexChecking/ActiveP2pRoleRemover.cs
Normal file
66
Tools/BiblioTech/CodexChecking/ActiveP2pRoleRemover.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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<SocketApplicationCommandOption> options)
|
||||
{
|
||||
return string.Join(",", options.Select(DescribeOption).ToArray());
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
/// <summary>
|
||||
/// Awarded when both checkupload and checkdownload have been completed.
|
||||
/// </summary>
|
||||
[Uniform("altruistic-role-id", "ar", "ALTRUISTICROLE", true, "ID of the Discord server role for Altruistic Mode.")]
|
||||
public ulong AltruisticRoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Awarded as long as either checkupload or checkdownload were completed within the last ActiveP2pRoleDuration minutes.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Awarded as long as the user is hosting at least 1 slot.
|
||||
/// </summary>
|
||||
[Uniform("active-host-role-id", "ahri", "ACTIVEHOSTROLEID", false, "Id of discord server role for active slot hosters.")]
|
||||
public ulong ActiveHostRoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Awarded as long as the user has at least 1 active storage purchase contract.
|
||||
/// </summary>
|
||||
[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");
|
||||
|
||||
@ -15,18 +15,37 @@ namespace BiblioTech
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public async Task GiveAltruisticRole(IUser user)
|
||||
public async Task RunRoleGiver(Func<IRoleGiver, Task> 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<IUser, bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
using Discord.WebSocket;
|
||||
using Discord;
|
||||
using DiscordRewards;
|
||||
|
||||
namespace BiblioTech.Rewards
|
||||
{
|
||||
public class RewardContext
|
||||
{
|
||||
private readonly Dictionary<ulong, IGuildUser> users;
|
||||
private readonly Dictionary<ulong, RoleReward> roles;
|
||||
private readonly SocketTextChannel? rewardsChannel;
|
||||
|
||||
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleReward> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,20 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BiblioTech.Rewards
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IDiscordRoleDriver
|
||||
{
|
||||
Task GiveRewards(GiveRewardsCommand rewards);
|
||||
Task RunRoleGiver(Func<IRoleGiver, Task> action);
|
||||
Task IterateRemoveActiveP2pParticipants(Func<IUser, bool> 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<string> Give(GiveRewardsCommand cmd)
|
||||
public async Task<string> Give(EventsAndErrors cmd)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Program.RoleDriver.GiveRewards(cmd);
|
||||
await Program.EventsSender.ProcessChainEvents(cmd.EventsOverview, cmd.Errors);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -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<IRoleGiver, Task> 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<IUser, bool> 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<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
|
||||
{
|
||||
log.Log("Loading all users..");
|
||||
var result = new Dictionary<ulong, IGuildUser>();
|
||||
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<ulong, RoleReward> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
|
||||
{
|
||||
var result = new Dictionary<ulong, RoleReward>();
|
||||
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<RoleModifyContext> 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<UserData>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
Tools/BiblioTech/Rewards/RoleModifyContext.cs
Normal file
115
Tools/BiblioTech/Rewards/RoleModifyContext.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using Discord.WebSocket;
|
||||
using Discord;
|
||||
using DiscordRewards;
|
||||
using Nethereum.Model;
|
||||
using Logging;
|
||||
|
||||
namespace BiblioTech.Rewards
|
||||
{
|
||||
public class RoleModifyContext
|
||||
{
|
||||
private Dictionary<ulong, IGuildUser> users = new();
|
||||
private Dictionary<ulong, SocketRole> 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<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
|
||||
{
|
||||
log.Log("Loading all users..");
|
||||
var result = new Dictionary<ulong, IGuildUser>();
|
||||
var users = guild.GetUsersAsync();
|
||||
await foreach (var ulist in users)
|
||||
{
|
||||
foreach (var u in ulist)
|
||||
{
|
||||
result.Add(u.Id, u);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<ulong, SocketRole> LoadAllRoles(SocketGuild guild)
|
||||
{
|
||||
var result = new Dictionary<ulong, SocketRole>();
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Ether>? eth, Transaction<TestToken>? tokens)
|
||||
{
|
||||
lock (repoLock)
|
||||
|
||||
@ -21,7 +21,7 @@ namespace TestNetRewarder
|
||||
return result == "Pong";
|
||||
}
|
||||
|
||||
public async Task<bool> SendRewards(GiveRewardsCommand command)
|
||||
public async Task<bool> SendRewards(EventsAndErrors command)
|
||||
{
|
||||
if (command == null) return false;
|
||||
var result = await HttpPostJson(command);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -3,38 +3,15 @@ using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class RequestBuilder : IRewardGiver
|
||||
public class RequestBuilder
|
||||
{
|
||||
private readonly Dictionary<ulong, List<EthAddress>> rewards = new Dictionary<ulong, List<EthAddress>>();
|
||||
|
||||
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<EthAddress> { 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user