@@ -0,0 +1,72 @@ | |||||
using Discord; | |||||
using Discord.Commands; | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using System; | |||||
using System.Reflection; | |||||
using System.Threading.Tasks; | |||||
namespace SlashCommandsExample | |||||
{ | |||||
class DiscordClient | |||||
{ | |||||
public static DiscordSocketClient socketClient { get; set; } = new DiscordSocketClient(); | |||||
public static SlashCommandService _commands { get; set; } | |||||
public static IServiceProvider _services { get; set; } | |||||
private string botToken = "<YOUR TOKEN HERE>"; | |||||
public DiscordClient() | |||||
{ | |||||
_commands = new SlashCommandService(); | |||||
_services = new ServiceCollection() | |||||
.AddSingleton(socketClient) | |||||
.AddSingleton(_commands) | |||||
.BuildServiceProvider(); | |||||
socketClient.Log += SocketClient_Log; | |||||
_commands.Log += SocketClient_Log; | |||||
socketClient.InteractionCreated += InteractionHandler; | |||||
// This is for dev purposes. | |||||
// To avoid the situation in which you accidentally push your bot token to upstream, you can use | |||||
// EnviromentVariables to store your key. | |||||
botToken = Environment.GetEnvironmentVariable("DiscordSlashCommandsBotToken", EnvironmentVariableTarget.User); | |||||
// Uncomment the next line of code to set the environment variable. | |||||
// ------------------------------------------------------------------ | |||||
// | WARNING! | | |||||
// | | | |||||
// | MAKE SURE TO DELETE YOUR TOKEN AFTER YOU HAVE SET THE VARIABLE | | |||||
// | | | |||||
// ------------------------------------------------------------------ | |||||
//Environment.SetEnvironmentVariable("DiscordSlashCommandsBotToken", | |||||
// "[YOUR TOKEN GOES HERE DELETE & COMMENT AFTER USE]", | |||||
// EnvironmentVariableTarget.User); | |||||
} | |||||
public async Task RunAsync() | |||||
{ | |||||
await socketClient.LoginAsync(TokenType.Bot, botToken); | |||||
await socketClient.StartAsync(); | |||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||||
await Task.Delay(-1); | |||||
} | |||||
private async Task InteractionHandler(SocketInteraction arg) | |||||
{ | |||||
if(arg.Type == InteractionType.ApplicationCommand) | |||||
{ | |||||
await _commands.ExecuteAsync(arg); | |||||
} | |||||
} | |||||
private Task SocketClient_Log(LogMessage arg) | |||||
{ | |||||
Console.WriteLine("[Discord] " + arg.ToString()); | |||||
return Task.CompletedTask; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,20 @@ | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace SlashCommandsExample.Modules | |||||
{ | |||||
// Doesn't inherit from SlashCommandModule | |||||
public class InvalidDefinition : Object | |||||
{ | |||||
// commands | |||||
} | |||||
// Isn't public | |||||
class PrivateDefinition : SlashCommandModule<SocketInteraction> | |||||
{ | |||||
// commands | |||||
} | |||||
} |
@@ -0,0 +1,33 @@ | |||||
using Discord.Commands; | |||||
using Discord.Commands.SlashCommands.Types; | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace SlashCommandsExample.Modules | |||||
{ | |||||
public class PingCommand : SlashCommandModule<SocketInteraction> | |||||
{ | |||||
[SlashCommand("johnny-test", "Ping the bot to see if it is alive!")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await Interaction.FollowupAsync(":white_check_mark: **Bot Online**"); | |||||
} | |||||
} | |||||
} | |||||
/* | |||||
The base way of defining a command using the regular command service: | |||||
public class PingModule : ModuleBase<SocketCommandContext> | |||||
{ | |||||
[Command("ping")] | |||||
[Summary("Pong! Check if the bot is alive.")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await ReplyAsync(":white_check_mark: **Bot Online**"); | |||||
} | |||||
} | |||||
*/ |
@@ -4,14 +4,23 @@ | |||||
* this project should be re-made into one that could be used as an example usage of the new Slash Command Service. | * this project should be re-made into one that could be used as an example usage of the new Slash Command Service. | ||||
*/ | */ | ||||
using System; | using System; | ||||
using System.Threading.Tasks; | |||||
using Discord; | |||||
using Discord.Commands; | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
namespace SlashCommandsExample | namespace SlashCommandsExample | ||||
{ | { | ||||
class Program | class Program | ||||
{ | { | ||||
static void Main(string[] args) | |||||
static void Main(string[] args) | |||||
{ | { | ||||
Console.WriteLine("Hello World!"); | Console.WriteLine("Hello World!"); | ||||
} | |||||
} | |||||
DiscordClient discordClient = new DiscordClient(); | |||||
// This could instead be handled in another thread, if for whatever reason you want to continue execution in the main Thread. | |||||
discordClient.RunAsync().GetAwaiter().GetResult(); | |||||
} | |||||
} | |||||
} | } |
@@ -5,4 +5,18 @@ | |||||
<TargetFramework>netcoreapp3.1</TargetFramework> | <TargetFramework>netcoreapp3.1</TargetFramework> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" /> | |||||
<ProjectReference Include="..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -10,6 +10,7 @@ | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | ||||
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -15,15 +15,21 @@ namespace Discord.SlashCommands | |||||
/// <summary> | /// <summary> | ||||
/// The name of this slash command. | /// The name of this slash command. | ||||
/// </summary> | /// </summary> | ||||
public string CommandName; | |||||
public string commandName; | |||||
/// <summary> | |||||
/// The description of this slash command. | |||||
/// </summary> | |||||
public string description; | |||||
/// <summary> | /// <summary> | ||||
/// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | /// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="CommandName">The name of this slash command.</param> | |||||
public SlashCommand(string CommandName) | |||||
/// <param name="commandName">The name of this slash command.</param> | |||||
public SlashCommand(string commandName, string description = "No description.") | |||||
{ | { | ||||
this.CommandName = CommandName; | |||||
this.commandName = commandName; | |||||
this.description = description; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -0,0 +1,64 @@ | |||||
using Discord.Commands; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public class SlashCommandInfo | |||||
{ | |||||
/// <summary> | |||||
/// Gets the module that the command belongs in. | |||||
/// </summary> | |||||
public SlashModuleInfo Module { get; } | |||||
/// <summary> | |||||
/// Gets the name of the command. | |||||
/// </summary> | |||||
public string Name { get; } | |||||
/// <summary> | |||||
/// Gets the name of the command. | |||||
/// </summary> | |||||
public string Description { get; } | |||||
/// <summary> | |||||
/// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters | |||||
/// </summary> | |||||
public Delegate userMethod; | |||||
/// <summary> | |||||
/// The callback that we call to start the delegate. | |||||
/// </summary> | |||||
public Func<object[], Task<IResult>> callback; | |||||
public SlashCommandInfo(SlashModuleInfo module, string name, string description, Delegate userMethod) | |||||
{ | |||||
Module = module; | |||||
Name = name; | |||||
Description = description; | |||||
this.userMethod = userMethod; | |||||
this.callback = new Func<object[], Task<IResult>>(async (args) => | |||||
{ | |||||
// Try-catch it and see what we get - error or success | |||||
try | |||||
{ | |||||
await Task.Run(() => | |||||
{ | |||||
userMethod.DynamicInvoke(args); | |||||
}).ConfigureAwait(false); | |||||
} | |||||
catch(Exception e) | |||||
{ | |||||
return ExecuteResult.FromError(e); | |||||
} | |||||
return ExecuteResult.FromSuccess(); | |||||
}); | |||||
} | |||||
public async Task<IResult> ExecuteAsync(object[] args) | |||||
{ | |||||
return await callback.Invoke(args).ConfigureAwait(false); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,47 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public class SlashModuleInfo | |||||
{ | |||||
public SlashModuleInfo(SlashCommandService service) | |||||
{ | |||||
Service = service; | |||||
} | |||||
/// <summary> | |||||
/// Gets the command service associated with this module. | |||||
/// </summary> | |||||
public SlashCommandService Service { get; } | |||||
/// <summary> | |||||
/// Gets a read-only list of commands associated with this module. | |||||
/// </summary> | |||||
public List<SlashCommandInfo> Commands { get; private set; } | |||||
/// <summary> | |||||
/// The user command module defined as the interface ISlashCommandModule | |||||
/// Used to set context. | |||||
/// </summary> | |||||
public ISlashCommandModule userCommandModule; | |||||
public void SetCommands(List<SlashCommandInfo> commands) | |||||
{ | |||||
if (this.Commands == null) | |||||
{ | |||||
this.Commands = commands; | |||||
} | |||||
} | |||||
public void SetCommandModule(object userCommandModule) | |||||
{ | |||||
if (this.userCommandModule == null) | |||||
{ | |||||
this.userCommandModule = userCommandModule as ISlashCommandModule; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,19 +1,37 @@ | |||||
using Discord.Commands; | using Discord.Commands; | ||||
using Discord.Logging; | |||||
using Discord.WebSocket; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Reflection; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.SlashCommands | namespace Discord.SlashCommands | ||||
{ | { | ||||
public class SlashCommandService | public class SlashCommandService | ||||
{ | { | ||||
private List<SlashCommandModule> _modules; | |||||
// This semaphore is used to prevent race conditions. | |||||
private readonly SemaphoreSlim _moduleLock; | |||||
// This contains a dictionary of all definde SlashCommands, based on it's name | |||||
public Dictionary<string, SlashCommandInfo> commandDefs; | |||||
// This contains a list of all slash command modules defined by their user in their assembly. | |||||
public Dictionary<Type, SlashModuleInfo> moduleDefs; | |||||
// This is such a complicated method to log stuff... | |||||
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||||
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||||
internal Logger _logger; | |||||
internal LogManager _logManager; | |||||
public SlashCommandService() // TODO: possible config? | public SlashCommandService() // TODO: possible config? | ||||
{ | { | ||||
// max one thread | |||||
_moduleLock = new SemaphoreSlim(1, 1); | |||||
_logManager = new LogManager(LogSeverity.Info); | |||||
_logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||||
_logger = new Logger(_logManager, "SlshCommand"); | |||||
} | } | ||||
public void AddAssembly() | public void AddAssembly() | ||||
@@ -21,10 +39,50 @@ namespace Discord.SlashCommands | |||||
} | } | ||||
public async Task<IResult> ExecuteAsync() | |||||
/// <summary> | |||||
/// Execute a slash command. | |||||
/// </summary> | |||||
/// <param name="interaction">Interaction data recieved from discord.</param> | |||||
/// <returns></returns> | |||||
public async Task<IResult> ExecuteAsync(SocketInteraction interaction) | |||||
{ | |||||
// First, get the info about this command, if it exists | |||||
SlashCommandInfo commandInfo; | |||||
if (commandDefs.TryGetValue(interaction.Data.Name, out commandInfo)) | |||||
{ | |||||
// TODO: implement everything that has to do with parameters :) | |||||
// Then, set the context in which the command will be executed | |||||
commandInfo.Module.userCommandModule.SetContext(interaction); | |||||
// Then run the command (with no parameters) | |||||
return await commandInfo.ExecuteAsync(new object[] { }).ConfigureAwait(false); | |||||
} | |||||
else | |||||
{ | |||||
return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); | |||||
} | |||||
} | |||||
public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | |||||
{ | { | ||||
// TODO: handle execution | |||||
return null; | |||||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
// Get all of the modules that were properly defined by the user. | |||||
IReadOnlyList<TypeInfo> types = await SlashCommandServiceHelper.GetValidModuleClasses(assembly, this).ConfigureAwait(false); | |||||
// Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | |||||
moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | |||||
// After that, internally register all of the commands into SlashCommandInfo | |||||
commandDefs = await SlashCommandServiceHelper.PrepareAsync(types,moduleDefs,this).ConfigureAwait(false); | |||||
// TODO: And finally, register the commands with discord. | |||||
await SlashCommandServiceHelper.RegisterCommands(commandDefs, this, services).ConfigureAwait(false); | |||||
} | |||||
finally | |||||
{ | |||||
_moduleLock.Release(); | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -0,0 +1,151 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Linq.Expressions; | |||||
using System.Reflection; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
internal static class SlashCommandServiceHelper | |||||
{ | |||||
/// <summary> | |||||
/// Get all of the valid user-defined slash command modules | |||||
/// </summary> | |||||
public static async Task<IReadOnlyList<TypeInfo>> GetValidModuleClasses(Assembly assembly, SlashCommandService service) | |||||
{ | |||||
var result = new List<TypeInfo>(); | |||||
foreach (TypeInfo typeInfo in assembly.DefinedTypes) | |||||
{ | |||||
if (IsValidModuleDefinition(typeInfo)) | |||||
{ | |||||
// To simplify our lives, we need the modules to be public. | |||||
if (typeInfo.IsPublic || typeInfo.IsNestedPublic) | |||||
{ | |||||
result.Add(typeInfo); | |||||
} | |||||
else | |||||
{ | |||||
await service._logger.WarningAsync($"Found class {typeInfo.FullName} as a valid SlashCommand Module, but it's not public!"); | |||||
} | |||||
} | |||||
} | |||||
return result; | |||||
} | |||||
private static bool IsValidModuleDefinition(TypeInfo typeInfo) | |||||
{ | |||||
// See if the base type (SlashCommandInfo<T>) implements interface ISlashCommandModule | |||||
return typeInfo.BaseType.GetInterfaces() | |||||
.Any(n => n == typeof(ISlashCommandModule)); | |||||
} | |||||
/// <summary> | |||||
/// Create an instance of each user-defined module | |||||
/// </summary> | |||||
public static async Task<Dictionary<Type, SlashModuleInfo>> InstantiateModules(IReadOnlyList<TypeInfo> types, SlashCommandService slashCommandService) | |||||
{ | |||||
var result = new Dictionary<Type, SlashModuleInfo>(); | |||||
foreach (Type userModuleType in types) | |||||
{ | |||||
SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); | |||||
// If they want a constructor with different parameters, this is the place to add them. | |||||
object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); | |||||
moduleInfo.SetCommandModule(instance); | |||||
result.Add(userModuleType, moduleInfo); | |||||
} | |||||
return result; | |||||
} | |||||
/// <summary> | |||||
/// Prepare all of the commands and register them internally. | |||||
/// </summary> | |||||
public static async Task<Dictionary<string, SlashCommandInfo>> PrepareAsync(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||||
{ | |||||
var result = new Dictionary<string, SlashCommandInfo>(); | |||||
// fore each user-defined module | |||||
foreach (var userModule in types) | |||||
{ | |||||
// Get its associated information | |||||
SlashModuleInfo moduleInfo; | |||||
if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | |||||
{ | |||||
// and get all of its method, and check if they are valid, and if so create a new SlashCommandInfo for them. | |||||
var commandMethods = userModule.GetMethods(); | |||||
List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||||
foreach (var commandMethod in commandMethods) | |||||
{ | |||||
SlashCommand slashCommand; | |||||
if (IsValidSlashCommand(commandMethod, out slashCommand)) | |||||
{ | |||||
Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | |||||
SlashCommandInfo commandInfo = new SlashCommandInfo( | |||||
module: moduleInfo, | |||||
name: slashCommand.commandName, | |||||
description: slashCommand.description, | |||||
userMethod: delegateMethod | |||||
); | |||||
result.Add(slashCommand.commandName, commandInfo); | |||||
commandInfos.Add(commandInfo); | |||||
} | |||||
} | |||||
moduleInfo.SetCommands(commandInfos); | |||||
} | |||||
} | |||||
return result; | |||||
} | |||||
private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | |||||
{ | |||||
// Verify that we only have one [SlashCommand(...)] attribute | |||||
IEnumerable<Attribute> slashCommandAttributes = method.GetCustomAttributes(typeof(SlashCommand)); | |||||
if (slashCommandAttributes.Count() > 1) | |||||
{ | |||||
throw new Exception("Too many SlashCommand attributes on a single method. It can only contain one!"); | |||||
} | |||||
// And at least one | |||||
if (slashCommandAttributes.Count() == 0) | |||||
{ | |||||
slashCommand = null; | |||||
return false; | |||||
} | |||||
// And return the first (and only) attribute | |||||
slashCommand = slashCommandAttributes.First() as SlashCommand; | |||||
return true; | |||||
} | |||||
/// <summary> | |||||
/// Creae a delegate from methodInfo. Taken from | |||||
/// https://stackoverflow.com/a/40579063/8455128 | |||||
/// </summary> | |||||
public static Delegate CreateDelegate(MethodInfo methodInfo, object target) | |||||
{ | |||||
Func<Type[], Type> getType; | |||||
var isAction = methodInfo.ReturnType.Equals((typeof(void))); | |||||
var types = methodInfo.GetParameters().Select(p => p.ParameterType); | |||||
if (isAction) | |||||
{ | |||||
getType = Expression.GetActionType; | |||||
} | |||||
else | |||||
{ | |||||
getType = Expression.GetFuncType; | |||||
types = types.Concat(new[] { methodInfo.ReturnType }); | |||||
} | |||||
if (methodInfo.IsStatic) | |||||
{ | |||||
return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo); | |||||
} | |||||
return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); | |||||
} | |||||
public static async Task RegisterCommands(Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, IServiceProvider services) | |||||
{ | |||||
return; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
using Discord.Commands.Builders; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public interface ISlashCommandModule | |||||
{ | |||||
void SetContext(IDiscordInteraction interaction); | |||||
//void BeforeExecute(CommandInfo command); | |||||
//void AfterExecute(CommandInfo command); | |||||
//void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); | |||||
} | |||||
} |
@@ -1,14 +1,21 @@ | |||||
using Discord.Commands.SlashCommands.Types; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | namespace Discord.SlashCommands | ||||
{ | { | ||||
internal class SlashCommandModule | |||||
public class SlashCommandModule<T> : ISlashCommandModule where T : class, IDiscordInteraction | |||||
{ | { | ||||
/// <summary> | |||||
/// The underlying interaction of the command. | |||||
/// </summary> | |||||
/// <seealso cref="T:Discord.IDiscordInteraction"/> | |||||
/// <seealso cref="T:Discord.WebSocket.SocketInteraction" /> | |||||
public T Interaction { get; private set; } | |||||
void ISlashCommandModule.SetContext(IDiscordInteraction interaction) | |||||
{ | |||||
var newValue = interaction as T; | |||||
Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | |||||
} | |||||
} | } | ||||
} | } |