From dd9a9a5fc739130d81907f8cd36d85a66dda02fc Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 30 Jul 2021 09:00:46 -0300 Subject: [PATCH] Decouple channel from messages --- .../RequireBotPermissionAttribute.cs | 5 ++- .../Preconditions/RequireContextAttribute.cs | 10 +---- .../Preconditions/RequireNsfwAttribute.cs | 2 +- .../RequireUserPermissionAttribute.cs | 15 +++---- src/Discord.Net.Commands/CommandContext.cs | 11 +++-- src/Discord.Net.Commands/ModuleBase.cs | 4 +- .../Readers/MessageTypeReader.cs | 2 +- .../Readers/UserTypeReader.cs | 12 +++--- .../Commands/ICommandContext.cs | 9 +++- .../Entities/Messages/IMessage.cs | 10 +++-- .../Extensions/MessageExtensions.cs | 5 ++- src/Discord.Net.Core/IDiscordClient.cs | 19 +++++++++ .../BaseSocketClient.Events.Examples.cs | 4 +- src/Discord.Net.Rest/BaseDiscordClient.cs | 4 ++ src/Discord.Net.Rest/DiscordRestClient.cs | 25 +++++++++++ .../Entities/Channels/ChannelHelper.cs | 33 +++++++++++++-- .../Entities/Messages/RestMessage.cs | 19 +++++++-- .../Entities/Messages/RestSystemMessage.cs | 4 +- .../Entities/Messages/RestUserMessage.cs | 8 ++-- src/Discord.Net.WebSocket/BaseSocketClient.cs | 25 ++++++++++- .../Commands/ShardedCommandContext.cs | 2 +- .../Commands/SocketCommandContext.cs | 16 ++++---- .../DiscordShardedClient.cs | 4 +- .../DiscordSocketClient.cs | 30 +++++--------- .../Entities/Messages/SocketMessage.cs | 41 +++++++++---------- .../Entities/Messages/SocketSystemMessage.cs | 4 +- .../Entities/Messages/SocketUserMessage.cs | 29 ++++++++----- 27 files changed, 234 insertions(+), 118 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 5b3b5bd47..042b0330b 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -71,10 +71,11 @@ namespace Discord.Commands if (ChannelPermission.HasValue) { ChannelPermissions perms; - if (context.Channel is IGuildChannel guildChannel) + IMessageChannel channel = await context.Channel.GetOrDownloadAsync().ConfigureAwait(false); + if (channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(context.Channel); + perms = ChannelPermissions.All(channel); if (!perms.Has(ChannelPermission.Value)) return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}."); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index a27469c88..9d68b3aab 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -17,10 +17,6 @@ namespace Discord.Commands /// Specifies the command to be executed within a DM. /// DM = 0x02, - /// - /// Specifies the command to be executed within a group. - /// - Group = 0x04 } /// @@ -59,11 +55,9 @@ namespace Discord.Commands bool isValid = false; if ((Contexts & ContextType.Guild) != 0) - isValid = context.Channel is IGuildChannel; + isValid = context.GuildId != null; if ((Contexts & ContextType.DM) != 0) - isValid = isValid || context.Channel is IDMChannel; - if ((Contexts & ContextType.Group) != 0) - isValid = isValid || context.Channel is IGroupChannel; + isValid = context.GuildId == null; if (isValid) return Task.FromResult(PreconditionResult.FromSuccess()); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index 2a9647cd2..6ee296071 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -36,7 +36,7 @@ namespace Discord.Commands /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { - if (context.Channel is ITextChannel text && text.IsNsfw) + if (context.Channel.HasValue && context.Channel.Value is ITextChannel text && text.IsNsfw) return Task.FromResult(PreconditionResult.FromSuccess()); else return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel.")); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 2908a18c1..447363d87 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -54,31 +54,32 @@ namespace Discord.Commands } /// - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; if (GuildPermission.HasValue) { if (guildUser == null) - return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}.")); + return PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}."); } if (ChannelPermission.HasValue) { ChannelPermissions perms; - if (context.Channel is IGuildChannel guildChannel) + IMessageChannel channel = await context.Channel.GetOrDownloadAsync().ConfigureAwait(false); + if (channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(context.Channel); + perms = ChannelPermissions.All(channel); if (!perms.Has(ChannelPermission.Value)) - return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}.")); + return PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}."); } - return Task.FromResult(PreconditionResult.FromSuccess()); + return PreconditionResult.FromSuccess(); } } } diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 393cdf97a..200d7bc48 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -8,24 +8,27 @@ namespace Discord.Commands /// public IGuild Guild { get; } /// - public IMessageChannel Channel { get; } + public ulong? GuildId { get; } + /// + public Cacheable Channel { get; } /// public IUser User { get; } /// public IUserMessage Message { get; } /// Indicates whether the channel that the command is executed in is a private channel. - public bool IsPrivate => Channel is IPrivateChannel; + public bool IsPrivate => GuildId == null; /// /// Initializes a new class with the provided client and message. /// /// The underlying client. /// The underlying message. - public CommandContext(IDiscordClient client, IUserMessage msg) + public CommandContext(IDiscordClient client, IUserMessage msg, IGuild guild) { Client = client; - Guild = (msg.Channel as IGuildChannel)?.Guild; + GuildId = msg.GuildId; + Guild = guild; Channel = msg.Channel; User = msg.Author; Message = msg; diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 6ec2db54d..1a527298e 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -36,9 +36,9 @@ namespace Discord.Commands /// If null, all mentioned roles and users will be notified. /// /// The message references to be included. Used to reply to specific messages. - protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, RequestOptions options = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions, messageReference).ConfigureAwait(false); + return await Context.Client.SendMessageAsync(Context.Channel.Id, message, isTTS, embed, allowedMentions, messageReference, options).ConfigureAwait(false); } /// /// The method to execute before executing the command. diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index acec2f12d..515901221 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -17,7 +17,7 @@ namespace Discord.Commands //By Id (1.0) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) { - if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) + if (context.Channel.HasValue && await context.Channel.Value.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) return TypeReaderResult.FromSuccess(msg); } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index c0104e341..b1fbd0bdd 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -18,7 +18,9 @@ namespace Discord.Commands public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); - IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better + IAsyncEnumerable channelUsers = context.Channel.HasValue + ? context.Channel.Value.GetUsersAsync(CacheMode.CacheOnly).Flatten() + : AsyncEnumerable.Empty(); IReadOnlyCollection guildUsers = ImmutableArray.Create(); if (context.Guild != null) @@ -29,8 +31,8 @@ namespace Discord.Commands { if (context.Guild != null) AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); - else - AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); + else if (context.Channel.HasValue) + AddResult(results, await context.Channel.Value.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); } //By Id (0.9) @@ -38,8 +40,8 @@ namespace Discord.Commands { if (context.Guild != null) AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); - else - AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); + else if (context.Channel.HasValue) + AddResult(results, await context.Channel.Value.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); } //By Username + Discriminator (0.7-0.85) diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index d56eb38a0..bd85e343f 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -14,9 +14,14 @@ namespace Discord.Commands /// IGuild Guild { get; } /// - /// Gets the that the command is executed in. + /// Gets the guild id that the command is executed in if it was in one. /// - IMessageChannel Channel { get; } + ulong? GuildId { get; } + /// + /// Gets the that the command is executed in if cached; + /// otherwise just the channel id. + /// + Cacheable Channel { get; } /// /// Gets the who executed the command. /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index b5023eb59..85bd90f38 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -66,11 +66,15 @@ namespace Discord /// Time of when the message was last edited; null if the message is never edited. /// DateTimeOffset? EditedTimestamp { get; } - + /// /// Gets the source channel of the message. /// - IMessageChannel Channel { get; } + Cacheable Channel { get; } + /// + /// Gets the source guild id of the message if it was sent in one. + /// + ulong? GuildId { get; } /// /// Gets the author of this message. /// @@ -171,7 +175,7 @@ namespace Discord /// A read-only collection of sticker objects. /// IReadOnlyCollection Stickers { get; } - + /// /// Gets the flags related to this message. /// diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index b043d7b77..6adae7233 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -17,7 +17,7 @@ namespace Discord public static string GetJumpUrl(this IMessage msg) { var channel = msg.Channel; - return $"https://discord.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; + return $"https://discord.com/channels/{(msg.GuildId.HasValue ? $"{msg.GuildId}" : "@me")}/{channel.Id}/{msg.Id}"; } /// @@ -89,7 +89,8 @@ namespace Discord /// public static async Task ReplyAsync(this IUserMessage msg, string text = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, RequestOptions options = null) { - return await msg.Channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, new MessageReference(messageId: msg.Id)).ConfigureAwait(false); + IMessageChannel channel = await msg.Channel.GetOrDownloadAsync().ConfigureAwait(false); + return await channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, new MessageReference(messageId: msg.Id)).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index d7d6d2856..e0dfa96e7 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -284,5 +284,24 @@ namespace Discord /// that represents the gateway information related to the bot. /// Task GetBotGatewayAsync(RequestOptions options = null); + + /// + /// Sends a message to a channel. + /// + /// The channel to send the message. + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The reference for this message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendMessageAsync(ulong channelId, string text = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, RequestOptions options = null); } } diff --git a/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs index 27d393c07..7f7f0a5f1 100644 --- a/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs +++ b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs @@ -84,12 +84,12 @@ namespace Discord.Net.Examples.WebSocket // check if the message is a user message as opposed to a system message (e.g. Clyde, pins, etc.) if (!(message is SocketUserMessage userMessage)) return Task.CompletedTask; // check if the message origin is a guild message channel - if (!(userMessage.Channel is SocketTextChannel textChannel)) return Task.CompletedTask; + if (userMessage.GuildId == null) return Task.CompletedTask; // check if the target user was mentioned var targetUsers = userMessage.MentionedUsers.Where(x => _targetUserIds.Contains(x.Id)); foreach (var targetUser in targetUsers) Console.WriteLine( - $"{targetUser} was mentioned in the message '{message.Content}' by {message.Author} in {textChannel.Name}."); + $"{targetUser} was mentioned in the message '{message.Content}' by {message.Author} in {MentionUtils.MentionChannel(message.Channel.Id)}."); return Task.CompletedTask; } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 68589a4f1..044f3f6b3 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -216,6 +216,10 @@ namespace Discord.Rest Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) => Task.FromResult(null); + /// + Task IDiscordClient.SendMessageAsync(ulong channelId, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, RequestOptions options) + => Task.FromResult(null); + /// Task IDiscordClient.StartAsync() => Task.Delay(0); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b5bdc4235..237132cb5 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -114,11 +114,36 @@ namespace Discord.Rest => MessageHelper.RemoveAllReactionsAsync(channelId, messageId, this, options); public Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) => MessageHelper.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote, this, options); + + /// + /// Sends a message to a channel. + /// + /// The channel to send the message. + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The reference for this message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public Task SendMessageAsync(ulong channelId, string text = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(channelId, this, text, isTTS, embed, allowedMentions, messageReference, options ?? RequestOptions.Default); + //IDiscordClient /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// + async Task IDiscordClient.SendMessageAsync(ulong channelId, string message, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, RequestOptions options) + => await SendMessageAsync(channelId, message, isTTS, embed, allowedMentions, messageReference, options ?? RequestOptions.Default).ConfigureAwait(false); + /// async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 22395ab3a..3b395cf95 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -223,7 +223,34 @@ namespace Discord.Rest var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel() }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); - return RestUserMessage.Create(client, channel, client.CurrentUser, model); + return (RestUserMessage)RestMessage.Create(client, channel, client.CurrentUser, model); + } + /// Message content is too long, length must be less or equal to . + public static async Task SendMessageAsync(ulong channelId, BaseDiscordClient client, + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, RequestOptions options) + { + 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."); + + // 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 args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel() }; + var model = await client.ApiClient.CreateMessageAsync(channelId, args, options).ConfigureAwait(false); + return (RestUserMessage)RestMessage.Create(client, null, client.CurrentUser, model); } /// @@ -283,14 +310,14 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() ?? Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, IsSpoiler = isSpoiler }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); - return RestUserMessage.Create(client, channel, client.CurrentUser, model); + return (RestUserMessage)RestMessage.Create(client, channel, client.CurrentUser, model); } public static async Task ModifyMessageAsync(IMessageChannel channel, ulong messageId, Action func, BaseDiscordClient client, RequestOptions options) { var msgModel = await MessageHelper.ModifyAsync(channel.Id, messageId, client, func, options).ConfigureAwait(false); - return RestUserMessage.Create(client, channel, msgModel.Author.IsSpecified ? RestUser.Create(client, msgModel.Author.Value) : client.CurrentUser, msgModel); + return (RestUserMessage)RestMessage.Create(client, channel, msgModel.Author.IsSpecified ? RestUser.Create(client, msgModel.Author.Value) : client.CurrentUser, msgModel); } public static Task DeleteMessageAsync(IMessageChannel channel, ulong messageId, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 0c54743a6..4cff43463 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -16,7 +16,9 @@ namespace Discord.Rest private ImmutableArray _reactions = ImmutableArray.Create(); /// - public IMessageChannel Channel { get; } + public Cacheable Channel { get; } + /// + public ulong? GuildId { get; private set; } /// /// Gets the Author of the message. /// @@ -74,7 +76,7 @@ namespace Discord.Rest /// public MessageType Type { get; private set; } - internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + internal RestMessage(BaseDiscordClient discord, ulong id, Cacheable channel, IUser author, MessageSource source) : base(discord, id) { Channel = channel; @@ -83,15 +85,24 @@ namespace Discord.Rest } internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { + var cacheableChannel = new Cacheable( + channel, + model.ChannelId, + channel != null, + async () => (IMessageChannel)await ClientHelper.GetChannelAsync(discord, model.ChannelId, RequestOptions.Default).ConfigureAwait(false)); + if (model.Type == MessageType.Default || model.Type == MessageType.Reply) - return RestUserMessage.Create(discord, channel, author, model); + return RestUserMessage.Create(discord, cacheableChannel, author, model); else - return RestSystemMessage.Create(discord, channel, author, model); + return RestSystemMessage.Create(discord, cacheableChannel, author, model); } internal virtual void Update(Model model) { Type = model.Type; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index 1c59d4f45..b5e21109e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -9,11 +9,11 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestSystemMessage : RestMessage, ISystemMessage { - internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + internal RestSystemMessage(BaseDiscordClient discord, ulong id, Cacheable channel, IUser author) : base(discord, id, channel, author, MessageSource.System) { } - internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal static RestSystemMessage Create(BaseDiscordClient discord, Cacheable channel, IUser author, Model model) { var entity = new RestSystemMessage(discord, model.Id, channel, author); entity.Update(model); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index aa6b44da6..629f14fca 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -50,11 +50,11 @@ namespace Discord.Rest /// public IUserMessage ReferencedMessage => _referencedMessage; - internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + internal RestUserMessage(BaseDiscordClient discord, ulong id, Cacheable channel, IUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal static RestUserMessage Create(BaseDiscordClient discord, Cacheable channel, IUser author, Model model) { var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); @@ -120,7 +120,7 @@ namespace Discord.Rest } } - var guildId = (Channel as IGuildChannel)?.GuildId; + ulong? guildId = model.GuildId.IsSpecified ? model.GuildId.Value : null; var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; if (model.Content.IsSpecified) { @@ -177,7 +177,7 @@ namespace Discord.Rest /// This operation may only be called on a channel. public async Task CrosspostAsync(RequestOptions options = null) { - if (!(Channel is INewsChannel)) + if (Channel.HasValue && !(Channel.Value is INewsChannel)) { throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 1cfe6c8bf..6704225c1 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -268,12 +268,35 @@ namespace Discord.WebSocket /// public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); - + /// + /// Sends a message to a channel. + /// + /// The channel to send the message. + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The reference for this message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public Task SendMessageAsync(ulong channelId, string text = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(channelId, this, text, isTTS, embed, allowedMentions, messageReference, options ?? RequestOptions.Default); + // IDiscordClient /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// + async Task IDiscordClient.SendMessageAsync(ulong channelId, string message, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, RequestOptions options) + => await SendMessageAsync(channelId, message, isTTS, embed, allowedMentions, messageReference, options ?? RequestOptions.Default).ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index f970c32fc..6eb8e2639 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -9,7 +9,7 @@ namespace Discord.Commands public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) - : base(client.GetShard(GetShardId(client, (msg.Channel as SocketGuildChannel)?.Guild)), msg) + : base(client.GetShardFor(msg.GuildId ?? 0), msg) { Client = client; } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index f4d517909..440aaa323 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -15,10 +15,10 @@ namespace Discord.Commands /// Gets the that the command is executed in. /// public SocketGuild Guild { get; } - /// - /// Gets the that the command is executed in. - /// - public ISocketMessageChannel Channel { get; } + /// + public ulong? GuildId { get; } + /// + public Cacheable Channel { get; } /// /// Gets the who executed the command. /// @@ -31,7 +31,7 @@ namespace Discord.Commands /// /// Indicates whether the channel that the command is executed in is a private channel. /// - public bool IsPrivate => Channel is IPrivateChannel; + public bool IsPrivate => GuildId == null; /// /// Initializes a new class with the provided client and message. @@ -41,7 +41,9 @@ namespace Discord.Commands public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) { Client = client; - Guild = (msg.Channel as SocketGuildChannel)?.Guild; + GuildId = msg.GuildId; + if (msg.GuildId != null) + Guild = client.GetGuild(msg.GuildId.Value); Channel = msg.Channel; User = msg.Author; Message = msg; @@ -53,7 +55,7 @@ namespace Discord.Commands /// IGuild ICommandContext.Guild => Guild; /// - IMessageChannel ICommandContext.Channel => Channel; + Cacheable ICommandContext.Channel => Channel; /// IUser ICommandContext.User => User; /// diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index e6d05b525..ce5f59016 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -179,11 +179,11 @@ namespace Discord.WebSocket return _shards[id]; return null; } - private int GetShardIdFor(ulong guildId) + public int GetShardIdFor(ulong guildId) => (int)((guildId >> 22) % (uint)_totalShards); public int GetShardIdFor(IGuild guild) => GetShardIdFor(guild?.Id ?? 0); - private DiscordSocketClient GetShardFor(ulong guildId) + public DiscordSocketClient GetShardFor(ulong guildId) => GetShard(GetShardIdFor(guildId)); public DiscordSocketClient GetShardFor(IGuild guild) => GetShardFor(guild?.Id ?? 0); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 2cdff662c..08781484f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1263,24 +1263,16 @@ namespace Discord.WebSocket var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; var guild = (channel as SocketGuildChannel)?.Guild; + if (channel == null && data.GuildId.IsSpecified) + guild = State.GetGuild(data.GuildId.Value); if (guild != null && !guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - if (channel == null) - { - if (!data.GuildId.IsSpecified) // assume it is a DM - { - channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } - } + if (channel == null && !data.GuildId.IsSpecified) // assume it is a DM + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); SocketUser author; if (guild != null) @@ -1291,7 +1283,7 @@ namespace Discord.WebSocket author = guild.GetUser(data.Author.Value.Id); } else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + author = (channel as SocketChannel)?.GetUser(data.Author.Value.Id); if (author == null) { @@ -1309,13 +1301,14 @@ namespace Discord.WebSocket author = groupChannel.GetOrAddUser(data.Author.Value); else { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.Author.Value.Id, data.ChannelId).ConfigureAwait(false); return; } } var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); + if (channel != null) + SocketChannelHelper.AddMessage(channel, this, msg); await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } break; @@ -1327,6 +1320,8 @@ namespace Discord.WebSocket var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; var guild = (channel as SocketGuildChannel)?.Guild; + if (channel == null && data.GuildId.IsSpecified) + guild = State.GetGuild(data.GuildId.Value); if (guild != null && !guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); @@ -1391,11 +1386,6 @@ namespace Discord.WebSocket else channel = CreateDMChannel(data.ChannelId, author, State); } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } } after = SocketMessage.Create(this, State, author, channel, data); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 353c26fb8..b3103d534 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -23,13 +23,10 @@ namespace Discord.WebSocket /// A WebSocket-based user object. /// public SocketUser Author { get; } - /// - /// Gets the source channel of the message. - /// - /// - /// A WebSocket-based message channel. - /// - public ISocketMessageChannel Channel { get; } + /// + public Cacheable Channel { get; } + /// + public ulong? GuildId { get; private set; } /// public MessageSource Source { get; } @@ -85,13 +82,8 @@ namespace Discord.WebSocket /// Collection of WebSocket-based guild channels. /// public virtual IReadOnlyCollection MentionedChannels => ImmutableArray.Create(); - /// - /// Returns the roles mentioned in this message. - /// - /// - /// Collection of WebSocket-based roles. - /// - public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); /// /// Returns the users mentioned in this message. /// @@ -109,24 +101,33 @@ namespace Discord.WebSocket /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + internal SocketMessage(DiscordSocketClient discord, ulong id, Cacheable channel, SocketUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; Source = source; } - internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, IMessageChannel channel, Model model) { + var cacheableChannel = new Cacheable( + channel, + model.ChannelId, + channel != null, + async () => (IMessageChannel)await ClientHelper.GetChannelAsync(discord, model.ChannelId, RequestOptions.Default).ConfigureAwait(false)); + if (model.Type == MessageType.Default || model.Type == MessageType.Reply) - return SocketUserMessage.Create(discord, state, author, channel, model); + return SocketUserMessage.Create(discord, state, author, cacheableChannel, model); else - return SocketSystemMessage.Create(discord, state, author, channel, model); + return SocketSystemMessage.Create(discord, state, author, cacheableChannel, model); } internal virtual void Update(ClientState state, Model model) { Type = model.Type; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; @@ -188,16 +189,12 @@ namespace Discord.WebSocket /// IUser IMessage.Author => Author; /// - IMessageChannel IMessage.Channel => Channel; - /// IReadOnlyCollection IMessage.Attachments => Attachments; /// IReadOnlyCollection IMessage.Embeds => Embeds; /// IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); /// - IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); - /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); /// IReadOnlyCollection IMessage.Stickers => Stickers; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index ec22a7703..41f9578fd 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -9,11 +9,11 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { - internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + internal SocketSystemMessage(DiscordSocketClient discord, ulong id, Cacheable channel, SocketUser author) : base(discord, id, channel, author, MessageSource.System) { } - internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, Cacheable channel, Model model) { var entity = new SocketSystemMessage(discord, model.Id, channel, author); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 597544f4d..f47709725 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -21,7 +21,7 @@ namespace Discord.WebSocket private ImmutableArray _attachments = ImmutableArray.Create(); private ImmutableArray _embeds = ImmutableArray.Create(); private ImmutableArray _tags = ImmutableArray.Create(); - private ImmutableArray _roleMentions = ImmutableArray.Create(); + private ImmutableArray _roleIdMentions = ImmutableArray.Create(); private ImmutableArray _userMentions = ImmutableArray.Create(); private ImmutableArray _stickers = ImmutableArray.Create(); @@ -44,7 +44,7 @@ namespace Discord.WebSocket /// public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); /// - public override IReadOnlyCollection MentionedRoles => _roleMentions; + public override IReadOnlyCollection MentionedRoleIds => _roleIdMentions; /// public override IReadOnlyCollection MentionedUsers => _userMentions; /// @@ -52,11 +52,11 @@ namespace Discord.WebSocket /// public IUserMessage ReferencedMessage => _referencedMessage; - internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + internal SocketUserMessage(DiscordSocketClient discord, ulong id, Cacheable channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, Cacheable channel, Model model) { var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); @@ -67,7 +67,11 @@ namespace Discord.WebSocket { base.Update(state, model); - SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + SocketGuild guild = null; + if (model.GuildId.IsSpecified) + guild = state.GetGuild(model.GuildId.Value); + else if (Channel.HasValue) + guild = (Channel.Value as SocketGuildChannel)?.Guild; if (model.IsTextToSpeech.IsSpecified) _isTTS = model.IsTextToSpeech.Value; @@ -78,7 +82,7 @@ namespace Discord.WebSocket if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; if (model.RoleMentions.IsSpecified) - _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); + _roleIdMentions = model.RoleMentions.Value.ToImmutableArray(); if (model.Attachments.IsSpecified) { @@ -119,7 +123,9 @@ namespace Discord.WebSocket var val = value[i]; if (val.Object != null) { - var user = Channel.GetUserAsync(val.Object.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; + SocketUser user = null; + if (Channel.HasValue) + user = Channel.Value.GetUserAsync(val.Object.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; if (user != null) newMentions.Add(user); else @@ -133,7 +139,7 @@ namespace Discord.WebSocket if (model.Content.IsSpecified) { var text = model.Content.Value; - _tags = MessageHelper.ParseTags(text, Channel, guild, _userMentions); + _tags = MessageHelper.ParseTags(text, Channel.HasValue ? Channel.Value : null, guild, _userMentions); model.Content = text; } @@ -151,8 +157,9 @@ namespace Discord.WebSocket else refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } - else - refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); + else if (Channel.HasValue) + refMsgAuthor = (Channel.Value as SocketChannel).GetUser(refMsg.Author.Value.Id); + if (refMsgAuthor == null) refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); } @@ -202,7 +209,7 @@ namespace Discord.WebSocket /// This operation may only be called on a channel. public async Task CrosspostAsync(RequestOptions options = null) { - if (!(Channel is INewsChannel)) + if (Channel.HasValue && !(Channel.Value is INewsChannel)) { throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); }