diff --git a/Lieb/Data/Constants.cs b/Lieb/Data/Constants.cs index f1e6df1..fc58894 100644 --- a/Lieb/Data/Constants.cs +++ b/Lieb/Data/Constants.cs @@ -8,6 +8,12 @@ public static readonly int RaidEditPowerLevel = Roles.Moderator.PowerLevel; public const int REMOVE_MAYBE_MINUTES = 15; + public static class Polls + { + public const string YES = "yes"; + public const string NO = "no"; + } + public static class Roles { public static readonly RoleConstant User = new RoleConstant("user", 20); diff --git a/Lieb/Data/DiscordService.cs b/Lieb/Data/DiscordService.cs index 96201fe..80110c0 100644 --- a/Lieb/Data/DiscordService.cs +++ b/Lieb/Data/DiscordService.cs @@ -306,8 +306,15 @@ namespace Lieb.Data public static ApiRaid ConvertRaid(Raid raid) { + string title = raid.Title; + if (raid.SignUps.Count < raid.MinUsers + && raid.MinUserDeadLineUTC.UtcDateTime > DateTimeOffset.UtcNow) + { + title = $"The raid was canceled because of not enough sign ups.\n\n{raid.Title}"; + } + ApiRaid apiRaid = new ApiRaid(){ - Title = raid.Title, + Title = title, Description = raid.Description, Guild = raid.Guild, Organizer = raid.Organizer, diff --git a/Lieb/Data/LiebContext.cs b/Lieb/Data/LiebContext.cs index 1d0a1ce..fc11641 100644 --- a/Lieb/Data/LiebContext.cs +++ b/Lieb/Data/LiebContext.cs @@ -2,6 +2,7 @@ using Lieb.Models; using Lieb.Models.GuildWars2; using Lieb.Models.GuildWars2.Raid; +using Lieb.Models.Poll; using Microsoft.EntityFrameworkCore; namespace Lieb.Data @@ -26,6 +27,9 @@ namespace Lieb.Data public DbSet RaidSignUps { get; set; } public DbSet DiscordRaidMessages { get; set; } public DbSet DiscordSettings { get; set; } + public DbSet Polls { get; set; } + public DbSet PollOptions { get; set; } + public DbSet PollAnswers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -43,6 +47,9 @@ namespace Lieb.Data modelBuilder.Entity().ToTable("RaidSignUp"); modelBuilder.Entity().ToTable("DiscordRaidMessage"); modelBuilder.Entity().ToTable("DiscordSettings"); + modelBuilder.Entity().ToTable("Poll"); + modelBuilder.Entity().ToTable("PollOption"); + modelBuilder.Entity().ToTable("PollAnswer"); } } } diff --git a/Lieb/Data/PollService.cs b/Lieb/Data/PollService.cs new file mode 100644 index 0000000..d8864be --- /dev/null +++ b/Lieb/Data/PollService.cs @@ -0,0 +1,113 @@ +using Lieb.Models.GuildWars2.Raid; +using Lieb.Models.Poll; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Lieb.Data +{ + public class PollService + { + private readonly IDbContextFactory _contextFactory; + private readonly DiscordService _discordService; + public PollService(IDbContextFactory contextFactory, DiscordService discordService) + { + _contextFactory = contextFactory; + _discordService = discordService; + } + + public Poll GetPoll(int pollId) + { + using var context = _contextFactory.CreateDbContext(); + Poll? poll = context.Polls + .Include(p => p.Options) + .Include(p => p.Answers) + .FirstOrDefault(p => p.PollId == pollId); + + if (poll != null) return poll; + + return new Poll(); + } + + public List GetPollsByRaidId(int raidId) + { + using var context = _contextFactory.CreateDbContext(); + return context.Polls + .Include(p => p.Options) + .Include(p => p.Answers) + .Where(p => p.RaidId == raidId).ToList(); + } + + public async Task CreatePoll(string question, List options, int raidId) + { + using var context = _contextFactory.CreateDbContext(); + Raid? raid = context.Raids + .Include(r => r.SignUps) + .FirstOrDefault(r => r.RaidId == raidId); + + if (raid == null) return 0; + List users = raid.SignUps.Where(s => s.LiebUserId != null).Select(s => (ulong)s.LiebUserId).ToList(); + return await CreatePoll(question, options, users, raidId); + } + + public async Task CreatePoll(string question, List options, List users, int? raidId = null) + { + Poll poll = new Poll() + { + Question = question, + RaidId = raidId + }; + foreach(string option in options) + { + poll.Options.Add(new PollOption() + { + Name = option + }); + } + foreach(ulong user in users) + { + poll.Answers.Add(new PollAnswer() + { + UserId = user + }); + } + + using var context = _contextFactory.CreateDbContext(); + context.Polls.Add(poll); + await context.SaveChangesAsync(); + return poll.PollId; + } + + public async Task DeletePoll(int pollId) + { + using var context = _contextFactory.CreateDbContext(); + Poll? poll = context.Polls + .Include(p => p.Options) + .Include(p => p.Answers) + .FirstOrDefault(p => p.PollId == pollId); + + if(poll == null) return; + + context.PollOptions.RemoveRange(poll.Options); + context.PollAnswers.RemoveRange(poll.Answers); + await context.SaveChangesAsync(); + + poll.Options.Clear(); + poll.Answers.Clear(); + context.Polls.Remove(poll); + await context.SaveChangesAsync(); + } + + public async Task UpdateAnswer(int pollId, int pollOptionId, ulong userId) + { + using var context = _contextFactory.CreateDbContext(); + Poll? poll = context.Polls + .Include(p => p.Answers) + .FirstOrDefault(p => p.PollId == pollId && p.Answers.Where(a => a.UserId == userId).Any()); + + if (poll == null) return; + + PollAnswer answer = poll.Answers.First(a => a.UserId == userId); + answer.PollOptionId = pollOptionId; + } + } +} diff --git a/Lieb/Data/RaidService.cs b/Lieb/Data/RaidService.cs index 678cabb..eff9c8b 100644 --- a/Lieb/Data/RaidService.cs +++ b/Lieb/Data/RaidService.cs @@ -1,6 +1,7 @@ using Lieb.Models; using Lieb.Models.GuildWars2; using Lieb.Models.GuildWars2.Raid; +using Lieb.Models.Poll; using Microsoft.EntityFrameworkCore; namespace Lieb.Data @@ -9,11 +10,13 @@ namespace Lieb.Data { private readonly IDbContextFactory _contextFactory; private readonly DiscordService _discordService; + private readonly PollService _pollService; - public RaidService(IDbContextFactory contextFactory, DiscordService discordService) + public RaidService(IDbContextFactory contextFactory, DiscordService discordService, PollService pollService) { _contextFactory = contextFactory; _discordService = discordService; + _pollService = pollService; } public List GetRaids() @@ -444,6 +447,7 @@ namespace Lieb.Data errorMessage = string.Empty; using var context = _contextFactory.CreateDbContext(); Raid? raid = context.Raids + .Include(r => r.SignUps) .AsNoTracking() .FirstOrDefault(r => r.RaidId == raidId); if(raid == null) @@ -451,6 +455,14 @@ namespace Lieb.Data errorMessage = "Raid not found."; return false; } + + if (raid.SignUps.Count < raid.MinUsers + && raid.MinUserDeadLineUTC.UtcDateTime > DateTimeOffset.UtcNow) + { + errorMessage = $"The raid was canceled because of not enough sign ups."; + return false; + } + LiebUser? user = context.LiebUsers .Include(u => u.RoleAssignments) .ThenInclude(a => a.LiebRole) @@ -670,5 +682,49 @@ namespace Lieb.Data await context.SaveChangesAsync(); } } + + public async Task CheckMinUsers() + { + using var context = _contextFactory.CreateDbContext(); + List raids = context.Raids + .Include(r => r.SignUps) + .Where(r => r.SignUps.Count < r.MinUsers && r.MinUserPollId == null).ToList(); + foreach (Raid raid in raids.Where(r => r.MinUserDeadLineUTC < DateTimeOffset.UtcNow && r.StartTimeUTC > DateTimeOffset.UtcNow)) + { + raid.MinUserPollId = await _pollService.CreatePoll( + "The raid has not the required users, do you want to raid anyway?", + new List() {Constants.Polls.YES, Constants.Polls.NO }, raid.RaidId); + await context.SaveChangesAsync(); + await _discordService.PostRaidMessage(raid.RaidId); + } + } + + public async Task CheckMinUserPollResult() + { + using var context = _contextFactory.CreateDbContext(); + List raids = context.Raids + .Include(r => r.SignUps) + .Where(r => r.SignUps.Count < r.MinUsers && r.MinUserPollId != null).ToList(); + foreach (Raid raid in raids.Where(r => r.MinUserDeadLineUTC < DateTimeOffset.UtcNow && r.StartTimeUTC > DateTimeOffset.UtcNow)) + { + Poll poll = _pollService.GetPoll(raid.MinUserPollId.Value); + + if (poll.Answers.Count == 0) continue; + if (poll.Answers.Where(a => a.PollOptionId == null).Any()) continue; + + int noOptionId = poll.Options.First(o => o.Name == Constants.Polls.NO).PollOptionId; + if(poll.Answers.Where(a => a.PollOptionId == noOptionId).Any()) + { + await _discordService.SendMessageToRaidUsers("The raid is canceled.", raid); + } + else + { + raid.MinUsers = 0; + await context.SaveChangesAsync(); + await _discordService.SendMessageToRaidUsers("The raid will take place. Signing up is allowed again.", raid); + await _discordService.PostRaidMessage(raid.RaidId); + } + } + } } } diff --git a/Lieb/Data/TimerService.cs b/Lieb/Data/TimerService.cs index d1814fe..4d6d6f5 100644 --- a/Lieb/Data/TimerService.cs +++ b/Lieb/Data/TimerService.cs @@ -49,6 +49,8 @@ namespace Lieb.Data .GetRequiredService(); await raidService.SendReminders(); await raidService.RemoveMaybes(); + await raidService.CheckMinUsers(); + await raidService.CheckMinUserPollResult(); } } diff --git a/Lieb/Models/GuildWars2/Raid/Raid.cs b/Lieb/Models/GuildWars2/Raid/Raid.cs index de8eed1..126f522 100644 --- a/Lieb/Models/GuildWars2/Raid/Raid.cs +++ b/Lieb/Models/GuildWars2/Raid/Raid.cs @@ -14,6 +14,10 @@ namespace Lieb.Models.GuildWars2.Raid public DateTimeOffset FreeForAllTimeUTC { get; set; } + public DateTimeOffset MinUserDeadLineUTC { get; set; } + + public int? MinUserPollId { get; set; } + public ICollection SignUps { get; set; } = new HashSet(); public Raid() { } @@ -24,6 +28,7 @@ namespace Lieb.Models.GuildWars2.Raid StartTimeUTC = TimeZoneInfo.ConvertTimeToUtc(template.StartTime, timeZone); EndTimeUTC = TimeZoneInfo.ConvertTimeToUtc(template.EndTime, timeZone); FreeForAllTimeUTC = TimeZoneInfo.ConvertTimeToUtc(template.FreeForAllTime, timeZone); + MinUserDeadLineUTC = TimeZoneInfo.ConvertTimeToUtc(template.MinUserDeadLine, timeZone); } } diff --git a/Lieb/Models/GuildWars2/Raid/RaidBase.cs b/Lieb/Models/GuildWars2/Raid/RaidBase.cs index 228e0e3..f4375f7 100644 --- a/Lieb/Models/GuildWars2/Raid/RaidBase.cs +++ b/Lieb/Models/GuildWars2/Raid/RaidBase.cs @@ -52,7 +52,8 @@ namespace Lieb.Models.GuildWars2.Raid public ulong? RaidOwnerId { get; set; } - //role name, number of spots + public int MinUsers { get; set; } = 0; + public ICollection Roles { get; set; } = new HashSet(); public ICollection Reminders { get; set; } = new List(); @@ -73,6 +74,7 @@ namespace Lieb.Models.GuildWars2.Raid this.MoveFlexUsers = template.MoveFlexUsers; this.RaidOwnerId = template.RaidOwnerId; this.EventType = template.EventType; + this.MinUsers = template.MinUsers; foreach (RaidRole role in template.Roles) { diff --git a/Lieb/Models/GuildWars2/Raid/RaidTemplate.cs b/Lieb/Models/GuildWars2/Raid/RaidTemplate.cs index a2b3f45..7c51a28 100644 --- a/Lieb/Models/GuildWars2/Raid/RaidTemplate.cs +++ b/Lieb/Models/GuildWars2/Raid/RaidTemplate.cs @@ -14,6 +14,8 @@ namespace Lieb.Models.GuildWars2.Raid public DateTime FreeForAllTime { get; set; } + public DateTime MinUserDeadLine { get; set; } + public string TimeZone { get; set; } = String.Empty; public int Interval { get; set; } diff --git a/Lieb/Models/Poll/Poll.cs b/Lieb/Models/Poll/Poll.cs new file mode 100644 index 0000000..c11eb4d --- /dev/null +++ b/Lieb/Models/Poll/Poll.cs @@ -0,0 +1,12 @@ +namespace Lieb.Models.Poll +{ + public class Poll + { + public int PollId { get; set; } + public string Question { get; set; } + public ICollection Options { get; set; } = new List(); + public ICollection Answers { get; set; } = new List(); + public int? RaidId { get; set; } + + } +} diff --git a/Lieb/Models/Poll/PollAnswer.cs b/Lieb/Models/Poll/PollAnswer.cs new file mode 100644 index 0000000..3f8be58 --- /dev/null +++ b/Lieb/Models/Poll/PollAnswer.cs @@ -0,0 +1,11 @@ +namespace Lieb.Models.Poll +{ + public class PollAnswer + { + public int PollAnswerId { get; set; } + + public int? PollOptionId { get; set; } + + public ulong UserId { get; set; } + } +} diff --git a/Lieb/Models/Poll/PollOption.cs b/Lieb/Models/Poll/PollOption.cs new file mode 100644 index 0000000..2d5161b --- /dev/null +++ b/Lieb/Models/Poll/PollOption.cs @@ -0,0 +1,8 @@ +namespace Lieb.Models.Poll +{ + public class PollOption + { + public int PollOptionId { get; set; } + public string Name { get; set;} + } +} diff --git a/Lieb/Pages/Raids/RaidEdit/RaidEdit.razor b/Lieb/Pages/Raids/RaidEdit/RaidEdit.razor index 8e643a2..36b2595 100644 --- a/Lieb/Pages/Raids/RaidEdit/RaidEdit.razor +++ b/Lieb/Pages/Raids/RaidEdit/RaidEdit.razor @@ -115,6 +115,26 @@

+

+ +

+

+ +

+ +

+ +

+

+

+ +

+

+ +

+ +

+ +

+