Sets up rewards api and handling.
This commit is contained in:
parent
391a2653d9
commit
2f10b30283
|
@ -1,4 +1,4 @@
|
||||||
namespace ContinuousTests
|
namespace Utils
|
||||||
{
|
{
|
||||||
public class TaskFactory
|
public class TaskFactory
|
||||||
{
|
{
|
|
@ -13,6 +13,11 @@
|
||||||
return task.Result;
|
return task.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Wait(Task task)
|
||||||
|
{
|
||||||
|
task.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
public static string FormatDuration(TimeSpan d)
|
public static string FormatDuration(TimeSpan d)
|
||||||
{
|
{
|
||||||
var result = "";
|
var result = "";
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{
|
{
|
||||||
public EthAddress(string address)
|
public EthAddress(string address)
|
||||||
{
|
{
|
||||||
Address = address;
|
Address = address.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Address { get; }
|
public string Address { get; }
|
||||||
|
|
|
@ -3,6 +3,7 @@ using DistTestCore.Logs;
|
||||||
using Logging;
|
using Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Utils;
|
using Utils;
|
||||||
|
using TaskFactory = Utils.TaskFactory;
|
||||||
|
|
||||||
namespace ContinuousTests
|
namespace ContinuousTests
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,7 @@ using CodexPlugin;
|
||||||
using DistTestCore.Logs;
|
using DistTestCore.Logs;
|
||||||
using Core;
|
using Core;
|
||||||
using KubernetesWorkflow.Types;
|
using KubernetesWorkflow.Types;
|
||||||
|
using TaskFactory = Utils.TaskFactory;
|
||||||
|
|
||||||
namespace ContinuousTests
|
namespace ContinuousTests
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using DistTestCore.Logs;
|
using DistTestCore.Logs;
|
||||||
using Logging;
|
using Logging;
|
||||||
|
using TaskFactory = Utils.TaskFactory;
|
||||||
|
|
||||||
namespace ContinuousTests
|
namespace ContinuousTests
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Discord;
|
using Discord;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using BiblioTech.Rewards;
|
||||||
|
|
||||||
namespace BiblioTech
|
namespace BiblioTech
|
||||||
{
|
{
|
||||||
|
@ -25,6 +26,9 @@ namespace BiblioTech
|
||||||
Program.AdminChecker.SetGuild(guild);
|
Program.AdminChecker.SetGuild(guild);
|
||||||
Program.Log.Log($"Initializing for guild: '{guild.Name}'");
|
Program.Log.Log($"Initializing for guild: '{guild.Name}'");
|
||||||
|
|
||||||
|
var roleController = new RoleController(guild);
|
||||||
|
var rewardsApi = new RewardsApi(roleController);
|
||||||
|
|
||||||
var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray();
|
var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray();
|
||||||
if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel");
|
if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel");
|
||||||
Program.AdminChecker.SetAdminChannel(adminChannels.First());
|
Program.AdminChecker.SetAdminChannel(adminChannels.First());
|
||||||
|
@ -58,6 +62,8 @@ namespace BiblioTech
|
||||||
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
|
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
|
||||||
Program.Log.Error(json);
|
Program.Log.Error(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rewardsApi.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SlashCommandHandler(SocketSlashCommand command)
|
private async Task SlashCommandHandler(SocketSlashCommand command)
|
||||||
|
|
|
@ -19,6 +19,10 @@ namespace BiblioTech
|
||||||
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")]
|
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")]
|
||||||
public string AdminChannelName { get; set; } = "admin-channel";
|
public string AdminChannelName { get; set; } = "admin-channel";
|
||||||
|
|
||||||
|
[Uniform("rewards-channel-name", "ac", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")]
|
||||||
|
public string RewardsChannelName { get; set; } = "";
|
||||||
|
|
||||||
|
|
||||||
public string EndpointsPath
|
public string EndpointsPath
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using ArgsUniform;
|
using ArgsUniform;
|
||||||
using BiblioTech.Commands;
|
using BiblioTech.Commands;
|
||||||
|
using BiblioTech.Rewards;
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Logging;
|
using Logging;
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
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,93 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Net;
|
||||||
|
using TaskFactory = Utils.TaskFactory;
|
||||||
|
|
||||||
|
namespace BiblioTech.Rewards
|
||||||
|
{
|
||||||
|
public interface IDiscordRoleController
|
||||||
|
{
|
||||||
|
void GiveRole(ulong roleId, UserData userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RewardsApi
|
||||||
|
{
|
||||||
|
private readonly HttpListener listener = new HttpListener();
|
||||||
|
private readonly TaskFactory taskFactory = new TaskFactory();
|
||||||
|
private readonly IDiscordRoleController roleController;
|
||||||
|
private CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public RewardsApi(IDiscordRoleController roleController)
|
||||||
|
{
|
||||||
|
this.roleController = roleController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
listener.Prefixes.Add($"http://*:31080/");
|
||||||
|
listener.Start();
|
||||||
|
taskFactory.Run(ConnectionDispatcher, nameof(ConnectionDispatcher));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
cts.Cancel();
|
||||||
|
taskFactory.WaitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectionDispatcher()
|
||||||
|
{
|
||||||
|
while (!cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var wait = listener.GetContextAsync();
|
||||||
|
wait.Wait(cts.Token);
|
||||||
|
if (wait.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
taskFactory.Run(() =>
|
||||||
|
{
|
||||||
|
var context = wait.Result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HandleConnection(context);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Log.Error("Exception during HTTP handler: " + ex);
|
||||||
|
}
|
||||||
|
// Whatever happens, everything's always OK.
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
context.Response.OutputStream.Close();
|
||||||
|
}, nameof(HandleConnection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnection(HttpListenerContext context)
|
||||||
|
{
|
||||||
|
var reader = new StreamReader(context.Request.InputStream);
|
||||||
|
var content = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var rewards = JsonConvert.DeserializeObject<GiveRewards>(content);
|
||||||
|
if (rewards != null) ProcessRewards(rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessRewards(GiveRewards rewards)
|
||||||
|
{
|
||||||
|
foreach (var reward in rewards.Rewards) ProcessReward(reward);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessReward(Reward reward)
|
||||||
|
{
|
||||||
|
foreach (var userAddress in reward.UserAddresses) GiveRoleToUser(reward.RewardId, userAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GiveRoleToUser(ulong rewardId, string userAddress)
|
||||||
|
{
|
||||||
|
var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(userAddress));
|
||||||
|
if (userData == null) return;
|
||||||
|
|
||||||
|
roleController.GiveRole(rewardId, userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
|
namespace BiblioTech.Rewards
|
||||||
|
{
|
||||||
|
public class RoleController : IDiscordRoleController
|
||||||
|
{
|
||||||
|
private const string UsernameTag = "<USER>";
|
||||||
|
private readonly SocketGuild guild;
|
||||||
|
private readonly SocketTextChannel? rewardsChannel;
|
||||||
|
|
||||||
|
private readonly RoleReward[] roleRewards = new[]
|
||||||
|
{
|
||||||
|
new RoleReward(1187039439558541498, $"Congratulations {UsernameTag}, you got the test-reward!")
|
||||||
|
};
|
||||||
|
|
||||||
|
public RoleController(SocketGuild guild)
|
||||||
|
{
|
||||||
|
this.guild = guild;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName))
|
||||||
|
{
|
||||||
|
rewardsChannel = guild.TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GiveRole(ulong roleId, UserData userData)
|
||||||
|
{
|
||||||
|
var reward = roleRewards.SingleOrDefault(r => r.RoleId == roleId);
|
||||||
|
if (reward == null) return;
|
||||||
|
|
||||||
|
var user = guild.Users.SingleOrDefault(u => u.Id == userData.DiscordId);
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
var role = guild.Roles.SingleOrDefault(r => r.Id == roleId);
|
||||||
|
if (role == null) return;
|
||||||
|
|
||||||
|
|
||||||
|
GiveRole(user, role);
|
||||||
|
SendNotification(reward, userData, user, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GiveRole(SocketGuildUser user, SocketRole role)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Time.Wait(user.AddRoleAsync(role));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendNotification(RoleReward reward, UserData userData, SocketGuildUser user, SocketRole role)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (userData.NotificationsEnabled && rewardsChannel != null)
|
||||||
|
{
|
||||||
|
Time.Wait(rewardsChannel.SendMessageAsync(reward.Message.Replace(UsernameTag, user.DisplayName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RoleReward
|
||||||
|
{
|
||||||
|
public RoleReward(ulong roleId, string message)
|
||||||
|
{
|
||||||
|
RoleId = roleId;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong RoleId { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,6 +96,29 @@ namespace BiblioTech
|
||||||
return userData.CreateOverview();
|
return userData.CreateOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserData? GetUserDataForAddress(EthAddress? address)
|
||||||
|
{
|
||||||
|
if (address == null) return null;
|
||||||
|
|
||||||
|
// If this becomes a performance problem, switch to in-memory cached list.
|
||||||
|
var files = Directory.GetFiles(Program.Config.UserDataPath);
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
|
||||||
|
if (user.CurrentAddress != null &&
|
||||||
|
user.CurrentAddress.Address == address.Address)
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private bool SetUserAddress(IUser user, EthAddress? address)
|
private bool SetUserAddress(IUser user, EthAddress? address)
|
||||||
{
|
{
|
||||||
if (GetUserDataForAddress(address) != null)
|
if (GetUserDataForAddress(address) != null)
|
||||||
|
@ -132,34 +155,11 @@ namespace BiblioTech
|
||||||
|
|
||||||
private UserData CreateAndSaveNewUserData(IUser user)
|
private UserData CreateAndSaveNewUserData(IUser user)
|
||||||
{
|
{
|
||||||
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
|
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>(), true);
|
||||||
SaveUserData(newUser);
|
SaveUserData(newUser);
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserData? GetUserDataForAddress(EthAddress? address)
|
|
||||||
{
|
|
||||||
if (address == null) return null;
|
|
||||||
|
|
||||||
// If this becomes a performance problem, switch to in-memory cached list.
|
|
||||||
var files = Directory.GetFiles(Program.Config.UserDataPath);
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
|
|
||||||
if (user.CurrentAddress != null &&
|
|
||||||
user.CurrentAddress.Address == address.Address)
|
|
||||||
{
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveUserData(UserData userData)
|
private void SaveUserData(UserData userData)
|
||||||
{
|
{
|
||||||
var filename = GetFilename(userData);
|
var filename = GetFilename(userData);
|
||||||
|
@ -185,7 +185,7 @@ namespace BiblioTech
|
||||||
|
|
||||||
public class UserData
|
public class UserData
|
||||||
{
|
{
|
||||||
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
|
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents, bool notificationsEnabled)
|
||||||
{
|
{
|
||||||
DiscordId = discordId;
|
DiscordId = discordId;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
@ -193,6 +193,7 @@ namespace BiblioTech
|
||||||
CurrentAddress = currentAddress;
|
CurrentAddress = currentAddress;
|
||||||
AssociateEvents = associateEvents;
|
AssociateEvents = associateEvents;
|
||||||
MintEvents = mintEvents;
|
MintEvents = mintEvents;
|
||||||
|
NotificationsEnabled = notificationsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong DiscordId { get; }
|
public ulong DiscordId { get; }
|
||||||
|
@ -201,6 +202,7 @@ namespace BiblioTech
|
||||||
public EthAddress? CurrentAddress { get; set; }
|
public EthAddress? CurrentAddress { get; set; }
|
||||||
public List<UserAssociateAddressEvent> AssociateEvents { get; }
|
public List<UserAssociateAddressEvent> AssociateEvents { get; }
|
||||||
public List<UserMintEvent> MintEvents { get; }
|
public List<UserMintEvent> MintEvents { get; }
|
||||||
|
public bool NotificationsEnabled { get; }
|
||||||
|
|
||||||
public string[] CreateOverview()
|
public string[] CreateOverview()
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue