@@ -43,7 +43,7 @@ namespace Discord.Interactions.Builders | |||
/// <returns> | |||
/// The builder instance. | |||
/// </returns> | |||
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) | |||
public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services) | |||
{ | |||
base.SetParameterType(type); | |||
@@ -41,14 +41,7 @@ namespace Discord.Interactions | |||
if (context.Interaction is not IAutocompleteInteraction) | |||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); | |||
try | |||
{ | |||
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false); | |||
} | |||
catch (Exception ex) | |||
{ | |||
return ExecuteResult.FromError(ex); | |||
} | |||
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false); | |||
} | |||
/// <inheritdoc/> | |||
@@ -31,6 +31,8 @@ namespace Discord.Interactions | |||
private readonly ExecuteCallback _action; | |||
private readonly ILookup<string, PreconditionAttribute> _groupedPreconditions; | |||
internal IReadOnlyDictionary<string, TParameter> _parameterDictionary; | |||
/// <inheritdoc/> | |||
public ModuleInfo Module { get; } | |||
@@ -120,10 +122,7 @@ namespace Discord.Interactions | |||
return moduleResult; | |||
var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); | |||
if (!commandResult.IsSuccess) | |||
return commandResult; | |||
return PreconditionResult.FromSuccess(); | |||
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); | |||
} | |||
protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services) | |||
@@ -137,8 +136,8 @@ namespace Discord.Interactions | |||
using var scope = services?.CreateScope(); | |||
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); | |||
} | |||
else | |||
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||
} | |||
case RunMode.Async: | |||
_ = Task.Run(async () => | |||
@@ -167,20 +166,14 @@ namespace Discord.Interactions | |||
{ | |||
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||
if (!preconditionResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false); | |||
return preconditionResult; | |||
} | |||
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); | |||
var index = 0; | |||
foreach (var parameter in Parameters) | |||
{ | |||
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); | |||
if (!result.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
} | |||
return await InvokeEventAndReturn(context, result).ConfigureAwait(false); | |||
} | |||
var task = _action(context, args, services, this); | |||
@@ -189,20 +182,16 @@ namespace Discord.Interactions | |||
{ | |||
var result = await resultTask.ConfigureAwait(false); | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
if (result is RuntimeResult || result is ExecuteResult) | |||
if (result is RuntimeResult or ExecuteResult) | |||
return result; | |||
} | |||
else | |||
{ | |||
await task.ConfigureAwait(false); | |||
var result = ExecuteResult.FromSuccess(); | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false); | |||
} | |||
var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason"); | |||
await InvokeModuleEvent(context, failResult).ConfigureAwait(false); | |||
return failResult; | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false); | |||
} | |||
catch (Exception ex) | |||
{ | |||
@@ -231,6 +220,12 @@ namespace Discord.Interactions | |||
} | |||
} | |||
protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result) | |||
{ | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
} | |||
private static bool CheckTopLevel(ModuleInfo parent) | |||
{ | |||
var currentParent = parent; | |||
@@ -48,6 +48,11 @@ namespace Discord.Interactions | |||
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> wildcardCaptures, IComponentInteractionData data, | |||
IServiceProvider services) | |||
{ | |||
var paramCount = paramList.Count(); | |||
var captureCount = wildcardCaptures?.Count() ?? 0; | |||
if (paramCount < captureCount + 1) | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too many parameters")).ConfigureAwait(false); | |||
if (context.Interaction is not IComponentInteraction messageComponent) | |||
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); | |||
@@ -58,27 +63,16 @@ namespace Discord.Interactions | |||
for (var i = 0; i < paramCount; i++) | |||
{ | |||
var parameter = Parameters.ElementAt(i); | |||
bool isCapture = i < captureCount; | |||
var isCapture = i < captureCount; | |||
if (isCapture ^ parameter.IsRouteSegmentParameter) | |||
{ | |||
var result = ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Argument type and parameter type didn't match (Wild Card capture/Component value)"); | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
} | |||
TypeConverterResult readResult; | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); | |||
if (isCapture) | |||
readResult = await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false); | |||
else | |||
readResult = await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); | |||
var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : | |||
await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); | |||
if (!readResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, readResult).ConfigureAwait(false); | |||
return readResult; | |||
} | |||
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | |||
args[i] = readResult.Value; | |||
} | |||
@@ -87,9 +81,7 @@ namespace Discord.Interactions | |||
} | |||
catch (Exception ex) | |||
{ | |||
var result = ExecuteResult.FromError(ex); | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||
} | |||
} | |||
@@ -1,6 +1,7 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics.Tracing; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.Interactions | |||
@@ -57,37 +58,28 @@ namespace Discord.Interactions | |||
if(i < captureCount) | |||
{ | |||
var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); | |||
if (!readResult.IsSuccess) | |||
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); | |||
if(!readResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, readResult).ConfigureAwait(false); | |||
return readResult; | |||
} | |||
args[i] = readResult.Value; | |||
} | |||
else | |||
{ | |||
var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); | |||
if (!modalResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, modalResult).ConfigureAwait(false); | |||
return modalResult; | |||
} | |||
return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); | |||
if (modalResult is not ParseResult parseResult) | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); | |||
if (modalResult is ParseResult parseResult) | |||
args[i] = parseResult.Value; | |||
else | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | |||
args[i] = parseResult.Value; | |||
} | |||
} | |||
return await RunAsync(context, args.ToArray(), services); | |||
return await RunAsync(context, args, services); | |||
} | |||
catch (Exception ex) | |||
{ | |||
var result = ExecuteResult.FromError(ex); | |||
await InvokeModuleEvent(context, result).ConfigureAwait(false); | |||
return result; | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||
} | |||
} | |||
@@ -56,48 +56,67 @@ namespace Discord.Interactions | |||
{ | |||
try | |||
{ | |||
if (paramList?.Count() < argList?.Count()) | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters"); | |||
var slashCommandParameterInfos = paramList.ToList(); | |||
var args = new object[slashCommandParameterInfos.Count]; | |||
var args = new object[paramList.Count()]; | |||
for (var i = 0; i < paramList.Count(); i++) | |||
for (var i = 0; i < slashCommandParameterInfos.Count; i++) | |||
{ | |||
var parameter = paramList.ElementAt(i); | |||
var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); | |||
if (arg == default) | |||
{ | |||
if (parameter.IsRequired) | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); | |||
else | |||
args[i] = parameter.DefaultValue; | |||
} | |||
else | |||
{ | |||
var typeConverter = parameter.TypeConverter; | |||
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | |||
if (!readResult.IsSuccess) | |||
{ | |||
await InvokeModuleEvent(context, readResult).ConfigureAwait(false); | |||
return readResult; | |||
} | |||
args[i] = readResult.Value; | |||
} | |||
} | |||
var parameter = slashCommandParameterInfos[i]; | |||
var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); | |||
if (!result.IsSuccess) | |||
return await InvokeEventAndReturn(context, result).ConfigureAwait(false); | |||
if (result is not ParseResult parseResult) | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); | |||
args[i] = parseResult.Value; | |||
} | |||
return await RunAsync(context, args, services).ConfigureAwait(false); | |||
} | |||
catch (Exception ex) | |||
{ | |||
return ExecuteResult.FromError(ex); | |||
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); | |||
} | |||
} | |||
private async Task<IResult> ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList, | |||
IServiceProvider services) | |||
{ | |||
if (parameterInfo.IsComplexParameter) | |||
{ | |||
var ctorArgs = new object[parameterInfo.ComplexParameterFields.Count]; | |||
for (var i = 0; i < ctorArgs.Length; i++) | |||
{ | |||
var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); | |||
if (!result.IsSuccess) | |||
return result; | |||
if (result is not ParseResult parseResult) | |||
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); | |||
ctorArgs[i] = parseResult.Value; | |||
} | |||
return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); | |||
} | |||
var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); | |||
if (arg == default) | |||
return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : | |||
ParseResult.FromSuccess(parameterInfo.DefaultValue); | |||
var typeConverter = parameterInfo.TypeConverter; | |||
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); | |||
if (!readResult.IsSuccess) | |||
return readResult; | |||
return ParseResult.FromSuccess(readResult.Value); | |||
} | |||
protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) | |||
=> CommandService._slashCommandExecutedEvent.InvokeAsync(this, context, result); | |||
@@ -193,7 +193,7 @@ namespace Discord.Interactions | |||
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), | |||
[typeof(IConvertible)] = typeof(DefaultValueConverter<>), | |||
[typeof(Enum)] = typeof(EnumConverter<>), | |||
[typeof(Nullable<>)] = typeof(NullableConverter<>), | |||
[typeof(Nullable<>)] = typeof(NullableConverter<>) | |||
}); | |||
_compTypeConverterMap = new TypeMap<ComponentTypeConverter, IComponentInteractionData>(this, new ConcurrentDictionary<Type, ComponentTypeConverter>(), | |||
@@ -209,7 +209,7 @@ namespace Discord.Interactions | |||
[typeof(IChannel)] = typeof(DefaultChannelReader<>), | |||
[typeof(IRole)] = typeof(DefaultRoleReader<>), | |||
[typeof(IUser)] = typeof(DefaultUserReader<>), | |||
[typeof(IMessage)] = typeof(DefaultUserReader<>), | |||
[typeof(IMessage)] = typeof(DefaultMessageReader<>), | |||
[typeof(IConvertible)] = typeof(DefaultValueReader<>), | |||
[typeof(Enum)] = typeof(EnumReader<>) | |||
}); | |||
@@ -311,7 +311,7 @@ namespace Discord.Interactions | |||
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services) | |||
{ | |||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) | |||
throw new ArgumentException("Type parameter must be a type of Slash Module", "T"); | |||
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); | |||
services ??= EmptyServiceProvider.Instance; | |||
@@ -344,7 +344,7 @@ namespace Discord.Interactions | |||
} | |||
/// <summary> | |||
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild. | |||
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild. | |||
/// </summary> | |||
/// <param name="guildId">Id of the target guild.</param> | |||
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
@@ -440,7 +440,7 @@ namespace Discord.Interactions | |||
} | |||
/// <summary> | |||
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild. | |||
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild. | |||
/// </summary> | |||
/// <param name="guild">The target guild.</param> | |||
/// <param name="modules">Modules to be registered to Discord.</param> | |||
@@ -467,7 +467,7 @@ namespace Discord.Interactions | |||
} | |||
/// <summary> | |||
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands. | |||
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands. | |||
/// </summary> | |||
/// <param name="modules">Modules to be registered to Discord.</param> | |||
/// <returns> | |||
@@ -695,7 +695,7 @@ namespace Discord.Interactions | |||
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) | |||
{ | |||
var interaction = context.Interaction; | |||
return interaction switch | |||
{ | |||
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), | |||
@@ -865,7 +865,7 @@ namespace Discord.Interactions | |||
/// Add a concrete type <see cref="TypeReader"/>. | |||
/// </summary> | |||
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</typeparam> | |||
/// <param name="converter">The <see cref="TypeReader"/> instance.</param> | |||
/// <param name="reader">The <see cref="TypeReader"/> instance.</param> | |||
public void AddTypeReader<T>(TypeReader reader) => | |||
AddTypeReader(typeof(T), reader); | |||
@@ -873,7 +873,7 @@ namespace Discord.Interactions | |||
/// Add a concrete type <see cref="TypeReader"/>. | |||
/// </summary> | |||
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeReader"/>.</param> | |||
/// <param name="converter">The <see cref="TypeReader"/> instance.</param> | |||
/// <param name="reader">The <see cref="TypeReader"/> instance.</param> | |||
public void AddTypeReader(Type type, TypeReader reader) => | |||
_typeReaderMap.AddConcrete(type, reader); | |||
@@ -1066,7 +1066,7 @@ namespace Discord.Interactions | |||
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class | |||
{ | |||
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) | |||
throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule"); | |||
throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); | |||
var module = _typedModuleDefs[typeof(TModule)]; | |||
@@ -25,8 +25,8 @@ namespace Discord.Interactions | |||
if (_concretes.TryGetValue(type, out var specific)) | |||
return specific; | |||
else if (_generics.Any(x => x.Key.IsAssignableFrom(type) | |||
|| (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))) | |||
if (_generics.Any(x => x.Key.IsAssignableFrom(type) | |||
|| x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())) | |||
{ | |||
services ??= EmptyServiceProvider.Instance; | |||
@@ -36,10 +36,10 @@ namespace Discord.Interactions | |||
return converter; | |||
} | |||
else if (_concretes.Any(x => x.Value.CanConvertTo(type))) | |||
if (_concretes.Any(x => x.Value.CanConvertTo(type))) | |||
return _concretes.First(x => x.Value.CanConvertTo(type)).Value; | |||
throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", "type"); | |||
throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type)); | |||
} | |||
public void AddConcrete<TTarget>(TConverter converter) => | |||
@@ -63,7 +63,7 @@ namespace Discord.Interactions | |||
var genericArguments = converterType.GetGenericArguments(); | |||
if (genericArguments.Count() > 1) | |||
if (genericArguments.Length > 1) | |||
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); | |||
var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); | |||
@@ -15,10 +15,8 @@ namespace Discord.Interactions | |||
var result = await GetEntity(snowflake, context).ConfigureAwait(false); | |||
if (result is not null) | |||
return TypeConverterResult.FromSuccess(result); | |||
else | |||
return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); | |||
return result is not null ? | |||
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); | |||
} | |||
public override Task<string> SerializeAsync(object obj) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); | |||
@@ -8,10 +8,8 @@ namespace Discord.Interactions | |||
{ | |||
public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||
{ | |||
if (Enum.TryParse<T>(option, out var result)) | |||
return Task.FromResult(TypeConverterResult.FromSuccess(result)); | |||
else | |||
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); | |||
return Task.FromResult(Enum.TryParse<T>(option, out var result) ? | |||
TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); | |||
} | |||
public override Task<string> SerializeAsync(object obj) | |||
@@ -20,8 +20,8 @@ namespace Discord.Interactions | |||
/// <summary> | |||
/// Will be used to read the incoming payload before executing the method body. | |||
/// </summary> | |||
/// <param name="context">Command exexution context.</param> | |||
/// <param name="option">Recieved option payload.</param> | |||
/// <param name="context">Command execution context.</param> | |||
/// <param name="option">Received option payload.</param> | |||
/// <param name="services">Service provider that will be used to initialize the command module.</param> | |||
/// <returns>The result of the read process.</returns> | |||
public abstract Task<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services); | |||
@@ -31,7 +31,7 @@ namespace Discord.Interactions | |||
/// </summary> | |||
/// <param name="obj">Object to be serialized.</param> | |||
/// <returns> | |||
/// A task represting the conversion process. The result of the task contains the conversion result. | |||
/// A task representing the conversion process. The result of the task contains the conversion result. | |||
/// </returns> | |||
public virtual Task<string> SerializeAsync(object obj) => Task.FromResult(obj.ToString()); | |||
} | |||
@@ -7,7 +7,7 @@ namespace Discord.Interactions | |||
{ | |||
internal static class ModalUtils | |||
{ | |||
private static ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | |||
private static readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new(); | |||
public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection(); | |||