Sets up rewards api and handling.

This commit is contained in:
benbierens 2023-12-20 15:56:03 +01:00
parent 391a2653d9
commit 2f10b30283
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
13 changed files with 236 additions and 27 deletions

View File

@ -1,4 +1,4 @@
namespace ContinuousTests namespace Utils
{ {
public class TaskFactory public class TaskFactory
{ {

View File

@ -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 = "";

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using DistTestCore.Logs; using DistTestCore.Logs;
using Logging; using Logging;
using TaskFactory = Utils.TaskFactory;
namespace ContinuousTests namespace ContinuousTests
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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