Setting up rewards
This commit is contained in:
parent
c6a7489f11
commit
1b7c11b849
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
private void ProcessRewards(GiveRewards rewards)
|
||||
if (!content.StartsWith("{")) return;
|
||||
var rewards = JsonConvert.DeserializeObject<GiveRewardsCommand>(content);
|
||||
if (rewards != null)
|
||||
{
|
||||
Program.Log.Log("Processing: " + JsonConvert.SerializeObject(rewards));
|
||||
foreach (var reward in rewards.Rewards) ProcessReward(reward);
|
||||
AttachUsers(rewards);
|
||||
await ProcessRewards(rewards);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessReward(Reward reward)
|
||||
private void AttachUsers(GiveRewardsCommand rewards)
|
||||
{
|
||||
foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress);
|
||||
foreach (var reward in rewards.Rewards)
|
||||
{
|
||||
reward.Users = reward.UserAddresses.Select(GetUserFromAddress).Where(u => u != null).Cast<UserData>().ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void GiveRoleToUser(ulong rewardId, string userAddress)
|
||||
private UserData? GetUserFromAddress(string address)
|
||||
{
|
||||
var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress));
|
||||
if (userData == null) { Program.Log.Log("no userdata"); return; }
|
||||
try
|
||||
{
|
||||
return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
roleController.GiveRole(rewardId, userData);
|
||||
private async Task ProcessRewards(GiveRewardsCommand rewards)
|
||||
{
|
||||
await roleController.GiveRewards(rewards);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
foreach (var u in ulist)
|
||||
{
|
||||
result.Add(u.Id, u);
|
||||
}
|
||||
catch (Exception ex)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<ulong, RoleRewardConfig> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
|
||||
{
|
||||
Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
|
||||
var result = new Dictionary<ulong, RoleRewardConfig>();
|
||||
foreach (var r in rewards.Rewards)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendNotification(RoleReward reward, UserData userData, SocketGuildUser user, SocketRole role)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (userData.NotificationsEnabled && rewardsChannel != null)
|
||||
{
|
||||
Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName)));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}");
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue