Browse Source

add rest conversions to interaction service

pull/2211/head
Cenngo 3 years ago
parent
commit
1cea40ce8e
8 changed files with 190 additions and 38 deletions
  1. +53
    -0
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  2. +54
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  3. +1
    -1
      src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs
  4. +0
    -12
      src/Discord.Net.Interactions/ILocalizationManager.cs
  5. +12
    -0
      src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs
  6. +6
    -6
      src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs
  7. +4
    -4
      src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs
  8. +60
    -14
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs

+ 53
- 0
src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs View File

@@ -1,3 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
/// <summary>
@@ -31,7 +36,10 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

private string _name;
private Dictionary<string, string> _nameLocalizations;

/// <summary>
/// Build the current builder into a <see cref="MessageCommandProperties"/> class.
@@ -73,5 +81,50 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}

public MessageCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

public MessageCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

internal static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

// Discord updated the docs, this regex prevents special characters like @!$%(... etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}
}
}

+ 54
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs View File

@@ -1,3 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
{
/// <summary>
@@ -5,7 +10,7 @@ namespace Discord
/// </summary>
public class UserCommandBuilder
{
/// <summary>
/// <summary>
/// Returns the maximum length a commands name allowed by Discord.
/// </summary>
public const int MaxNameLength = 32;
@@ -31,7 +36,10 @@ namespace Discord
/// </summary>
public bool IsDefaultPermission { get; set; } = true;

public IReadOnlyDictionary<string, string> NameLocalizations => _nameLocalizations;

private string _name;
private Dictionary<string, string> _nameLocalizations;

/// <summary>
/// Build the current builder into a <see cref="UserCommandProperties"/> class.
@@ -71,5 +79,50 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}

public UserCommandBuilder WithNameLocalizations(IDictionary<string, string> nameLocalizations)
{
if (nameLocalizations is null)
throw new ArgumentNullException(nameof(nameLocalizations));

foreach (var (locale, name) in nameLocalizations)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);
}

_nameLocalizations = new Dictionary<string, string>(nameLocalizations);
return this;
}

public UserCommandBuilder AddNameLocalization(string locale, string name)
{
if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$"))
throw new ArgumentException($"Invalid locale: {locale}", nameof(locale));

EnsureValidCommandName(name);

_nameLocalizations ??= new();
_nameLocalizations.Add(locale, name);

return this;
}

internal static void EnsureValidCommandName(string name)
{
Preconditions.NotNullOrEmpty(name, nameof(name));
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));

// Discord updated the docs, this regex prevents special characters like @!$%(... etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));

if (name.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");
}
}
}

+ 1
- 1
src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs View File

@@ -1,4 +1,4 @@
namespace System.Collections.Generic;
namespace System.Collections.Generic;

public static class GenericCollectionExtensions
{


+ 0
- 12
src/Discord.Net.Interactions/ILocalizationManager.cs View File

@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
public interface ILocalizationManager
{
Task<IDictionary<string, string>> GetAllNamesAsync(IList<string> key, LocalizationTarget destinationType);
Task<IDictionary<string, string>> GetAllDescriptionsAsync(IList<string> key, LocalizationTarget destinationType);
}
}

+ 12
- 0
src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
public interface ILocalizationManager
{
IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType);
IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType);
}
}

+ 6
- 6
src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs View File

@@ -23,16 +23,16 @@ namespace Discord.Interactions
_fileName = fileName;
}

public Task<IDictionary<string, string>> GetAllDescriptionsAsync(IList<string> key, LocalizationTarget destinationType) =>
GetValuesAsync(key, DescriptionIdentifier);
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, DescriptionIdentifier);

public Task<IDictionary<string, string>> GetAllNamesAsync(IList<string> key, LocalizationTarget destinationType) =>
GetValuesAsync(key, NameIdentifier);
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, NameIdentifier);

private string[] GetAllFiles() =>
Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly);

private async Task<IDictionary<string, string>> GetValuesAsync(IList<string> key, string identifier)
private IDictionary<string, string> GetValues(IList<string> key, string identifier)
{
var result = new Dictionary<string, string>();
var files = GetAllFiles();
@@ -47,7 +47,7 @@ namespace Discord.Interactions

using var sr = new StreamReader(file);
using var jr = new JsonTextReader(sr);
var obj = await JObject.LoadAsync(jr);
var obj = JObject.Load(jr);
var token = string.Join(".", key) + $".{identifier}";
var value = (string)obj.SelectToken(token);
if (value is not null)


+ 4
- 4
src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs View File

@@ -25,11 +25,11 @@ namespace Discord.Interactions
_supportedLocales = supportedLocales;
}

public Task<IDictionary<string, string>> GetAllDescriptionsAsync(IList<string> key, LocalizationTarget destinationType) =>
Task.FromResult(GetValues(key, DescriptionIdentifier));
public IDictionary<string, string> GetAllDescriptions(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, DescriptionIdentifier);

public Task<IDictionary<string, string>> GetAllNamesAsync(IList<string> key, LocalizationTarget destinationType) =>
Task.FromResult(GetValues(key, NameIdentifier));
public IDictionary<string, string> GetAllNames(IList<string> key, LocalizationTarget destinationType) =>
GetValues(key, NameIdentifier);

private IDictionary<string, string> GetValues(IList<string> key, string identifier)
{


+ 60
- 14
src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Discord.Interactions
@@ -9,6 +10,9 @@ namespace Discord.Interactions
#region Parameters
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo)
{
var localizationManager = parameterInfo.Command.Module.CommandService._localizationManager;
var parameterPath = parameterInfo.GetParameterPath();

var props = new ApplicationCommandOptionProperties
{
Name = parameterInfo.Name,
@@ -23,7 +27,9 @@ namespace Discord.Interactions
ChannelTypes = parameterInfo.ChannelTypes?.ToList(),
IsAutocomplete = parameterInfo.IsAutocomplete,
MaxValue = parameterInfo.MaxValue,
MinValue = parameterInfo.MinValue
MinValue = parameterInfo.MinValue,
NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary<string, string>.Empty
};

parameterInfo.TypeConverter.Write(props, parameterInfo);
@@ -36,12 +42,17 @@ namespace Discord.Interactions

public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo)
{
var commandPath = commandInfo.GetCommandPath();
var localizationManager = commandInfo.Module.CommandService._localizationManager;

var props = new SlashCommandBuilder()
{
Name = commandInfo.Name,
Description = commandInfo.Description,
IsDefaultPermission = commandInfo.DefaultPermission,
}.Build();
}.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.Build();

if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters");
@@ -51,23 +62,40 @@ namespace Discord.Interactions
return props;
}

public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) =>
new ApplicationCommandOptionProperties
public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo)
{
var localizationManager = commandInfo.Module.CommandService._localizationManager;
var commandPath = commandInfo.GetCommandPath();

return new ApplicationCommandOptionProperties
{
Name = commandInfo.Name,
Description = commandInfo.Description,
Type = ApplicationCommandOptionType.SubCommand,
IsRequired = false,
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList()
Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())
?.ToList(),
NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty
};
}

public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo)
=> commandInfo.CommandType switch
{
var localizationManager = commandInfo.Module.CommandService._localizationManager;
var commandPath = commandInfo.GetCommandPath();

return commandInfo.CommandType switch
{
ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(),
ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(),
ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.Build(),
ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
};
}
#endregion

#region Modules
@@ -108,12 +136,18 @@ namespace Discord.Interactions

options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister)));

var localizationManager = moduleInfo.CommandService._localizationManager;
var modulePath = moduleInfo.GetModulePath();

var props = new SlashCommandBuilder
{
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
IsDefaultPermission = moduleInfo.DefaultPermission,
}.Build();
}
.WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty)
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty)
.Build();

if (options.Count > SlashCommandBuilder.MaxOptionsCount)
throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters");
@@ -151,7 +185,11 @@ namespace Discord.Interactions
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
Type = ApplicationCommandOptionType.SubCommandGroup,
Options = options
Options = options,
NameLocalizations = moduleInfo.CommandService._localizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group)
?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = moduleInfo.CommandService._localizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group)
?? ImmutableDictionary<string, string>.Empty,
} };
}

@@ -166,17 +204,23 @@ namespace Discord.Interactions
Name = command.Name,
Description = command.Description,
IsDefaultPermission = command.IsDefaultPermission,
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary(),
},
ApplicationCommandType.User => new UserCommandProperties
{
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission
IsDefaultPermission = command.IsDefaultPermission,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary()
},
ApplicationCommandType.Message => new MessageCommandProperties
{
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission
IsDefaultPermission = command.IsDefaultPermission,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary(),
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary()
},
_ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"),
};
@@ -194,7 +238,9 @@ namespace Discord.Interactions
Name = x.Name,
Value = x.Value
}).ToList(),
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList()
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(),
NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(),
DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary()
};
}
}

Loading…
Cancel
Save