From 715b14ecc5f9f791ab5ba08867cf2fbfc7551bbf Mon Sep 17 00:00:00 2001 From: Sarah Faey Date: Sat, 14 Jan 2023 16:44:18 +0100 Subject: [PATCH] implemented polls in the discord bot --- DiscordBot/CommandHandlers/ButtonHandler.cs | 16 +++++ DiscordBot/CommandHandlers/ModalHandler.cs | 13 ++++ .../CommandHandlers/SelectMenuHandler.cs | 12 ++++ DiscordBot/Constants.cs | 7 +++ DiscordBot/Controllers/RaidController.cs | 33 ++++++++++ DiscordBot/Messages/PollCustomModal.cs | 38 +++++++++++ DiscordBot/Messages/PollMessage.cs | 63 +++++++++++++++++++ DiscordBot/Services/HttpService.cs | 14 +++++ Lieb/Controllers/DiscordBotController.cs | 11 +++- Lieb/Data/DbInitializer.cs | 10 +-- Lieb/Data/DiscordService.cs | 38 +++++++++++ Lieb/Data/PollService.cs | 9 +++ SharedClasses/SharedModels/ApiPoll.cs | 19 ++++++ 13 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 DiscordBot/Messages/PollCustomModal.cs create mode 100644 DiscordBot/Messages/PollMessage.cs create mode 100644 SharedClasses/SharedModels/ApiPoll.cs diff --git a/DiscordBot/CommandHandlers/ButtonHandler.cs b/DiscordBot/CommandHandlers/ButtonHandler.cs index 3ebcf7d..705a32d 100644 --- a/DiscordBot/CommandHandlers/ButtonHandler.cs +++ b/DiscordBot/CommandHandlers/ButtonHandler.cs @@ -64,6 +64,22 @@ namespace DiscordBot.CommandHandlers await component.RespondAsync("Opting out failed, please try again later or change the setting on the website."); } break; + case Constants.ComponentIds.POLL_ANSWER_BUTTON: + PollMessage.Parameters pollAnswerParameters = PollMessage.ParseId(component.Data.CustomId); + ApiPollAnswer answer = new ApiPollAnswer() + { + Answer = string.Empty, + OptionId = pollAnswerParameters.OptionId, + PollId = pollAnswerParameters.PollId, + UserId = component.User.Id + }; + await _httpService.AnswerPoll(answer); + await component.RespondAsync("Answer sent.", ephemeral: true); + break; + case Constants.ComponentIds.POLL_CUSTOM_ANSWER_BUTTON: + PollMessage.Parameters pollCustomParameters = PollMessage.ParseId(component.Data.CustomId); + await component.RespondWithModalAsync(PollCustomModal.buildMessage(pollCustomParameters.PollId, component.Message.Content)); + break; } } diff --git a/DiscordBot/CommandHandlers/ModalHandler.cs b/DiscordBot/CommandHandlers/ModalHandler.cs index 5a5d653..ea0c8db 100644 --- a/DiscordBot/CommandHandlers/ModalHandler.cs +++ b/DiscordBot/CommandHandlers/ModalHandler.cs @@ -63,6 +63,19 @@ namespace DiscordBot.CommandHandlers await modal.RespondAsync($"signing up failed", ephemeral: true); } break; + case Constants.ComponentIds.POLL_CUSTOM_ANSWER_MODAL: + PollCustomModal.Parameters pollParameters = PollCustomModal.ParseId(modal.Data.CustomId); + string modalAnswer = components.First(x => x.CustomId == Constants.ComponentIds.POLL_CUSTOM_ANSWER_TEXT_BOX).Value; + ApiPollAnswer answer = new ApiPollAnswer() + { + Answer = modalAnswer, + OptionId = 0, + PollId = pollParameters.PollId, + UserId = modal.User.Id + }; + await _httpService.AnswerPoll(answer); + await modal.RespondAsync("Answer sent.", ephemeral: true); + break; } } } diff --git a/DiscordBot/CommandHandlers/SelectMenuHandler.cs b/DiscordBot/CommandHandlers/SelectMenuHandler.cs index c723493..286026b 100644 --- a/DiscordBot/CommandHandlers/SelectMenuHandler.cs +++ b/DiscordBot/CommandHandlers/SelectMenuHandler.cs @@ -54,6 +54,18 @@ namespace DiscordBot.CommandHandlers }); } break; + case Constants.ComponentIds.POLL_DROP_DOWN: + PollMessage.Parameters pollParameters = PollMessage.ParseId(component.Data.CustomId); + ApiPollAnswer answer = new ApiPollAnswer() + { + Answer = string.Empty, + OptionId = int.Parse(component.Data.Values.First()), + PollId = pollParameters.PollId, + UserId = component.User.Id + }; + await _httpService.AnswerPoll(answer); + await component.RespondAsync("Answer sent.", ephemeral: true); + break; } } diff --git a/DiscordBot/Constants.cs b/DiscordBot/Constants.cs index 4afe09b..72d4135 100644 --- a/DiscordBot/Constants.cs +++ b/DiscordBot/Constants.cs @@ -22,6 +22,13 @@ public const string CREATE_ACCOUNT_MODAL = "createAccountModal"; public const string SIGN_UP_EXTERNAL_MODAL = "signUpExternalModal"; + + + public const string POLL_DROP_DOWN = "pollDropDown"; + public const string POLL_ANSWER_BUTTON = "pollAnswerButton"; + public const string POLL_CUSTOM_ANSWER_BUTTON = "pollCustomAnswerButton"; + public const string POLL_CUSTOM_ANSWER_MODAL = "pollCustomAnswerModal"; + public const string POLL_CUSTOM_ANSWER_TEXT_BOX = "pollCustomAnswerTextBox"; } public class SlashCommands diff --git a/DiscordBot/Controllers/RaidController.cs b/DiscordBot/Controllers/RaidController.cs index 43fce8f..40f482f 100644 --- a/DiscordBot/Controllers/RaidController.cs +++ b/DiscordBot/Controllers/RaidController.cs @@ -118,5 +118,38 @@ namespace DiscordBot.Controllers { await ReminderSubscriptionMessage.sendMessage(_client, userId); } + + [HttpPost] + [Route("[action]")] + public async Task> SendDropdownPoll(ApiPoll poll) + { + return await SendPoll(poll, true); + } + + [HttpPost] + [Route("[action]")] + public async Task> SendButtonPoll(ApiPoll poll) + { + return await SendPoll(poll, false); + } + + private async Task> SendPoll(ApiPoll poll, bool isDropdown) + { + List sent = new List(); + foreach(ulong userId in poll.UserIds) + { + var user = await _client.GetUserAsync(userId); + if(user != null) + { + try + { + await user.SendMessageAsync(poll.Question, components: PollMessage.buildMessage(poll, isDropdown)); + sent.Add(user.Id); + } + catch {} + } + } + return sent; + } } } \ No newline at end of file diff --git a/DiscordBot/Messages/PollCustomModal.cs b/DiscordBot/Messages/PollCustomModal.cs new file mode 100644 index 0000000..7d48be8 --- /dev/null +++ b/DiscordBot/Messages/PollCustomModal.cs @@ -0,0 +1,38 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.ComponentModel.DataAnnotations; +using SharedClasses.SharedModels; + +namespace DiscordBot.Messages +{ + public class PollCustomModal + { + public static Modal buildMessage(int pollId, string question) + { + var mb = new ModalBuilder() + .WithTitle(question) + .WithCustomId($"{Constants.ComponentIds.POLL_CUSTOM_ANSWER_MODAL}-{pollId}") + .AddTextInput("Answer", Constants.ComponentIds.POLL_CUSTOM_ANSWER_TEXT_BOX, placeholder: "Yes", required: true); + + return mb.Build(); + } + + public static Parameters ParseId(string customId) + { + Parameters parameters = new Parameters(); + + string[] ids = customId.Split('-'); + if(ids.Length > 1) + { + int.TryParse(ids[1],out parameters.PollId); + } + return parameters; + } + + public class Parameters + { + public int PollId; + } + } +} \ No newline at end of file diff --git a/DiscordBot/Messages/PollMessage.cs b/DiscordBot/Messages/PollMessage.cs new file mode 100644 index 0000000..e7f1384 --- /dev/null +++ b/DiscordBot/Messages/PollMessage.cs @@ -0,0 +1,63 @@ +using Discord; +using SharedClasses.SharedModels; + +namespace DiscordBot.Messages +{ + public class PollMessage + { + public static MessageComponent buildMessage(ApiPoll poll, bool isDropdown) + { + var builder = new ComponentBuilder(); + if(isDropdown) + { + var signUpSelect = new SelectMenuBuilder() + .WithPlaceholder(poll.Question) + .WithCustomId($"{Constants.ComponentIds.POLL_DROP_DOWN}-{poll.PollId}") + .WithMinValues(1) + .WithMaxValues(1); + + foreach(KeyValuePair option in poll.Options) + { + signUpSelect.AddOption(option.Value, option.Key.ToString()); + } + + builder.WithSelectMenu(signUpSelect, 0); + } + else + { + foreach(KeyValuePair option in poll.Options) + { + builder.WithButton(option.Value, $"{Constants.ComponentIds.POLL_ANSWER_BUTTON}-{poll.PollId}-{option.Key}", ButtonStyle.Secondary); + } + } + + if(poll.AllowCustomAnswer) + { + builder.WithButton("Custom", $"{Constants.ComponentIds.POLL_CUSTOM_ANSWER_BUTTON}-{poll.PollId}", ButtonStyle.Secondary); + } + return builder.Build(); + } + + public static Parameters ParseId(string customId) + { + Parameters parameters = new Parameters(); + + string[] ids = customId.Split('-'); + if(ids.Length > 1) + { + int.TryParse(ids[1], out parameters.PollId); + } + if(ids.Length > 2) + { + int.TryParse(ids[2], out parameters.OptionId); + } + return parameters; + } + + public class Parameters + { + public int PollId; + public int OptionId; + } + } +} \ No newline at end of file diff --git a/DiscordBot/Services/HttpService.cs b/DiscordBot/Services/HttpService.cs index d931350..1a06dd4 100644 --- a/DiscordBot/Services/HttpService.cs +++ b/DiscordBot/Services/HttpService.cs @@ -269,5 +269,19 @@ namespace DiscordBot.Services } return false; } + + public async Task AnswerPoll(ApiPollAnswer answer) + { + var httpClient = _httpClientFactory.CreateClient(Constants.HTTP_CLIENT_NAME); + + var raidItemJson = new StringContent( + JsonSerializer.Serialize(answer), + Encoding.UTF8, + Application.Json); + + var httpResponseMessage = await httpClient.PostAsync("DiscordBot/AnswerPoll", raidItemJson); + + httpResponseMessage.EnsureSuccessStatusCode(); + } } } \ No newline at end of file diff --git a/Lieb/Controllers/DiscordBotController.cs b/Lieb/Controllers/DiscordBotController.cs index 368d2a7..deeeddc 100644 --- a/Lieb/Controllers/DiscordBotController.cs +++ b/Lieb/Controllers/DiscordBotController.cs @@ -17,15 +17,17 @@ namespace Lieb.Controllers { RaidService _raidService; UserService _userService; + PollService _pollService; GuildWars2AccountService _gw2AccountService; DiscordService _discordService; - public DiscordBotController(RaidService raidService, UserService userService, GuildWars2AccountService gw2AccountService, DiscordService discordService) + public DiscordBotController(RaidService raidService, UserService userService, GuildWars2AccountService gw2AccountService, DiscordService discordService, PollService pollService) { _raidService = raidService; _userService = userService; _gw2AccountService = gw2AccountService; _discordService = discordService; + _pollService = pollService; } [HttpGet] @@ -306,5 +308,12 @@ namespace Lieb.Controllers { return Ok(await _userService.ReminderOptOut(userId)); } + + [HttpPost] + [Route("[action]")] + public async Task AnswerPoll(ApiPollAnswer answer) + { + await _pollService.UpdateAnswer(answer.PollId, answer.OptionId, answer.Answer, answer.UserId); + } } } \ No newline at end of file diff --git a/Lieb/Data/DbInitializer.cs b/Lieb/Data/DbInitializer.cs index c4d076f..455d41a 100644 --- a/Lieb/Data/DbInitializer.cs +++ b/Lieb/Data/DbInitializer.cs @@ -171,11 +171,11 @@ namespace Lieb.Data context.RaidSignUps.AddRange(signUps); context.SaveChanges(); - GuildWars2Build healTempest = new GuildWars2Build() { BuildName = "HealTempest", Class = GuildWars2Class.Elementalist, EliteSpecialization = EliteSpecialization.Tempest, Might = true, Alacrity = true, DamageType = DamageType.Heal }; - GuildWars2Build condiScourge = new GuildWars2Build() { BuildName = "CondiScourge", Class = GuildWars2Class.Necromancer, EliteSpecialization = EliteSpecialization.Scourge, DamageType = DamageType.Condition }; - GuildWars2Build quickBrand = new GuildWars2Build() { BuildName = "QuickBrand", Class = GuildWars2Class.Guard, EliteSpecialization = EliteSpecialization.Firebrand, Quickness = true, DamageType = DamageType.Condition }; - GuildWars2Build alacregate = new GuildWars2Build() { BuildName = "Alacregate", Class = GuildWars2Class.Revenant, EliteSpecialization = EliteSpecialization.Renegade, Alacrity = true, DamageType = DamageType.Power }; - GuildWars2Build chrono = new GuildWars2Build() { BuildName = "Chrono", Class = GuildWars2Class.Mesmer, EliteSpecialization = EliteSpecialization.Chronomancer, Alacrity = true, Quickness = true, DamageType = DamageType.Power }; + GuildWars2Build healTempest = new GuildWars2Build() { BuildName = "HealTempest", Class = GuildWars2Class.Elementalist, EliteSpecialization = EliteSpecialization.Tempest, Might = true, Alacrity = true, DamageType = DamageType.Heal, UseInRandomRaid = true }; + GuildWars2Build condiScourge = new GuildWars2Build() { BuildName = "CondiScourge", Class = GuildWars2Class.Necromancer, EliteSpecialization = EliteSpecialization.Scourge, DamageType = DamageType.Condition, UseInRandomRaid = true }; + GuildWars2Build quickBrand = new GuildWars2Build() { BuildName = "QuickBrand", Class = GuildWars2Class.Guard, EliteSpecialization = EliteSpecialization.Firebrand, Quickness = true, DamageType = DamageType.Condition, UseInRandomRaid = true }; + GuildWars2Build alacregate = new GuildWars2Build() { BuildName = "Alacregate", Class = GuildWars2Class.Revenant, EliteSpecialization = EliteSpecialization.Renegade, Alacrity = true, DamageType = DamageType.Power, UseInRandomRaid = true }; + GuildWars2Build chrono = new GuildWars2Build() { BuildName = "Chrono", Class = GuildWars2Class.Mesmer, EliteSpecialization = EliteSpecialization.Chronomancer, Alacrity = true, Quickness = true, DamageType = DamageType.Power, UseInRandomRaid = true }; GuildWars2Build daredevil = new GuildWars2Build() { BuildName = "Daredevil", Class = GuildWars2Class.Thief, EliteSpecialization = EliteSpecialization.DareDevil }; context.GuildWars2Builds.AddRange(new List(){healTempest, condiScourge, quickBrand, alacregate, chrono, daredevil }); context.SaveChanges(); diff --git a/Lieb/Data/DiscordService.cs b/Lieb/Data/DiscordService.cs index 80110c0..1a16857 100644 --- a/Lieb/Data/DiscordService.cs +++ b/Lieb/Data/DiscordService.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text; using Lieb.Models.GuildWars2.Raid; using Lieb.Models; +using Lieb.Models.Poll; using Microsoft.EntityFrameworkCore; namespace Lieb.Data @@ -421,5 +422,42 @@ namespace Lieb.Data } catch {} } + + public async Task SendPoll(Poll poll, List? userIds = null) + { + try + { + var httpClient = _httpClientFactory.CreateClient(Constants.HttpClientName); + + if(userIds == null) + { + userIds = poll.Answers.Select(a => a.UserId).ToList(); + } + + ApiPoll apiPoll = new ApiPoll() + { + AllowCustomAnswer = poll.AllowCustomAnswer, + PollId = poll.PollId, + Question = poll.Question, + UserIds = userIds, + Options = poll.Options.ToDictionary(o => o.PollOptionId, o => o.Name) + }; + + var messageItemJson = new StringContent( + JsonSerializer.Serialize(apiPoll), + Encoding.UTF8, + Application.Json); + + if(poll.AnswerType == AnswerType.Dropdown) + { + var httpResponseMessage = await httpClient.PostAsync("raid/SendDropdownPoll", messageItemJson); + } + else + { + var httpResponseMessage = await httpClient.PostAsync("raid/SendButtonPoll", messageItemJson); + } + } + catch {} + } } } \ No newline at end of file diff --git a/Lieb/Data/PollService.cs b/Lieb/Data/PollService.cs index f561eb7..db9bc5c 100644 --- a/Lieb/Data/PollService.cs +++ b/Lieb/Data/PollService.cs @@ -74,6 +74,7 @@ namespace Lieb.Data using var context = _contextFactory.CreateDbContext(); context.Polls.Add(poll); await context.SaveChangesAsync(); + await _discordService.SendPoll(poll); return poll.PollId; } @@ -102,11 +103,18 @@ namespace Lieb.Data using var context = _contextFactory.CreateDbContext(); Poll? poll = context.Polls .Include(p => p.Answers) + .Include(p => p.Options) .FirstOrDefault(p => p.PollId == pollId && p.Answers.Where(a => a.UserId == userId).Any()); if (poll == null) return; PollAnswer pollAnswer = poll.Answers.First(a => a.UserId == userId); + if(string.IsNullOrEmpty(answer) && pollOptionId > 0) + { + PollOption option = poll.Options.FirstOrDefault(o => o.PollOptionId == pollOptionId); + answer = option != null ? option.Name : string.Empty; + } + pollAnswer.Answer = answer; pollAnswer.PollOptionId = pollOptionId; await context.SaveChangesAsync(); @@ -126,6 +134,7 @@ namespace Lieb.Data UserId = userId }); await context.SaveChangesAsync(); + await _discordService.SendPoll(poll, new List(){userId}); } public async Task RemoveUser(int pollId, ulong userId) diff --git a/SharedClasses/SharedModels/ApiPoll.cs b/SharedClasses/SharedModels/ApiPoll.cs new file mode 100644 index 0000000..aade177 --- /dev/null +++ b/SharedClasses/SharedModels/ApiPoll.cs @@ -0,0 +1,19 @@ +namespace SharedClasses.SharedModels +{ + public class ApiPoll + { + public int PollId { get; set; } + public string Question { get; set; } = string.Empty; + public Dictionary Options { get; set; } = new Dictionary(); + public bool AllowCustomAnswer {get; set;} = false; + public List UserIds {get; set;} = new List(); + } + + public class ApiPollAnswer + { + public int PollId { get; set; } + public int OptionId {get; set;} + public string Answer {get; set;} = string.Empty; + public ulong UserId {get; set;} + } +} \ No newline at end of file