diff --git a/src/Color-Chan.Discord.Commands/Attributes/SlashCommandPermissionAttribute.cs b/src/Color-Chan.Discord.Commands/Attributes/SlashCommandPermissionAttribute.cs
index c168358f..7293a313 100644
--- a/src/Color-Chan.Discord.Commands/Attributes/SlashCommandPermissionAttribute.cs
+++ b/src/Color-Chan.Discord.Commands/Attributes/SlashCommandPermissionAttribute.cs
@@ -17,12 +17,14 @@ public class SlashCommandPermissionAttribute : Attribute
///
/// The id of the role or user.
/// Specifies the type that the ID belongs to.
- public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermissionsType type)
+ /// Whether to allow the user/role to use the command.
+ public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermissionsType type, bool allow = true)
{
Id = id;
Type = type;
+ Allow = allow;
}
-
+
///
/// The id of the role or user..
///
@@ -32,5 +34,10 @@ public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermis
/// Specifies the type that the ID belongs to.
///
public DiscordApplicationCommandPermissionsType Type { get; init; }
+
+ ///
+ /// The id of the role or user..
+ ///
+ public bool Allow { get; init; } = true;
}
}
\ No newline at end of file
diff --git a/src/Color-Chan.Discord.Commands/Extensions/DiscordGuildApplicationCommandPermissionsExtensions.cs b/src/Color-Chan.Discord.Commands/Extensions/DiscordGuildApplicationCommandPermissionsExtensions.cs
new file mode 100644
index 00000000..07421e6f
--- /dev/null
+++ b/src/Color-Chan.Discord.Commands/Extensions/DiscordGuildApplicationCommandPermissionsExtensions.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Linq;
+using Color_Chan.Discord.Core.Common.API.Params.Application;
+using Color_Chan.Discord.Core.Common.Models.Guild;
+
+namespace Color_Chan.Discord.Commands.Extensions
+{
+ internal static class DiscordGuildApplicationCommandPermissionsExtensions
+ {
+ internal static bool ShouldUpdatePermissions(this List localCommandPerms,
+ IReadOnlyList existingPerms)
+ {
+ foreach (var localCommandPerm in localCommandPerms)
+ {
+ var existingCommandPerm = existingPerms.FirstOrDefault(x => x.CommandId.Equals(localCommandPerm.CommandId));
+
+ if (existingCommandPerm is null)
+ {
+ // New command perms found.
+ return true;
+ }
+
+ // Found existing perm.
+
+ if (ContainsNewOrUpdatedPerm(localCommandPerm, existingCommandPerm))
+ {
+ return true;
+ }
+ }
+
+ foreach (var existingPerm in existingPerms)
+ {
+ var localCommandPerm = localCommandPerms.FirstOrDefault(x => x.CommandId.Equals(existingPerm.CommandId));
+
+ if (localCommandPerm is null)
+ {
+ // Deleted command perms found.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool ContainsNewOrUpdatedPerm(DiscordBatchEditApplicationCommandPermissions localCommandPerm, IDiscordGuildApplicationCommandPermissions existingCommandPerm)
+ {
+ if (localCommandPerm.Permissions.Count() != existingCommandPerm.Permissions.Count())
+ {
+ return true;
+ }
+
+ foreach (var localPerm in localCommandPerm.Permissions)
+ {
+ var existingPerm = existingCommandPerm.Permissions.FirstOrDefault(x => x.Id == localPerm.Id);
+
+ if (existingPerm is null)
+ {
+ return true;
+ }
+
+ if (localPerm.Type != existingPerm.Type)
+ {
+ return true;
+ }
+
+ if (localPerm.Allow != existingPerm.Allow)
+ {
+ return true;
+ }
+ }
+
+ foreach (var existingPerm in existingCommandPerm.Permissions)
+ {
+ var localPerm = localCommandPerm.Permissions.FirstOrDefault(x => x.Id == existingPerm.Id);
+
+ if (localPerm is null)
+ {
+ return true;
+ }
+
+ if (existingPerm.Type != localPerm.Type)
+ {
+ return true;
+ }
+
+ if (existingPerm.Allow != localPerm.Allow)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Color-Chan.Discord.Commands/Services/Builders/ISlashCommandGuildBuildService.cs b/src/Color-Chan.Discord.Commands/Services/Builders/ISlashCommandGuildBuildService.cs
index 10b49666..83dd9478 100644
--- a/src/Color-Chan.Discord.Commands/Services/Builders/ISlashCommandGuildBuildService.cs
+++ b/src/Color-Chan.Discord.Commands/Services/Builders/ISlashCommandGuildBuildService.cs
@@ -68,10 +68,10 @@ public interface ISlashCommandGuildBuildService
///
/// Builds the from the data.
///
- /// The s containing the information needed for the .
+ /// A dictionary where the are ordered by the command ID.
///
/// The converted .
///
- IEnumerable BuildGuildPermissions(IEnumerable commandInfos);
+ IEnumerable BuildGuildPermissions(Dictionary> attributePairs);
}
}
\ No newline at end of file
diff --git a/src/Color-Chan.Discord.Commands/Services/Implementations/Builders/SlashCommandGuildBuildService.cs b/src/Color-Chan.Discord.Commands/Services/Implementations/Builders/SlashCommandGuildBuildService.cs
index e18ff8e9..7aa79808 100644
--- a/src/Color-Chan.Discord.Commands/Services/Implementations/Builders/SlashCommandGuildBuildService.cs
+++ b/src/Color-Chan.Discord.Commands/Services/Implementations/Builders/SlashCommandGuildBuildService.cs
@@ -3,8 +3,8 @@
using System.Linq;
using System.Reflection;
using Color_Chan.Discord.Commands.Attributes;
-using Color_Chan.Discord.Commands.Info;
using Color_Chan.Discord.Commands.Services.Builders;
+using Color_Chan.Discord.Core.Common.API.DataModels.Application;
using Color_Chan.Discord.Core.Common.API.Params.Application;
using Microsoft.Extensions.Logging;
@@ -60,7 +60,7 @@ public IEnumerable GetCommandGuildPermissions(M
if (!GetCommandGuilds(command).Any() && attributes.Any())
{
- _logger.LogWarning("Skipping slash permission for {CommandName}, they can not be used with global commands", command.Name);
+ _logger.LogWarning("Skipping slash command permission for {CommandName}, they can not be used with global commands", command.Name);
return new List();
}
@@ -78,7 +78,7 @@ public IEnumerable GetCommandGuildPermissions(T
if (!GetCommandGuilds(commandModule).Any() && attributes.Any())
{
- _logger.LogWarning("Skipping slash permission for {ModuleName}, they can not be used with global commands", commandModule.Name);
+ _logger.LogWarning("Skipping slash command permission for {ModuleName}, they can not be used with global commands", commandModule.Name);
return new List();
}
@@ -86,9 +86,31 @@ public IEnumerable GetCommandGuildPermissions(T
}
///
- public IEnumerable BuildGuildPermissions(IEnumerable commandInfos)
+ public IEnumerable BuildGuildPermissions(Dictionary> attributePairs)
{
- throw new NotImplementedException();
+ var permBatch = new List();
+
+ foreach (var (commandId, attributes) in attributePairs)
+ {
+ var perms = new List();
+ foreach (var attribute in attributes)
+ {
+ perms.Add(new DiscordApplicationCommandPermissionsData
+ {
+ Allow = true,
+ Id = attribute.Id,
+ Type = attribute.Type
+ });
+ }
+
+ permBatch.Add(new DiscordBatchEditApplicationCommandPermissions
+ {
+ CommandId = commandId,
+ Permissions = perms
+ });
+ }
+
+ return permBatch;
}
}
}
\ No newline at end of file
diff --git a/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandAutoSyncService.cs b/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandAutoSyncService.cs
index a5f8f332..0b82c55a 100644
--- a/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandAutoSyncService.cs
+++ b/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandAutoSyncService.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Color_Chan.Discord.Commands.Attributes;
using Color_Chan.Discord.Commands.Configurations;
using Color_Chan.Discord.Commands.Exceptions;
using Color_Chan.Discord.Commands.Extensions;
@@ -9,6 +10,7 @@
using Color_Chan.Discord.Commands.Services.Builders;
using Color_Chan.Discord.Core;
using Color_Chan.Discord.Core.Common.API.Rest;
+using Color_Chan.Discord.Core.Common.Models.Application;
using Color_Chan.Discord.Core.Results;
using Microsoft.Extensions.Logging;
@@ -20,6 +22,7 @@ public class SlashCommandAutoSyncService : ISlashCommandAutoSyncService
private const int MaxGlobalCommands = 100;
private const int MaxGuildCommands = 100;
private readonly ISlashCommandBuildService _commandBuildService;
+ private readonly ISlashCommandGuildBuildService _guildBuildService;
private readonly DiscordTokens _discordTokens;
private readonly ILogger _logger;
private readonly IDiscordRestApplication _restApplication;
@@ -36,10 +39,14 @@ public class SlashCommandAutoSyncService : ISlashCommandAutoSyncService
/// The that will be used to build the slash
/// commands parameters.
///
+ ///
+ /// The that will be used to build the guild command permissions.
+ ///
public SlashCommandAutoSyncService(IDiscordRestApplication restApplication, DiscordTokens discordTokens, ILogger logger,
- ISlashCommandBuildService commandBuildService)
+ ISlashCommandBuildService commandBuildService, ISlashCommandGuildBuildService guildBuildService)
{
_commandBuildService = commandBuildService;
+ _guildBuildService = guildBuildService;
_restApplication = restApplication;
_discordTokens = discordTokens;
_logger = logger;
@@ -132,8 +139,10 @@ private async Task UpdateGuildCommandsAsync(IReadOnlyCollection();
+
// Build the slash commands.
- var guildCommandInfos = GetGuildCommandInfos(slashCommandInfos, guildId);
+ var guildCommandInfos = GetGuildCommandInfos(slashCommandInfos, guildId).ToList();
var guildCommands = _commandBuildService.BuildSlashCommandsParams(guildCommandInfos).ToList();
if (guildCommands.Count > MaxGuildCommands) throw new UpdateSlashCommandException($"A guild can not have more then {MaxGuildCommands} slash commands.");
@@ -153,6 +162,8 @@ private async Task UpdateGuildCommandsAsync(IReadOnlyCollection UpdateGuildCommandsAsync(IReadOnlyCollection UpdateGuildCommandsAsync(IReadOnlyCollection x.Name).Contains(existingCommand.Name))
+ if (guildCommands.Select(x => x.Name).Contains(existingCommand.Name))
{
- // Delete old guild slash command.
- var result = await _restApplication.DeleteGuildApplicationCommandAsync(_discordTokens.ApplicationId, guildId, existingCommand.Id).ConfigureAwait(false);
- if (!result.IsSuccessful) return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult("Failed to delete existing guild slash commands."));
+ remainingCommands.Add(existingCommand);
+ continue;
+ }
+
+ // Delete old guild slash command.
+ var result = await _restApplication.DeleteGuildApplicationCommandAsync(_discordTokens.ApplicationId, guildId, existingCommand.Id).ConfigureAwait(false);
+ if (!result.IsSuccessful) return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult("Failed to delete existing guild slash commands."));
+
+ _logger.LogInformation("Deleted old guild slash command {CommandName} {Id}", existingCommand.Name, existingCommand.Id.ToString());
+ }
- _logger.LogInformation("Deleted old guild slash command {CommandName} {Id}", existingCommand.Name, existingCommand.Id.ToString());
+ // Link local command perms with their command ID.
+ var localCommandPerms = new Dictionary>();
+ foreach (var remainingCommand in remainingCommands)
+ {
+ var commandInfo = guildCommandInfos.FirstOrDefault(x => x.CommandName == remainingCommand.Name);
+ if (commandInfo is null) continue;
+
+ localCommandPerms.Add(remainingCommand.Id, commandInfo.Permissions ?? new List());
+ }
+
+ // Get the existing command perms.
+ var existingCommandPerms = await _restApplication.GetGuildApplicationCommandPermissions(_discordTokens.ApplicationId, guildId).ConfigureAwait(false);
+ if (!existingCommandPerms.IsSuccessful)
+ {
+ return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult($"Failed to get existing guild command permissions for guild {guildId.ToString()}"));
+ }
+
+ // Only update the perms if needed.
+ var batchPerms = _guildBuildService.BuildGuildPermissions(localCommandPerms).ToList();
+ if (batchPerms.ShouldUpdatePermissions(existingCommandPerms.Entity!))
+ {
+ var batchPermResult = await _restApplication.BatchEditApplicationCommandPermissions(_discordTokens.ApplicationId, guildId, batchPerms).ConfigureAwait(false);
+
+ if (!batchPermResult.IsSuccessful)
+ {
+ return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult($"Failed batch edit guild command permissions for guild {guildId.ToString()}"));
}
+
+ _logger.LogInformation("Updated command permissions for guild {GuildId}", guildId.ToString());
}
+
+
_logger.LogInformation("Finished syncing guild slash commands for guild {Id}", guildId.ToString());
}
diff --git a/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandService.cs b/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandService.cs
index f9c49c0d..0103e944 100644
--- a/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandService.cs
+++ b/src/Color-Chan.Discord.Commands/Services/Implementations/SlashCommandService.cs
@@ -56,17 +56,21 @@ public SlashCommandService(ILogger logger, ISlashCommandBui
///
public async Task AddInteractionCommandsAsync(Assembly assembly)
{
- _logger.LogDebug("Loading interaction commands");
+ _logger.LogDebug("Registering slash commands...");
// Build all commands in a specific assembly.
var commandInfos = _slashCommandBuildService.BuildSlashCommandInfos(assembly);
foreach (var (key, commandInfo) in commandInfos)
{
- if (!_slashCommands.TryAdd(key, commandInfo))
- throw new Exception($"Failed to register {commandInfo.CommandName}");
+ if (_slashCommands.TryAdd(key, commandInfo)) continue;
+
+ // The command already existed.
+ var registeringException = new Exception($"Failed to register {commandInfo.CommandName}");
+ _logger.LogError(registeringException, "Can not register multiple commands with the same name");
+ throw registeringException;
}
- _logger.LogInformation("Finished adding {Count} commands to the command registry", _slashCommands.Count.ToString());
+ _logger.LogInformation("Registered {Count} slash commands to the command registry", _slashCommands.Count.ToString());
// Default config if no config was set.
_configurations ??= new SlashCommandConfiguration();
diff --git a/src/Color-Chan.Discord.Core/Common/API/Params/Application/DiscordBatchEditApplicationCommandPermissions.cs b/src/Color-Chan.Discord.Core/Common/API/Params/Application/DiscordBatchEditApplicationCommandPermissions.cs
index 8601d53c..a1679f78 100644
--- a/src/Color-Chan.Discord.Core/Common/API/Params/Application/DiscordBatchEditApplicationCommandPermissions.cs
+++ b/src/Color-Chan.Discord.Core/Common/API/Params/Application/DiscordBatchEditApplicationCommandPermissions.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using Color_Chan.Discord.Core.Common.Models.Application;
+using Color_Chan.Discord.Core.Common.API.DataModels.Application;
namespace Color_Chan.Discord.Core.Common.API.Params.Application
{
@@ -16,6 +16,6 @@ public class DiscordBatchEditApplicationCommandPermissions
/// The permissions for the command in the guild.
///
[JsonPropertyName("permissions")]
- public IEnumerable Permissions { get; set; } = new List();
+ public IEnumerable Permissions { get; set; } = new List();
}
}
\ No newline at end of file
diff --git a/tests/Color-Chan.Discord.Commands.Tests/Services/Implementations/SlashCommandRequirementServiceTests.cs b/tests/Color-Chan.Discord.Commands.Tests/Services/Implementations/SlashCommandRequirementServiceTests.cs
index b2e129bf..f3112a2e 100644
--- a/tests/Color-Chan.Discord.Commands.Tests/Services/Implementations/SlashCommandRequirementServiceTests.cs
+++ b/tests/Color-Chan.Discord.Commands.Tests/Services/Implementations/SlashCommandRequirementServiceTests.cs
@@ -30,7 +30,7 @@ public async Task ExecuteSlashCommandRequirementsAsync_should_get_one_error(int
var module = typeof(ValidMockCommandModule1).GetTypeInfo();
var method = module.GetMethods()[methodIndex];
var requirementsAttributes = method.GetCustomAttributes();
- var commandInfo = new SlashCommandInfo("test", "desc", method, module)
+ var commandInfo = new SlashCommandInfo("test", "desc", false, method, module)
{
Requirements = requirementsAttributes
};