diff --git a/RegexBot-Modules/VoiceRoleSync/ModuleConfig.cs b/RegexBot-Modules/VoiceRoleSync/ModuleConfig.cs new file mode 100644 index 0000000..abd5f25 --- /dev/null +++ b/RegexBot-Modules/VoiceRoleSync/ModuleConfig.cs @@ -0,0 +1,42 @@ +using System.Collections.ObjectModel; + +namespace RegexBot.Modules.VoiceRoleSync; +/// +/// Dictionary wrapper. Key = voice channel ID, Value = role. +/// +class ModuleConfig { + private readonly ReadOnlyDictionary _values; + + public ModuleConfig(JObject config) { + // Configuration format is expected to be an object that contains other objects. + // The objects themselves should have their name be the voice channel, + // and the value be the role to be applied. + + // TODO Make it accept names; currently only accepts ulongs + + var values = new Dictionary(); + + foreach (var item in config.Properties()) { + if (!ulong.TryParse(item.Name, out var voice)) throw new ModuleLoadException($"{item.Name} is not a voice channel ID."); + var valstr = item.Value.Value(); + if (!ulong.TryParse(valstr, out var role)) throw new ModuleLoadException($"{valstr} is not a role ID."); + + values[voice] = role; + } + + _values = new ReadOnlyDictionary(values); + } + + public SocketRole? GetAssociatedRoleFor(SocketVoiceChannel voiceChannel) { + if (voiceChannel == null) return null; + if (_values.TryGetValue(voiceChannel.Id, out var roleId)) return voiceChannel.Guild.GetRole(roleId); + return null; + } + + public IEnumerable GetTrackedRoles(SocketGuild guild) { + foreach (var pair in _values) { + var r = guild.GetRole(pair.Value); + if (r != null) yield return r; + } + } +} diff --git a/RegexBot-Modules/VoiceRoleSync/VoiceRoleSync.cs b/RegexBot-Modules/VoiceRoleSync/VoiceRoleSync.cs new file mode 100644 index 0000000..6856c81 --- /dev/null +++ b/RegexBot-Modules/VoiceRoleSync/VoiceRoleSync.cs @@ -0,0 +1,53 @@ +namespace RegexBot.Modules.VoiceRoleSync; +/// +/// Synchronizes a user's state in a voice channel with a role. +/// In other words: applies a role to a user entering a voice channel. Removes the role when exiting. +/// +[RegexbotModule] +public class VoiceRoleSync : RegexbotModule { + // TODO wishlist? specify multiple definitions - multiple channels associated with multiple roles. + + public VoiceRoleSync(RegexbotClient bot) : base(bot) { + DiscordClient.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + } + + private async Task Client_UserVoiceStateUpdated(SocketUser argUser, SocketVoiceState before, SocketVoiceState after) { + // Gather data. + if (argUser is not SocketGuildUser user) return; // not a guild user + var settings = GetGuildState(user.Guild.Id); + if (settings == null) return; // not enabled here + + async Task RemoveAllAssociatedRoles() + => await user.RemoveRolesAsync(settings.GetTrackedRoles(user.Guild).Intersect(user.Roles)); + + if (after.VoiceChannel == null) { + // Not in any voice channel. Remove all roles being tracked by this instance. Clear. + await RemoveAllAssociatedRoles(); + } else { + // In a voice channel, and... + if (after.IsDeafened || after.IsSelfDeafened) { + // Is defeaned, which is like not being in a voice channel for our purposes. Clear. + await RemoveAllAssociatedRoles(); + } else { + var targetRole = settings.GetAssociatedRoleFor(after.VoiceChannel); + if (targetRole == null) { + // In an untracked voice channel. Clear. + await RemoveAllAssociatedRoles(); + } else { + // In a tracked voice channel: Clear all except target, add target if needed. + await user.RemoveRolesAsync(settings.GetTrackedRoles(user.Guild) + .Where(role => role.Id != targetRole.Id) + .Intersect(user.Roles)); + if (!user.Roles.Contains(targetRole)) await user.AddRoleAsync(targetRole); + } + } + } + } + + public override Task CreateGuildStateAsync(ulong guildID, JToken config) { + if (config == null) return Task.FromResult(null); + if (config.Type != JTokenType.Object) + throw new ModuleLoadException("Configuration for this section is invalid."); + return Task.FromResult(new ModuleConfig((JObject)config)); + } +}