diff --git a/ProjectPlugins/GethPlugin/EthAddress.cs b/ProjectPlugins/GethPlugin/EthAddress.cs index 54c86386..2954c488 100644 --- a/ProjectPlugins/GethPlugin/EthAddress.cs +++ b/ProjectPlugins/GethPlugin/EthAddress.cs @@ -13,5 +13,10 @@ } public string Address { get; } + + public override string ToString() + { + return Address; + } } } diff --git a/Tools/BiblioTech/BaseCommand.cs b/Tools/BiblioTech/BaseCommand.cs index d62ba448..6086d341 100644 --- a/Tools/BiblioTech/BaseCommand.cs +++ b/Tools/BiblioTech/BaseCommand.cs @@ -1,5 +1,6 @@ using Discord.WebSocket; using Discord; +using BiblioTech.TokenCommands; namespace BiblioTech { @@ -33,19 +34,33 @@ namespace BiblioTech } protected abstract Task Invoke(SocketSlashCommand command); + + protected bool IsSenderAdmin(SocketSlashCommand command) + { + + } + + protected ulong GetUserId(UserOption userOption, SocketSlashCommand command) + { + var targetUser = userOption.GetOptionUserId(command); + if (IsSenderAdmin(command) && targetUser != null) return targetUser.Value; + return command.User.Id; + } } public class CommandOption { - public CommandOption(string name, string description, ApplicationCommandOptionType type) + public CommandOption(string name, string description, ApplicationCommandOptionType type, bool isRequired) { Name = name; Description = description; Type = type; + IsRequired = isRequired; } public string Name { get; } public string Description { get; } public ApplicationCommandOptionType Type { get; } + public bool IsRequired { get; } } } diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index 963de2a0..4e5d8de2 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -31,7 +31,7 @@ namespace BiblioTech foreach (var option in c.Options) { - builder.AddOption(option.Name, option.Type, option.Description, isRequired: true); + builder.AddOption(option.Name, option.Type, option.Description, isRequired: option.IsRequired); } return builder; diff --git a/Tools/BiblioTech/TokenCommands/ClearUserAssociationCommand.cs b/Tools/BiblioTech/TokenCommands/ClearUserAssociationCommand.cs new file mode 100644 index 00000000..10aa2b2d --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/ClearUserAssociationCommand.cs @@ -0,0 +1,34 @@ +using Discord.WebSocket; + +namespace BiblioTech.TokenCommands +{ + public class ClearUserAssociationCommand : BaseCommand + { + private readonly UserOption user = new UserOption( + description: "User to clear Eth address for.", + isRequired: true); + + public override string Name => "clear"; + public override string StartingMessage => "Hold on..."; + public override string Description => "Admin only. Clears current Eth address for a user, allowing them to set a new one."; + + protected override async Task Invoke(SocketSlashCommand command) + { + if (!IsSenderAdmin(command)) + { + await command.FollowupAsync("You're not an admin."); + return; + } + + var userId = user.GetOptionUserId(command); + if (userId == null) + { + await command.FollowupAsync("Failed to get user ID"); + return; + } + + Program.UserRepo.ClearUserAssociatedAddress(userId.Value); + await command.FollowupAsync("Done."); ; + } + } +} diff --git a/Tools/BiblioTech/TokenCommands/EthAddressOption.cs b/Tools/BiblioTech/TokenCommands/EthAddressOption.cs index 6c351bad..3298ee8b 100644 --- a/Tools/BiblioTech/TokenCommands/EthAddressOption.cs +++ b/Tools/BiblioTech/TokenCommands/EthAddressOption.cs @@ -8,7 +8,8 @@ namespace BiblioTech.TokenCommands public EthAddressOption() : base(name: "ethaddress", description: "Ethereum address starting with '0x'.", - type: Discord.ApplicationCommandOptionType.String) + type: Discord.ApplicationCommandOptionType.String, + isRequired: true) { } diff --git a/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs b/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs index add4c4a7..000cfaae 100644 --- a/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs +++ b/Tools/BiblioTech/TokenCommands/GetBalanceCommand.cs @@ -1,5 +1,4 @@ using CodexContractsPlugin; -using CodexPlugin; using Core; using Discord.WebSocket; using GethPlugin; @@ -8,29 +7,36 @@ namespace BiblioTech.TokenCommands { public class GetBalanceCommand : BaseNetCommand { - private readonly EthAddressOption ethOption = new EthAddressOption(); + private readonly UserAssociateCommand userAssociateCommand; + private readonly UserOption optionalUser = new UserOption( + description: "If set, get balance for another user. (Optional, admin-only)", + isRequired: false); - public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci) + public GetBalanceCommand(DeploymentsFilesMonitor monitor, CoreInterface ci, UserAssociateCommand userAssociateCommand) : base(monitor, ci) { + this.userAssociateCommand = userAssociateCommand; } - // connect users to eth address! - public override string Name => "balance"; public override string StartingMessage => "Fetching balance..."; public override string Description => "Shows Eth and TestToken balance of an eth address."; - public override CommandOption[] Options => new[] { ethOption }; + public override CommandOption[] Options => new[] { optionalUser }; protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts) { - var addr = await ethOption.Parse(command); - if (addr == null) return; + var userId = GetUserId(optionalUser, command); + var addr = Program.UserRepo.GetCurrentAddressForUser(userId); + if (addr == null) + { + await command.FollowupAsync($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first."); + return; + } var eth = gethNode.GetEthBalance(addr); var testTokens = contracts.GetTestTokenBalance(gethNode, addr); - await command.RespondAsync($"Address '{addr.Address}' has {eth} and {testTokens}."); + await command.FollowupAsync($"{command.User.Username} has {eth} and {testTokens}."); } } } diff --git a/Tools/BiblioTech/TokenCommands/MintCommand.cs b/Tools/BiblioTech/TokenCommands/MintCommand.cs index c22c91a7..330d2333 100644 --- a/Tools/BiblioTech/TokenCommands/MintCommand.cs +++ b/Tools/BiblioTech/TokenCommands/MintCommand.cs @@ -10,7 +10,9 @@ namespace BiblioTech.TokenCommands private readonly string nl = Environment.NewLine; private readonly Ether defaultEthToSend = 10.Eth(); private readonly TestToken defaultTestTokensToMint = 1024.TestTokens(); - private readonly EthAddressOption ethOption = new EthAddressOption(); + private readonly UserOption optionalUser = new UserOption( + description: "If set, mint tokens for this user. (Optional, admin-only)", + isRequired: true); public MintCommand(DeploymentsFilesMonitor monitor, CoreInterface ci) : base(monitor, ci) @@ -19,12 +21,13 @@ namespace BiblioTech.TokenCommands public override string Name => "mint"; public override string StartingMessage => "Minting some tokens..."; - public override string Description => "Mint some TestTokens and send some Eth to the address if its balance is low."; - public override CommandOption[] Options => new[] { ethOption }; + public override string Description => "Mint some TestTokens and send some Eth to the user if their balance is low."; + public override CommandOption[] Options => new[] { optionalUser }; protected override async Task Execute(SocketSlashCommand command, IGethNode gethNode, ICodexContracts contracts) { - var addr = await ethOption.Parse(command); + var userId = GetUserId(optionalUser, command); + var addr = Program.UserRepo.GetCurrentAddressForUser(userId); if (addr == null) return; var report = diff --git a/Tools/BiblioTech/TokenCommands/ReportHistoryCommand.cs b/Tools/BiblioTech/TokenCommands/ReportHistoryCommand.cs new file mode 100644 index 00000000..6cf58f1b --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/ReportHistoryCommand.cs @@ -0,0 +1,34 @@ +using Discord.WebSocket; + +namespace BiblioTech.TokenCommands +{ + public class ReportHistoryCommand : BaseCommand + { + private readonly UserOption user = new UserOption( + description: "User to report history for.", + isRequired: true); + + public override string Name => "report"; + public override string StartingMessage => "Getting that data..."; + public override string Description => "Admin only. Reports bot-interaction history for a user."; + + protected override async Task Invoke(SocketSlashCommand command) + { + if (!IsSenderAdmin(command)) + { + await command.FollowupAsync("You're not an admin."); + return; + } + + var userId = user.GetOptionUserId(command); + if (userId == null) + { + await command.FollowupAsync("Failed to get user ID"); + return; + } + + var report = Program.UserRepo.GetInteractionReport(userId.Value); + await command.FollowupAsync(string.Join(Environment.NewLine, report)); + } + } +} diff --git a/Tools/BiblioTech/TokenCommands/ShowIdCommand.cs b/Tools/BiblioTech/TokenCommands/ShowIdCommand.cs new file mode 100644 index 00000000..39d9bf4c --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/ShowIdCommand.cs @@ -0,0 +1,16 @@ +using Discord.WebSocket; + +namespace BiblioTech.TokenCommands +{ + public class ShowIdCommand : BaseCommand + { + public override string Name => "my-id"; + public override string StartingMessage => "..."; + public override string Description => "Shows you your Discord ID. (Useful for admins)"; + + protected override async Task Invoke(SocketSlashCommand command) + { + await command.FollowupAsync("Your ID: " + command.User.Id); + } + } +} diff --git a/Tools/BiblioTech/TokenCommands/UserAssociateCommand.cs b/Tools/BiblioTech/TokenCommands/UserAssociateCommand.cs new file mode 100644 index 00000000..e989f33e --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/UserAssociateCommand.cs @@ -0,0 +1,33 @@ +using Discord.WebSocket; + +namespace BiblioTech.TokenCommands +{ + public class UserAssociateCommand : BaseCommand + { + private readonly EthAddressOption ethOption = new EthAddressOption(); + + public override string Name => "set"; + public override string StartingMessage => "hold on..."; + public override string Description => "Associates a Discord user with an Ethereum address in the TestNet. " + + "Warning: You can set your Ethereum address only once! Double-check before hitting enter."; + + public override CommandOption[] Options => new[] { ethOption }; + + protected override async Task Invoke(SocketSlashCommand command) + { + var userId = command.User.Id; + var data = await ethOption.Parse(command); + if (data == null) return; + + var currentAddress = Program.UserRepo.GetCurrentAddressForUser(userId); + if (currentAddress != null) + { + await command.FollowupAsync($"You've already set your Ethereum address to {currentAddress}."); + return; + } + + Program.UserRepo.AssociateUserWithAddress(userId, data); + await command.FollowupAsync("Done! Thank you for joining the test net!"); + } + } +} diff --git a/Tools/BiblioTech/TokenCommands/UserOption.cs b/Tools/BiblioTech/TokenCommands/UserOption.cs new file mode 100644 index 00000000..3f1628f6 --- /dev/null +++ b/Tools/BiblioTech/TokenCommands/UserOption.cs @@ -0,0 +1,22 @@ +using Discord; +using Discord.WebSocket; + +namespace BiblioTech.TokenCommands +{ + public class UserOption : CommandOption + { + public UserOption(string description, bool isRequired) + : base("user", description, ApplicationCommandOptionType.User, isRequired) + { + } + + public ulong? GetOptionUserId(SocketSlashCommand command) + { + var userOptionData = command.Data.Options.SingleOrDefault(o => o.Name == Name); + if (userOptionData == null) return null; + var user = userOptionData.Value as IUser; + if (user == null) return null; + return user.Id; + } + } +} diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index e4cfa2c3..5348f990 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -42,6 +42,43 @@ namespace BiblioTech } } + public string[] GetInteractionReport(ulong discordId) + { + var result = new List(); + + lock (repoLock) + { + var filename = GetFilename(discordId); + if (!File.Exists(filename)) + { + result.Add("User has not joined the test net."); + } + else + { + var user = JsonConvert.DeserializeObject(File.ReadAllText(filename)); + if (user == null) + { + result.Add("Failed to load user records."); + } + else + { + result.Add("User joined on " + user.CreatedUtc.ToString("o")); + result.Add("Current address: " + user.CurrentAddress); + foreach (var ae in user.AssociateEvents) + { + result.Add($"{ae.Utc.ToString("o")} - Address set to: {ae.NewAddress}"); + } + foreach (var me in user.MintEvents) + { + result.Add($"{me.Utc.ToString("o")} - Minted {me.EthReceived} and {me.TestTokensMinted} to {me.UsedAddress}."); + } + } + } + } + + return result.ToArray(); + } + private void SetUserAddress(ulong discordId, EthAddress? address) { var user = GetOrCreate(discordId);