/ DiscordBotService.cs
DiscordBotService.cs
  1  using System.Reflection;
  2  using _5uhr.Data;
  3  using Discord;
  4  using Discord.Interactions;
  5  using Discord.WebSocket;
  6  using Microsoft.EntityFrameworkCore;
  7  
  8  namespace _5uhr;
  9  
 10  public class DiscordBotService(DiscordSocketClient client, InteractionService interactionService, ILoggerFactory loggerFactory, IConfiguration configuration, IServiceProvider serviceProvider) : IHostedService
 11  {
 12      public const ulong MAIN_GUILD_ID = 789364983019601930;
 13      private ILogger _logger = loggerFactory.CreateLogger<DiscordBotService>();
 14  
 15      public async Task StartAsync(CancellationToken cancellationToken)
 16      {
 17          _logger.LogInformation("Starting Discord Bot Service...");
 18          var logger = loggerFactory.CreateLogger<DiscordSocketClient>();
 19          client.Log += async (logMessage) =>
 20          {
 21              logger.Log(logMessage.Severity switch
 22              {
 23                  Discord.LogSeverity.Critical => LogLevel.Critical,
 24                  Discord.LogSeverity.Error => LogLevel.Error,
 25                  Discord.LogSeverity.Warning => LogLevel.Warning,
 26                  Discord.LogSeverity.Info => LogLevel.Information,
 27                  Discord.LogSeverity.Verbose => LogLevel.Debug,
 28                  Discord.LogSeverity.Debug => LogLevel.Trace,
 29                  _ => LogLevel.None
 30              }, logMessage.Exception, logMessage.Message);
 31          };
 32          interactionService.Log += async (logMessage) =>
 33          {
 34              logger.Log(logMessage.Severity switch
 35              {
 36                  Discord.LogSeverity.Critical => LogLevel.Critical,
 37                  Discord.LogSeverity.Error => LogLevel.Error,
 38                  Discord.LogSeverity.Warning => LogLevel.Warning,
 39                  Discord.LogSeverity.Info => LogLevel.Information,
 40                  Discord.LogSeverity.Verbose => LogLevel.Debug,
 41                  Discord.LogSeverity.Debug => LogLevel.Trace,
 42                  _ => LogLevel.None
 43              }, logMessage.Exception, logMessage.Message);
 44          };
 45          client.Rest.Log += async (logMessage) =>
 46          {
 47              logger.Log(logMessage.Severity switch
 48              {
 49                  Discord.LogSeverity.Critical => LogLevel.Critical,
 50                  Discord.LogSeverity.Error => LogLevel.Error,
 51                  Discord.LogSeverity.Warning => LogLevel.Warning,
 52                  Discord.LogSeverity.Info => LogLevel.Information,
 53                  Discord.LogSeverity.Verbose => LogLevel.Debug,
 54                  Discord.LogSeverity.Debug => LogLevel.Trace,
 55                  _ => LogLevel.None
 56              }, logMessage.Exception, logMessage.Message);
 57          };
 58  
 59          await interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), serviceProvider);
 60  
 61          client.Ready += async () =>
 62          {
 63              _logger.LogInformation("Discord Client is ready.");
 64              foreach (var guild in client.Guilds)
 65              {
 66                  _logger.LogInformation($"Guild: {guild.Name} {guild.Id}");
 67                  if (guild.Id != MAIN_GUILD_ID)
 68                  {
 69                      _logger.LogWarning("Not the main Guild. Leaving.");
 70                      await guild.LeaveAsync();
 71                  }
 72              }
 73  
 74              _logger.LogInformation("Registering slash commands to main guild...");
 75              await interactionService.RegisterCommandsToGuildAsync(MAIN_GUILD_ID, true);
 76              _logger.LogInformation("Registered slash commands to main guild.");
 77          };
 78  
 79          client.InteractionCreated += async (interaction) =>
 80          {
 81              try
 82              {
 83                  var ctx = new SocketInteractionContext(client, interaction);
 84                  await interactionService.ExecuteCommandAsync(ctx, serviceProvider);
 85              }
 86              catch (Exception ex)
 87              {
 88                  _logger.LogError(ex, $"Error handling interaction {interaction}");
 89              }
 90          };
 91  
 92          client.ButtonExecuted += async (context) =>
 93          {
 94              await using var scope = serviceProvider.CreateAsyncScope();
 95              await using var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 96              var infoService = scope.ServiceProvider.GetRequiredService<InfoService>();
 97              var idData = context.Data.CustomId.Split(':');
 98              switch (idData[0])
 99              {
100                  case "checkin_button_4":
101                  {
102                      var g = Guid.Parse(idData[1]);
103                      var day = await db.Days.Where(x => x.Id == g).SingleAsync();
104                      if (day.Start <= DateTime.UtcNow.AddDays(-3))
105                      {
106                          await context.RespondAsync("Dieser Check-In ist zu alt und kann nicht mehr genutzt werden!", ephemeral: true);
107                          return;
108                      }
109  
110                      await Checkin(logger, context, db, infoService, TimeCheckpoint.Uhr4, day);
111                      break;
112                  }
113                  case "checkin_button_5":
114                  {
115                      var g = Guid.Parse(idData[1]);
116                      var day = await db.Days.Where(x => x.Id == g).SingleAsync();
117                      if (day.Start <= DateTime.UtcNow.AddDays(-3))
118                      {
119                          await context.RespondAsync("Dieser Check-In ist zu alt und kann nicht mehr genutzt werden!", ephemeral: true);
120                          return;
121                      }
122                      
123                      await Checkin(logger, context, db, infoService, TimeCheckpoint.Uhr5, day);
124                      break;
125                  }
126                  case "precheckin_button_4":
127                  {
128                      var g = Guid.Parse(idData[1]);
129                      var day = await db.Days.Where(x => x.Id == g).SingleAsync();
130                      if (day.Start <= DateTime.UtcNow)
131                      {
132                          await context.RespondAsync("Dieser Pre-Check-In ist zu alt und kann nicht mehr genutzt werden!", ephemeral: true);
133                          return;
134                      }
135  
136                      await PreCheckIn(db, context, day, TimeCheckpoint.Uhr4);
137                      infoService.UpdateInfoAsync();
138                      break;
139                  }
140                  case "precheckin_button_5":
141                  {
142                      var g = Guid.Parse(idData[1]);
143                      var day = await db.Days.Where(x => x.Id == g).SingleAsync();
144                      if (day.Start <= DateTime.UtcNow)
145                      {
146                          await context.RespondAsync("Dieser Pre-Check-In ist zu alt und kann nicht mehr genutzt werden!", ephemeral: true);
147                          return;
148                      }
149  
150                      await PreCheckIn(db, context, day, TimeCheckpoint.Uhr5);
151                      infoService.UpdateInfoAsync();
152                      break;
153                  }
154                  case "verify_button":
155                  {
156                      var id = Guid.Parse(idData[1]);
157                      logger.LogInformation("Verify button invoked by {User}", context.User.Username);
158                      var ts = DateTimeOffset.UtcNow;
159                      var memberVerifying = await RegistrationModule.MemberForUser(db, context.User);
160  
161                      var checkIn = await db.CheckIn
162                          .Include(x => x.DayOfCheckIn)
163                          .Include(x => x.MemberThatCheckedIn)
164                          .Include(x => x.Verifications)
165                          .ThenInclude(x => x.MemberThatVerified)
166                          .Include(x => x.Reports)
167                          .ThenInclude(x => x.MemberThatReported)
168                          .SingleAsync(x => x.Id == id);
169  
170                      if (checkIn.DayOfCheckIn.Start < DateTimeOffset.UtcNow.AddDays(-7))
171                      {
172                          await context.RespondAsync("Dieser Check-In ist zu alt, um ihn zu verifizieren!", ephemeral: true);
173                          return;
174                      }
175  
176                      if (checkIn.MemberThatCheckedIn.Id == memberVerifying.Id)
177                      {
178                          await context.RespondAsync("Du kannst deinen eigenen Check-In nicht verifizieren!", ephemeral: true);
179                          return;
180                      }
181  
182                      if (checkIn.Verifications.Any(x => x.MemberThatVerified.Id == memberVerifying.Id))
183                      {
184                          await context.RespondAsync("Du hast diesen Check-In bereits verifiziert!", ephemeral: true);
185                          return;
186                      }
187  
188                      var report = checkIn.Reports.SingleOrDefault(x => x.MemberThatReported.Id == memberVerifying.Id);
189                      if (report is not null)
190                      {
191                          db.Reports.Remove(report);
192                      }
193  
194                      await db.Verifications.AddAsync(new Verification()
195                      {
196                          Id = Guid.CreateVersion7(ts),
197                          VerificationTime = ts,
198                          MemberThatVerified = memberVerifying,
199                          CheckInThatWasVerified = checkIn,
200                      });
201                      await db.SaveChangesAsync();
202  
203                      await context.Message.Thread!.SendMessageAsync($"{context.User.Mention} verifiziert dies!");
204  
205                      await context.RespondAsync(":white_check_mark:", ephemeral: true);
206                      infoService.UpdateInfoAsync();
207                      break;
208                  }
209                  case "report_button":
210                  {
211                      var id = Guid.Parse(idData[1]);
212                      logger.LogInformation("Verify button invoked by {User}", context.User.Username);
213                      var memberVerifying = await RegistrationModule.MemberForUser(db, context.User);
214  
215                      var checkIn = await db.CheckIn
216                          .Include(x => x.DayOfCheckIn)
217                          .Include(x => x.MemberThatCheckedIn)
218                          .Include(x => x.Verifications)
219                          .ThenInclude(x => x.MemberThatVerified)
220                          .Include(x => x.Reports)
221                          .ThenInclude(x => x.MemberThatReported)
222                          .SingleAsync(x => x.Id == id);
223  
224                      if (checkIn.DayOfCheckIn.Start < DateTimeOffset.UtcNow.AddDays(-7))
225                      {
226                          await context.RespondAsync("Dieser Check-In ist zu alt, um ihn zu reporten!", ephemeral: true);
227                          return;
228                      }
229  
230                      if (checkIn.MemberThatCheckedIn.Id == memberVerifying.Id)
231                      {
232                          await context.RespondAsync("Du kannst deinen eigenen Check-In nicht reporten!", ephemeral: true);
233                          return;
234                      }
235  
236                      if (checkIn.Reports.Any(x => x.MemberThatReported.Id == memberVerifying.Id))
237                      {
238                          await context.RespondAsync("Du hast diesen Check-In bereits reported!", ephemeral: true);
239                          return;
240                      }
241  
242                      await context.RespondWithModalAsync<RegistrationModule.ReportModal>("report_" + context.Message.Id + "," + checkIn.Id);
243                      infoService.UpdateInfoAsync();
244                      break;
245                  }
246                  default:
247                      _logger.LogWarning("Unknown button interaction: {CustomId}", context.Data.CustomId);
248                      break;
249              }
250          };
251  
252          _logger.LogInformation("Logging in Bot...");
253          await client.LoginAsync(TokenType.Bot, configuration.GetSection("Discord").GetValue("Token", string.Empty));
254  
255          await client.StartAsync();
256          _logger.LogInformation("Started Discord Bot Service.");
257      }
258  
259      public static string GangString(TimeCheckpoint checkInClaimedCheckpoint)
260      {
261          return checkInClaimedCheckpoint switch
262          {
263              TimeCheckpoint.Uhr4 => "4 Uhr Gang",
264              TimeCheckpoint.Uhr5 => "5 Uhr Gang",
265              _ => "Unknown Gang"
266          };
267      }
268  
269      private async Task Checkin(ILogger<DiscordSocketClient> logger, SocketMessageComponent context, ApplicationDbContext db,
270          InfoService infoService, TimeCheckpoint timeCheckpoint, GangDay day)
271      {
272          logger.LogInformation("Checkin button invoked by {User}", context.User.Username);
273          var ts = DateTimeOffset.UtcNow;
274          var member = await RegistrationModule.MemberForUser(db, context.User);
275          var checkIn = await db.CheckIn.Where(x => x.DayOfCheckIn.Id == day.Id)
276              .Where(x => x.MemberThatCheckedIn.Id == member.Id)
277              .SingleOrDefaultAsync();
278  
279          if (checkIn is not null)
280          {
281              await context.RespondAsync("Du hast dich heute bereits eingecheckt!", ephemeral: true);
282              return;
283          }
284  
285          var id = Guid.CreateVersion7(ts);
286          await db.CheckIn.AddAsync(new CheckIn()
287          {
288              Id = id,
289              CheckInTime = ts,
290              MemberThatCheckedIn = member,
291              ClaimedCheckpoint = timeCheckpoint,
292              DayOfCheckIn = day,
293          });
294          await db.SaveChangesAsync();
295  
296          var checkinChannel = await client.GetChannelAsync((await db.Configurations.SingleOrDefaultAsync(x => x.GuildId == MAIN_GUILD_ID))!.CheckInChannelId) as ITextChannel;
297          var message = await checkinChannel!.SendMessageAsync($"{context.User.Mention} hat sich eingecheckt für die {GangString(timeCheckpoint)}!", components: new ComponentBuilder()
298              .AddRow(new ActionRowBuilder()
299                  .WithButton("Verify", "verify_button:" + id, ButtonStyle.Success)
300                  .WithButton("Report", "report_button:" + id, ButtonStyle.Danger)
301              )
302              .Build()
303          );
304          await checkinChannel.CreateThreadAsync("Check-In von " + context.User.Username, message: message, autoArchiveDuration: ThreadArchiveDuration.OneDay);
305  
306          await context.RespondAsync("Du hast dich erfolgreich eingecheckt!", ephemeral: true);
307          infoService.UpdateInfoAsync();
308      }
309  
310      private async Task PreCheckIn(ApplicationDbContext db, SocketMessageComponent context, GangDay day, TimeCheckpoint checkpoint)
311      {
312          var member = await RegistrationModule.MemberForUser(db, context.User);
313          var any = await db.PreCheckIns.Where(x => x.Day.Id == day.Id && x.MemberThatPreCheckedIn.Id == member.Id)
314              .AnyAsync();
315  
316          if (any)
317          {
318              await context.RespondAsync("Du hast dich für diesen Tag bereits vor-eingecheckt!", ephemeral: true);
319              return;
320          }
321  
322          db.PreCheckIns.Add(new PreCheckIn()
323          {
324              Id = Guid.CreateVersion7(),
325              Day = day,
326              MemberThatPreCheckedIn = member,
327              ClaimedCheckpoint = checkpoint,
328              Timestamp = DateTimeOffset.UtcNow,
329          });
330          await db.SaveChangesAsync();
331          await context.RespondAsync("Du hast dich erfolgreich vor-eingecheckt!", ephemeral: true);
332      }
333  
334      public async Task StopAsync(CancellationToken cancellationToken)
335      {
336          _logger.LogInformation("Stopping Discord Bot Service...");
337          await client.StopAsync();
338      }
339  }