diff --git a/ApplicationCommands/BirthdayModule.cs b/ApplicationCommands/BirthdayModule.cs index 2a7fa34..4769182 100644 --- a/ApplicationCommands/BirthdayModule.cs +++ b/ApplicationCommands/BirthdayModule.cs @@ -14,6 +14,7 @@ public class BirthdayModule : BotModuleBase { public const string HelpCmdGet = "Gets a user's birthday."; public const string HelpCmdNearest = "Get a list of users who recently had or will have a birthday."; public const string HelpCmdExport = "Generates a text file with all known and available birthdays."; + public const string ErrNotSetFk = $":x: The bot has not yet been set up. Please configure a birthday role."; // foreign key violation // Note that these methods have largely been copied to BirthdayOverrideModule. Changes here should be reflected there as needed. @@ -47,10 +48,16 @@ public class BirthdayModule : BotModuleBase { user.BirthMonth = inmonth; user.BirthDay = inday; user.TimeZone = inzone; - await db.SaveChangesAsync(); + try { + await db.SaveChangesAsync(); + } catch (Microsoft.EntityFrameworkCore.DbUpdateException e) + when (e.InnerException is Npgsql.PostgresException ex && ex.SqlState == Npgsql.PostgresErrorCodes.ForeignKeyViolation) { + await RespondAsync(ErrNotSetFk); + return; + } await RespondAsync($":white_check_mark: Your birthday has been set to **{FormatDate(inmonth, inday)}**" + - (inzone == null ? "" : $", with time zone {inzone}") + ".").ConfigureAwait(false); + (inzone == null ? "" : $" at time zone **{inzone}**") + ".").ConfigureAwait(false); } [SlashCommand("timezone", HelpCmdSetZone)] diff --git a/ApplicationCommands/BirthdayOverrideModule.cs b/ApplicationCommands/BirthdayOverrideModule.cs index c2346d8..d0d16e9 100644 --- a/ApplicationCommands/BirthdayOverrideModule.cs +++ b/ApplicationCommands/BirthdayOverrideModule.cs @@ -29,7 +29,13 @@ public class BirthdayOverrideModule : BotModuleBase { if (user.IsNew) db.UserEntries.Add(user); user.BirthMonth = inmonth; user.BirthDay = inday; - await db.SaveChangesAsync(); + try { + await db.SaveChangesAsync(); + } catch (Microsoft.EntityFrameworkCore.DbUpdateException e) + when (e.InnerException is Npgsql.PostgresException ex && ex.SqlState == Npgsql.PostgresErrorCodes.ForeignKeyViolation) { + await RespondAsync(BirthdayModule.ErrNotSetFk); + return; + } await RespondAsync($":white_check_mark: {Common.FormatName(target, false)}'s birthday has been set to " + $"**{FormatDate(inmonth, inday)}**.").ConfigureAwait(false); diff --git a/ApplicationCommands/ConfigModule.cs b/ApplicationCommands/ConfigModule.cs index dd8e61f..d32aeeb 100644 --- a/ApplicationCommands/ConfigModule.cs +++ b/ApplicationCommands/ConfigModule.cs @@ -122,13 +122,21 @@ public class ConfigModule : BotModuleBase { [Group("role", HelpPfxModOnly + HelpCmdRole)] public class SubCmdsConfigRole : BotModuleBase { [SlashCommand("set-birthday-role", HelpPfxModOnly + "Set the role given to users having a birthday.")] - public async Task CmdSetBRole([Summary(description: HelpOptRole)] SocketRole role) { + public async Task CmdSetBRole([Summary(description: HelpOptRole)]SocketRole role) { + if (role.IsEveryone || role.IsManaged) { + await RespondAsync(":x: This role cannot be used for this setting.", ephemeral: true); + return; + } await DoDatabaseUpdate(Context, s => s.RoleId = (long)role.Id); await RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false); } [SlashCommand("set-moderator-role", HelpPfxModOnly + "Designate a role whose members can configure the bot." + HelpPofxBlankUnset)] public async Task CmdSetModRole([Summary(description: HelpOptRole)]SocketRole? role = null) { + if (role != null && (role.IsEveryone || role.IsManaged)) { + await RespondAsync(":x: This role cannot be used for this setting.", ephemeral: true); + return; + } await DoDatabaseUpdate(Context, s => s.ModeratorRole = (long?)role?.Id); await RespondAsync(":white_check_mark: The moderator role has been " + (role == null ? "unset." : $"set to **{role.Name}**.")); @@ -188,7 +196,7 @@ public class ConfigModule : BotModuleBase { using var db = new BotDatabaseContext(); var guildconf = guild.GetConfigOrNew(db); - await db.Entry(guildconf).Collection(t => t.UserEntries).LoadAsync(); + if (!guildconf.IsNew) await db.Entry(guildconf).Collection(t => t.UserEntries).LoadAsync(); var result = new StringBuilder(); @@ -200,14 +208,16 @@ public class ConfigModule : BotModuleBase { bool hasMembers = Common.HasMostMembersDownloaded(guild); result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers)); result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members."); - int bdayCount = -1; + int bdayCount = default; result.Append(DoTestFor("Birthday processing", delegate { if (!hasMembers) return false; + if (guildconf.IsNew) return false; bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(guildconf.UserEntries, guildconf.TimeZone).Count; return true; })); - if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday."); - else result.AppendLine(" - Previous step failed."); + if (!hasMembers) result.AppendLine(" - Previous step failed."); + else if (guildconf.IsNew) result.AppendLine(" - No data."); + else result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday."); result.AppendLine(); result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate { diff --git a/BackgroundServices/BirthdayRoleUpdate.cs b/BackgroundServices/BirthdayRoleUpdate.cs index cf847ea..f019dcd 100644 --- a/BackgroundServices/BirthdayRoleUpdate.cs +++ b/BackgroundServices/BirthdayRoleUpdate.cs @@ -40,6 +40,13 @@ class BirthdayRoleUpdate : BackgroundService { if (role == null || !guild.CurrentUser.GuildPermissions.ManageRoles || role.Position >= guild.CurrentUser.Hierarchy) continue; + if (role.IsEveryone || role.IsManaged) { + // Invalid role was configured. Clear the setting and quit. + settings.RoleId = null; + db.Update(settings); + await db.SaveChangesAsync(); + continue; + } // Load up user configs and begin processing birthdays await db.Entry(settings).Collection(t => t.UserEntries).LoadAsync(CancellationToken.None); @@ -132,10 +139,8 @@ class BirthdayRoleUpdate : BackgroundService { if (!toApply.Contains(user.Id)) removals.Add(user); else no_ops.Add(user.Id); } - int removalAllowance = 15; // Limit removals per run, to not get continuously stuck on rate limits in misconfigured servers foreach (var user in removals) { await user.RemoveRoleAsync(r); - if (--removalAllowance == 0) break; } foreach (var target in toApply) { diff --git a/ShardInstance.cs b/ShardInstance.cs index c8878b1..5faf5ba 100644 --- a/ShardInstance.cs +++ b/ShardInstance.cs @@ -92,14 +92,18 @@ public sealed class ShardInstance : IDisposable { case "Disconnected": case "Resumed previous session": case "Failed to resume previous session": - case "Discord.WebSocket.GatewayReconnectException: Server requested a reconnect": + case "Serializer Error": // The exception associated with this log appears a lot as of v3.2-ish return Task.CompletedTask; } - Log("Discord.Net", $"{arg.Severity}: {arg.Message}"); } - if (arg.Exception != null) Log("Discord.Net exception", arg.Exception.ToString()); + if (arg.Exception != null) { + if (arg.Exception is GatewayReconnectException + || arg.Exception.Message == "WebSocket connection was closed") return Task.CompletedTask; + + Log("Discord.Net exception", arg.Exception.ToString()); + } return Task.CompletedTask; } diff --git a/ShardManager.cs b/ShardManager.cs index f3cdb9b..7f1cb36 100644 --- a/ShardManager.cs +++ b/ShardManager.cs @@ -113,7 +113,7 @@ class ShardManager : IDisposable { ShardId = shardId, TotalShards = Config.ShardTotal, LogLevel = LogSeverity.Info, - DefaultRetryMode = RetryMode.AlwaysRetry, + DefaultRetryMode = RetryMode.Retry502 | RetryMode.RetryTimeouts, GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages }; var services = new ServiceCollection()