diff --git a/Cocotte/Modules/Activities/CompositeRolesListener.cs b/Cocotte/Modules/Activities/CompositeRolesListener.cs new file mode 100644 index 0000000..e617fee --- /dev/null +++ b/Cocotte/Modules/Activities/CompositeRolesListener.cs @@ -0,0 +1,60 @@ +using Cocotte.Options; +using Discord; +using Discord.WebSocket; +using Microsoft.Extensions.Options; + +namespace Cocotte.Modules.Activities; + +public class CompositeRolesListener +{ + private readonly ILogger _logger; + private readonly IDictionary _compositeRoles; + + public CompositeRolesListener(ILogger logger, + IOptions compositeRolesOptions) + { + _logger = logger; + // Initialize dictionary: transform string guildIds to ulong + _compositeRoles = compositeRolesOptions.Value.CompositeRoles.ToDictionary(kp => ulong.Parse(kp.Key), kp => kp.Value); + } + + public async Task UserUpdated(Cacheable cacheable, SocketGuildUser guildUser) + { + // Fetch composite roles for this guild + if (!_compositeRoles.TryGetValue(guildUser.Guild.Id, out var guildCompositeRoles)) + { + return; + } + + _logger.LogTrace("Guild {Guild} has at least one composite role, checking for user {User}", guildUser.Guild.Name, guildUser.DisplayName); + + // Check roles for each composite roles + var roles = guildUser.Roles; + foreach (var compositeRole in guildCompositeRoles) + { + // If the user has the target role, check if we need to remove it + if (roles.FirstOrDefault(r => r.Id == compositeRole.TargetRoleId) is { } presentTargetRole) + { + // Check that the user no associated role + if (!roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id))) + { + await guildUser.RemoveRoleAsync(presentTargetRole); + + _logger.LogInformation("CompositeRoles removed role {Role} from {User}", presentTargetRole.Name, guildUser.DisplayName); + } + } + // It the user doesn't have the target role, check if we need to add it + else + { + // Check that the user has at least one of the desired roles + if (roles.Any(r => compositeRole.CompositeRolesIds.Contains(r.Id))) + { + var missingTargetRole = guildUser.Guild.GetRole(compositeRole.TargetRoleId); + await guildUser.AddRoleAsync(missingTargetRole); + + _logger.LogInformation("CompositeRoles added role {Role} from {User}", missingTargetRole.Name, guildUser.DisplayName); + } + } + } + } +} \ No newline at end of file diff --git a/Cocotte/Modules/Activities/Models/Activity.cs b/Cocotte/Modules/Activities/Models/Activity.cs index e1ba1f7..79f5d12 100644 --- a/Cocotte/Modules/Activities/Models/Activity.cs +++ b/Cocotte/Modules/Activities/Models/Activity.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; namespace Cocotte.Modules.Activities.Models; diff --git a/Cocotte/Options/CompositeRolesOptions.cs b/Cocotte/Options/CompositeRolesOptions.cs new file mode 100644 index 0000000..97b3c45 --- /dev/null +++ b/Cocotte/Options/CompositeRolesOptions.cs @@ -0,0 +1,14 @@ +namespace Cocotte.Options; + +public class CompositeRolesOptions +{ + public const string SectionName = "CompositeRolesOptions"; + + public required IReadOnlyDictionary CompositeRoles { get; init; } +} + +public class GuildCompositeRoles +{ + public required ulong TargetRoleId { get; init; } + public required ulong[] CompositeRolesIds { get; init; } +} diff --git a/Cocotte/Program.cs b/Cocotte/Program.cs index e22f8ad..ad327bf 100644 --- a/Cocotte/Program.cs +++ b/Cocotte/Program.cs @@ -21,12 +21,14 @@ IHost host = Host.CreateDefaultBuilder(args) { configuration.AddJsonFile("discord.json", false, false); configuration.AddJsonFile("activity.json", false, false); + configuration.AddJsonFile("compositeRoles.json", false, false); }) .ConfigureServices((context, services) => { // Options services.Configure(context.Configuration.GetSection(DiscordOptions.SectionName)); services.Configure(context.Configuration.GetSection(ActivityOptions.SectionName)); + services.Configure(context.Configuration.GetSection(CompositeRolesOptions.SectionName)); // Database services.AddDbContext(options => @@ -53,6 +55,9 @@ IHost host = Host.CreateDefaultBuilder(args) services.AddTransient(); services.AddTransient(); + // Composite roles + services.AddSingleton(); + // Raids services.AddTransient(); services.AddSingleton(); diff --git a/Cocotte/Services/CocotteService.cs b/Cocotte/Services/CocotteService.cs index db8a65c..0c22a9b 100644 --- a/Cocotte/Services/CocotteService.cs +++ b/Cocotte/Services/CocotteService.cs @@ -65,9 +65,19 @@ public class CocotteService : BackgroundService await _client.LoginAsync(TokenType.Bot, _options.Token); await _client.StartAsync(); + // Register events + RegisterEvents(); + await Task.Delay(Timeout.Infinite, stoppingToken); } + private void RegisterEvents() + { + var composteRolesListener = _serviceProvider.GetRequiredService(); + + _client.GuildMemberUpdated += composteRolesListener.UserUpdated; + } + private bool ValidateOptions() { // Validate group options diff --git a/Cocotte/compositeRoles.json b/Cocotte/compositeRoles.json new file mode 100644 index 0000000..7459f62 --- /dev/null +++ b/Cocotte/compositeRoles.json @@ -0,0 +1,26 @@ +{ + "CompositeRolesOptions": { + "CompositeRoles": { + "someGuildId (ulong)": [ + { + "TargetRoleId": 1, + "CompositeRolesIds": [ + 0, + 1, + 2 + ] + } + ], + "anotherGuildId (ulong)": [ + { + "TargetRoleId": 45, + "CompositeRolesIds": [ + 98, + 1, + 2 + ] + } + ] + } + } +}