diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs index c0ced154a..ed86b0b4a 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -17,5 +17,8 @@ namespace Discord.API [JsonProperty("resolved")] public Optional Resolved { get; set; } + [JsonProperty("type")] + public Optional Type { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs index 46eca6b71..5b4b83e23 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs @@ -16,5 +16,7 @@ namespace Discord.API [JsonProperty("roles")] public Optional> Roles { get; set; } + [JsonProperty("messages")] + public Optional> Messages { get; set; } } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml index bec25d285..c2af7b358 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml @@ -3599,6 +3599,70 @@ + + + Represents a Websocket-based slash command received over the gateway. + + + + + The data associated with this interaction. + + + + + + + + + + + Acknowledges this interaction with the . + + + A task that represents the asynchronous operation of acknowledging the interaction. + + + + + Represents the data tied with the interaction. + + + + + + + + Represents a Websocket-based slash command received over the gateway. + + + + + The data associated with this interaction. + + + + + + + + + + + Acknowledges this interaction with the . + + + A task that represents the asynchronous operation of acknowledging the interaction. + + + + + Represents the data tied with the interaction. + + + + + Represents a Websocket-based interaction type for Message Components. diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommand.cs new file mode 100644 index 000000000..0a483b2bf --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommand.cs @@ -0,0 +1,165 @@ +using Discord.Rest; +using System; +using System.Linq; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketApplicationMessageCommand : SocketSlashCommand + { + /// + /// The data associated with this interaction. + /// + new public SocketApplicationMessageCommandData Data { get; } + + internal SocketApplicationMessageCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (this.Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketApplicationMessageCommandData.Create(client, dataModel, model.Id, guildId); + } + + new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketApplicationMessageCommand(client, model, channel); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + var data = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + this.Data.Update(data); + + base.Update(model); + } + + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + + if (Discord.AlwaysAcknowledgeInteractions) + { + await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component); + return; + } + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + TTS = isTTS ? true : Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = 64; + + await InteractionHelper.SendInteractionResponse(this.Discord, response, this.Id, Token, options); + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = 64; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public override Task DeferAsync(RequestOptions options = null) + { + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + }; + + return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommandData.cs new file mode 100644 index 000000000..e548e4cf7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketApplicationMessageCommandData.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketApplicationMessageCommandData : SocketEntity, IApplicationCommandInteractionData + { + /// + public string Name { get; private set; } + public SocketMessage Message { get; private set; } + + internal Dictionary guildMembers { get; private set; } + = new Dictionary(); + internal Dictionary users { get; private set; } + = new Dictionary(); + internal Dictionary channels { get; private set; } + = new Dictionary(); + internal Dictionary roles { get; private set; } + = new Dictionary(); + + IReadOnlyCollection IApplicationCommandInteractionData.Options => throw new System.NotImplementedException(); + + private ulong? guildId; + + private ApplicationCommandType Type; + + internal SocketApplicationMessageCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model.Id) + { + this.guildId = guildId; + + this.Type = (ApplicationCommandType)model.Type; + + if (model.Resolved.IsSpecified) + { + var guild = this.guildId.HasValue ? Discord.GetGuild(this.guildId.Value) : null; + + var resolved = model.Resolved.Value; + + if (resolved.Users.IsSpecified) + { + foreach (var user in resolved.Users.Value) + { + var socketUser = Discord.GetOrCreateUser(this.Discord.State, user.Value); + + this.users.Add(ulong.Parse(user.Key), socketUser); + } + } + + if (resolved.Channels.IsSpecified) + { + foreach (var channel in resolved.Channels.Value) + { + SocketChannel socketChannel = guild != null + ? guild.GetChannel(channel.Value.Id) + : Discord.GetChannel(channel.Value.Id); + + if (socketChannel == null) + { + var channelModel = guild != null + ? Discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() + : Discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, Discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(Discord, Discord.State, channelModel); + } + + Discord.State.AddChannel(socketChannel); + this.channels.Add(ulong.Parse(channel.Key), socketChannel); + } + } + + if (resolved.Members.IsSpecified) + { + foreach (var member in resolved.Members.Value) + { + member.Value.User = resolved.Users.Value[member.Key]; + var user = guild.AddOrUpdateUser(member.Value); + this.guildMembers.Add(ulong.Parse(member.Key), user); + } + } + + if (resolved.Roles.IsSpecified) + { + foreach (var role in resolved.Roles.Value) + { + var socketRole = guild.AddOrUpdateRole(role.Value); + this.roles.Add(ulong.Parse(role.Key), socketRole); + } + } + + if (resolved.Messages.IsSpecified) + { + foreach (var msg in resolved.Messages.Value) + { + var channel = client.GetChannel(msg.Value.ChannelId) as ISocketMessageChannel; + + SocketUser author; + if (guild != null) + { + if (msg.Value.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, client.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); + else + author = guild.GetUser(msg.Value.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id); + + if (channel == null) + { + if (!msg.Value.GuildId.IsSpecified) // assume it is a DM + { + channel = client.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, client.State); + } + } + + this.Message = SocketMessage.Create(client, client.State, author, channel, msg.Value); + } + } + } + } + + internal static SocketApplicationMessageCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketApplicationMessageCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + this.Name = model.Name; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommand.cs new file mode 100644 index 000000000..37bdd40bd --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommand.cs @@ -0,0 +1,165 @@ +using Discord.Rest; +using System; +using System.Linq; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketApplicationUserCommand : SocketSlashCommand + { + /// + /// The data associated with this interaction. + /// + new public SocketApplicationUserCommandData Data { get; } + + internal SocketApplicationUserCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (this.Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketApplicationUserCommandData.Create(client, dataModel, model.Id, guildId); + } + + new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketApplicationUserCommand(client, model, channel); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + var data = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + this.Data.Update(data); + + base.Update(model); + } + + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + + if (Discord.AlwaysAcknowledgeInteractions) + { + await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component); + return; + } + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + TTS = isTTS ? true : Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = 64; + + await InteractionHelper.SendInteractionResponse(this.Discord, response, this.Id, Token, options); + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = 64; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public override Task DeferAsync(RequestOptions options = null) + { + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + }; + + return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommandData.cs new file mode 100644 index 000000000..0c5a540c7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketApplicationUserCommandData.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketApplicationUserCommandData : SocketEntity, IApplicationCommandInteractionData + { + /// + public string Name { get; private set; } + + public SocketUser Member { get; private set; } + + internal Dictionary guildMembers { get; private set; } + = new Dictionary(); + internal Dictionary users { get; private set; } + = new Dictionary(); + internal Dictionary channels { get; private set; } + = new Dictionary(); + internal Dictionary roles { get; private set; } + = new Dictionary(); + + IReadOnlyCollection IApplicationCommandInteractionData.Options => throw new System.NotImplementedException(); + + private ulong? guildId; + + private ApplicationCommandType Type; + + internal SocketApplicationUserCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model.Id) + { + this.guildId = guildId; + + this.Type = (ApplicationCommandType)model.Type; + + if (model.Resolved.IsSpecified) + { + var guild = this.guildId.HasValue ? Discord.GetGuild(this.guildId.Value) : null; + + var resolved = model.Resolved.Value; + + if (resolved.Users.IsSpecified) + { + foreach (var user in resolved.Users.Value) + { + var socketUser = Discord.GetOrCreateUser(this.Discord.State, user.Value); + + this.users.Add(ulong.Parse(user.Key), socketUser); + } + } + + if (resolved.Channels.IsSpecified) + { + foreach (var channel in resolved.Channels.Value) + { + SocketChannel socketChannel = guild != null + ? guild.GetChannel(channel.Value.Id) + : Discord.GetChannel(channel.Value.Id); + + if (socketChannel == null) + { + var channelModel = guild != null + ? Discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() + : Discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, Discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(Discord, Discord.State, channelModel); + } + + Discord.State.AddChannel(socketChannel); + this.channels.Add(ulong.Parse(channel.Key), socketChannel); + } + } + + if (resolved.Members.IsSpecified) + { + foreach (var member in resolved.Members.Value) + { + member.Value.User = resolved.Users.Value[member.Key]; + var user = guild.AddOrUpdateUser(member.Value); + this.guildMembers.Add(ulong.Parse(member.Key), user); + this.Member = user; + } + } + + if (resolved.Roles.IsSpecified) + { + foreach (var role in resolved.Roles.Value) + { + var socketRole = guild.AddOrUpdateRole(role.Value); + this.roles.Add(ulong.Parse(role.Key), socketRole); + } + } + } + } + + internal static SocketApplicationUserCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketApplicationUserCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + this.Name = model.Name; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 4b2e3baec..01b3874f7 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -2,6 +2,7 @@ using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.Interaction; +using DataModel = Discord.API.ApplicationCommandInteractionData; namespace Discord.WebSocket { @@ -61,6 +62,19 @@ namespace Discord.WebSocket internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) { if (model.Type == InteractionType.ApplicationCommand) + if(model.ApplicationId != null) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + if(dataModel != null) + { + if (dataModel.Type.Equals(ApplicationCommandType.User)) + return SocketApplicationUserCommand.Create(client, model, channel); + if (dataModel.Type.Equals(ApplicationCommandType.Message)) + return SocketApplicationMessageCommand.Create(client, model, channel); + } + } return SocketSlashCommand.Create(client, model, channel); if (model.Type == InteractionType.MessageComponent) return SocketMessageComponent.Create(client, model, channel);