Skip to content

Commit

Permalink
Added auto sync for guild command permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
BrammyS committed Aug 5, 2021
1 parent 960bfa0 commit 8f5b5ce
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ public class SlashCommandPermissionAttribute : Attribute
/// </summary>
/// <param name="id">The id of the role or user.</param>
/// <param name="type">Specifies the type that the ID belongs to.</param>
public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermissionsType type)
/// <param name="allow">Whether to allow the user/role to use the command.</param>
public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermissionsType type, bool allow = true)
{
Id = id;
Type = type;
Allow = allow;
}

/// <summary>
/// The id of the role or user..
/// </summary>
Expand All @@ -32,5 +34,10 @@ public SlashCommandPermissionAttribute(ulong id, DiscordApplicationCommandPermis
/// Specifies the type that the ID belongs to.
/// </summary>
public DiscordApplicationCommandPermissionsType Type { get; init; }

/// <summary>
/// The id of the role or user..
/// </summary>
public bool Allow { get; init; } = true;
}
}
Original file line number Diff line number Diff line change
@@ -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<DiscordBatchEditApplicationCommandPermissions> localCommandPerms,
IReadOnlyList<IDiscordGuildApplicationCommandPermissions> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public interface ISlashCommandGuildBuildService
/// <summary>
/// Builds the <see cref="DiscordBatchEditApplicationCommandPermissions"/> from the <see cref="SlashCommandPermissionAttribute"/> data.
/// </summary>
/// <param name="commandInfos">The <see cref="ISlashCommandInfo"/>s containing the information needed for the <see cref="DiscordBatchEditApplicationCommandPermissions"/>.</param>
/// <param name="attributePairs">A dictionary where the <see cref="SlashCommandPermissionAttribute" /> are ordered by the command ID.</param>
/// <returns>
/// The converted <see cref="DiscordBatchEditApplicationCommandPermissions"/>.
/// </returns>
IEnumerable<DiscordBatchEditApplicationCommandPermissions> BuildGuildPermissions(IEnumerable<ISlashCommandInfo> commandInfos);
IEnumerable<DiscordBatchEditApplicationCommandPermissions> BuildGuildPermissions(Dictionary<ulong, IEnumerable<SlashCommandPermissionAttribute>> attributePairs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -60,7 +60,7 @@ public IEnumerable<SlashCommandPermissionAttribute> 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<SlashCommandPermissionAttribute>();
}

Expand All @@ -78,17 +78,39 @@ public IEnumerable<SlashCommandPermissionAttribute> 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<SlashCommandPermissionAttribute>();
}

return attributes;
}

/// <inheritdoc />
public IEnumerable<DiscordBatchEditApplicationCommandPermissions> BuildGuildPermissions(IEnumerable<ISlashCommandInfo> commandInfos)
public IEnumerable<DiscordBatchEditApplicationCommandPermissions> BuildGuildPermissions(Dictionary<ulong, IEnumerable<SlashCommandPermissionAttribute>> attributePairs)
{
throw new NotImplementedException();
var permBatch = new List<DiscordBatchEditApplicationCommandPermissions>();

foreach (var (commandId, attributes) in attributePairs)
{
var perms = new List<DiscordApplicationCommandPermissionsData>();
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
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;
using Color_Chan.Discord.Commands.Info;
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;

Expand All @@ -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<SlashCommandAutoSyncService> _logger;
private readonly IDiscordRestApplication _restApplication;
Expand All @@ -36,10 +39,14 @@ public class SlashCommandAutoSyncService : ISlashCommandAutoSyncService
/// The <see cref="ISlashCommandBuildService" /> that will be used to build the slash
/// commands parameters.
/// </param>
/// <param name="guildBuildService">
/// The <see cref="ISlashCommandGuildBuildService" /> that will be used to build the guild command permissions.
/// </param>
public SlashCommandAutoSyncService(IDiscordRestApplication restApplication, DiscordTokens discordTokens, ILogger<SlashCommandAutoSyncService> logger,
ISlashCommandBuildService commandBuildService)
ISlashCommandBuildService commandBuildService, ISlashCommandGuildBuildService guildBuildService)
{
_commandBuildService = commandBuildService;
_guildBuildService = guildBuildService;
_restApplication = restApplication;
_discordTokens = discordTokens;
_logger = logger;
Expand Down Expand Up @@ -132,8 +139,10 @@ private async Task<Result> UpdateGuildCommandsAsync(IReadOnlyCollection<ISlashCo

foreach (var guildId in guildIds)
{
var remainingCommands = new List<IDiscordApplicationCommand>();

// 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.");

Expand All @@ -153,6 +162,8 @@ private async Task<Result> UpdateGuildCommandsAsync(IReadOnlyCollection<ISlashCo
// Creating a new slash command.
var result = await _restApplication.CreateGuildApplicationCommandAsync(_discordTokens.ApplicationId, guildId, newCommand).ConfigureAwait(false);
if (!result.IsSuccessful) return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult("Failed to create guild slash commands."));

remainingCommands.Add(result.Entity ?? throw new ArgumentNullException(nameof(result.Entity)));
_logger.LogInformation("Created new guild slash command {CommandName} {Id}", result.Entity!.Name, result.Entity!.Id.ToString());
}
else
Expand All @@ -166,23 +177,61 @@ private async Task<Result> UpdateGuildCommandsAsync(IReadOnlyCollection<ISlashCo
// Updating a slash command.
var result = await _restApplication.EditGuildApplicationCommandAsync(_discordTokens.ApplicationId, guildId, commandId.Value, newCommand).ConfigureAwait(false);
if (!result.IsSuccessful) return Result.FromError(existingCommands.ErrorResult ?? new ErrorResult("Failed to create guild slash commands."));


_logger.LogInformation("Updated existing guild slash command {CommandName} {Id}", result.Entity!.Name, result.Entity!.Id.ToString());
}
}

// Delete old guild guild commands.
foreach (var existingCommand in existingCommands.Entity!)
{
if (!guildCommands.Select(x => 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<ulong, IEnumerable<SlashCommandPermissionAttribute>>();
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<SlashCommandPermissionAttribute>());
}

// 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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,21 @@ public SlashCommandService(ILogger<SlashCommandService> logger, ISlashCommandBui
/// <inheritdoc />
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -16,6 +16,6 @@ public class DiscordBatchEditApplicationCommandPermissions
/// The permissions for the command in the guild.
/// </summary>
[JsonPropertyName("permissions")]
public IEnumerable<IDiscordApplicationCommandPermissions> Permissions { get; set; } = new List<IDiscordApplicationCommandPermissions>();
public IEnumerable<DiscordApplicationCommandPermissionsData> Permissions { get; set; } = new List<DiscordApplicationCommandPermissionsData>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoolRequirement>();
var commandInfo = new SlashCommandInfo("test", "desc", method, module)
var commandInfo = new SlashCommandInfo("test", "desc", false, method, module)
{
Requirements = requirementsAttributes
};
Expand Down

0 comments on commit 8f5b5ce

Please sign in to comment.