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