/ 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  }