New Rest entities: RestApplicationCommand,RestGlobalCommand, RestGuildCommand, RestApplicationCommandOption, RestApplicationCommandChoice, RestApplicationCommandType. Added public methods to the RestClient to fetch/create/edit interactions.pull/1717/head
| @@ -11,19 +11,41 @@ namespace Discord | |||
| /// </summary> | |||
| public class ApplicationCommandProperties | |||
| { | |||
| private string _name { get; set; } | |||
| private string _description { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the name of this command. | |||
| /// </summary> | |||
| public string Name { get; set; } | |||
| public string Name | |||
| { | |||
| get => _name; | |||
| set | |||
| { | |||
| if(value.Length > 32) | |||
| throw new ArgumentException("Name length must be less than or equal to 32"); | |||
| _name = value; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the discription of this command. | |||
| /// </summary> | |||
| public string Description { get; set; } | |||
| public string Description | |||
| { | |||
| get => _description; | |||
| set | |||
| { | |||
| if (value.Length > 100) | |||
| throw new ArgumentException("Description length must be less than or equal to 100"); | |||
| _description = value; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets or sets the options for this command. | |||
| /// </summary> | |||
| public Optional<IEnumerable<IApplicationCommandOption>> Options { get; set; } | |||
| public Optional<List<IApplicationCommandOption>> Options { get; set; } | |||
| } | |||
| } | |||
| @@ -34,16 +34,13 @@ namespace Discord | |||
| /// <summary> | |||
| /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. | |||
| /// </summary> | |||
| IEnumerable<IApplicationCommandOption>? Options { get; } | |||
| IReadOnlyCollection<IApplicationCommandOption> Options { get; } | |||
| /// <summary> | |||
| /// Modifies this command. | |||
| /// Deletes this command | |||
| /// </summary> | |||
| /// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous modification operation. | |||
| /// </returns> | |||
| Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null); | |||
| /// <returns></returns> | |||
| Task DeleteAsync(RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -39,11 +39,11 @@ namespace Discord | |||
| /// <summary> | |||
| /// Choices for string and int types for the user to pick from. | |||
| /// </summary> | |||
| IEnumerable<IApplicationCommandOptionChoice>? Choices { get; } | |||
| IReadOnlyCollection<IApplicationCommandOptionChoice>? Choices { get; } | |||
| /// <summary> | |||
| /// if the option is a subcommand or subcommand group type, this nested options will be the parameters. | |||
| /// </summary> | |||
| IEnumerable<IApplicationCommandOption>? Options { get; } | |||
| IReadOnlyCollection<IApplicationCommandOption>? Options { get; } | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| @@ -8,10 +9,15 @@ namespace Discord.API | |||
| { | |||
| internal class ApplicationCommand | |||
| { | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("application_id")] | |||
| public ulong ApplicationId { get; set; } | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| [JsonProperty("description")] | |||
| public string Description { get; set; } | |||
| public ApplicationCommand[] Options { get; set; } | |||
| [JsonProperty("options")] | |||
| public Optional<ApplicationCommandOption[]> Options { get; set; } | |||
| } | |||
| } | |||
| @@ -201,5 +201,24 @@ namespace Discord.Rest | |||
| } | |||
| }; | |||
| } | |||
| public static async Task<RestGlobalCommand[]> GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options) | |||
| { | |||
| var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); | |||
| if (!response.Any()) | |||
| return null; | |||
| return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); | |||
| } | |||
| public static async Task<RestGuildCommand[]> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) | |||
| { | |||
| var response = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, options).ConfigureAwait(false); | |||
| if (!response.Any()) | |||
| return null; | |||
| return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToArray(); | |||
| } | |||
| } | |||
| } | |||
| @@ -107,6 +107,14 @@ namespace Discord.Rest | |||
| => ClientHelper.GetVoiceRegionAsync(this, id, options); | |||
| public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null) | |||
| => ClientHelper.GetWebhookAsync(this, id, options); | |||
| public Task<RestGlobalCommand> CreateGobalCommand(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| => InteractionHelper.CreateGlobalCommand(this, func, options); | |||
| public Task<RestGuildCommand> CreateGuildCommand(Action<ApplicationCommandProperties> func, ulong guildId, RequestOptions options = null) | |||
| => InteractionHelper.CreateGuildCommand(this, guildId, func, options); | |||
| public Task<RestGlobalCommand[]> GetGlobalApplicationCommands(RequestOptions options = null) | |||
| => ClientHelper.GetGlobalApplicationCommands(this, options); | |||
| public Task<RestGuildCommand[]> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) | |||
| => ClientHelper.GetGuildApplicationCommands(this, guildId, options); | |||
| //IDiscordClient | |||
| /// <inheritdoc /> | |||
| @@ -1,4 +1,5 @@ | |||
| using Discord.API; | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| @@ -12,10 +13,131 @@ namespace Discord.Rest | |||
| internal static async Task<RestUserMessage> SendFollowupAsync(BaseDiscordClient client, API.Rest.CreateWebhookMessageParams args, | |||
| string token, IMessageChannel channel, RequestOptions options = null) | |||
| { | |||
| var model = await client.ApiClient.CreateInteractionFollowupMessage(args, token, options); | |||
| var model = await client.ApiClient.CreateInteractionFollowupMessage(args, token, options).ConfigureAwait(false); | |||
| var entity = RestUserMessage.Create(client, channel, client.CurrentUser, model); | |||
| return entity; | |||
| } | |||
| // Global commands | |||
| internal static async Task<RestGlobalCommand> CreateGlobalCommand(BaseDiscordClient client, | |||
| Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| { | |||
| var args = new ApplicationCommandProperties(); | |||
| func(args); | |||
| if (args.Options.IsSpecified) | |||
| { | |||
| if (args.Options.Value.Count > 10) | |||
| throw new ArgumentException("Option count must be 10 or less"); | |||
| } | |||
| var model = new ApplicationCommandParams() | |||
| { | |||
| Name = args.Name, | |||
| Description = args.Description, | |||
| Options = args.Options.IsSpecified | |||
| ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() | |||
| : Optional<ApplicationCommandOption[]>.Unspecified | |||
| }; | |||
| var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); | |||
| return RestGlobalCommand.Create(client, cmd); | |||
| } | |||
| internal static async Task<RestGlobalCommand> ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, | |||
| Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| { | |||
| ApplicationCommandProperties args = new ApplicationCommandProperties(); | |||
| func(args); | |||
| if (args.Options.IsSpecified) | |||
| { | |||
| if (args.Options.Value.Count > 10) | |||
| throw new ArgumentException("Option count must be 10 or less"); | |||
| } | |||
| var model = new Discord.API.Rest.ApplicationCommandParams() | |||
| { | |||
| Name = args.Name, | |||
| Description = args.Description, | |||
| Options = args.Options.IsSpecified | |||
| ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() | |||
| : Optional<ApplicationCommandOption[]>.Unspecified | |||
| }; | |||
| var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); | |||
| command.Update(msg); | |||
| return command; | |||
| } | |||
| internal static async Task DeleteGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotNull(command, nameof(command)); | |||
| Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); | |||
| await client.ApiClient.DeleteGlobalApplicationCommandAsync(command.Id, options).ConfigureAwait(false); | |||
| } | |||
| // Guild Commands | |||
| internal static async Task<RestGuildCommand> CreateGuildCommand(BaseDiscordClient client, ulong guildId, | |||
| Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| { | |||
| var args = new ApplicationCommandProperties(); | |||
| func(args); | |||
| if (args.Options.IsSpecified) | |||
| { | |||
| if (args.Options.Value.Count > 10) | |||
| throw new ArgumentException("Option count must be 10 or less"); | |||
| } | |||
| var model = new ApplicationCommandParams() | |||
| { | |||
| Name = args.Name, | |||
| Description = args.Description, | |||
| Options = args.Options.IsSpecified | |||
| ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() | |||
| : Optional<ApplicationCommandOption[]>.Unspecified | |||
| }; | |||
| var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); | |||
| return RestGuildCommand.Create(client, cmd, guildId); | |||
| } | |||
| internal static async Task<RestGuildCommand> ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command, | |||
| Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| { | |||
| ApplicationCommandProperties args = new ApplicationCommandProperties(); | |||
| func(args); | |||
| if (args.Options.IsSpecified) | |||
| { | |||
| if (args.Options.Value.Count > 10) | |||
| throw new ArgumentException("Option count must be 10 or less"); | |||
| } | |||
| var model = new Discord.API.Rest.ApplicationCommandParams() | |||
| { | |||
| Name = args.Name, | |||
| Description = args.Description, | |||
| Options = args.Options.IsSpecified | |||
| ? args.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() | |||
| : Optional<ApplicationCommandOption[]>.Unspecified | |||
| }; | |||
| var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.Id, command.GuildId, options).ConfigureAwait(false); | |||
| command.Update(msg); | |||
| return command; | |||
| } | |||
| internal static async Task DeleteGuildCommand(BaseDiscordClient client, RestGuildCommand command, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotNull(command, nameof(command)); | |||
| Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); | |||
| await client.ApiClient.DeleteGuildApplicationCommandAsync(command.Id, command.GuildId, options).ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ApplicationCommand; | |||
| namespace Discord.Rest | |||
| { | |||
| /// <summary> | |||
| /// Represents a rest implementation of the <see cref="IApplicationCommand"/> | |||
| /// </summary> | |||
| public abstract class RestApplicationCommand : RestEntity<ulong>, IApplicationCommand | |||
| { | |||
| public ulong ApplicationId { get; private set; } | |||
| public string Name { get; private set; } | |||
| public string Description { get; private set; } | |||
| public IReadOnlyCollection<IApplicationCommandOption> Options { get; private set; } | |||
| public RestApplicationCommandType CommandType { get; private set; } | |||
| public DateTimeOffset CreatedAt | |||
| => SnowflakeUtils.FromSnowflake(this.Id); | |||
| internal RestApplicationCommand(BaseDiscordClient client, ulong id) | |||
| : base(client, id) | |||
| { | |||
| } | |||
| internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, RestApplicationCommandType type, ulong guildId = 0) | |||
| { | |||
| if (type == RestApplicationCommandType.GlobalCommand) | |||
| return RestGlobalCommand.Create(client, model); | |||
| if (type == RestApplicationCommandType.GuildCommand) | |||
| return RestGuildCommand.Create(client, model, guildId); | |||
| return null; | |||
| } | |||
| internal virtual void Update(Model model) | |||
| { | |||
| this.ApplicationId = model.ApplicationId; | |||
| this.Name = model.Name; | |||
| this.Options = model.Options.IsSpecified | |||
| ? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray() | |||
| : null; | |||
| } | |||
| public virtual Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ApplicationCommandOptionChoice; | |||
| namespace Discord.Rest | |||
| { | |||
| public class RestApplicationCommandChoice : IApplicationCommandOptionChoice | |||
| { | |||
| public string Name { get; } | |||
| public string Value { get; } | |||
| internal RestApplicationCommandChoice(Model model) | |||
| { | |||
| this.Name = model.Name; | |||
| this.Value = model.Value; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ApplicationCommandOption; | |||
| namespace Discord.Rest | |||
| { | |||
| public class RestApplicationCommandOption : IApplicationCommandOption | |||
| { | |||
| public ApplicationCommandOptionType Type { get; private set; } | |||
| public string Name { get; private set; } | |||
| public string Description { get; private set; } | |||
| public bool? Default { get; private set; } | |||
| public bool? Required { get; private set; } | |||
| public IReadOnlyCollection<IApplicationCommandOptionChoice> Choices { get; private set; } | |||
| public IReadOnlyCollection<IApplicationCommandOption> Options { get; private set; } | |||
| internal RestApplicationCommandOption() { } | |||
| internal static RestApplicationCommandOption Create(Model model) | |||
| { | |||
| var options = new RestApplicationCommandOption(); | |||
| options.Update(model); | |||
| return options; | |||
| } | |||
| internal void Update(Model model) | |||
| { | |||
| this.Type = model.Type; | |||
| this.Name = model.Name; | |||
| this.Description = model.Description; | |||
| if (model.Default.IsSpecified) | |||
| this.Default = model.Default.Value; | |||
| if (model.Required.IsSpecified) | |||
| this.Required = model.Required.Value; | |||
| this.Options = model.Options.IsSpecified | |||
| ? model.Options.Value.Select(x => Create(x)).ToImmutableArray() | |||
| : null; | |||
| this.Choices = model.Choices.IsSpecified | |||
| ? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray() | |||
| : null; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Rest | |||
| { | |||
| public enum RestApplicationCommandType | |||
| { | |||
| GlobalCommand, | |||
| GuildCommand | |||
| } | |||
| } | |||
| @@ -0,0 +1,41 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ApplicationCommand; | |||
| namespace Discord.Rest | |||
| { | |||
| /// <summary> | |||
| /// Represents a global Slash command | |||
| /// </summary> | |||
| public class RestGlobalCommand : RestApplicationCommand | |||
| { | |||
| internal RestGlobalCommand(BaseDiscordClient client, ulong id) | |||
| : base(client, id) | |||
| { | |||
| } | |||
| internal static RestGlobalCommand Create(BaseDiscordClient client, Model model) | |||
| { | |||
| var entity = new RestGlobalCommand(client, model.Id); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| public override async Task DeleteAsync(RequestOptions options = null) | |||
| => await InteractionHelper.DeleteGlobalCommand(Discord, this).ConfigureAwait(false); | |||
| /// <summary> | |||
| /// Modifies this <see cref="RestApplicationCommand"/>. | |||
| /// </summary> | |||
| /// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// The modified command | |||
| /// </returns> | |||
| public async Task<RestGlobalCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| => await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ApplicationCommand; | |||
| namespace Discord.Rest | |||
| { | |||
| public class RestGuildCommand : RestApplicationCommand | |||
| { | |||
| public ulong GuildId { get; set; } | |||
| internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId) | |||
| : base(client, id) | |||
| { | |||
| this.GuildId = guildId; | |||
| } | |||
| internal static RestGuildCommand Create(BaseDiscordClient client, Model model, ulong guildId) | |||
| { | |||
| var entity = new RestGuildCommand(client, model.Id, guildId); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| public override async Task DeleteAsync(RequestOptions options = null) | |||
| => await InteractionHelper.DeleteGuildCommand(Discord, this).ConfigureAwait(false); | |||
| /// <summary> | |||
| /// Modifies this <see cref="RestApplicationCommand"/>. | |||
| /// </summary> | |||
| /// <param name="func">The delegate containing the properties to modify the command with.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// The modified command | |||
| /// </returns> | |||
| public async Task<RestGuildCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null) | |||
| => await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false); | |||
| } | |||
| } | |||
| @@ -110,20 +110,20 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <para> | |||
| /// Discord interactions will not go thru in chat until the client responds to them. With this option set to | |||
| /// <see langword="true"/> the client will automatically acknowledge the interaction with <see cref="InteractionResponseType.ACKWithSource"/>. | |||
| /// see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-interactionresponsetype">the docs</see> on | |||
| /// responding to interactions for more info | |||
| /// Discord interactions will not appear in chat until the client responds to them. With this option set to | |||
| /// <see langword="true"/>, the client will automatically acknowledge the interaction with <see cref="InteractionResponseType.ACKWithSource"/>. | |||
| /// See <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-interactionresponsetype">the docs</see> on | |||
| /// responding to interactions for more info. | |||
| /// </para> | |||
| /// <para> | |||
| /// With this option set to <see langword="false"/> you will have to acknowledge the interaction with | |||
| /// <see cref="SocketInteraction.RespondAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/>, | |||
| /// only after the interaction is captured the origional slash command message will be visible. | |||
| /// With this option set to <see langword="false"/>, you will have to acknowledge the interaction with | |||
| /// <see cref="SocketInteraction.RespondAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/>. | |||
| /// Only after the interaction is acknowledged, the origional slash command message will be visible. | |||
| /// </para> | |||
| /// <note> | |||
| /// Please note that manually acknowledging the interaction with a message reply will not provide any return data. | |||
| /// By autmatically acknowledging the interaction without sending the message will allow for follow up responses to | |||
| /// be used, follow up responses return the message data sent. | |||
| /// Automatically acknowledging the interaction without sending the message will allow for follow up responses to | |||
| /// be used; follow up responses return the message data sent. | |||
| /// </note> | |||
| /// </remarks> | |||
| public bool AlwaysAcknowledgeInteractions { get; set; } = true; | |||
| @@ -95,10 +95,10 @@ namespace Discord.WebSocket | |||
| } | |||
| /// <summary> | |||
| /// Responds to an Interaction, eating its input | |||
| /// Responds to an Interaction. | |||
| /// <para> | |||
| /// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, this method | |||
| /// will be obsolete and will use <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> | |||
| /// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use | |||
| /// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead. | |||
| /// </para> | |||
| /// </summary> | |||
| /// <param name="text">The text of the message to be sent</param> | |||
| @@ -111,10 +111,11 @@ namespace Discord.WebSocket | |||
| /// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null; | |||
| /// </returns> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid</exception> | |||
| public async Task<IMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, AllowedMentions allowedMentions = null, RequestOptions options = null) | |||
| { | |||
| if (Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.Pong) | |||
| if (Type == InteractionResponseType.Pong) | |||
| throw new InvalidOperationException($"Cannot use {Type} on a send message function"); | |||
| if (!IsValidToken) | |||