/ RegistrationModule.cs
RegistrationModule.cs
1 using _5uhr.Data; 2 using Discord; 3 using Discord.Interactions; 4 using Discord.WebSocket; 5 using Microsoft.EntityFrameworkCore; 6 7 namespace _5uhr; 8 9 public class RegistrationModule(ILogger<RegistrationModule> logger, ApplicationDbContext db, InfoService infoService) : InteractionModuleBase<SocketInteractionContext> 10 { 11 [SlashCommand("refreshinfo", "For debugging. Refresh the info service data.")] 12 public async Task RefreshInfoAsync() 13 { 14 infoService.UpdateInfoAsync(); 15 await Context.Interaction.RespondAsync("Info refreshed.", ephemeral: true); 16 } 17 18 [SlashCommand("configure", "Configure the guild")] 19 public async Task ConfigureAsync(IChannel checkinChannel, IChannel leaderboardChannel) 20 { 21 logger.LogInformation("Configure command invoked by {User}", Context.User.Username); 22 var guild = Context.Guild; 23 var existingConfig = await db.Configurations.Where(x => x.GuildId == guild.Id).FirstOrDefaultAsync(); 24 if (existingConfig is not null) 25 { 26 existingConfig.CheckInChannelId = checkinChannel.Id; 27 existingConfig.LeaderboardChannelId = leaderboardChannel.Id; 28 db.Configurations.Update(existingConfig); 29 await db.SaveChangesAsync(); 30 } 31 else 32 { 33 await db.Configurations.AddAsync(new Configuration() 34 { 35 Id = Guid.CreateVersion7(), 36 GuildId = guild.Id, 37 CheckInChannelId = checkinChannel.Id, 38 LeaderboardChannelId = leaderboardChannel.Id, 39 }); 40 await db.SaveChangesAsync(); 41 } 42 await RespondAsync("Konfiguration gespeichert! Alte nachrichten müssen ggf selbst gelöscht werden.", ephemeral: true); 43 infoService.UpdateInfoAsync(); 44 } 45 46 [SlashCommand("member", "Show information about a member")] 47 public async Task MemberInfoAsync(IUser user) 48 { 49 var member = await MemberForUser(db, user); 50 51 await RespondMemberInfo(member, user.Mention); 52 } 53 54 [SlashCommand("externalmember", "Show information about an external member")] 55 public async Task ExternalMemberInfoAsync(string user) 56 { 57 var member = await db.Member.Where(x => x.FriendlyName == user).SingleOrDefaultAsync(); 58 59 if (member is null) 60 { 61 await RespondAsync("Kein Member mit dem Namen gefunden.", ephemeral: true); 62 return; 63 } 64 65 await RespondMemberInfo(member, user); 66 } 67 68 private async Task RespondMemberInfo(Member member, String mention) 69 { 70 var tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); 71 var currentDate = DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz)); 72 var maxStreakDays = 365; // Only look back this far for streak calculation 73 var streakStartDate = TimeZoneInfo.ConvertTimeToUtc( 74 currentDate.AddDays(-maxStreakDays).ToDateTime(new TimeOnly(0, 0, 0, 0, 0)), 75 tz); 76 77 var memberInfo = await db.Member 78 .Where(x => x.Id == member.Id) 79 .Select(x => new 80 { 81 Uhr4 = x.CheckIns 82 .Where(c => c.Verifications.Count > c.Reports.Count) 83 .Where(c => c.ClaimedCheckpoint == TimeCheckpoint.Uhr4) 84 .Count() 85 + x.ImportedCheckIns4Uhr, 86 Uhr5 = x.CheckIns 87 .Where(c => c.Verifications.Count > c.Reports.Count) 88 .Where(c => c.ClaimedCheckpoint == TimeCheckpoint.Uhr5) 89 .Count() 90 + x.ImportedCheckIns5Uhr, 91 Last4Uhr = x.CheckIns 92 .Where(c => c.Verifications.Count > c.Reports.Count) 93 .Where(c => c.ClaimedCheckpoint == TimeCheckpoint.Uhr4) 94 .OrderBy(c => c.DayOfCheckIn.Start) 95 .Select(x => x.DayOfCheckIn) 96 .FirstOrDefault(), 97 Last5Uhr = x.CheckIns 98 .Where(c => c.Verifications.Count > c.Reports.Count) 99 .Where(c => c.ClaimedCheckpoint == TimeCheckpoint.Uhr5) 100 .OrderByDescending(c => c.DayOfCheckIn.Start) 101 .Select(x => x.DayOfCheckIn) 102 .FirstOrDefault(), 103 // Query only recent days for streak calculation (last 365 days) 104 RecentCheckInDays = x.CheckIns 105 .Where(c => c.Verifications.Count > c.Reports.Count) 106 .Where(c => c.DayOfCheckIn.Start >= streakStartDate) 107 .Select(c => c.DayOfCheckIn.Start) 108 .Distinct() 109 .ToList() 110 }) 111 .SingleAsync(); 112 113 // Calculate streak: count consecutive days starting from today, looking back 114 int streak = 0; 115 var checkInDaysSet = new HashSet<DateTime>(memberInfo.RecentCheckInDays); 116 117 while (streak < maxStreakDays) 118 { 119 var currentDayStart = TimeZoneInfo.ConvertTimeToUtc( 120 currentDate.ToDateTime(new TimeOnly(0, 0, 0, 0, 0)), 121 tz); 122 123 if (!checkInDaysSet.Contains(currentDayStart)) 124 { 125 break; 126 } 127 128 streak++; 129 currentDate = currentDate.AddDays(-1); 130 } 131 132 await RespondAsync("Info für " + mention, embeds: 133 [ 134 new EmbedBuilder() 135 .WithTitle("Member " + mention) 136 // TODO: Show role here 137 .AddField("4Uhr", memberInfo.Uhr4) 138 .AddField("5Uhr", memberInfo.Uhr5) 139 .AddField("Groschen", memberInfo.Uhr4 * 2 + memberInfo.Uhr5 * 1) 140 .AddField("Streak", streak + " Tag" + (streak != 1 ? "e" : "") + (streak > 1 ? " :fire:" : null) + (streak > 3 ? ":fire:" : null) + (streak > 7 ? ":fire:" : null)) 141 .Build() 142 ], ephemeral: true); 143 } 144 145 public class ReportModal : IModal 146 { 147 public string Title => "Reporte den Hurensohn"; 148 149 [InputLabel("Grund")] 150 [ModalTextInput("reason", TextInputStyle.Paragraph, null, 1, 4000, null)] 151 public string Reason { get; set; } 152 153 [RequiredInput(false)] 154 [InputLabel("Beweise")] 155 [ModalTextInput("evidence", TextInputStyle.Paragraph, "Falls du Beweise hast, füge sie hier ein.", 0, 4000, null)] 156 public string Evidence { get; set; } 157 } 158 159 [ModalInteraction("report_*,*")] 160 public async Task ReportResponse(String messageId, String checkinid, ReportModal modal) 161 { 162 var checkInId = Guid.Parse(checkinid); 163 logger.LogInformation("Report modal submitted by {User}", Context.User.Username); 164 var memberReporting = await MemberForUser(db, Context.User); 165 166 var checkIn = await db.CheckIn 167 .Include(x => x.Reports) 168 .ThenInclude(x => x.MemberThatReported) 169 .Include(x => x.Verifications) 170 .ThenInclude(x => x.MemberThatVerified) 171 .Where(x => x.Id == checkInId) 172 .SingleAsync(); 173 174 var existing = checkIn.Verifications.SingleOrDefault(x => x.MemberThatVerified.Id == memberReporting.Id); 175 if (existing is not null) 176 { 177 db.Verifications.Remove(existing); 178 } 179 180 if (checkIn.Reports.Any(x => x.MemberThatReported.Id == memberReporting.Id)) 181 { 182 await Context.Interaction.RespondAsync("Du hast diesen Check-In bereits Reported!", ephemeral: true); 183 return; 184 } 185 186 var id = Guid.CreateVersion7(); 187 188 await db.Reports.AddAsync(new Report() 189 { 190 Id = id, 191 ReportTime = DateTimeOffset.UtcNow, 192 MemberThatReported = memberReporting, 193 CheckInThatWasReported = checkIn, 194 Reason = modal.Reason, 195 EvidenceString = modal.Evidence ?? string.Empty, 196 }); 197 var checkinChannel = await Context.Client.GetChannelAsync((await db.Configurations.SingleOrDefaultAsync(x => x.GuildId == DiscordBotService.MAIN_GUILD_ID))!.CheckInChannelId) as ITextChannel; 198 var message = await checkinChannel!.GetMessageAsync(ulong.Parse(messageId)) as IUserMessage; 199 await message!.Thread!.SendMessageAsync($"von {Context.User.Mention} reported.\n\nGrund: " + modal.Reason + "\nBeweise: " + (string.IsNullOrWhiteSpace(modal.Evidence) ? "Keine" : modal.Evidence)); 200 201 await RespondAsync($"Du hast <@{message.MentionedUserIds.First()}> erfolgreich reportet!", ephemeral: true); 202 await db.SaveChangesAsync(); 203 infoService.UpdateInfoAsync(); 204 } 205 206 public static async Task<Member> MemberForUser(ApplicationDbContext db, IUser user) 207 { 208 var existing = await db.Member.Where(x => x.DiscordUserId == user.Id).SingleOrDefaultAsync(); 209 if (existing is not null) 210 { 211 // TODO: Update nick and stuffs 212 return existing; 213 } 214 var member = new Member() 215 { 216 Id = Guid.CreateVersion7(), 217 DiscordUserId = user.Id, 218 FriendlyName = user.Username, 219 }; 220 await db.Member.AddAsync(member); 221 await db.SaveChangesAsync(); 222 return member; 223 } 224 225 public static async Task<GangDay> GetGangDay(ApplicationDbContext db, DateTime ts) 226 { 227 var tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); 228 var localDateTime = TimeZoneInfo.ConvertTime(ts, tz); 229 var localDateOnly = DateOnly.FromDateTime(localDateTime); 230 DateTime startTimeOfRelevantDay = TimeZoneInfo.ConvertTimeToUtc( 231 localDateOnly.ToDateTime(new TimeOnly(0, 0, 0, 0, 0)), 232 tz); 233 234 if (DateOnly.FromDateTime( 235 TimeZoneInfo.ConvertTime(startTimeOfRelevantDay, tz)) 236 != localDateOnly) 237 { 238 throw new Exception("This timezone code is so shit"); 239 } 240 241 var existingDay = await db.Days.Where(x => x.Start == startTimeOfRelevantDay).SingleOrDefaultAsync(); 242 if (existingDay is not null) 243 { 244 return existingDay; 245 } 246 247 var oldestDay = await db.Days.OrderBy(x => x.Start).Take(1).SingleOrDefaultAsync(); 248 if (oldestDay is null) 249 { 250 var day = new GangDay() 251 { 252 Id = Guid.CreateVersion7(), 253 Start = startTimeOfRelevantDay, 254 }; 255 db.Days.Add(day); 256 await db.SaveChangesAsync(); 257 return day; 258 } 259 260 if (oldestDay.Previous is not null) 261 { 262 throw new Exception("Somehow we created an oldest day, that also has a previous day?"); 263 } 264 265 if (oldestDay.Start > startTimeOfRelevantDay) 266 { 267 // We are older than the oldest day, need to create new oldest days 268 269 var oldestDayDate = DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(oldestDay.Start, tz)); 270 271 for (int i = oldestDayDate.DayNumber - 1; i >= localDateOnly.DayNumber; i--) 272 { 273 var newDayDate = DateOnly.FromDayNumber(i); 274 var newDay = new GangDay() 275 { 276 Id = Guid.CreateVersion7(), 277 Start = TimeZoneInfo.ConvertTimeToUtc(newDayDate.ToDateTime(new TimeOnly(0, 0, 0, 0, 0)), tz), 278 Next = oldestDay, 279 }; 280 oldestDay.Previous = newDay; 281 db.Days.Add(newDay); 282 oldestDay = newDay; 283 } 284 285 await db.SaveChangesAsync(); 286 return oldestDay; 287 } 288 289 var newestDay = await db.Days.OrderByDescending(x => x.Start).Take(1).SingleAsync(); 290 if (newestDay.Next is not null) 291 { 292 throw new Exception("Somehow we created a newest day, that also has a next day?"); 293 } 294 295 if (newestDay.Start > startTimeOfRelevantDay) 296 { 297 throw new Exception("Logic error: we are neither older than the oldest day, nor newer than the newest day, but we could not find an existing day?"); 298 } 299 300 // We are newer than the newest day, need to create new newest days 301 var newestDayDate = DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(newestDay.Start, tz)); 302 303 for (int i = newestDayDate.DayNumber; i <= localDateOnly.DayNumber; i++) 304 { 305 var newDayDate = DateOnly.FromDayNumber(i); 306 var newDay = new GangDay() 307 { 308 Id = Guid.CreateVersion7(), 309 Next = null, 310 Previous = newestDay, 311 Start = TimeZoneInfo.ConvertTimeToUtc(newDayDate.ToDateTime(new TimeOnly(0, 0, 0, 0, 0)), tz), 312 }; 313 newestDay.Next = newDay; 314 db.Days.Add(newDay); 315 newestDay = newDay; 316 } 317 318 await db.SaveChangesAsync(); 319 return newestDay; 320 } 321 }