diff --git a/Izzy-Moonbot/Describers/ConfigDescriber.cs b/Izzy-Moonbot/Describers/ConfigDescriber.cs index 6c81af2e..90791ff7 100644 --- a/Izzy-Moonbot/Describers/ConfigDescriber.cs +++ b/Izzy-Moonbot/Describers/ConfigDescriber.cs @@ -108,6 +108,10 @@ public ConfigDescriber() new ConfigItem("RolesToReapplyOnRejoin", ConfigItemType.RoleSet, "The roles I'll reapply to a user when they join **if they had that role when they left**.", ConfigItemCategory.ManagedRoles)); + _config.Add("ZeroJoinRoles", + new ConfigItem("ZeroJoinRoles", ConfigItemType.Boolean, + "TEMPORARY, I HOPE I HOPE.", + ConfigItemCategory.ManagedRoles)); // Filter settings diff --git a/Izzy-Moonbot/EventListeners/UserListener.cs b/Izzy-Moonbot/EventListeners/UserListener.cs index 890dea52..0f1be129 100644 --- a/Izzy-Moonbot/EventListeners/UserListener.cs +++ b/Izzy-Moonbot/EventListeners/UserListener.cs @@ -255,21 +255,21 @@ private async Task MemberUpdateEvent(Cacheable oldUser, S if (_config.MemberRole != null) { if (_users[newUser.Id].Silenced && - newUser.Roles.Select(role => role.Id).Contains((ulong)_config.MemberRole)) + !newUser.Roles.Select(role => role.Id).Contains(DiscordHelper.BanishedRoleId)) { // Unsilenced, Remove the flag. _logger.Log( - $"{newUser.DisplayName} ({newUser.Username}/{newUser.Id}) unsilenced, removing silence flag..."); + $"{newUser.DisplayName} ({newUser.Username}/{newUser.Id}) unbanished, removing silence flag..."); _users[newUser.Id].Silenced = false; changed = true; } if (!_users[newUser.Id].Silenced && - !newUser.Roles.Select(role => role.Id).Contains((ulong)_config.MemberRole)) + newUser.Roles.Select(role => role.Id).Contains(DiscordHelper.BanishedRoleId)) { // Silenced, add the flag _logger.Log( - $"{newUser.DisplayName} ({newUser.Username}/{newUser.Id}) silenced, adding silence flag..."); + $"{newUser.DisplayName} ({newUser.Username}/{newUser.Id}) banished, adding silence flag..."); _users[newUser.Id].Silenced = true; changed = true; } diff --git a/Izzy-Moonbot/Helpers/DiscordHelper.cs b/Izzy-Moonbot/Helpers/DiscordHelper.cs index d9c845e6..105b8bc7 100644 --- a/Izzy-Moonbot/Helpers/DiscordHelper.cs +++ b/Izzy-Moonbot/Helpers/DiscordHelper.cs @@ -362,4 +362,6 @@ public static string DisplayName(IIzzyUser user, IIzzyGuild? guild) // This is essentially https://stackoverflow.com/a/3809435 with added lookaround for <>s. public static Regex UnfurlableUrl = new(@"(?)"); + + public static ulong BanishedRoleId = 368961099925553153ul; } diff --git a/Izzy-Moonbot/Helpers/UserHelper.cs b/Izzy-Moonbot/Helpers/UserHelper.cs index 8f19c50a..562c2941 100644 --- a/Izzy-Moonbot/Helpers/UserHelper.cs +++ b/Izzy-Moonbot/Helpers/UserHelper.cs @@ -52,7 +52,7 @@ public static bool updateUserInfoFromDiscord(User userInfo, SocketGuildUser sock // - MemberRole (depending on whether the user was silenced when they left) // - NewMemberRole ("applying" this includes scheduling its automated removal) // - RolesToReapplyOnRejoin (varies by config) - public record JoinRoleResult(bool userInfoChanged, bool configChanged, HashSet rolesAdded, ScheduledJob? newMemberRemovalJob = null); + public record JoinRoleResult(bool userInfoChanged, bool configChanged, HashSet rolesAdded, ScheduledJob? newUserRoleUpdateJob = null); public static async Task applyJoinRolesToUser( User userInfo, @@ -65,31 +65,54 @@ ScheduleService scheduleService bool userInfoChanged = false; bool configChanged = false; HashSet rolesToAddIfMissing = new HashSet(); - ScheduledJob? newMemberRemovalJob = null; + ScheduledJob? newUserRoleUpdateJob = null; if (config.ManageNewUserRoles) { bool silencingUser = config.AutoSilenceNewJoins || userInfo.Silenced; - if (config.MemberRole != null && config.MemberRole > 0 && !silencingUser) + if (silencingUser) { - rolesToAddIfMissing.Add((ulong)config.MemberRole); + rolesToAddIfMissing.Add(DiscordHelper.BanishedRoleId); } - if (config.NewMemberRole != null && config.NewMemberRole > 0 && socketGuildUser.JoinedAt is not null) + if (config.ZeroJoinRoles) { - var joinTime = socketGuildUser.JoinedAt.Value; - var newMemberExpiry = joinTime.AddMinutes(config.NewMemberRoleDecay); + if (config.MemberRole != null && config.MemberRole > 0 && socketGuildUser.JoinedAt is not null) + { + var joinTime = socketGuildUser.JoinedAt.Value; + var memberAcceptanceTime = joinTime.AddMinutes(config.NewMemberRoleDecay); + + if (DateTimeOffset.UtcNow < memberAcceptanceTime) + { + // no change to rolesToAddIfMissing - if (DateTimeOffset.UtcNow < newMemberExpiry) + var action = new ScheduledRoleAdditionJob(config.MemberRole.Value, socketGuildUser.Id, + $"Member role added, {config.NewMemberRoleDecay} minutes (`NewMemberRoleDecay`) passed."); + var task = new ScheduledJob(DateTimeOffset.UtcNow, memberAcceptanceTime, action); + await scheduleService.CreateScheduledJob(task); + + newUserRoleUpdateJob = task; + } + } + } + else + { + if (config.NewMemberRole != null && config.NewMemberRole > 0 && socketGuildUser.JoinedAt is not null) { - rolesToAddIfMissing.Add((ulong)config.NewMemberRole); + var joinTime = socketGuildUser.JoinedAt.Value; + var newMemberExpiry = joinTime.AddMinutes(config.NewMemberRoleDecay); + + if (DateTimeOffset.UtcNow < newMemberExpiry) + { + rolesToAddIfMissing.Add((ulong)config.NewMemberRole); - var action = new ScheduledRoleRemovalJob(config.NewMemberRole.Value, socketGuildUser.Id, - $"New member role removal, {config.NewMemberRoleDecay} minutes (`NewMemberRoleDecay`) passed."); - var task = new ScheduledJob(DateTimeOffset.UtcNow, newMemberExpiry, action); - await scheduleService.CreateScheduledJob(task); + var action = new ScheduledRoleRemovalJob(config.NewMemberRole.Value, socketGuildUser.Id, + $"New member role removal, {config.NewMemberRoleDecay} minutes (`NewMemberRoleDecay`) passed."); + var task = new ScheduledJob(DateTimeOffset.UtcNow, newMemberExpiry, action); + await scheduleService.CreateScheduledJob(task); - newMemberRemovalJob = task; + newUserRoleUpdateJob = task; + } } } } @@ -121,15 +144,15 @@ ScheduleService scheduleService rolesToAddIfMissing.UnionWith(userInfo.RolesToReapplyOnRejoin); if (rolesToAddIfMissing.Count == 0) - return new JoinRoleResult(userInfoChanged, configChanged, [], newMemberRemovalJob); + return new JoinRoleResult(userInfoChanged, configChanged, [], newUserRoleUpdateJob); HashSet existingRoleIds = socketGuildUser.Roles.Select(r => r.Id).ToHashSet(); HashSet rolesToActuallyAdd = rolesToAddIfMissing.Where(r => !existingRoleIds.Contains(r)).ToHashSet(); await modService.AddRoles(socketGuildUser, rolesToActuallyAdd, auditLogMessage); - return new JoinRoleResult(userInfoChanged, configChanged, rolesToActuallyAdd, newMemberRemovalJob); + return new JoinRoleResult(userInfoChanged, configChanged, rolesToActuallyAdd, newUserRoleUpdateJob); } - public record UserScanResult(int totalUsersCount, int updatedUserCount, int newUserCount, Dictionary roleAddedCounts, HashSet newMemberRemovalsScheduled); + public record UserScanResult(int totalUsersCount, int updatedUserCount, int newUserCount, Dictionary roleAddedCounts, HashSet newUserRoleUpdatesScheduled); public static async Task scanAllUsers( SocketGuild guild, @@ -156,7 +179,7 @@ LoggingService logger var newUserCount = 0; var updatedUserCount = 0; Dictionary roleAddedCounts = new(); - HashSet newMemberRemovalsScheduled = new(); + HashSet newUserRoleUpdatesScheduled = new(); bool userInfoChanged = false; bool configChanged = false; @@ -174,8 +197,8 @@ LoggingService logger var result = await applyJoinRolesToUser(userInfo, socketGuildUser, config, modService, scheduleService); userInfoChanged |= result.userInfoChanged; configChanged |= result.configChanged; - if (result.newMemberRemovalJob != null) - newMemberRemovalsScheduled.Add(socketGuildUser.Id); + if (result.newUserRoleUpdateJob != null) + newUserRoleUpdatesScheduled.Add(socketGuildUser.Id); foreach (var roleId in result.rolesAdded) if (roleAddedCounts.ContainsKey(roleId)) roleAddedCounts[roleId] += 1; else roleAddedCounts[roleId] = 1; @@ -203,6 +226,6 @@ LoggingService logger if (userInfoChanged) await FileHelper.SaveUsersAsync(allUserInfo); // we don't save the schedule file here because scheduling the job already does that; it's likely not worth batching that - return new UserScanResult(totalUsersCount, updatedUserCount, newUserCount, roleAddedCounts, newMemberRemovalsScheduled); + return new UserScanResult(totalUsersCount, updatedUserCount, newUserCount, roleAddedCounts, newUserRoleUpdatesScheduled); } } diff --git a/Izzy-Moonbot/Modules/ModMiscModule.cs b/Izzy-Moonbot/Modules/ModMiscModule.cs index c4d44446..e4d13ca9 100644 --- a/Izzy-Moonbot/Modules/ModMiscModule.cs +++ b/Izzy-Moonbot/Modules/ModMiscModule.cs @@ -123,8 +123,9 @@ public async Task ScanCommandAsync() $"The other {result.totalUsersCount - result.updatedUserCount} were up to date."; if (result.roleAddedCounts.Count > 0) reply += $"\nAdded {string.Join(", ", result.roleAddedCounts.Select(rac => $"{rac.Value} <@&{rac.Key}>(s)"))}"; - if (result.newMemberRemovalsScheduled.Count > 0) - reply += $"\nScheduled <@&{_config.NewMemberRole!.Value}> removal(s) for {string.Join(", ", result.newMemberRemovalsScheduled.Select(u => $"<@{u}>"))}"; + if (result.newUserRoleUpdatesScheduled.Count > 0) + // TODO: change after ZJR + reply += $"\nScheduled <@&{_config.NewMemberRole!.Value}> removal(s) for {string.Join(", ", result.newUserRoleUpdatesScheduled.Select(u => $"<@{u}>"))}"; _logger.Log(reply); await Context.Message.ReplyAsync(reply, allowedMentions: AllowedMentions.None); @@ -198,13 +199,6 @@ public async Task TestableEchoCommandAsync( [DevCommand(Group = "Permissions")] public async Task StowawaysCommandAsync() { - if (_config.MemberRole == null) - { - await ReplyAsync( - "I'm unable to detect stowaways because the `MemberRole` config value is set to nothing."); - return; - } - await Task.Run(async () => { if (!Context.Guild.HasAllMembers) await Context.Guild.DownloadUsersAsync(); @@ -216,7 +210,7 @@ await Task.Run(async () => if (socketGuildUser.IsBot) continue; // Bots aren't stowaways if (socketGuildUser.Roles.Select(role => role.Id).Contains(_config.ModRole)) continue; // Mods aren't stowaways - if (!socketGuildUser.Roles.Select(role => role.Id).Contains((ulong)_config.MemberRole)) + if (socketGuildUser.Roles.Select(role => role.Id).Contains(DiscordHelper.BanishedRoleId)) { // Doesn't have member role, add to stowaway set. stowawaySet.Add(socketGuildUser); diff --git a/Izzy-Moonbot/Service/ModService.cs b/Izzy-Moonbot/Service/ModService.cs index c7babe63..bdba7180 100644 --- a/Izzy-Moonbot/Service/ModService.cs +++ b/Izzy-Moonbot/Service/ModService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -27,9 +27,7 @@ public async Task SilenceUser(SocketGuildUser user, string? reason) } public async Task SilenceUser(IIzzyGuildUser user, string? reason) { - if (_config.MemberRole == null) throw new TargetException("MemberRole config value is null (not set)"); - - await user.RemoveRoleAsync((ulong) _config.MemberRole, reason is null ? null : new Discord.RequestOptions { AuditLogReason = reason }); + await user.RemoveRoleAsync(DiscordHelper.BanishedRoleId, reason is null ? null : new Discord.RequestOptions { AuditLogReason = reason }); _users[user.Id].Silenced = true; await FileHelper.SaveUsersAsync(_users); @@ -41,11 +39,9 @@ public async Task SilenceUsers(IEnumerable users, string? reaso } public async Task SilenceUsers(IEnumerable users, string? reason) { - if (_config.MemberRole == null) throw new TargetException("MemberRole config value is null (not set)"); - foreach (var user in users) { - await user.RemoveRoleAsync((ulong)_config.MemberRole, reason is null ? null : new Discord.RequestOptions { AuditLogReason = reason }); + await user.AddRoleAsync(DiscordHelper.BanishedRoleId, reason is null ? null : new Discord.RequestOptions { AuditLogReason = reason }); _users[user.Id].Silenced = true; await FileHelper.SaveUsersAsync(_users); @@ -78,4 +74,4 @@ public async Task AddRoles(IIzzyGuildUser user, IEnumerable roles, string { await user.AddRolesAsync(roles, reason is null ? null : new Discord.RequestOptions { AuditLogReason = reason }); } -} \ No newline at end of file +} diff --git a/Izzy-Moonbot/Settings/Config.cs b/Izzy-Moonbot/Settings/Config.cs index cddc554f..518ef5c9 100644 --- a/Izzy-Moonbot/Settings/Config.cs +++ b/Izzy-Moonbot/Settings/Config.cs @@ -41,6 +41,7 @@ public Config() NewMemberRole = 0; NewMemberRoleDecay = 0; RolesToReapplyOnRejoin = new HashSet(); + ZeroJoinRoles = false; // Filter Settings FilterEnabled = true; @@ -141,6 +142,7 @@ public double BannerInterval public ulong? NewMemberRole { get; set; } public double NewMemberRoleDecay { get; set; } public HashSet RolesToReapplyOnRejoin { get; set; } + public bool ZeroJoinRoles { get; set; } // Filter settings public bool FilterEnabled { get; set; } diff --git a/Izzy-Moonbot/Worker.cs b/Izzy-Moonbot/Worker.cs index fa0104ee..beb60cb1 100644 --- a/Izzy-Moonbot/Worker.cs +++ b/Izzy-Moonbot/Worker.cs @@ -517,14 +517,15 @@ private void ResyncUsers() new LoggingService(_logger) ); - if (result.newUserCount > 0 || result.roleAddedCounts.Count > 0 || result.newMemberRemovalsScheduled.Count > 0) + if (result.newUserCount > 0 || result.roleAddedCounts.Count > 0 || result.newUserRoleUpdatesScheduled.Count > 0) { var msg = $"After rebooting I found {result.newUserCount} user(s) who were new to me."; if (result.roleAddedCounts.Count > 0) msg += $"\nAdded {string.Join(", ", result.roleAddedCounts.Select(rac => $"{rac.Value} {guild.GetRole(rac.Key).Name}(s)"))}"; - if (result.newMemberRemovalsScheduled.Count > 0) - msg += $"\nScheduled `NewMemberRole` removal(s) for {string.Join(", ", result.newMemberRemovalsScheduled.Select(u => $"<@{u}>"))}"; - if (result.roleAddedCounts.Count == 0 && result.newMemberRemovalsScheduled.Count == 0) + if (result.newUserRoleUpdatesScheduled.Count > 0) + // TODO: change after ZJR + msg += $"\nScheduled `NewMemberRole` removal(s) for {string.Join(", ", result.newUserRoleUpdatesScheduled.Select(u => $"<@{u}>"))}"; + if (result.roleAddedCounts.Count == 0 && result.newUserRoleUpdatesScheduled.Count == 0) msg += " They required no role changes."; _logger.LogInformation(msg); diff --git a/Izzy-MoonbotTests/Tests/ConfigCommandTests.cs b/Izzy-MoonbotTests/Tests/ConfigCommandTests.cs index 8170d87a..56b91eb3 100644 --- a/Izzy-MoonbotTests/Tests/ConfigCommandTests.cs +++ b/Izzy-MoonbotTests/Tests/ConfigCommandTests.cs @@ -533,6 +533,13 @@ public async Task ConfigCommand_EditEveryItemAsync() $"New Pony", generalChannel.Messages.Last().Content); + // post ".config ZeroJoinRoles true" + Assert.AreEqual(cfg.ZeroJoinRoles, false); + context = await client.AddMessageAsync(guild.Id, generalChannel.Id, sunny.Id, ".config ZeroJoinRoles true"); + await ConfigCommand.TestableConfigCommandAsync(context, cfg, cd, "ZeroJoinRoles", "true"); + Assert.AreEqual(cfg.ZeroJoinRoles, true); + Assert.AreEqual("I've set `ZeroJoinRoles` to the following content: True", generalChannel.Messages.Last().Content); + // post ".config FilterEnabled false" Assert.AreEqual(cfg.FilterEnabled, true); context = await client.AddMessageAsync(guild.Id, generalChannel.Id, sunny.Id, ".config FilterEnabled false"); @@ -800,7 +807,7 @@ public async Task ConfigCommand_EditEveryItemAsync() // Ensure we can't forget to keep this test up to date var configPropsCount = typeof(Config).GetProperties().Length; - Assert.AreEqual(59, configPropsCount, + Assert.AreEqual(60, configPropsCount, $"\nIf you just added or removed a config item, then this test is probably out of date"); Assert.AreEqual(configPropsCount * 2, generalChannel.Messages.Count(), diff --git a/Izzy-MoonbotTests/Tests/FileHelperTests.cs b/Izzy-MoonbotTests/Tests/FileHelperTests.cs index f7643055..5977a042 100644 --- a/Izzy-MoonbotTests/Tests/FileHelperTests.cs +++ b/Izzy-MoonbotTests/Tests/FileHelperTests.cs @@ -260,6 +260,7 @@ public void ConfigRoundTrip() "NewMemberRole": 1039194817231601695, "NewMemberRoleDecay": 120.0, "RolesToReapplyOnRejoin": [], + "ZeroJoinRoles": false, "FilterEnabled": true, "FilterIgnoredChannels": [ 964283764240973844