Setting up rewards

This commit is contained in:
benbierens 2024-01-22 10:27:07 +01:00
parent c6a7489f11
commit 1b7c11b849
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
21 changed files with 665 additions and 150 deletions

View File

@ -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;
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
</ItemGroup>
</Project>

View File

@ -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<string>();
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<string> error, string name)
{
var result = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
return result;
}
}
}

View File

@ -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<bool> 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<string> 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}";
}
}
}

View File

@ -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");
}
}
}
}

View File

@ -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<Configuration>(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);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\Tools\BiblioTech\BiblioTech.csproj" />
</ItemGroup>
</Project>

View File

@ -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<TimeRange, Task> 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);
}
}
}

View File

@ -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<string>();
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<string> 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())
{

View File

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\GethConnector\GethConnector.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>

View File

@ -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!");
}
}
}

View File

@ -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
{

View File

@ -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<bool?> 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;
}
}
}

View File

@ -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)
);

View File

@ -1,13 +0,0 @@
namespace BiblioTech.Rewards
{
public class GiveRewards
{
public Reward[] Rewards { get; set; } = Array.Empty<Reward>();
}
public class Reward
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace BiblioTech.Rewards
{
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
}
public class RewardUsersCommand
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
[JsonIgnore]
public UserData[] Users { get; set; } = Array.Empty<UserData>();
}
}

View File

@ -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<GiveRewards>(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<GiveRewardsCommand>(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<UserData>().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);
}
}
}

View File

@ -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!"),
};
}
}
}

View File

@ -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 = "<USER>";
public const string UsernameTag = "<USER>";
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<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
try
var result = new Dictionary<ulong, IGuildUser>();
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<ulong, RoleRewardConfig> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
{
var result = new Dictionary<ulong, RoleRewardConfig>();
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<ulong, IGuildUser> users;
private readonly Dictionary<ulong, RoleRewardConfig> roles;
private readonly SocketTextChannel? rewardsChannel;
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleRewardConfig> 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; }
}
}

View File

@ -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<Ether>? eth, Transaction<TestToken>? 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<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> MintEvents { get; }
public bool NotificationsEnabled { get; }
public bool NotificationsEnabled { get; set; }
public string[] CreateOverview()
{

View File

@ -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}