Compare commits

..

3 Commits

@ -135,7 +135,8 @@ namespace Discord.Commands
if (builder.Name == null) if (builder.Name == null)
builder.Name = typeInfo.Name; builder.Name = typeInfo.Name;
var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition); // Get all methods (including from inherited members), that are valid commands
var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition);
foreach (var method in validCommands) foreach (var method in validCommands)
{ {

@ -36,7 +36,7 @@ namespace Discord.Commands
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();
/// <summary> /// <summary>
/// Occurs when a command is successfully executed without any error. /// Occurs when a command is executed.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This event is fired when a command has been executed, successfully or not. When a command fails to /// This event is fired when a command has been executed, successfully or not. When a command fails to

@ -44,7 +44,7 @@ namespace Discord.Commands
/// </summary> /// </summary>
/// <example> /// <example>
/// <code language="cs"> /// <code language="cs">
/// QuotationMarkAliasMap = new Dictionary&lt;char, char%gt;() /// QuotationMarkAliasMap = new Dictionary&lt;char, char&gt;()
/// { /// {
/// {'\"', '\"' }, /// {'\"', '\"' },
/// {'“', '”' }, /// {'“', '”' },

@ -31,9 +31,13 @@ namespace Discord.Commands
/// </param> /// </param>
/// <param name="isTTS">Specifies if Discord should read this <paramref name="message"/> aloud using text-to-speech.</param> /// <param name="isTTS">Specifies if Discord should read this <paramref name="message"/> aloud using text-to-speech.</param>
/// <param name="embed">An embed to be displayed alongside the <paramref name="message"/>.</param> /// <param name="embed">An embed to be displayed alongside the <paramref name="message"/>.</param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) /// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the <paramref name="message"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
{ {
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
} }
/// <summary> /// <summary>
/// The method to execute before executing the command. /// The method to execute before executing the command.

@ -1,10 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Rest")]
[assembly: InternalsVisibleTo("Discord.Net.Rpc")]
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

@ -13,7 +13,7 @@ namespace Discord
/// <returns> /// <returns>
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord. /// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord.
/// <para>A list of available API version can be seen on the official /// <para>A list of available API version can be seen on the official
/// <see href="https://discordapp.com/developers/docs/reference#api-versioning">Discord API documentation</see> /// <see href="https://discord.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para> /// .</para>
/// </returns> /// </returns>
public const int APIVersion = 6; public const int APIVersion = 6;
@ -50,7 +50,7 @@ namespace Discord
/// <returns> /// <returns>
/// The Discord API URL using <see cref="APIVersion"/>. /// The Discord API URL using <see cref="APIVersion"/>.
/// </returns> /// </returns>
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public static readonly string APIUrl = $"https://discord.com/api/v{APIVersion}/";
/// <summary> /// <summary>
/// Returns the base Discord CDN URL. /// Returns the base Discord CDN URL.
/// </summary> /// </summary>

@ -61,6 +61,18 @@ namespace Discord
/// A guild member's role collection was updated. /// A guild member's role collection was updated.
/// </summary> /// </summary>
MemberRoleUpdated = 25, MemberRoleUpdated = 25,
/// <summary>
/// A guild member moved to a voice channel.
/// </summary>
MemberMoved = 26,
/// <summary>
/// A guild member disconnected from a voice channel.
/// </summary>
MemberDisconnected = 27,
/// <summary>
/// A bot was added to this guild.
/// </summary>
BotAdded = 28,
/// <summary> /// <summary>
/// A role was created in this guild. /// A role was created in this guild.
@ -117,6 +129,18 @@ namespace Discord
/// <summary> /// <summary>
/// A message was deleted from this guild. /// A message was deleted from this guild.
/// </summary> /// </summary>
MessageDeleted = 72 MessageDeleted = 72,
/// <summary>
/// Multiple messages were deleted from this guild.
/// </summary>
MessageBulkDeleted = 73,
/// <summary>
/// A message was pinned from this guild.
/// </summary>
MessagePinned = 74,
/// <summary>
/// A message was unpinned from this guild.
/// </summary>
MessageUnpinned = 75,
} }
} }

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
@ -26,9 +28,13 @@ namespace Discord
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Setting this value to a category's snowflake identifier will change or set this channel's parent to the /// Setting this value to a category's snowflake identifier will change or set this channel's parent to the
/// specified channel; setting this value to <c>0</c> will detach this channel from its parent if one /// specified channel; setting this value to <see langword="null"/> will detach this channel from its parent if one
/// is set. /// is set.
/// </remarks> /// </remarks>
public Optional<ulong?> CategoryId { get; set; } public Optional<ulong?> CategoryId { get; set; }
/// <summary>
/// Gets or sets the permission overwrites for this channel.
/// </summary>
public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; }
} }
} }

@ -23,11 +23,15 @@ namespace Discord
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
@ -55,11 +59,15 @@ namespace Discord
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
@ -84,11 +92,15 @@ namespace Discord
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Gets a message from this message channel. /// Gets a message from this message channel.

@ -40,8 +40,8 @@ namespace Discord
/// Creates a new invite to this channel. /// Creates a new invite to this channel.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only /// <para>The following example creates a new invite to this channel; the invite lasts for 12 hours and can only
/// be used 3 times throughout its lifespan. /// be used 3 times throughout its lifespan.</para>
/// <code language="cs"> /// <code language="cs">
/// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3);
/// </code> /// </code>
@ -60,8 +60,8 @@ namespace Discord
/// Gets a collection of all invites to this channel. /// Gets a collection of all invites to this channel.
/// </summary>B /// </summary>B
/// <example> /// <example>
/// The following example gets all of the invites that have been created in this channel and selects the /// <para>The following example gets all of the invites that have been created in this channel and selects the
/// most used invite. /// most used invite.</para>
/// <code language="cs"> /// <code language="cs">
/// var invites = await channel.GetInvitesAsync(); /// var invites = await channel.GetInvitesAsync();
/// if (invites.Count == 0) return; /// if (invites.Count == 0) return;

@ -30,7 +30,7 @@ namespace Discord
/// Gets the current slow-mode delay for this channel. /// Gets the current slow-mode delay for this channel.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another /// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled. /// message; <c>0</c> if disabled.
/// </returns> /// </returns>
int SlowModeInterval { get; } int SlowModeInterval { get; }
@ -39,7 +39,7 @@ namespace Discord
/// Bulk-deletes multiple messages. /// Bulk-deletes multiple messages.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example gets 250 messages from the channel and deletes them. /// <para>The following example gets 250 messages from the channel and deletes them.</para>
/// <code language="cs"> /// <code language="cs">
/// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync();
/// await textChannel.DeleteMessagesAsync(messages); /// await textChannel.DeleteMessagesAsync(messages);

@ -510,7 +510,7 @@ namespace Discord
/// Creates a new text channel in this guild. /// Creates a new text channel in this guild.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic. /// <para>The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic.</para>
/// <code language="cs" region="CreateTextChannelAsync" /// <code language="cs" region="CreateTextChannelAsync"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Guilds\IGuild.Examples.cs"/> /// source="..\..\..\Discord.Net.Examples\Core\Entities\Guilds\IGuild.Examples.cs"/>
/// </example> /// </example>
@ -683,6 +683,9 @@ namespace Discord
/// <summary> /// <summary>
/// Downloads all users for this guild if the current list is incomplete. /// Downloads all users for this guild if the current list is incomplete.
/// </summary> /// </summary>
/// <remarks>
/// This method downloads all users found within this guild throught the Gateway and caches them.
/// </remarks>
/// <returns> /// <returns>
/// A task that represents the asynchronous download operation. /// A task that represents the asynchronous download operation.
/// </returns> /// </returns>
@ -707,6 +710,22 @@ namespace Discord
/// be or has been removed from this guild. /// be or has been removed from this guild.
/// </returns> /// </returns>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);
/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
/// provided <see cref="string"/> at <paramref name="query"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</param>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
Task<IReadOnlyCollection<IGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> /// <summary>
/// Gets the specified number of audit log entries for this guild. /// Gets the specified number of audit log entries for this guild.

@ -0,0 +1,34 @@
using System;
namespace Discord
{
/// <summary>
/// Specifies the type of mentions that will be notified from the message content.
/// </summary>
[Flags]
public enum AllowedMentionTypes
{
/// <summary>
/// No flag is set.
/// </summary>
/// <remarks>
/// This flag is not used to control mentions.
/// <note type="warning">
/// It will always be present and does not mean mentions will not be allowed.
/// </note>
/// </remarks>
None = 0,
/// <summary>
/// Controls role mentions.
/// </summary>
Roles = 1,
/// <summary>
/// Controls user mentions.
/// </summary>
Users = 2,
/// <summary>
/// Controls <code>@everyone</code> and <code>@here</code> mentions.
/// </summary>
Everyone = 4,
}
}

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
namespace Discord
{
/// <summary>
/// Defines which mentions and types of mentions that will notify users from the message content.
/// </summary>
public class AllowedMentions
{
private static readonly Lazy<AllowedMentions> none = new Lazy<AllowedMentions>(() => new AllowedMentions());
private static readonly Lazy<AllowedMentions> all = new Lazy<AllowedMentions>(() =>
new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles));
/// <summary>
/// Gets a value which indicates that no mentions in the message content should notify users.
/// </summary>
public static AllowedMentions None => none.Value;
/// <summary>
/// Gets a value which indicates that all mentions in the message content should notify users.
/// </summary>
public static AllowedMentions All => all.Value;
/// <summary>
/// Gets or sets the type of mentions that will be parsed from the message content.
/// </summary>
/// <remarks>
/// The <see cref="AllowedMentionTypes.Users"/> flag is mutually exclusive with the <see cref="UserIds"/>
/// property, and the <see cref="AllowedMentionTypes.Roles"/> flag is mutually exclusive with the
/// <see cref="RoleIds"/> property.
/// If <c>null</c>, only the ids specified in <see cref="UserIds"/> and <see cref="RoleIds"/> will be mentioned.
/// </remarks>
public AllowedMentionTypes? AllowedTypes { get; set; }
/// <summary>
/// Gets or sets the list of all role ids that will be mentioned.
/// This property is mutually exclusive with the <see cref="AllowedMentionTypes.Roles"/>
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> RoleIds { get; set; } = new List<ulong>();
/// <summary>
/// Gets or sets the list of all user ids that will be mentioned.
/// This property is mutually exclusive with the <see cref="AllowedMentionTypes.Users"/>
/// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property
/// must be <c>null</c> or empty.
/// </summary>
public List<ulong> UserIds { get; set; } = new List<ulong>();
/// <summary>
/// Initializes a new instance of the <see cref="AllowedMentions"/> class.
/// </summary>
/// <param name="allowedTypes">
/// The types of mentions to parse from the message content.
/// If <c>null</c>, only the ids specified in <see cref="UserIds"/> and <see cref="RoleIds"/> will be mentioned.
/// </param>
public AllowedMentions(AllowedMentionTypes? allowedTypes = null)
{
AllowedTypes = allowedTypes;
}
}
}

@ -39,6 +39,13 @@ namespace Discord
/// </returns> /// </returns>
bool IsSuppressed { get; } bool IsSuppressed { get; }
/// <summary> /// <summary>
/// Gets the value that indicates whether this message mentioned everyone.
/// </summary>
/// <returns>
/// <c>true</c> if this message mentioned everyone; otherwise <c>false</c>.
/// </returns>
bool MentionedEveryone { get; }
/// <summary>
/// Gets the content for this message. /// Gets the content for this message.
/// </summary> /// </summary>
/// <returns> /// <returns>
@ -161,7 +168,7 @@ namespace Discord
/// Adds a reaction to this message. /// Adds a reaction to this message.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example adds the reaction, <c>💕</c>, to the message. /// <para>The following example adds the reaction, <c>💕</c>, to the message.</para>
/// <code language="cs"> /// <code language="cs">
/// await msg.AddReactionAsync(new Emoji("\U0001f495")); /// await msg.AddReactionAsync(new Emoji("\U0001f495"));
/// </code> /// </code>
@ -177,7 +184,7 @@ namespace Discord
/// Removes a reaction from message. /// Removes a reaction from message.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example removes the reaction, <c>💕</c>, added by the message author from the message. /// <para>The following example removes the reaction, <c>💕</c>, added by the message author from the message.</para>
/// <code language="cs"> /// <code language="cs">
/// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author);
/// </code> /// </code>
@ -194,7 +201,7 @@ namespace Discord
/// Removes a reaction from message. /// Removes a reaction from message.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example removes the reaction, <c>💕</c>, added by the user with ID 84291986575613952 from the message. /// <para>The following example removes the reaction, <c>💕</c>, added by the user with ID 84291986575613952 from the message.</para>
/// <code language="cs"> /// <code language="cs">
/// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952);
/// </code> /// </code>
@ -215,12 +222,38 @@ namespace Discord
/// A task that represents the asynchronous removal operation. /// A task that represents the asynchronous removal operation.
/// </returns> /// </returns>
Task RemoveAllReactionsAsync(RequestOptions options = null); Task RemoveAllReactionsAsync(RequestOptions options = null);
/// <summary>
/// Removes all reactions with a specific emoji from this message.
/// </summary>
/// <param name="emote">The emoji used to react to this message.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null);
/// <summary> /// <summary>
/// Gets all users that reacted to a message with a given emote. /// Gets all users that reacted to a message with a given emote.
/// </summary> /// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the users as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of reactions specified under <paramref name="limit"/>.
/// The library will attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxUserReactionsPerBatch"/>. In other words, should the user request 500 reactions,
/// and the <see cref="Discord.DiscordConfig.MaxUserReactionsPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <example> /// <example>
/// The following example gets the users that have reacted with the emoji <c>💕</c> to the message. /// <para>The following example gets the users that have reacted with the emoji <c>💕</c> to the message.</para>
/// <code language="cs"> /// <code language="cs">
/// var emoji = new Emoji("\U0001f495"); /// var emoji = new Emoji("\U0001f495");
/// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync();
@ -230,9 +263,7 @@ namespace Discord
/// <param name="limit">The number of users to request.</param> /// <param name="limit">The number of users to request.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <returns> /// <returns>
/// A paged collection containing a read-only collection of users that has reacted to this message. /// Paged collection of users.
/// Flattening the paginated response into a collection of users with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the users.
/// </returns> /// </returns>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null);
} }

@ -17,7 +17,7 @@ namespace Discord
/// method and what properties are available, please refer to <see cref="MessageProperties"/>. /// method and what properties are available, please refer to <see cref="MessageProperties"/>.
/// </remarks> /// </remarks>
/// <example> /// <example>
/// The following example replaces the content of the message with <c>Hello World!</c>. /// <para>The following example replaces the content of the message with <c>Hello World!</c>.</para>
/// <code language="cs"> /// <code language="cs">
/// await msg.ModifyAsync(x =&gt; x.Content = "Hello World!"); /// await msg.ModifyAsync(x =&gt; x.Content = "Hello World!");
/// </code> /// </code>
@ -57,6 +57,21 @@ namespace Discord
/// </returns> /// </returns>
Task UnpinAsync(RequestOptions options = null); Task UnpinAsync(RequestOptions options = null);
/// <summary>
/// Publishes (crossposts) this message.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous operation for publishing this message.
/// </returns>
/// <remarks>
/// <note type="warning">
/// This call will throw an <see cref="InvalidOperationException"/> if attempted in a non-news channel.
/// </note>
/// This method will publish (crosspost) the message. Please note, publishing (crossposting), is only available in news channels.
/// </remarks>
Task CrosspostAsync(RequestOptions options = null);
/// <summary> /// <summary>
/// Transforms this message's text into a human-readable form by resolving its tags. /// Transforms this message's text into a human-readable form by resolving its tags.
/// </summary> /// </summary>

@ -76,6 +76,10 @@ namespace Discord
public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers);
/// <summary> If Allowed, a user may use voice-activity-detection rather than push-to-talk. </summary> /// <summary> If Allowed, a user may use voice-activity-detection rather than push-to-talk. </summary>
public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD);
/// <summary> If Allowed, a user may use priority speaker in a voice channel. </summary>
public PermValue PrioritySpeaker => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.PrioritySpeaker);
/// <summary> If Allowed, a user may go live in a voice channel. </summary>
public PermValue Stream => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Stream);
/// <summary> If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. </summary> /// <summary> If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. </summary>
public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles);
@ -109,7 +113,9 @@ namespace Discord
PermValue? moveMembers = null, PermValue? moveMembers = null,
PermValue? useVoiceActivation = null, PermValue? useVoiceActivation = null,
PermValue? manageRoles = null, PermValue? manageRoles = null,
PermValue? manageWebhooks = null) PermValue? manageWebhooks = null,
PermValue? prioritySpeaker = null,
PermValue? stream = null)
{ {
Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite);
Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels);
@ -129,6 +135,8 @@ namespace Discord
Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers);
Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers);
Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD);
Permissions.SetValue(ref allowValue, ref denyValue, prioritySpeaker, ChannelPermission.PrioritySpeaker);
Permissions.SetValue(ref allowValue, ref denyValue, stream, ChannelPermission.Stream);
Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles);
Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks);
@ -159,10 +167,12 @@ namespace Discord
PermValue moveMembers = PermValue.Inherit, PermValue moveMembers = PermValue.Inherit,
PermValue useVoiceActivation = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit,
PermValue manageRoles = PermValue.Inherit, PermValue manageRoles = PermValue.Inherit,
PermValue manageWebhooks = PermValue.Inherit) PermValue manageWebhooks = PermValue.Inherit,
PermValue prioritySpeaker = PermValue.Inherit,
PermValue stream = PermValue.Inherit)
: this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, : this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream) { }
/// <summary> /// <summary>
/// Initializes a new <see cref="OverwritePermissions" /> from the current one, changing the provided /// Initializes a new <see cref="OverwritePermissions" /> from the current one, changing the provided
@ -188,10 +198,12 @@ namespace Discord
PermValue? moveMembers = null, PermValue? moveMembers = null,
PermValue? useVoiceActivation = null, PermValue? useVoiceActivation = null,
PermValue? manageRoles = null, PermValue? manageRoles = null,
PermValue? manageWebhooks = null) PermValue? manageWebhooks = null,
PermValue? prioritySpeaker = null,
PermValue? stream = null)
=> new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages,
embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers,
moveMembers, useVoiceActivation, manageRoles, manageWebhooks); moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream);
/// <summary> /// <summary>
/// Creates a <see cref="List{T}"/> of all the <see cref="ChannelPermission"/> values that are allowed. /// Creates a <see cref="List{T}"/> of all the <see cref="ChannelPermission"/> values that are allowed.

@ -72,8 +72,8 @@ namespace Discord
/// Gets the level permissions granted to this user to a given channel. /// Gets the level permissions granted to this user to a given channel.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example checks if the current user has the ability to send a message with attachment in /// <para>The following example checks if the current user has the ability to send a message with attachment in
/// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>. /// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.</para>
/// <code language="cs"> /// <code language="cs">
/// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles)
/// await targetChannel.SendFileAsync("fortnite.png"); /// await targetChannel.SendFileAsync("fortnite.png");

@ -19,5 +19,9 @@ namespace Discord
/// Gets the set of clients where this user is currently active. /// Gets the set of clients where this user is currently active.
/// </summary> /// </summary>
IImmutableSet<ClientType> ActiveClients { get; } IImmutableSet<ClientType> ActiveClients { get; }
/// <summary>
/// Gets the list of activities that this user currently has available.
/// </summary>
IImmutableList<IActivity> Activities { get; }
} }
} }

@ -21,8 +21,8 @@ namespace Discord
/// example). /// example).
/// </remarks> /// </remarks>
/// <example> /// <example>
/// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is /// <para>The following example attempts to retrieve the user's current avatar and send it to a channel; if one is
/// not set, a default avatar for this user will be returned instead. /// not set, a default avatar for this user will be returned instead.</para>
/// <code language="cs" region="GetAvatarUrl" /// <code language="cs" region="GetAvatarUrl"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Users\IUser.Examples.cs"/> /// source="..\..\..\Discord.Net.Examples\Core\Entities\Users\IUser.Examples.cs"/>
/// </example> /// </example>
@ -90,8 +90,8 @@ namespace Discord
/// </note> /// </note>
/// </remarks> /// </remarks>
/// <example> /// <example>
/// The following example attempts to send a direct message to the target user and logs the incident should /// <para>The following example attempts to send a direct message to the target user and logs the incident should
/// it fail. /// it fail.</para>
/// <code region="GetOrCreateDMChannelAsync" language="cs" /// <code region="GetOrCreateDMChannelAsync" language="cs"
/// source="../../../Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs"/> /// source="../../../Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs"/>
/// </example> /// </example>

@ -1,4 +1,4 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -15,7 +15,7 @@ namespace Discord
//public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source) //public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source)
// => new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count); // => new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IDictionary<TKey, TValue> source) public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IDictionary<TKey, TValue> source)
=> new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count); => new CollectionWrapper<TValue>(source.Values, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source) public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source)
=> new CollectionWrapper<TValue>(query, () => source.Count); => new CollectionWrapper<TValue>(query, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc) public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc)

@ -17,7 +17,7 @@ namespace Discord
public static string GetJumpUrl(this IMessage msg) public static string GetJumpUrl(this IMessage msg)
{ {
var channel = msg.Channel; var channel = msg.Channel;
return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; return $"https://discord.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}";
} }
/// <summary> /// <summary>
@ -39,7 +39,7 @@ namespace Discord
/// <returns> /// <returns>
/// A task that represents the asynchronous operation for adding a reaction to this message. /// A task that represents the asynchronous operation for adding a reaction to this message.
/// </returns> /// </returns>
/// <seealso cref="IUserMessage.AddReactionAsync(IEmote, RequestOptions)"/> /// <seealso cref="IMessage.AddReactionAsync(IEmote, RequestOptions)"/>
/// <seealso cref="IEmote"/> /// <seealso cref="IEmote"/>
public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null)
{ {
@ -51,7 +51,7 @@ namespace Discord
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method does not bulk remove reactions! If you want to clear reactions from a message, /// This method does not bulk remove reactions! If you want to clear reactions from a message,
/// <see cref="IUserMessage.RemoveAllReactionsAsync(RequestOptions)"/> /// <see cref="IMessage.RemoveAllReactionsAsync(RequestOptions)"/>
/// </remarks> /// </remarks>
/// <example> /// <example>
/// <code language="cs"> /// <code language="cs">
@ -64,7 +64,7 @@ namespace Discord
/// <returns> /// <returns>
/// A task that represents the asynchronous operation for removing a reaction to this message. /// A task that represents the asynchronous operation for removing a reaction to this message.
/// </returns> /// </returns>
/// <seealso cref="IUserMessage.RemoveReactionAsync(IEmote, IUser, RequestOptions)"/> /// <seealso cref="IMessage.RemoveReactionAsync(IEmote, IUser, RequestOptions)"/>
/// <seealso cref="IEmote"/> /// <seealso cref="IEmote"/>
public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null)
{ {

@ -28,6 +28,10 @@ namespace Discord
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents the asynchronous send operation. The task result contains the sent message. /// A task that represents the asynchronous send operation. The task result contains the sent message.
/// </returns> /// </returns>
@ -35,17 +39,18 @@ namespace Discord
string text = null, string text = null,
bool isTTS = false, bool isTTS = false,
Embed embed = null, Embed embed = null,
RequestOptions options = null) RequestOptions options = null,
AllowedMentions allowedMentions = null)
{ {
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
} }
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <example> /// <example>
/// The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a /// <para>The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel. /// rich embed to the channel.</para>
/// <code language="cs"> /// <code language="cs">
/// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());

@ -0,0 +1,43 @@
using System;
namespace Discord
{
[Flags]
public enum GatewayIntents
{
/// <summary> This intent includes no events </summary>
None = 0,
/// <summary> This intent includes GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE </summary>
Guilds = 1 << 0,
/// <summary> This intent includes GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE </summary>
/// <remarks> This is a privileged intent and must be enabled in the Developer Portal. </remarks>
GuildMembers = 1 << 1,
/// <summary> This intent includes GUILD_BAN_ADD, GUILD_BAN_REMOVE </summary>
GuildBans = 1 << 2,
/// <summary> This intent includes GUILD_EMOJIS_UPDATE </summary>
GuildEmojis = 1 << 3,
/// <summary> This intent includes GUILD_INTEGRATIONS_UPDATE </summary>
GuildIntegrations = 1 << 4,
/// <summary> This intent includes WEBHOOKS_UPDATE </summary>
GuildWebhooks = 1 << 5,
/// <summary> This intent includes INVITE_CREATE, INVITE_DELETE </summary>
GuildInvites = 1 << 6,
/// <summary> This intent includes VOICE_STATE_UPDATE </summary>
GuildVoiceStates = 1 << 7,
/// <summary> This intent includes PRESENCE_UPDATE </summary>
/// <remarks> This is a privileged intent and must be enabled in the Developer Portal. </remarks>
GuildPresences = 1 << 8,
/// <summary> This intent includes MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK </summary>
GuildMessages = 1 << 9,
/// <summary> This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI </summary>
GuildMessageReactions = 1 << 10,
/// <summary> This intent includes TYPING_START </summary>
GuildMessageTyping = 1 << 11,
/// <summary> This intent includes CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE </summary>
DirectMessages = 1 << 12,
/// <summary> This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI </summary>
DirectMessageReactions = 1 << 13,
/// <summary> This intent includes TYPING_START </summary>
DirectMessageTyping = 1 << 14,
}
}

@ -270,7 +270,7 @@ namespace Discord
/// </summary> /// </summary>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <returns> /// <returns>
/// A task that represents the asynchronous get operation. The task result contains an <see cref="Int32"/> /// A task that represents the asynchronous get operation. The task result contains an <see cref="int"/>
/// that represents the number of shards that should be used with this account. /// that represents the number of shards that should be used with this account.
/// </returns> /// </returns>
Task<int> GetRecommendedShardCountAsync(RequestOptions options = null); Task<int> GetRecommendedShardCountAsync(RequestOptions options = null);

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Discord.Net
{
/// <summary>
/// Represents a ratelimit bucket.
/// </summary>
public class BucketId : IEquatable<BucketId>
{
/// <summary>
/// Gets the http method used to make the request if available.
/// </summary>
public string HttpMethod { get; }
/// <summary>
/// Gets the endpoint that is going to be requested if available.
/// </summary>
public string Endpoint { get; }
/// <summary>
/// Gets the major parameters of the route.
/// </summary>
public IOrderedEnumerable<KeyValuePair<string, string>> MajorParameters { get; }
/// <summary>
/// Gets the hash of this bucket.
/// </summary>
/// <remarks>
/// The hash is provided by Discord to group ratelimits.
/// </remarks>
public string BucketHash { get; }
/// <summary>
/// Gets if this bucket is a hash type.
/// </summary>
public bool IsHashBucket { get => BucketHash != null; }
private BucketId(string httpMethod, string endpoint, IEnumerable<KeyValuePair<string, string>> majorParameters, string bucketHash)
{
HttpMethod = httpMethod;
Endpoint = endpoint;
MajorParameters = majorParameters.OrderBy(x => x.Key);
BucketHash = bucketHash;
}
/// <summary>
/// Creates a new <see cref="BucketId"/> based on the
/// <see cref="HttpMethod"/> and <see cref="Endpoint"/>.
/// </summary>
/// <param name="httpMethod">Http method used to make the request.</param>
/// <param name="endpoint">Endpoint that is going to receive requests.</param>
/// <param name="majorParams">Major parameters of the route of this endpoint.</param>
/// <returns>
/// A <see cref="BucketId"/> based on the <see cref="HttpMethod"/>
/// and the <see cref="Endpoint"/> with the provided data.
/// </returns>
public static BucketId Create(string httpMethod, string endpoint, Dictionary<string, string> majorParams)
{
Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint));
majorParams = majorParams ?? new Dictionary<string, string>();
return new BucketId(httpMethod, endpoint, majorParams, null);
}
/// <summary>
/// Creates a new <see cref="BucketId"/> based on a
/// <see cref="BucketHash"/> and a previous <see cref="BucketId"/>.
/// </summary>
/// <param name="hash">Bucket hash provided by Discord.</param>
/// <param name="oldBucket"><see cref="BucketId"/> that is going to be upgraded to a hash type.</param>
/// <returns>
/// A <see cref="BucketId"/> based on the <see cref="BucketHash"/>
/// and <see cref="MajorParameters"/>.
/// </returns>
public static BucketId Create(string hash, BucketId oldBucket)
{
Preconditions.NotNullOrWhitespace(hash, nameof(hash));
Preconditions.NotNull(oldBucket, nameof(oldBucket));
return new BucketId(null, null, oldBucket.MajorParameters, hash);
}
/// <summary>
/// Gets the string that will define this bucket as a hash based one.
/// </summary>
/// <returns>
/// A <see cref="string"/> that defines this bucket as a hash based one.
/// </returns>
public string GetBucketHash()
=> IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null;
/// <summary>
/// Gets the string that will define this bucket as an endpoint based one.
/// </summary>
/// <returns>
/// A <see cref="string"/> that defines this bucket as an endpoint based one.
/// </returns>
public string GetUniqueEndpoint()
=> HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint;
public override bool Equals(object obj)
=> Equals(obj as BucketId);
public override int GetHashCode()
=> IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode();
public override string ToString()
=> GetBucketHash() ?? GetUniqueEndpoint();
public bool Equals(BucketId other)
{
if (other is null)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
return ToString() == other.ToString();
}
}
}

@ -13,7 +13,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An /// An
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#http">HTTP status code</see> /// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#http">HTTP status code</see>
/// from Discord. /// from Discord.
/// </returns> /// </returns>
public HttpStatusCode HttpCode { get; } public HttpStatusCode HttpCode { get; }
@ -22,7 +22,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A /// A
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see> /// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see>
/// from Discord, or <c>null</c> if none. /// from Discord, or <c>null</c> if none.
/// </returns> /// </returns>
public int? DiscordCode { get; } public int? DiscordCode { get; }

@ -11,7 +11,7 @@ namespace Discord.Net
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A /// A
/// <see href="https://discordapp.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes">close code</see> /// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes">close code</see>
/// from Discord. /// from Discord.
/// </returns> /// </returns>
public int CloseCode { get; } public int CloseCode { get; }

@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets
void SetCancelToken(CancellationToken cancelToken); void SetCancelToken(CancellationToken cancelToken);
Task ConnectAsync(string host); Task ConnectAsync(string host);
Task DisconnectAsync(); Task DisconnectAsync(int closeCode = 1000);
Task SendAsync(byte[] data, int index, int count, bool isText); Task SendAsync(byte[] data, int index, int count, bool isText);
} }

@ -1,3 +1,4 @@
using Discord.Net;
using System.Threading; using System.Threading;
namespace Discord namespace Discord
@ -49,8 +50,7 @@ namespace Discord
/// clock for rate-limiting. Defaults to <c>true</c>. /// clock for rate-limiting. Defaults to <c>true</c>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This property can also be set in <see cref="DiscordConfig">. /// This property can also be set in <see cref="DiscordConfig"/>.
///
/// On a per-request basis, the system clock should only be disabled /// On a per-request basis, the system clock should only be disabled
/// when millisecond precision is especially important, and the /// when millisecond precision is especially important, and the
/// hosting system is known to have a desynced clock. /// hosting system is known to have a desynced clock.
@ -58,7 +58,7 @@ namespace Discord
public bool? UseSystemClock { get; set; } public bool? UseSystemClock { get; set; }
internal bool IgnoreState { get; set; } internal bool IgnoreState { get; set; }
internal string BucketId { get; set; } internal BucketId BucketId { get; set; }
internal bool IsClientBucket { get; set; } internal bool IsClientBucket { get; set; }
internal bool IsReactionBucket { get; set; } internal bool IsReactionBucket { get; set; }

@ -5,6 +5,7 @@ namespace Discord
/// <summary> Specifies the type of token to use with the client. </summary> /// <summary> Specifies the type of token to use with the client. </summary>
public enum TokenType public enum TokenType
{ {
[Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)]
User, User,
/// <summary> /// <summary>
/// An OAuth2 token type. /// An OAuth2 token type.

@ -8,27 +8,26 @@ namespace Discord
/// </summary> /// </summary>
public static class DiscordComparers public static class DiscordComparers
{ {
// TODO: simplify with '??=' slated for C# 8.0
/// <summary> /// <summary>
/// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare users. /// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare users.
/// </summary> /// </summary>
public static IEqualityComparer<IUser> UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer<IUser, ulong>()); public static IEqualityComparer<IUser> UserComparer => _userComparer =_userComparer ?? new EntityEqualityComparer<IUser, ulong>();
/// <summary> /// <summary>
/// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare guilds. /// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare guilds.
/// </summary> /// </summary>
public static IEqualityComparer<IGuild> GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer<IGuild, ulong>()); public static IEqualityComparer<IGuild> GuildComparer => _guildComparer = _guildComparer ?? new EntityEqualityComparer<IGuild, ulong>();
/// <summary> /// <summary>
/// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare channels. /// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare channels.
/// </summary> /// </summary>
public static IEqualityComparer<IChannel> ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer<IChannel, ulong>()); public static IEqualityComparer<IChannel> ChannelComparer => _channelComparer = _channelComparer ?? new EntityEqualityComparer<IChannel, ulong>();
/// <summary> /// <summary>
/// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare roles. /// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare roles.
/// </summary> /// </summary>
public static IEqualityComparer<IRole> RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer<IRole, ulong>()); public static IEqualityComparer<IRole> RoleComparer => _roleComparer = _roleComparer ?? new EntityEqualityComparer<IRole, ulong>();
/// <summary> /// <summary>
/// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare messages. /// Gets an <see cref="IEqualityComparer{T}"/> to be used to compare messages.
/// </summary> /// </summary>
public static IEqualityComparer<IMessage> MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer<IMessage, ulong>()); public static IEqualityComparer<IMessage> MessageComparer => _messageComparer = _messageComparer ?? new EntityEqualityComparer<IMessage, ulong>();
private static IEqualityComparer<IUser> _userComparer; private static IEqualityComparer<IUser> _userComparer;
private static IEqualityComparer<IGuild> _guildComparer; private static IEqualityComparer<IGuild> _guildComparer;
@ -42,16 +41,11 @@ namespace Discord
{ {
public override bool Equals(TEntity x, TEntity y) public override bool Equals(TEntity x, TEntity y)
{ {
bool xNull = x == null; if (x == null && y == null) return true;
bool yNull = y == null; if (x == null && y != null) return false;
if (x != null && y == null) return false;
if (xNull && yNull) var tuple = new Tuple<TEntity, TEntity>(x, y);
return true; return tuple.Item1.Id.Equals(tuple.Item2.Id);
if (xNull ^ yNull)
return false;
return x.Id.Equals(y.Id);
} }
public override int GetHashCode(TEntity obj) public override int GetHashCode(TEntity obj)

@ -4,11 +4,12 @@ namespace Discord.API
{ {
internal class AuditLogOptions internal class AuditLogOptions
{ {
//Message delete
[JsonProperty("count")] [JsonProperty("count")]
public int? MessageDeleteCount { get; set; } public int? Count { get; set; }
[JsonProperty("channel_id")] [JsonProperty("channel_id")]
public ulong? MessageDeleteChannelId { get; set; } public ulong? ChannelId { get; set; }
[JsonProperty("message_id")]
public ulong? MessageId { get; set; }
//Prune //Prune
[JsonProperty("delete_member_days")] [JsonProperty("delete_member_days")]

@ -54,5 +54,7 @@ namespace Discord.API
public Optional<MessageReference> Reference { get; set; } public Optional<MessageReference> Reference { get; set; }
[JsonProperty("flags")] [JsonProperty("flags")]
public Optional<MessageFlags> Flags { get; set; } public Optional<MessageFlags> Flags { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
} }
} }

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Discord.API namespace Discord.API
@ -26,5 +27,9 @@ namespace Discord.API
// "client_status": { "desktop": "dnd", "mobile": "dnd" } // "client_status": { "desktop": "dnd", "mobile": "dnd" }
[JsonProperty("client_status")] [JsonProperty("client_status")]
public Optional<Dictionary<string, string>> ClientStatus { get; set; } public Optional<Dictionary<string, string>> ClientStatus { get; set; }
[JsonProperty("activities")]
public List<Game> Activities { get; set; }
[JsonProperty("premium_since")]
public Optional<DateTimeOffset?> PremiumSince { get; set; }
} }
} }

@ -14,12 +14,16 @@ namespace Discord.API.Rest
public Optional<ulong?> CategoryId { get; set; } public Optional<ulong?> CategoryId { get; set; }
[JsonProperty("position")] [JsonProperty("position")]
public Optional<int> Position { get; set; } public Optional<int> Position { get; set; }
[JsonProperty("permission_overwrites")]
public Optional<Overwrite[]> Overwrites { get; set; }
//Text channels //Text channels
[JsonProperty("topic")] [JsonProperty("topic")]
public Optional<string> Topic { get; set; } public Optional<string> Topic { get; set; }
[JsonProperty("nsfw")] [JsonProperty("nsfw")]
public Optional<bool> IsNsfw { get; set; } public Optional<bool> IsNsfw { get; set; }
[JsonProperty("rate_limit_per_user")]
public Optional<int> SlowModeInterval { get; set; }
//Voice channels //Voice channels
[JsonProperty("bitrate")] [JsonProperty("bitrate")]

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Discord.API.Rest namespace Discord.API.Rest
@ -15,6 +15,8 @@ namespace Discord.API.Rest
public Optional<bool> IsTTS { get; set; } public Optional<bool> IsTTS { get; set; }
[JsonProperty("embed")] [JsonProperty("embed")]
public Optional<Embed> Embed { get; set; } public Optional<Embed> Embed { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
public CreateMessageParams(string content) public CreateMessageParams(string content)
{ {

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Discord.API.Rest namespace Discord.API.Rest
@ -19,6 +19,8 @@ namespace Discord.API.Rest
public Optional<string> Username { get; set; } public Optional<string> Username { get; set; }
[JsonProperty("avatar_url")] [JsonProperty("avatar_url")]
public Optional<string> AvatarUrl { get; set; } public Optional<string> AvatarUrl { get; set; }
[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
public CreateWebhookMessageParams(string content) public CreateWebhookMessageParams(string content)
{ {

@ -0,0 +1,9 @@
#pragma warning disable CS1591
namespace Discord.API.Rest
{
internal class SearchGuildMembersParams
{
public string Query { get; set; }
public Optional<int> Limit { get; set; }
}
}

@ -19,6 +19,7 @@ namespace Discord.API.Rest
public Optional<string> Nonce { get; set; } public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; } public Optional<bool> IsTTS { get; set; }
public Optional<Embed> Embed { get; set; } public Optional<Embed> Embed { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public bool IsSpoiler { get; set; } = false; public bool IsSpoiler { get; set; } = false;
public UploadFileParams(Stream file) public UploadFileParams(Stream file)
@ -43,6 +44,8 @@ namespace Discord.API.Rest
payload["nonce"] = Nonce.Value; payload["nonce"] = Nonce.Value;
if (Embed.IsSpecified) if (Embed.IsSpecified)
payload["embed"] = Embed.Value; payload["embed"] = Embed.Value;
if (AllowedMentions.IsSpecified)
payload["allowed_mentions"] = AllowedMentions.Value;
if (IsSpoiler) if (IsSpoiler)
payload["hasSpoiler"] = IsSpoiler.ToString(); payload["hasSpoiler"] = IsSpoiler.ToString();

@ -21,6 +21,7 @@ namespace Discord.API.Rest
public Optional<string> Username { get; set; } public Optional<string> Username { get; set; }
public Optional<string> AvatarUrl { get; set; } public Optional<string> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; } public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public bool IsSpoiler { get; set; } = false; public bool IsSpoiler { get; set; } = false;
@ -51,6 +52,8 @@ namespace Discord.API.Rest
payload["avatar_url"] = AvatarUrl.Value; payload["avatar_url"] = AvatarUrl.Value;
if (Embeds.IsSpecified) if (Embeds.IsSpecified)
payload["embeds"] = Embeds.Value; payload["embeds"] = Embeds.Value;
if (AllowedMentions.IsSpecified)
payload["allowed_mentions"] = AllowedMentions.Value;
var json = new StringBuilder(); var json = new StringBuilder();
using (var text = new StringWriter(json)) using (var text = new StringWriter(json))

@ -49,9 +49,9 @@ namespace Discord.Rest
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
{ {
if (info == null) if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
else else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
}; };
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
} }

@ -24,7 +24,7 @@ namespace Discord.API
{ {
internal class DiscordRestApiClient : IDisposable internal class DiscordRestApiClient : IDisposable
{ {
private static readonly ConcurrentDictionary<string, Func<BucketIds, string>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, string>>(); private static readonly ConcurrentDictionary<string, Func<BucketIds, BucketId>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, BucketId>>();
public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
@ -47,7 +47,7 @@ namespace Discord.API
internal ulong? CurrentUserId { get; set; } internal ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; } public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; } internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer; internal JsonSerializer Serializer => _serializer;
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception> /// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
@ -82,12 +82,12 @@ namespace Discord.API
{ {
switch (tokenType) switch (tokenType)
{ {
case default(TokenType):
return token;
case TokenType.Bot: case TokenType.Bot:
return $"Bot {token}"; return $"Bot {token}";
case TokenType.Bearer: case TokenType.Bearer:
return $"Bearer {token}"; return $"Bearer {token}";
case TokenType.User:
return token;
default: default:
throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)); throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType));
} }
@ -127,16 +127,15 @@ namespace Discord.API
{ {
_loginCancelToken?.Dispose(); _loginCancelToken?.Dispose();
_loginCancelToken = new CancellationTokenSource(); _loginCancelToken = new CancellationTokenSource();
AuthTokenType = TokenType.User;
AuthToken = null; AuthToken = null;
await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false);
RestClient.SetCancelToken(_loginCancelToken.Token); RestClient.SetCancelToken(_loginCancelToken.Token);
AuthTokenType = tokenType; AuthTokenType = tokenType;
AuthToken = token; AuthToken = token?.TrimEnd();
var temp = GetPrefixedToken(AuthTokenType, AuthToken);
if (tokenType != TokenType.Webhook) if (tokenType != TokenType.Webhook)
RestClient.SetHeader("authorization", temp); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
LoginState = LoginState.LoggedIn; LoginState = LoginState.LoggedIn;
} }
@ -165,7 +164,7 @@ namespace Discord.API
try { _loginCancelToken?.Cancel(false); } try { _loginCancelToken?.Cancel(false); }
catch { } catch { }
await DisconnectInternalAsync().ConfigureAwait(false); await DisconnectInternalAsync(null).ConfigureAwait(false);
await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false);
await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false);
@ -176,18 +175,18 @@ namespace Discord.API
} }
internal virtual Task ConnectInternalAsync() => Task.Delay(0); internal virtual Task ConnectInternalAsync() => Task.Delay(0);
internal virtual Task DisconnectInternalAsync() => Task.Delay(0); internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0);
//Core //Core
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids, internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendAsync(string method, string endpoint, public async Task SendAsync(string method, string endpoint,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = bucketId;
var request = new RestRequest(RestClient, method, endpoint, options); var request = new RestRequest(RestClient, method, endpoint, options);
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
@ -195,13 +194,13 @@ namespace Discord.API
internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids, internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendJsonAsync(string method, string endpoint, object payload, public async Task SendJsonAsync(string method, string endpoint, object payload,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = bucketId;
string json = payload != null ? SerializeJson(payload) : null; string json = payload != null ? SerializeJson(payload) : null;
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
@ -210,13 +209,13 @@ namespace Discord.API
internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.HeaderOnly = true; options.HeaderOnly = true;
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = bucketId;
var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options);
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
@ -224,12 +223,12 @@ namespace Discord.API
internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids, internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
=> SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = bucketId;
var request = new RestRequest(RestClient, method, endpoint, options); var request = new RestRequest(RestClient, method, endpoint, options);
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
@ -237,12 +236,12 @@ namespace Discord.API
internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids, internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
=> SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload, public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = bucketId;
string json = payload != null ? SerializeJson(payload) : null; string json = payload != null ? SerializeJson(payload) : null;
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
@ -251,13 +250,12 @@ namespace Discord.API
internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();
options.BucketId = bucketId;
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options);
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
@ -526,7 +524,8 @@ namespace Discord.API
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); var ids = new BucketIds(webhookId: webhookId);
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
} }
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
@ -565,7 +564,8 @@ namespace Discord.API
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
} }
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); var ids = new BucketIds(webhookId: webhookId);
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
} }
public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{ {
@ -604,13 +604,8 @@ namespace Discord.API
Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotEqual(messageId, 0, nameof(messageId));
Preconditions.NotNull(args, nameof(args)); Preconditions.NotNull(args, nameof(args));
if (args.Content.IsSpecified) if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
{ throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
if (!args.Embed.IsSpecified)
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));
if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
}
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds(channelId: channelId); var ids = new BucketIds(channelId: channelId);
@ -667,6 +662,18 @@ namespace Discord.API
await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false);
} }
public async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
Preconditions.NotNullOrWhitespace(emoji, nameof(emoji));
options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds(channelId: channelId);
await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null)
{ {
Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(channelId, 0, nameof(channelId));
@ -702,6 +709,15 @@ namespace Discord.API
var ids = new BucketIds(channelId: channelId); var ids = new BucketIds(channelId: channelId);
await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false);
} }
public async Task CrosspostAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId));
options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds(channelId: channelId);
await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/crosspost", ids, options: options).ConfigureAwait(false);
}
//Channel Permissions //Channel Permissions
public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null)
@ -862,8 +878,12 @@ namespace Discord.API
Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds(guildId: guildId); try
return await SendAsync<Ban>("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); {
var ids = new BucketIds(guildId: guildId);
return await SendAsync<Ban>("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
} }
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
/// <paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero. /// <paramref name="guildId"/> and <paramref name="userId"/> must not be equal to zero.
@ -1064,7 +1084,7 @@ namespace Discord.API
{ {
foreach (var roleId in args.RoleIds.Value) foreach (var roleId in args.RoleIds.Value)
Preconditions.NotEqual(roleId, 0, nameof(roleId)); Preconditions.NotEqual(roleId, 0, nameof(roleId));
} }
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
@ -1134,6 +1154,22 @@ namespace Discord.API
await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false); await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false);
} }
} }
public async Task<IReadOnlyCollection<GuildMember>> SearchGuildMembersAsync(ulong guildId, SearchGuildMembersParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit));
Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit));
Preconditions.NotNullOrEmpty(args.Query, nameof(args.Query));
options = RequestOptions.CreateOrClone(options);
int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUsersPerBatch);
string query = args.Query;
var ids = new BucketIds(guildId: guildId);
Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members/search?limit={limit}&query={query}";
return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
}
//Guild Roles //Guild Roles
public async Task<IReadOnlyCollection<Role>> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) public async Task<IReadOnlyCollection<Role>> GetGuildRolesAsync(ulong guildId, RequestOptions options = null)
@ -1144,13 +1180,13 @@ namespace Discord.API
var ids = new BucketIds(guildId: guildId); var ids = new BucketIds(guildId: guildId);
return await SendAsync<IReadOnlyCollection<Role>>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<Role>>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false);
} }
public async Task<Role> CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) public async Task<Role> CreateGuildRoleAsync(ulong guildId, Rest.ModifyGuildRoleParams args, RequestOptions options = null)
{ {
Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
var ids = new BucketIds(guildId: guildId); var ids = new BucketIds(guildId: guildId);
return await SendAsync<Role>("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); return await SendJsonAsync<Role>("POST", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false);
} }
public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null)
{ {
@ -1440,21 +1476,39 @@ namespace Discord.API
{ {
public ulong GuildId { get; internal set; } public ulong GuildId { get; internal set; }
public ulong ChannelId { get; internal set; } public ulong ChannelId { get; internal set; }
public ulong WebhookId { get; internal set; }
public string HttpMethod { get; internal set; }
internal BucketIds(ulong guildId = 0, ulong channelId = 0) internal BucketIds(ulong guildId = 0, ulong channelId = 0, ulong webhookId = 0)
{ {
GuildId = guildId; GuildId = guildId;
ChannelId = channelId; ChannelId = channelId;
WebhookId = webhookId;
} }
internal object[] ToArray() internal object[] ToArray()
=> new object[] { GuildId, ChannelId }; => new object[] { HttpMethod, GuildId, ChannelId, WebhookId };
internal Dictionary<string, string> ToMajorParametersDictionary()
{
var dict = new Dictionary<string, string>();
if (GuildId != 0)
dict["GuildId"] = GuildId.ToString();
if (ChannelId != 0)
dict["ChannelId"] = ChannelId.ToString();
if (WebhookId != 0)
dict["WebhookId"] = WebhookId.ToString();
return dict;
}
internal static int? GetIndex(string name) internal static int? GetIndex(string name)
{ {
switch (name) switch (name)
{ {
case "guildId": return 0; case "httpMethod": return 0;
case "channelId": return 1; case "guildId": return 1;
case "channelId": return 2;
case "webhookId": return 3;
default: default:
return null; return null;
} }
@ -1465,18 +1519,19 @@ namespace Discord.API
{ {
return endpointExpr.Compile()(); return endpointExpr.Compile()();
} }
private static string GetBucketId(BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod) private static BucketId GetBucketId(string httpMethod, BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod)
{ {
ids.HttpMethod = ids.HttpMethod ?? httpMethod;
return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids);
} }
private static Func<BucketIds, string> CreateBucketId(Expression<Func<string>> endpoint) private static Func<BucketIds, BucketId> CreateBucketId(Expression<Func<string>> endpoint)
{ {
try try
{ {
//Is this a constant string? //Is this a constant string?
if (endpoint.Body.NodeType == ExpressionType.Constant) if (endpoint.Body.NodeType == ExpressionType.Constant)
return x => (endpoint.Body as ConstantExpression).Value.ToString(); return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary());
var builder = new StringBuilder(); var builder = new StringBuilder();
var methodCall = endpoint.Body as MethodCallExpression; var methodCall = endpoint.Body as MethodCallExpression;
@ -1513,7 +1568,7 @@ namespace Discord.API
var mappedId = BucketIds.GetIndex(fieldName); var mappedId = BucketIds.GetIndex(fieldName);
if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash if (!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
rightIndex++; rightIndex++;
if (mappedId.HasValue) if (mappedId.HasValue)
@ -1526,7 +1581,7 @@ namespace Discord.API
format = builder.ToString(); format = builder.ToString();
return x => string.Format(format, x.ToArray()); return x => BucketId.Create(x.HttpMethod, string.Format(format, x.ToArray()), x.ToMajorParametersDictionary());
} }
catch (Exception ex) catch (Exception ex)
{ {

@ -27,6 +27,9 @@ namespace Discord.Rest
[ActionType.Unban] = UnbanAuditLogData.Create, [ActionType.Unban] = UnbanAuditLogData.Create,
[ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create,
[ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create,
[ActionType.MemberMoved] = MemberMoveAuditLogData.Create,
[ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create,
[ActionType.BotAdded] = BotAddAuditLogData.Create,
[ActionType.RoleCreated] = RoleCreateAuditLogData.Create, [ActionType.RoleCreated] = RoleCreateAuditLogData.Create,
[ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create,
@ -45,6 +48,9 @@ namespace Discord.Rest
[ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create,
[ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create,
[ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create,
[ActionType.MessagePinned] = MessagePinAuditLogData.Create,
[ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create,
}; };
public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry)

@ -0,0 +1,32 @@
using System.Linq;
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to a adding a bot to a guild.
/// </summary>
public class BotAddAuditLogData : IAuditLogData
{
private BotAddAuditLogData(IUser bot)
{
Target = bot;
}
internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new BotAddAuditLogData(RestUser.Create(discord, userInfo));
}
/// <summary>
/// Gets the bot that was added.
/// </summary>
/// <returns>
/// A user object representing the bot.
/// </returns>
public IUser Target { get; }
}
}

@ -25,7 +25,6 @@ namespace Discord.Rest
internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{ {
var changes = entry.Changes; var changes = entry.Changes;
var overwrites = new List<Overwrite>();
var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
@ -34,23 +33,17 @@ namespace Discord.Rest
var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw");
var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate");
var overwrites = overwritesModel.NewValue.ToObject<API.Overwrite[]>(discord.ApiClient.Serializer)
.Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny)))
.ToList();
var type = typeModel.NewValue.ToObject<ChannelType>(discord.ApiClient.Serializer); var type = typeModel.NewValue.ToObject<ChannelType>(discord.ApiClient.Serializer);
var name = nameModel.NewValue.ToObject<string>(discord.ApiClient.Serializer); var name = nameModel.NewValue.ToObject<string>(discord.ApiClient.Serializer);
int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer); int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer);
bool? nsfw = nsfwModel?.NewValue?.ToObject<bool>(discord.ApiClient.Serializer); bool? nsfw = nsfwModel?.NewValue?.ToObject<bool>(discord.ApiClient.Serializer);
int? bitrate = bitrateModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer); int? bitrate = bitrateModel?.NewValue?.ToObject<int>(discord.ApiClient.Serializer);
var id = entry.TargetId.Value;
foreach (var overwrite in overwritesModel.NewValue) return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection());
{
var deny = overwrite["deny"].ToObject<ulong>(discord.ApiClient.Serializer);
var permType = overwrite["type"].ToObject<PermissionTarget>(discord.ApiClient.Serializer);
var id = overwrite["id"].ToObject<ulong>(discord.ApiClient.Serializer);
var allow = overwrite["allow"].ToObject<ulong>(discord.ApiClient.Serializer);
overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny)));
}
return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection());
} }
/// <summary> /// <summary>
@ -78,7 +71,7 @@ namespace Discord.Rest
/// Gets the current slow-mode delay of the created channel. /// Gets the current slow-mode delay of the created channel.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another /// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled. /// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>
@ -95,7 +88,7 @@ namespace Discord.Rest
/// Gets the bit-rate that the clients in the created voice channel are requested to use. /// Gets the bit-rate that the clients in the created voice channel are requested to use.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the bit-rate (bps) that the created voice channel defines and requests the /// An <see cref="int"/> representing the bit-rate (bps) that the created voice channel defines and requests the
/// client(s) to use. /// client(s) to use.
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>

@ -71,7 +71,7 @@ namespace Discord.Rest
/// Gets the slow-mode delay of the deleted channel. /// Gets the slow-mode delay of the deleted channel.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another /// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled. /// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>
@ -88,7 +88,7 @@ namespace Discord.Rest
/// Gets the bit-rate of this channel if applicable. /// Gets the bit-rate of this channel if applicable.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the bit-rate set of the voice channel. /// An <see cref="int"/> representing the bit-rate set of the voice channel.
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>
public int? Bitrate { get; } public int? Bitrate { get; }

@ -32,7 +32,7 @@ namespace Discord.Rest
/// Gets the current slow-mode delay of this channel. /// Gets the current slow-mode delay of this channel.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another /// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled. /// message; <c>0</c> if disabled.
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>
@ -49,7 +49,7 @@ namespace Discord.Rest
/// Gets the bit-rate of this channel if applicable. /// Gets the bit-rate of this channel if applicable.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// An <see cref="Int32"/> representing the bit-rate set for the voice channel; /// An <see cref="int"/> representing the bit-rate set for the voice channel;
/// <c>null</c> if this is not mentioned in this entry. /// <c>null</c> if this is not mentioned in this entry.
/// </returns> /// </returns>
public int? Bitrate { get; } public int? Bitrate { get; }

@ -36,13 +36,17 @@ namespace Discord.Rest
var maxAge = maxAgeModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var maxAge = maxAgeModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);
var code = codeModel.NewValue.ToObject<string>(discord.ApiClient.Serializer); var code = codeModel.NewValue.ToObject<string>(discord.ApiClient.Serializer);
var temporary = temporaryModel.NewValue.ToObject<bool>(discord.ApiClient.Serializer); var temporary = temporaryModel.NewValue.ToObject<bool>(discord.ApiClient.Serializer);
var inviterId = inviterIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var channelId = channelIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer); var channelId = channelIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var uses = usesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var uses = usesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);
var maxUses = maxUsesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer); var maxUses = maxUsesModel.NewValue.ToObject<int>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); RestUser inviter = null;
var inviter = RestUser.Create(discord, inviterInfo); if (inviterIdModel != null)
{
var inviterId = inviterIdModel.NewValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
}
return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
} }
@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns> /// </returns>
public bool Temporary { get; } public bool Temporary { get; }
/// <summary> /// <summary>
/// Gets the user that created this invite. /// Gets the user that created this invite if available.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A user that created this invite. /// A user that created this invite or <see langword="null"/>.
/// </returns> /// </returns>
public IUser Creator { get; } public IUser Creator { get; }
/// <summary> /// <summary>

@ -36,13 +36,17 @@ namespace Discord.Rest
var maxAge = maxAgeModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var maxAge = maxAgeModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);
var code = codeModel.OldValue.ToObject<string>(discord.ApiClient.Serializer); var code = codeModel.OldValue.ToObject<string>(discord.ApiClient.Serializer);
var temporary = temporaryModel.OldValue.ToObject<bool>(discord.ApiClient.Serializer); var temporary = temporaryModel.OldValue.ToObject<bool>(discord.ApiClient.Serializer);
var inviterId = inviterIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var channelId = channelIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer); var channelId = channelIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var uses = usesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var uses = usesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);
var maxUses = maxUsesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer); var maxUses = maxUsesModel.OldValue.ToObject<int>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); RestUser inviter = null;
var inviter = RestUser.Create(discord, inviterInfo); if (inviterIdModel != null)
{
var inviterId = inviterIdModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
inviter = RestUser.Create(discord, inviterInfo);
}
return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
} }
@ -70,10 +74,10 @@ namespace Discord.Rest
/// </returns> /// </returns>
public bool Temporary { get; } public bool Temporary { get; }
/// <summary> /// <summary>
/// Gets the user that created this invite. /// Gets the user that created this invite if available.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A user that created this invite. /// A user that created this invite or <see langword="null"/>.
/// </returns> /// </returns>
public IUser Creator { get; } public IUser Creator { get; }
/// <summary> /// <summary>

@ -0,0 +1,29 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to disconnecting members from voice channels.
/// </summary>
public class MemberDisconnectAuditLogData : IAuditLogData
{
private MemberDisconnectAuditLogData(int count)
{
MemberCount = count;
}
internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MemberDisconnectAuditLogData(entry.Options.Count.Value);
}
/// <summary>
/// Gets the number of members that were disconnected.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of members that were disconnected from a voice channel.
/// </returns>
public int MemberCount { get; }
}
}

@ -0,0 +1,37 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to moving members between voice channels.
/// </summary>
public class MemberMoveAuditLogData : IAuditLogData
{
private MemberMoveAuditLogData(ulong channelId, int count)
{
ChannelId = channelId;
MemberCount = count;
}
internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value);
}
/// <summary>
/// Gets the ID of the channel that the members were moved to.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the members were moved to.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the number of members that were moved.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of members that were moved to another voice channel.
/// </returns>
public int MemberCount { get; }
}
}

@ -0,0 +1,38 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to message deletion(s).
/// </summary>
public class MessageBulkDeleteAuditLogData : IAuditLogData
{
private MessageBulkDeleteAuditLogData(ulong channelId, int count)
{
ChannelId = channelId;
MessageCount = count;
}
internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value);
}
/// <summary>
/// Gets the ID of the channel that the messages were deleted from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the messages were
/// deleted from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the number of messages that were deleted.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the number of messages that were deleted from the channel.
/// </returns>
public int MessageCount { get; }
}
}

@ -1,3 +1,5 @@
using System.Linq;
using Model = Discord.API.AuditLog; using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry; using EntryModel = Discord.API.AuditLogEntry;
@ -8,16 +10,17 @@ namespace Discord.Rest
/// </summary> /// </summary>
public class MessageDeleteAuditLogData : IAuditLogData public class MessageDeleteAuditLogData : IAuditLogData
{ {
private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId) private MessageDeleteAuditLogData(ulong channelId, int count, IUser user)
{ {
ChannelId = channelId; ChannelId = channelId;
MessageCount = count; MessageCount = count;
AuthorId = authorId; Target = user;
} }
internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{ {
return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value); var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo));
} }
/// <summary> /// <summary>
@ -36,11 +39,11 @@ namespace Discord.Rest
/// </returns> /// </returns>
public ulong ChannelId { get; } public ulong ChannelId { get; }
/// <summary> /// <summary>
/// Gets the author of the messages that were deleted. /// Gets the user of the messages that were deleted.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the user that created the deleted messages. /// A user object representing the user that created the deleted messages.
/// </returns> /// </returns>
public ulong AuthorId { get; } public IUser Target { get; }
} }
} }

@ -0,0 +1,48 @@
using System.Linq;
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to a pinned message.
/// </summary>
public class MessagePinAuditLogData : IAuditLogData
{
private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user)
{
MessageId = messageId;
ChannelId = channelId;
Target = user;
}
internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
}
/// <summary>
/// Gets the ID of the messages that was pinned.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the messages that was pinned.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the ID of the channel that the message was pinned from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the message was pinned from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was pinned.
/// </summary>
/// <returns>
/// A user object representing the user that created the pinned message.
/// </returns>
public IUser Target { get; }
}
}

@ -0,0 +1,48 @@
using System.Linq;
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
namespace Discord.Rest
{
/// <summary>
/// Contains a piece of audit log data related to an unpinned message.
/// </summary>
public class MessageUnpinAuditLogData : IAuditLogData
{
private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user)
{
MessageId = messageId;
ChannelId = channelId;
Target = user;
}
internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo));
}
/// <summary>
/// Gets the ID of the messages that was unpinned.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the messages that was unpinned.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the ID of the channel that the message was unpinned from.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the channel that the message was unpinned from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the user of the message that was unpinned.
/// </summary>
/// <returns>
/// A user object representing the user that created the unpinned message.
/// </returns>
public IUser Target { get; }
}
}

@ -21,16 +21,17 @@ namespace Discord.Rest
var changes = entry.Changes; var changes = entry.Changes;
var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id");
var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");
var deny = denyModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer); var deny = denyModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var type = typeModel.OldValue.ToObject<PermissionTarget>(discord.ApiClient.Serializer);
var id = idModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
var allow = allowModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer); var allow = allowModel.OldValue.ToObject<ulong>(discord.ApiClient.Serializer);
return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny))); var permissions = new OverwritePermissions(allow, deny);
var id = entry.Options.OverwriteTargetId.Value;
var type = entry.Options.OverwriteType;
return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions));
} }
/// <summary> /// <summary>

@ -28,7 +28,16 @@ namespace Discord.Rest
{ {
Name = args.Name, Name = args.Name,
Position = args.Position, Position = args.Position,
CategoryId = args.CategoryId CategoryId = args.CategoryId,
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@ -46,6 +55,15 @@ namespace Discord.Rest
Topic = args.Topic, Topic = args.Topic,
IsNsfw = args.IsNsfw, IsNsfw = args.IsNsfw,
SlowModeInterval = args.SlowModeInterval, SlowModeInterval = args.SlowModeInterval,
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@ -61,7 +79,16 @@ namespace Discord.Rest
Name = args.Name, Name = args.Name,
Position = args.Position, Position = args.Position,
CategoryId = args.CategoryId, CategoryId = args.CategoryId,
UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create<int>() UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create<int>(),
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
} }
@ -109,12 +136,19 @@ namespace Discord.Rest
public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client,
ulong? fromMessageId, Direction dir, int limit, RequestOptions options) ulong? fromMessageId, Direction dir, int limit, RequestOptions options)
{ {
if (dir == Direction.Around)
throw new NotImplementedException(); //TODO: Impl
var guildId = (channel as IGuildChannel)?.GuildId; var guildId = (channel as IGuildChannel)?.GuildId;
var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null;
if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch)
{
int around = limit / 2;
if (fromMessageId.HasValue)
return GetMessagesAsync(channel, client, fromMessageId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself
.Concat(GetMessagesAsync(channel, client, fromMessageId, Direction.After, around, options));
else //Shouldn't happen since there's no public overload for ulong? and Direction
return GetMessagesAsync(channel, client, null, Direction.Before, around + 1, options);
}
return new PagedAsyncEnumerable<RestMessage>( return new PagedAsyncEnumerable<RestMessage>(
DiscordConfig.MaxMessagesPerBatch, DiscordConfig.MaxMessagesPerBatch,
async (info, ct) => async (info, ct) =>
@ -167,9 +201,28 @@ namespace Discord.Rest
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, public static async Task<RestUserMessage> SendMessageAsync(IMessageChannel channel, BaseDiscordClient client,
string text, bool isTTS, Embed embed, RequestOptions options) string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options)
{ {
var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; 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() };
var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model); return RestUserMessage.Create(client, channel, client.CurrentUser, model);
} }
@ -199,18 +252,37 @@ namespace Discord.Rest
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
string filename = Path.GetFileName(filePath); string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath)) using (var file = File.OpenRead(filePath))
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, options, isSpoiler).ConfigureAwait(false);
} }
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
{ {
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional<API.Embed>.Unspecified, IsSpoiler = isSpoiler }; 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 UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() ?? Optional<API.Embed>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, IsSpoiler = isSpoiler };
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model); return RestUserMessage.Create(client, channel, client.CurrentUser, model);
} }
@ -368,7 +440,8 @@ namespace Discord.Rest
var apiArgs = new ModifyGuildChannelParams var apiArgs = new ModifyGuildChannelParams
{ {
Overwrites = category.PermissionOverwrites Overwrites = category.PermissionOverwrites
.Select(overwrite => new API.Overwrite{ .Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId, TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType, TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue, Allow = overwrite.Permissions.AllowValue,

@ -20,17 +20,21 @@ namespace Discord.Rest
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in /// This method follows the same behavior as described in
/// <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>. Please visit /// <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>. Please visit
/// its documentation for more details on this method. /// its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="filePath">The file path of the file.</param> /// <param name="filePath">The file path of the file.</param>
@ -38,16 +42,21 @@ namespace Discord.Rest
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool)"/>. /// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param> /// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@ -56,11 +65,16 @@ namespace Discord.Rest
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Gets a message from this message channel. /// Gets a message from this message channel.

@ -93,8 +93,8 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
@ -121,12 +121,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@ -200,14 +200,14 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
//IChannel //IChannel
/// <inheritdoc /> /// <inheritdoc />

@ -95,8 +95,8 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
@ -123,12 +123,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
@ -178,13 +178,14 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
//IAudioChannel //IAudioChannel
/// <inheritdoc /> /// <inheritdoc />

@ -42,7 +42,8 @@ namespace Discord.Rest
base.Update(model); base.Update(model);
CategoryId = model.CategoryId; CategoryId = model.CategoryId;
Topic = model.Topic.Value; Topic = model.Topic.Value;
SlowModeInterval = model.SlowMode.Value; if (model.SlowMode.IsSpecified)
SlowModeInterval = model.SlowMode.Value;
IsNsfw = model.Nsfw.GetValueOrDefault(); IsNsfw = model.Nsfw.GetValueOrDefault();
} }
@ -101,8 +102,8 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
@ -129,13 +130,13 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception> /// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception> /// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@ -266,15 +267,15 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
//IGuildChannel //IGuildChannel
/// <inheritdoc /> /// <inheritdoc />

@ -132,7 +132,7 @@ namespace Discord.Rest
public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options)
{ {
var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false);
return RestBan.Create(client, model); return model == null ? null : RestBan.Create(client, model);
} }
public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client,
@ -176,7 +176,17 @@ namespace Discord.Rest
CategoryId = props.CategoryId, CategoryId = props.CategoryId,
Topic = props.Topic, Topic = props.Topic,
IsNsfw = props.IsNsfw, IsNsfw = props.IsNsfw,
Position = props.Position Position = props.Position,
SlowModeInterval = props.SlowModeInterval,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestTextChannel.Create(client, guild, model); return RestTextChannel.Create(client, guild, model);
@ -195,7 +205,16 @@ namespace Discord.Rest
CategoryId = props.CategoryId, CategoryId = props.CategoryId,
Bitrate = props.Bitrate, Bitrate = props.Bitrate,
UserLimit = props.UserLimit, UserLimit = props.UserLimit,
Position = props.Position Position = props.Position,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestVoiceChannel.Create(client, guild, model); return RestVoiceChannel.Create(client, guild, model);
@ -211,7 +230,16 @@ namespace Discord.Rest
var args = new CreateGuildChannelParams(name, ChannelType.Category) var args = new CreateGuildChannelParams(name, ChannelType.Category)
{ {
Position = props.Position Position = props.Position,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue,
Deny = overwrite.Permissions.DenyValue
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
}; };
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
@ -264,19 +292,18 @@ namespace Discord.Rest
{ {
if (name == null) throw new ArgumentNullException(paramName: nameof(name)); if (name == null) throw new ArgumentNullException(paramName: nameof(name));
var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams
var role = RestRole.Create(client, guild, model);
await role.ModifyAsync(x =>
{ {
x.Name = name; Color = color?.RawValue ?? Optional.Create<uint>(),
x.Permissions = (permissions ?? role.Permissions); Hoist = isHoisted,
x.Color = (color ?? Color.Default); Mentionable = isMentionable,
x.Hoist = isHoisted; Name = name,
x.Mentionable = isMentionable; Permissions = permissions?.RawValue ?? Optional.Create<ulong>()
}, options).ConfigureAwait(false); };
return role; var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false);
return RestRole.Create(client, guild, model);
} }
//Users //Users
@ -387,6 +414,17 @@ namespace Discord.Rest
model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false);
return model.Pruned; return model.Pruned;
} }
public static async Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(IGuild guild, BaseDiscordClient client,
string query, int? limit, RequestOptions options)
{
var apiArgs = new SearchGuildMembersParams
{
Query = query,
Limit = limit ?? Optional.Create<int>()
};
var models = await client.ApiClient.SearchGuildMembersAsync(guild.Id, apiArgs, options).ConfigureAwait(false);
return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray();
}
// Audit logs // Audit logs
public static IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, public static IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client,

@ -634,6 +634,23 @@ namespace Discord.Rest
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
/// provided <see cref="string"/> at <paramref name="query"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
public Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null)
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);
//Audit logs //Audit logs
/// <summary> /// <summary>
/// Gets the specified number of audit log entries for this guild. /// Gets the specified number of audit log entries for this guild.
@ -884,6 +901,14 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception> /// <exception cref="NotSupportedException">Downloading users is not supported for a REST-based guild.</exception>
Task IGuild.DownloadUsersAsync() => Task IGuild.DownloadUsersAsync() =>
throw new NotSupportedException(); throw new NotSupportedException();
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await SearchUsersAsync(query, limit, options).ConfigureAwait(false);
else
return ImmutableArray.Create<IGuildUser>();
}
async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options,
ulong? beforeId, ulong? userId, ActionType? actionType) ulong? beforeId, ulong? userId, ActionType? actionType)

@ -0,0 +1,15 @@
using Newtonsoft.Json;
namespace Discord.API
{
public class AllowedMentions
{
[JsonProperty("parse")]
public Optional<string[]> Parse { get; set; }
// Roles and Users have a max size of 100
[JsonProperty("roles")]
public Optional<ulong[]> Roles { get; set; }
[JsonProperty("users")]
public Optional<ulong[]> Users { get; set; }
}
}

@ -32,6 +32,11 @@ namespace Discord.Rest
var args = new MessageProperties(); var args = new MessageProperties();
func(args); func(args);
bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content);
bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any();
if (!hasText && !hasEmbed)
Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content));
var apiArgs = new API.Rest.ModifyMessageParams var apiArgs = new API.Rest.ModifyMessageParams
{ {
Content = args.Content, Content = args.Content,
@ -39,8 +44,10 @@ namespace Discord.Rest
}; };
return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false);
} }
public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
=> DeleteAsync(msg.Channel.Id, msg.Id, client, options); => DeleteAsync(msg.Channel.Id, msg.Id, client, options);
public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client, public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client,
RequestOptions options) RequestOptions options)
{ {
@ -71,6 +78,11 @@ namespace Discord.Rest
await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false);
} }
public static async Task RemoveAllReactionsForEmoteAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options)
{
await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false);
}
public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote, public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote,
int? limit, BaseDiscordClient client, RequestOptions options) int? limit, BaseDiscordClient client, RequestOptions options)
{ {
@ -110,6 +122,7 @@ namespace Discord.Rest
{ {
await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false);
} }
public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client,
RequestOptions options) RequestOptions options)
{ {
@ -235,6 +248,7 @@ namespace Discord.Rest
return tags.ToImmutable(); return tags.ToImmutable();
} }
private static int? FindIndex(IReadOnlyList<ITag> tags, int index) private static int? FindIndex(IReadOnlyList<ITag> tags, int index)
{ {
int i = 0; int i = 0;
@ -248,6 +262,7 @@ namespace Discord.Rest
return null; //Overlaps tag before this return null; //Overlaps tag before this
return i; return i;
} }
public static ImmutableArray<ulong> FilterTagsByKey(TagType type, ImmutableArray<ITag> tags) public static ImmutableArray<ulong> FilterTagsByKey(TagType type, ImmutableArray<ITag> tags)
{ {
return tags return tags
@ -255,6 +270,7 @@ namespace Discord.Rest
.Select(x => x.Key) .Select(x => x.Key)
.ToImmutableArray(); .ToImmutableArray();
} }
public static ImmutableArray<T> FilterTagsByValue<T>(TagType type, ImmutableArray<ITag> tags) public static ImmutableArray<T> FilterTagsByValue<T>(TagType type, ImmutableArray<ITag> tags)
{ {
return tags return tags
@ -274,5 +290,14 @@ namespace Discord.Rest
return MessageSource.Bot; return MessageSource.Bot;
return MessageSource.User; return MessageSource.User;
} }
public static Task CrosspostAsync(IMessage msg, BaseDiscordClient client, RequestOptions options)
=> CrosspostAsync(msg.Channel.Id, msg.Id, client, options);
public static async Task CrosspostAsync(ulong channelId, ulong msgId, BaseDiscordClient client,
RequestOptions options)
{
await client.ApiClient.CrosspostAsync(channelId, msgId, options).ConfigureAwait(false);
}
} }
} }

@ -37,6 +37,9 @@ namespace Discord.Rest
public virtual bool IsSuppressed => false; public virtual bool IsSuppressed => false;
/// <inheritdoc /> /// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null; public virtual DateTimeOffset? EditedTimestamp => null;
/// <inheritdoc />
public virtual bool MentionedEveryone => false;
/// <summary> /// <summary>
/// Gets a collection of the <see cref="Attachment"/>'s on the message. /// Gets a collection of the <see cref="Attachment"/>'s on the message.
/// </summary> /// </summary>
@ -165,7 +168,7 @@ namespace Discord.Rest
IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds; IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds;
/// <inheritdoc /> /// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me });
@ -182,6 +185,9 @@ namespace Discord.Rest
public Task RemoveAllReactionsAsync(RequestOptions options = null) public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options); => MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc /> /// <inheritdoc />
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
} }

@ -18,6 +18,8 @@ namespace Discord.Rest
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<ulong> _roleMentionIds = ImmutableArray.Create<ulong>();
private ImmutableArray<RestUser> _userMentions = ImmutableArray.Create<RestUser>();
/// <inheritdoc /> /// <inheritdoc />
public override bool IsTTS => _isTTS; public override bool IsTTS => _isTTS;
@ -28,15 +30,17 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc /> /// <inheritdoc />
public override bool MentionedEveryone => _isMentioningEveryone;
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments; public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds; public override IReadOnlyCollection<Embed> Embeds => _embeds;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); public override IReadOnlyCollection<ulong> MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags);
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ulong> MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection<ulong> MentionedRoleIds => _roleMentionIds;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<RestUser> MentionedUsers => MessageHelper.FilterTagsByValue<RestUser>(TagType.UserMention, _tags); public override IReadOnlyCollection<RestUser> MentionedUsers => _userMentions;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<ITag> Tags => _tags; public override IReadOnlyCollection<ITag> Tags => _tags;
@ -67,6 +71,8 @@ namespace Discord.Rest
{ {
_isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed);
} }
if (model.RoleMentions.IsSpecified)
_roleMentionIds = model.RoleMentions.Value.ToImmutableArray();
if (model.Attachments.IsSpecified) if (model.Attachments.IsSpecified)
{ {
@ -96,20 +102,19 @@ namespace Discord.Rest
_embeds = ImmutableArray.Create<Embed>(); _embeds = ImmutableArray.Create<Embed>();
} }
ImmutableArray<IUser> mentions = ImmutableArray.Create<IUser>();
if (model.UserMentions.IsSpecified) if (model.UserMentions.IsSpecified)
{ {
var value = model.UserMentions.Value; var value = model.UserMentions.Value;
if (value.Length > 0) if (value.Length > 0)
{ {
var newMentions = ImmutableArray.CreateBuilder<IUser>(value.Length); var newMentions = ImmutableArray.CreateBuilder<RestUser>(value.Length);
for (int i = 0; i < value.Length; i++) for (int i = 0; i < value.Length; i++)
{ {
var val = value[i]; var val = value[i];
if (val.Object != null) if (val.Object != null)
newMentions.Add(RestUser.Create(Discord, val.Object)); newMentions.Add(RestUser.Create(Discord, val.Object));
} }
mentions = newMentions.ToImmutable(); _userMentions = newMentions.ToImmutable();
} }
} }
@ -118,7 +123,7 @@ namespace Discord.Rest
var text = model.Content.Value; var text = model.Content.Value;
var guildId = (Channel as IGuildChannel)?.GuildId; var guildId = (Channel as IGuildChannel)?.GuildId;
var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null;
_tags = MessageHelper.ParseTags(text, null, guild, mentions); _tags = MessageHelper.ParseTags(text, null, guild, _userMentions);
model.Content = text; model.Content = text;
} }
} }
@ -148,6 +153,18 @@ namespace Discord.Rest
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="RestNewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is RestNewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}
await MessageHelper.CrosspostAsync(this, Discord, options);
}
private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})";
} }
} }

@ -35,6 +35,8 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty;
/// <inheritdoc />
public virtual bool IsWebhook => false; public virtual bool IsWebhook => false;
internal RestUser(BaseDiscordClient discord, ulong id) internal RestUser(BaseDiscordClient discord, ulong id)

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
@ -60,6 +61,24 @@ namespace Discord.Rest
model.Video = entity.Video.Value.ToModel(); model.Video = entity.Video.Value.ToModel();
return model; return model;
} }
public static API.AllowedMentions ToModel(this AllowedMentions entity)
{
return new API.AllowedMentions()
{
Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(),
Roles = entity.RoleIds?.ToArray(),
Users = entity.UserIds?.ToArray(),
};
}
public static IEnumerable<string> EnumerateMentionTypes(this AllowedMentionTypes mentionTypes)
{
if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone))
yield return "everyone";
if (mentionTypes.HasFlag(AllowedMentionTypes.Roles))
yield return "roles";
if (mentionTypes.HasFlag(AllowedMentionTypes.Users))
yield return "users";
}
public static EmbedAuthor ToEntity(this API.EmbedAuthor model) public static EmbedAuthor ToEntity(this API.EmbedAuthor model)
{ {
return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl);

@ -10,14 +10,14 @@ namespace Discord.Net.Queue
internal struct ClientBucket internal struct ClientBucket
{ {
private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType; private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType;
private static readonly ImmutableDictionary<string, ClientBucket> DefsById; private static readonly ImmutableDictionary<BucketId, ClientBucket> DefsById;
static ClientBucket() static ClientBucket()
{ {
var buckets = new[] var buckets = new[]
{ {
new ClientBucket(ClientBucketType.Unbucketed, "<unbucketed>", 10, 10), new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "<unbucketed>", null), 10, 10),
new ClientBucket(ClientBucketType.SendEdit, "<send_edit>", 10, 10) new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "<send_edit>", null), 10, 10)
}; };
var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>(); var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>();
@ -25,21 +25,21 @@ namespace Discord.Net.Queue
builder.Add(bucket.Type, bucket); builder.Add(bucket.Type, bucket);
DefsByType = builder.ToImmutable(); DefsByType = builder.ToImmutable();
var builder2 = ImmutableDictionary.CreateBuilder<string, ClientBucket>(); var builder2 = ImmutableDictionary.CreateBuilder<BucketId, ClientBucket>();
foreach (var bucket in buckets) foreach (var bucket in buckets)
builder2.Add(bucket.Id, bucket); builder2.Add(bucket.Id, bucket);
DefsById = builder2.ToImmutable(); DefsById = builder2.ToImmutable();
} }
public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; public static ClientBucket Get(ClientBucketType type) => DefsByType[type];
public static ClientBucket Get(string id) => DefsById[id]; public static ClientBucket Get(BucketId id) => DefsById[id];
public ClientBucketType Type { get; } public ClientBucketType Type { get; }
public string Id { get; } public BucketId Id { get; }
public int WindowCount { get; } public int WindowCount { get; }
public int WindowSeconds { get; } public int WindowSeconds { get; }
public ClientBucket(ClientBucketType type, string id, int count, int seconds) public ClientBucket(ClientBucketType type, BucketId id, int count, int seconds)
{ {
Type = type; Type = type;
Id = id; Id = id;

@ -12,9 +12,9 @@ namespace Discord.Net.Queue
{ {
internal class RequestQueue : IDisposable internal class RequestQueue : IDisposable
{ {
public event Func<string, RateLimitInfo?, Task> RateLimitTriggered; public event Func<BucketId, RateLimitInfo?, Task> RateLimitTriggered;
private readonly ConcurrentDictionary<string, RequestBucket> _buckets; private readonly ConcurrentDictionary<BucketId, object> _buckets;
private readonly SemaphoreSlim _tokenLock; private readonly SemaphoreSlim _tokenLock;
private readonly CancellationTokenSource _cancelTokenSource; //Dispose token private readonly CancellationTokenSource _cancelTokenSource; //Dispose token
private CancellationTokenSource _clearToken; private CancellationTokenSource _clearToken;
@ -34,7 +34,7 @@ namespace Discord.Net.Queue
_requestCancelToken = CancellationToken.None; _requestCancelToken = CancellationToken.None;
_parentToken = CancellationToken.None; _parentToken = CancellationToken.None;
_buckets = new ConcurrentDictionary<string, RequestBucket>(); _buckets = new ConcurrentDictionary<BucketId, object>();
_cleanupTask = RunCleanup(); _cleanupTask = RunCleanup();
} }
@ -82,7 +82,7 @@ namespace Discord.Net.Queue
else else
request.Options.CancelToken = _requestCancelToken; request.Options.CancelToken = _requestCancelToken;
var bucket = GetOrCreateBucket(request.Options.BucketId, request); var bucket = GetOrCreateBucket(request.Options, request);
var result = await bucket.SendAsync(request).ConfigureAwait(false); var result = await bucket.SendAsync(request).ConfigureAwait(false);
createdTokenSource?.Dispose(); createdTokenSource?.Dispose();
return result; return result;
@ -110,14 +110,32 @@ namespace Discord.Net.Queue
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0));
} }
private RequestBucket GetOrCreateBucket(string id, RestRequest request) private RequestBucket GetOrCreateBucket(RequestOptions options, RestRequest request)
{ {
return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); var bucketId = options.BucketId;
object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x));
if (obj is BucketId hashBucket)
{
options.BucketId = hashBucket;
return (RequestBucket)_buckets.GetOrAdd(hashBucket, x => new RequestBucket(this, request, x));
}
return (RequestBucket)obj;
} }
internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info) internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info)
{ {
await RateLimitTriggered(bucketId, info).ConfigureAwait(false); await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
} }
internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash)
{
if (!id.IsHashBucket)
{
var bucket = BucketId.Create(discordHash, id);
var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]);
_buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket);
return (hashReqQueue, bucket);
}
return (null, null);
}
private async Task RunCleanup() private async Task RunCleanup()
{ {
@ -126,10 +144,15 @@ namespace Discord.Net.Queue
while (!_cancelTokenSource.IsCancellationRequested) while (!_cancelTokenSource.IsCancellationRequested)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
foreach (var bucket in _buckets.Select(x => x.Value)) foreach (var bucket in _buckets.Where(x => x.Value is RequestBucket).Select(x => (RequestBucket)x.Value))
{ {
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
{
if (bucket.Id.IsHashBucket)
foreach (var redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value))
_buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket
_buckets.TryRemove(bucket.Id, out _); _buckets.TryRemove(bucket.Id, out _);
}
} }
await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute
} }

@ -13,16 +13,19 @@ namespace Discord.Net.Queue
{ {
internal class RequestBucket internal class RequestBucket
{ {
private const int MinimumSleepTimeMs = 750;
private readonly object _lock; private readonly object _lock;
private readonly RequestQueue _queue; private readonly RequestQueue _queue;
private int _semaphore; private int _semaphore;
private DateTimeOffset? _resetTick; private DateTimeOffset? _resetTick;
private RequestBucket _redirectBucket;
public string Id { get; private set; } public BucketId Id { get; private set; }
public int WindowCount { get; private set; } public int WindowCount { get; private set; }
public DateTimeOffset LastAttemptAt { get; private set; } public DateTimeOffset LastAttemptAt { get; private set; }
public RequestBucket(RequestQueue queue, RestRequest request, string id) public RequestBucket(RequestQueue queue, RestRequest request, BucketId id)
{ {
_queue = queue; _queue = queue;
Id = id; Id = id;
@ -30,14 +33,14 @@ namespace Discord.Net.Queue
_lock = new object(); _lock = new object();
if (request.Options.IsClientBucket) if (request.Options.IsClientBucket)
WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; WindowCount = ClientBucket.Get(Id).WindowCount;
else else
WindowCount = 1; //Only allow one request until we get a header back WindowCount = 1; //Only allow one request until we get a header back
_semaphore = WindowCount; _semaphore = WindowCount;
_resetTick = null; _resetTick = null;
LastAttemptAt = DateTimeOffset.UtcNow; LastAttemptAt = DateTimeOffset.UtcNow;
} }
static int nextId = 0; static int nextId = 0;
public async Task<Stream> SendAsync(RestRequest request) public async Task<Stream> SendAsync(RestRequest request)
{ {
@ -50,6 +53,8 @@ namespace Discord.Net.Queue
{ {
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
await EnterAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false);
if (_redirectBucket != null)
return await _redirectBucket.SendAsync(request);
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sending..."); Debug.WriteLine($"[{id}] Sending...");
@ -158,6 +163,9 @@ namespace Discord.Net.Queue
while (true) while (true)
{ {
if (_redirectBucket != null)
break;
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{ {
if (!isRateLimited) if (!isRateLimited)
@ -173,7 +181,8 @@ namespace Discord.Net.Queue
} }
DateTimeOffset? timeoutAt = request.TimeoutAt; DateTimeOffset? timeoutAt = request.TimeoutAt;
if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0) int semaphore = Interlocked.Decrement(ref _semaphore);
if (windowCount > 0 && semaphore < 0)
{ {
if (!isRateLimited) if (!isRateLimited)
{ {
@ -183,10 +192,11 @@ namespace Discord.Net.Queue
ThrowRetryLimit(request); ThrowRetryLimit(request);
if (resetAt.HasValue) if (resetAt.HasValue && resetAt > DateTimeOffset.UtcNow)
{ {
if (resetAt > timeoutAt) if (resetAt > timeoutAt)
ThrowRetryLimit(request); ThrowRetryLimit(request);
int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds);
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
@ -196,31 +206,63 @@ namespace Discord.Net.Queue
} }
else else
{ {
if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < MinimumSleepTimeMs)
ThrowRetryLimit(request); ThrowRetryLimit(request);
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)");
#endif #endif
await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); await Task.Delay(MinimumSleepTimeMs, request.Options.CancelToken).ConfigureAwait(false);
} }
continue; continue;
} }
#if DEBUG_LIMITS #if DEBUG_LIMITS
else else
Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)");
#endif #endif
break; break;
} }
} }
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429, bool redirected = false)
{ {
if (WindowCount == 0) if (WindowCount == 0)
return; return;
lock (_lock) lock (_lock)
{ {
if (redirected)
{
Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Decrease Semaphore");
#endif
}
bool hasQueuedReset = _resetTick != null; bool hasQueuedReset = _resetTick != null;
if (info.Bucket != null && !redirected)
{
(RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket);
if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null))
{
if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue
{
Id = hashBucket.Item2;
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})");
#endif
}
else
{
_redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything
_redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}");
#endif
return;
}
}
}
if (info.Limit.HasValue && WindowCount != info.Limit.Value) if (info.Limit.HasValue && WindowCount != info.Limit.Value)
{ {
WindowCount = info.Limit.Value; WindowCount = info.Limit.Value;
@ -230,7 +272,6 @@ namespace Discord.Net.Queue
#endif #endif
} }
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
DateTimeOffset? resetTick = null; DateTimeOffset? resetTick = null;
//Using X-RateLimit-Remaining causes a race condition //Using X-RateLimit-Remaining causes a race condition
@ -247,16 +288,18 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
#endif #endif
} }
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
{ {
resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value);
} #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)");
#endif
}
else if (info.Reset.HasValue) else if (info.Reset.HasValue)
{ {
resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0);
/* millisecond precision makes this unnecessary, retaining in case of regression /* millisecond precision makes this unnecessary, retaining in case of regression
if (request.Options.IsReactionBucket) if (request.Options.IsReactionBucket)
resetTick = DateTimeOffset.Now.AddMilliseconds(250); resetTick = DateTimeOffset.Now.AddMilliseconds(250);
*/ */
@ -266,17 +309,17 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
#endif #endif
} }
else if (request.Options.IsClientBucket && request.Options.BucketId != null) else if (request.Options.IsClientBucket && Id != null)
{ {
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds);
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)");
#endif #endif
} }
if (resetTick == null) if (resetTick == null)
{ {
WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) WindowCount = 0; //No rate limit info, disable limits on this bucket
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Disabled Semaphore"); Debug.WriteLine($"[{id}] Disabled Semaphore");
#endif #endif

@ -11,7 +11,8 @@ namespace Discord.Net
public int? Remaining { get; } public int? Remaining { get; }
public int? RetryAfter { get; } public int? RetryAfter { get; }
public DateTimeOffset? Reset { get; } public DateTimeOffset? Reset { get; }
public TimeSpan? ResetAfter { get; } public TimeSpan? ResetAfter { get; }
public string Bucket { get; }
public TimeSpan? Lag { get; } public TimeSpan? Lag { get; }
internal RateLimitInfo(Dictionary<string, string> headers) internal RateLimitInfo(Dictionary<string, string> headers)
@ -26,8 +27,9 @@ namespace Discord.Net
double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null;
RetryAfter = headers.TryGetValue("Retry-After", out temp) && RetryAfter = headers.TryGetValue("Retry-After", out temp) &&
int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null;
ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) &&
float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
Bucket = headers.TryGetValue("X-RateLimit-Bucket", out temp) ? temp : null;
Lag = headers.TryGetValue("Date", out temp) && Lag = headers.TryGetValue("Date", out temp) &&
DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null;
} }

@ -1,4 +1,4 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
@ -17,5 +17,7 @@ namespace Discord.API.Gateway
public Optional<int[]> ShardingParams { get; set; } public Optional<int[]> ShardingParams { get; set; }
[JsonProperty("guild_subscriptions")] [JsonProperty("guild_subscriptions")]
public Optional<bool> GuildSubscriptions { get; set; } public Optional<bool> GuildSubscriptions { get; set; }
[JsonProperty("intents")]
public Optional<int> Intents { get; set; }
} }
} }

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class RemoveAllReactionsForEmoteEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
[JsonProperty("message_id")]
public ulong MessageId { get; set; }
[JsonProperty("emoji")]
public Emoji Emoji { get; set; }
}
}

@ -1,5 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]

@ -234,6 +234,28 @@ namespace Discord.WebSocket
remove { _reactionsClearedEvent.Remove(value); } remove { _reactionsClearedEvent.Remove(value); }
} }
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>>(); internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task>>();
/// <summary>
/// Fired when all reactions to a message with a specific emote are removed.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when all reactions to a message with a specific emote are removed.
/// The event handler must return a <see cref="Task"/> and accept a <see cref="ISocketMessageChannel"/> and
/// a <see cref="IEmote"/> as its parameters.
/// </para>
/// <para>
/// The channel where this message was sent will be passed into the <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// <para>
/// The emoji that all reactions had and were removed will be passed into the <see cref="IEmote"/> parameter.
/// </para>
/// </remarks>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task> ReactionsRemovedForEmote
{
add { _reactionsRemovedForEmoteEvent.Add(value); }
remove { _reactionsRemovedForEmoteEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, IEmote, Task>>();
//Roles //Roles
/// <summary> Fired when a role is created. </summary> /// <summary> Fired when a role is created. </summary>

@ -82,6 +82,20 @@ namespace Discord.WebSocket
} }
return null; return null;
} }
internal void PurgeAllChannels()
{
foreach (var guild in _guilds.Values)
guild.PurgeChannelCache(this);
PurgeDMChannels();
}
internal void PurgeDMChannels()
{
foreach (var channel in _dmChannels.Values)
_channels.TryRemove(channel.Id, out _);
_dmChannels.Clear();
}
internal SocketGuild GetGuild(ulong id) internal SocketGuild GetGuild(ulong id)
{ {
@ -96,7 +110,11 @@ namespace Discord.WebSocket
internal SocketGuild RemoveGuild(ulong id) internal SocketGuild RemoveGuild(ulong id)
{ {
if (_guilds.TryRemove(id, out SocketGuild guild)) if (_guilds.TryRemove(id, out SocketGuild guild))
{
guild.PurgeChannelCache(this);
guild.PurgeGuildUserCache();
return guild; return guild;
}
return null; return null;
} }
@ -116,5 +134,10 @@ namespace Discord.WebSocket
return user; return user;
return null; return null;
} }
internal void PurgeUsers()
{
foreach (var guild in _guilds.Values)
guild.PurgeGuildUserCache();
}
} }
} }

@ -44,6 +44,8 @@ namespace Discord
var ex2 = ex as WebSocketClosedException; var ex2 = ex as WebSocketClosedException;
if (ex2?.CloseCode == 4006) if (ex2?.CloseCode == 4006)
CriticalError(new Exception("WebSocket session expired", ex)); CriticalError(new Exception("WebSocket session expired", ex));
else if (ex2?.CloseCode == 4014)
CriticalError(new Exception("WebSocket connection was closed", ex));
else else
Error(new Exception("WebSocket connection was closed", ex)); Error(new Exception("WebSocket connection was closed", ex));
} }
@ -141,7 +143,16 @@ namespace Discord
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
}); });
await _onConnecting().ConfigureAwait(false); try
{
await _onConnecting().ConfigureAwait(false);
}
catch (TaskCanceledException ex)
{
Exception innerEx = ex.InnerException ?? new OperationCanceledException("Failed to connect.");
Error(innerEx);
throw innerEx;
}
await _logger.InfoAsync("Connected").ConfigureAwait(false); await _logger.InfoAsync("Connected").ConfigureAwait(false);
State = ConnectionState.Connected; State = ConnectionState.Connected;

@ -313,6 +313,7 @@ namespace Discord.WebSocket
client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction); client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction);
client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction); client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction);
client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel); client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel);
client.ReactionsRemovedForEmote += (cache, channel, emote) => _reactionsRemovedForEmoteEvent.InvokeAsync(cache, channel, emote);
client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role); client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role);
client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role); client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role);
@ -389,8 +390,11 @@ namespace Discord.WebSocket
{ {
if (disposing) if (disposing)
{ {
foreach (var client in _shards) if (_shards != null)
client?.Dispose(); {
foreach (var client in _shards)
client?.Dispose();
}
_connectionGroupLock?.Dispose(); _connectionGroupLock?.Dispose();
} }

@ -164,26 +164,17 @@ namespace Discord.API
} }
} }
public async Task DisconnectAsync() public async Task DisconnectAsync(Exception ex = null)
{ {
await _stateLock.WaitAsync().ConfigureAwait(false); await _stateLock.WaitAsync().ConfigureAwait(false);
try try
{ {
await DisconnectInternalAsync().ConfigureAwait(false); await DisconnectInternalAsync(ex).ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
public async Task DisconnectAsync(Exception ex)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
} }
finally { _stateLock.Release(); } finally { _stateLock.Release(); }
} }
/// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception> /// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception>
internal override async Task DisconnectInternalAsync() internal override async Task DisconnectInternalAsync(Exception ex = null)
{ {
if (WebSocketClient == null) if (WebSocketClient == null)
throw new NotSupportedException("This client is not configured with WebSocket support."); throw new NotSupportedException("This client is not configured with WebSocket support.");
@ -194,6 +185,9 @@ namespace Discord.API
try { _connectCancelToken?.Cancel(false); } try { _connectCancelToken?.Cancel(false); }
catch { } catch { }
if (ex is GatewayReconnectException)
await WebSocketClient.DisconnectAsync(4000);
else
await WebSocketClient.DisconnectAsync().ConfigureAwait(false); await WebSocketClient.DisconnectAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Disconnected; ConnectionState = ConnectionState.Disconnected;
@ -215,7 +209,7 @@ namespace Discord.API
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
} }
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, RequestOptions options = null) public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, GatewayIntents? gatewayIntents = null, RequestOptions options = null)
{ {
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
var props = new Dictionary<string, string> var props = new Dictionary<string, string>
@ -226,12 +220,16 @@ namespace Discord.API
{ {
Token = AuthToken, Token = AuthToken,
Properties = props, Properties = props,
LargeThreshold = largeThreshold, LargeThreshold = largeThreshold
GuildSubscriptions = guildSubscriptions
}; };
if (totalShards > 1) if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards }; msg.ShardingParams = new int[] { shardID, totalShards };
if (gatewayIntents.HasValue)
msg.Intents = (int)gatewayIntents.Value;
else
msg.GuildSubscriptions = guildSubscriptions;
await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
} }
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)

@ -21,7 +21,13 @@ namespace Discord.WebSocket
remove { _disconnectedEvent.Remove(value); } remove { _disconnectedEvent.Remove(value); }
} }
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
/// <summary> Fired when guild data has finished downloading. </summary> /// <summary>
/// Fired when guild data has finished downloading.
/// </summary>
/// <remarks>
/// It is possible that some guilds might be unsynced if <see cref="DiscordSocketConfig.MaxWaitBetweenGuildAvailablesBeforeReady" />
/// was not long enough to receive all GUILD_AVAILABLEs before READY.
/// </remarks>
public event Func<Task> Ready public event Func<Task> Ready
{ {
add { _readyEvent.Add(value); } add { _readyEvent.Add(value); }

@ -44,6 +44,7 @@ namespace Discord.WebSocket
private RestApplication _applicationInfo; private RestApplication _applicationInfo;
private bool _isDisposed; private bool _isDisposed;
private bool _guildSubscriptions; private bool _guildSubscriptions;
private GatewayIntents? _gatewayIntents;
/// <summary> /// <summary>
/// Provides access to a REST-only client with a shared state from this client. /// Provides access to a REST-only client with a shared state from this client.
@ -137,6 +138,7 @@ namespace Discord.WebSocket
Rest = new DiscordSocketRestClient(config, ApiClient); Rest = new DiscordSocketRestClient(config, ApiClient);
_heartbeatTimes = new ConcurrentQueue<long>(); _heartbeatTimes = new ConcurrentQueue<long>();
_guildSubscriptions = config.GuildSubscriptions; _guildSubscriptions = config.GuildSubscriptions;
_gatewayIntents = config.GatewayIntents;
_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);
_gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}");
@ -167,7 +169,7 @@ namespace Discord.WebSocket
GuildAvailable += g => GuildAvailable += g =>
{ {
if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) if (_guildDownloadTask?.IsCompleted == true && ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers)
{ {
var _ = g.DownloadUsersAsync(); var _ = g.DownloadUsersAsync();
} }
@ -242,7 +244,7 @@ namespace Discord.WebSocket
else else
{ {
await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false);
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions).ConfigureAwait(false); await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
} }
//Wait for READY //Wait for READY
@ -264,7 +266,7 @@ namespace Discord.WebSocket
{ {
await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
await ApiClient.DisconnectAsync().ConfigureAwait(false); await ApiClient.DisconnectAsync(ex).ConfigureAwait(false);
//Wait for tasks to complete //Wait for tasks to complete
await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false);
@ -306,6 +308,14 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override SocketChannel GetChannel(ulong id) public override SocketChannel GetChannel(ulong id)
=> State.GetChannel(id); => State.GetChannel(id);
/// <summary>
/// Clears all cached channels from the client.
/// </summary>
public void PurgeChannelCache() => State.PurgeAllChannels();
/// <summary>
/// Clears cached DM channels from the client.
/// </summary>
public void PurgeDMChannelCache() => State.PurgeDMChannels();
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(ulong id) public override SocketUser GetUser(ulong id)
@ -313,6 +323,10 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator) public override SocketUser GetUser(string username, string discriminator)
=> State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username);
/// <summary>
/// Clears cached users from the client.
/// </summary>
public void PurgeUserCache() => State.PurgeUsers();
internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model)
{ {
return state.GetOrAddUser(model.Id, x => return state.GetOrAddUser(model.Id, x =>
@ -328,7 +342,7 @@ namespace Discord.WebSocket
{ {
var user = SocketGlobalUser.Create(this, state, model); var user = SocketGlobalUser.Create(this, state, model);
user.GlobalUser.AddRef(); user.GlobalUser.AddRef();
user.Presence = new SocketPresence(UserStatus.Online, null, null); user.Presence = new SocketPresence(UserStatus.Online, null, null, null);
return user; return user;
}); });
} }
@ -356,7 +370,7 @@ namespace Discord.WebSocket
{ {
var cachedGuilds = guilds.ToImmutableArray(); var cachedGuilds = guilds.ToImmutableArray();
const short batchSize = 50; int batchSize = _gatewayIntents.HasValue ? 1 : 100;
ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)];
Task[] batchTasks = new Task[batchIds.Length]; Task[] batchTasks = new Task[batchIds.Length];
int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize;
@ -364,7 +378,7 @@ namespace Discord.WebSocket
for (int i = 0, k = 0; i < batchCount; i++) for (int i = 0, k = 0; i < batchCount; i++)
{ {
bool isLast = i == batchCount - 1; bool isLast = i == batchCount - 1;
int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; int count = isLast ? (cachedGuilds.Length - (batchCount - 1) * batchSize) : batchSize;
for (int j = 0; j < count; j++, k++) for (int j = 0; j < count; j++, k++)
{ {
@ -436,7 +450,7 @@ namespace Discord.WebSocket
return; return;
var status = Status; var status = Status;
var statusSince = _statusSince; var statusSince = _statusSince;
CurrentUser.Presence = new SocketPresence(status, Activity, null); CurrentUser.Presence = new SocketPresence(status, Activity, null, null);
var gameModel = new GameModel(); var gameModel = new GameModel();
// Discord only accepts rich presence over RPC, don't even bother building a payload // Discord only accepts rich presence over RPC, don't even bother building a payload
@ -505,13 +519,13 @@ namespace Discord.WebSocket
_sessionId = null; _sessionId = null;
_lastSeq = 0; _lastSeq = 0;
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false);
} }
break; break;
case GatewayOpCode.Reconnect: case GatewayOpCode.Reconnect:
{ {
await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false);
_connection.Error(new Exception("Server requested a reconnect")); _connection.Error(new GatewayReconnectException("Server requested a reconnect"));
} }
break; break;
case GatewayOpCode.Dispatch: case GatewayOpCode.Dispatch:
@ -534,7 +548,7 @@ namespace Discord.WebSocket
{ {
var model = data.Guilds[i]; var model = data.Guilds[i];
var guild = AddGuild(model, state); var guild = AddGuild(model, state);
if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) if (!guild.IsAvailable)
unavailableGuilds++; unavailableGuilds++;
else else
await GuildAvailableAsync(guild).ConfigureAwait(false); await GuildAvailableAsync(guild).ConfigureAwait(false);
@ -553,9 +567,6 @@ namespace Discord.WebSocket
return; return;
} }
if (ApiClient.AuthTokenType == TokenType.User)
await SyncGuildsAsync().ConfigureAwait(false);
_lastGuildAvailableTime = Environment.TickCount; _lastGuildAvailableTime = Environment.TickCount;
_guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger)
.ContinueWith(async x => .ContinueWith(async x =>
@ -567,6 +578,9 @@ namespace Discord.WebSocket
} }
else if (_connection.CancelToken.IsCancellationRequested) else if (_connection.CancelToken.IsCancellationRequested)
return; return;
if (BaseConfig.AlwaysDownloadUsers)
_ = DownloadUsersAsync(Guilds.Where(x1 => x1.IsAvailable && !x1.HasAllMembers));
await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false);
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
@ -630,9 +644,8 @@ namespace Discord.WebSocket
var guild = AddGuild(data, State); var guild = AddGuild(data, State);
if (guild != null) if (guild != null)
{ {
if (ApiClient.AuthTokenType == TokenType.User)
await SyncGuildsAsync().ConfigureAwait(false);
await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false);
await GuildAvailableAsync(guild).ConfigureAwait(false);
} }
else else
{ {
@ -890,6 +903,13 @@ namespace Discord.WebSocket
if (user != null) if (user != null)
{ {
var globalBefore = user.GlobalUser.Clone();
if (user.GlobalUser.Update(State, data.User))
{
//Global data was updated, trigger UserUpdated
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false);
}
var before = user.Clone(); var before = user.Clone();
user.Update(State, data); user.Update(State, data);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false);
@ -1383,6 +1403,34 @@ namespace Discord.WebSocket
} }
} }
break; break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false);
var data = (payload as JToken).ToObject<API.Gateway.RemoveAllReactionsForEmoteEvent>(_serializer);
if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel)
{
var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage;
bool isCached = cachedMsg != null;
var optionalMsg = !isCached
? Optional.Create<SocketUserMessage>()
: Optional.Create(cachedMsg);
var cacheable = new Cacheable<IUserMessage, ulong>(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage);
var emote = data.Emoji.ToIEmote();
cachedMsg?.RemoveAllReactionsForEmoteAsync(emote);
await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheable, channel, emote).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;
case "MESSAGE_DELETE_BULK": case "MESSAGE_DELETE_BULK":
{ {
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false);
@ -1694,7 +1742,7 @@ namespace Discord.WebSocket
{ {
if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true))
{ {
_connection.Error(new Exception("Server missed last heartbeat")); _connection.Error(new GatewayReconnectException("Server missed last heartbeat"));
return; return;
} }
} }
@ -1734,7 +1782,7 @@ namespace Discord.WebSocket
try try
{ {
await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false);
while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady))
await Task.Delay(500, cancelToken).ConfigureAwait(false); await Task.Delay(500, cancelToken).ConfigureAwait(false);
await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false);
} }
@ -1763,17 +1811,7 @@ namespace Discord.WebSocket
return guild; return guild;
} }
internal SocketGuild RemoveGuild(ulong id) internal SocketGuild RemoveGuild(ulong id)
{ => State.RemoveGuild(id);
var guild = State.RemoveGuild(id);
if (guild != null)
{
foreach (var _ in guild.Channels)
State.RemoveChannel(id);
foreach (var user in guild.Users)
user.GlobalUser.RemoveRef(this);
}
return guild;
}
/// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception> /// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception>
internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state)

@ -87,7 +87,7 @@ namespace Discord.WebSocket
/// </para> /// </para>
/// <para> /// <para>
/// For more information, please see /// For more information, please see
/// <see href="https://discordapp.com/developers/docs/topics/gateway#request-guild-members">Request Guild Members</see> /// <see href="https://discord.com/developers/docs/topics/gateway#request-guild-members">Request Guild Members</see>
/// on the official Discord API documentation. /// on the official Discord API documentation.
/// </para> /// </para>
/// <note> /// <note>
@ -121,9 +121,45 @@ namespace Discord.WebSocket
/// <summary> /// <summary>
/// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events. /// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events.
/// This is not used if <see cref="GatewayIntents"/> are provided.
/// </summary> /// </summary>
public bool GuildSubscriptions { get; set; } = true; public bool GuildSubscriptions { get; set; } = true;
/// <summary>
/// Gets or sets the maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY.
///
/// If zero, READY will fire as soon as it is received and all guilds will be unavailable.
/// </summary>
/// <remarks>
/// <para>This property is measured in milliseconds, negative values will throw an exception.</para>
/// <para>If a guild is not received before READY, it will be unavailable.</para>
/// </remarks>
/// <returns>
/// The maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY.
/// </returns>
/// <exception cref="System.ArgumentException">Value must be at least 0.</exception>
public int MaxWaitBetweenGuildAvailablesBeforeReady {
get
{
return _maxWaitForGuildAvailable;
}
set
{
Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady));
_maxWaitForGuildAvailable = value;
}
}
private int _maxWaitForGuildAvailable = 10000;
/// Gets or sets gateway intents to limit what events are sent from Discord. Allows for more granular control than the <see cref="GuildSubscriptions"/> property.
/// </summary>
/// <remarks>
/// For more information, please see
/// <see href="https://discord.com/developers/docs/topics/gateway#gateway-intents">GatewayIntents</see>
/// on the official Discord API documentation.
/// </remarks>
public GatewayIntents? GatewayIntents { get; set; }
/// <summary> /// <summary>
/// Initializes a default configuration. /// Initializes a default configuration.
/// </summary> /// </summary>

@ -29,16 +29,20 @@ namespace Discord.WebSocket
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param> /// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool)"/>. /// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="filePath">The file path of the file.</param> /// <param name="filePath">The file path of the file.</param>
@ -47,16 +51,20 @@ namespace Discord.WebSocket
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Sends a file to this message channel with an optional caption. /// Sends a file to this message channel with an optional caption.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool)"/>. /// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions)"/>.
/// Please visit its documentation for more details on this method. /// Please visit its documentation for more details on this method.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param> /// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@ -66,11 +74,15 @@ namespace Discord.WebSocket
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <param name="allowedMentions">
/// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>.
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <returns> /// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result /// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message. /// contains the sent message.
/// </returns> /// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null);
/// <summary> /// <summary>
/// Gets a cached message from this channel. /// Gets a cached message from this channel.

@ -11,23 +11,11 @@ namespace Discord.WebSocket
public static IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, public static IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages,
ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
{ {
if (dir == Direction.Around)
throw new NotImplementedException(); //TODO: Impl
IReadOnlyCollection<SocketMessage> cachedMessages = null;
IAsyncEnumerable<IReadOnlyCollection<IMessage>> result = null;
if (dir == Direction.After && fromMessageId == null) if (dir == Direction.After && fromMessageId == null)
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>();
if (dir == Direction.Before || mode == CacheMode.CacheOnly) var cachedMessages = GetCachedMessages(channel, discord, messages, fromMessageId, dir, limit);
{ var result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable<IReadOnlyCollection<IMessage>>();
if (messages != null) //Cache enabled
cachedMessages = messages.GetMany(fromMessageId, dir, limit);
else
cachedMessages = ImmutableArray.Create<SocketMessage>();
result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable<IReadOnlyCollection<IMessage>>();
}
if (dir == Direction.Before) if (dir == Direction.Before)
{ {
@ -38,18 +26,35 @@ namespace Discord.WebSocket
//Download remaining messages //Download remaining messages
ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId;
var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options); var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options);
return result.Concat(downloadedMessages); if (cachedMessages.Count != 0)
return result.Concat(downloadedMessages);
else
return downloadedMessages;
} }
else else if (dir == Direction.After)
{
limit -= cachedMessages.Count;
if (mode == CacheMode.CacheOnly || limit <= 0)
return result;
//Download remaining messages
ulong maxId = cachedMessages.Count > 0 ? cachedMessages.Max(x => x.Id) : fromMessageId.Value;
var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, maxId, dir, limit, options);
if (cachedMessages.Count != 0)
return result.Concat(downloadedMessages);
else
return downloadedMessages;
}
else //Direction.Around
{ {
if (mode == CacheMode.CacheOnly) if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count)
return result; return result;
//Dont use cache in this case //Cache isn't useful here since Discord will send them anyways
return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options); return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options);
} }
} }
public static IReadOnlyCollection<SocketMessage> GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, public static IReadOnlyCollection<SocketMessage> GetCachedMessages(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages,
ulong? fromMessageId, Direction dir, int limit) ulong? fromMessageId, Direction dir, int limit)
{ {
if (messages != null) //Cache enabled if (messages != null) //Cache enabled

@ -135,16 +135,16 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options);
@ -229,14 +229,14 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
//IChannel //IChannel
/// <inheritdoc /> /// <inheritdoc />

@ -163,15 +163,15 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@ -293,14 +293,14 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
//IAudioChannel //IAudioChannel
/// <inheritdoc /> /// <inheritdoc />

@ -125,7 +125,6 @@ namespace Discord.WebSocket
public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
{ {
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
} }
/// <summary> /// <summary>
@ -140,7 +139,6 @@ namespace Discord.WebSocket
public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{ {
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
} }
/// <summary> /// <summary>
/// Removes the permission overwrite for the given user, if one exists. /// Removes the permission overwrite for the given user, if one exists.
@ -153,15 +151,6 @@ namespace Discord.WebSocket
public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
{ {
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false);
for (int i = 0; i < _overwrites.Length; i++)
{
if (_overwrites[i].TargetId == user.Id)
{
_overwrites = _overwrites.RemoveAt(i);
return;
}
}
} }
/// <summary> /// <summary>
/// Removes the permission overwrite for the given role, if one exists. /// Removes the permission overwrite for the given role, if one exists.
@ -174,15 +163,6 @@ namespace Discord.WebSocket
public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
{ {
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false);
for (int i = 0; i < _overwrites.Length; i++)
{
if (_overwrites[i].TargetId == role.Id)
{
_overwrites = _overwrites.RemoveAt(i);
return;
}
}
} }
public new virtual SocketGuildUser GetUser(ulong id) => null; public new virtual SocketGuildUser GetUser(ulong id) => null;

@ -161,17 +161,17 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler);
/// <inheritdoc /> /// <inheritdoc />
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
@ -302,14 +302,14 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false);
// INestedChannel // INestedChannel
/// <inheritdoc /> /// <inheritdoc />

@ -304,13 +304,6 @@ namespace Discord.WebSocket
_features = ImmutableArray.Create<string>();*/ _features = ImmutableArray.Create<string>();*/
_syncPromise = new TaskCompletionSource<bool>(); _syncPromise = new TaskCompletionSource<bool>();
_downloaderPromise = new TaskCompletionSource<bool>(); _downloaderPromise = new TaskCompletionSource<bool>();
if (Discord.ApiClient.AuthTokenType != TokenType.User)
{
_syncPromise.TrySetResultAsync(true);
/*if (!model.Large)
_ = _downloaderPromise.TrySetResultAsync(true);*/
}
return; return;
} }
@ -388,7 +381,7 @@ namespace Discord.WebSocket
Description = model.Description; Description = model.Description;
PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault();
PreferredLocale = model.PreferredLocale; PreferredLocale = model.PreferredLocale;
PreferredCulture = new CultureInfo(PreferredLocale); PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale);
if (model.Emojis != null) if (model.Emojis != null)
{ {
@ -630,6 +623,13 @@ namespace Discord.WebSocket
return state.RemoveChannel(id) as SocketGuildChannel; return state.RemoveChannel(id) as SocketGuildChannel;
return null; return null;
} }
internal void PurgeChannelCache(ClientState state)
{
foreach (var channelId in _channels)
state.RemoveChannel(channelId);
_channels.Clear();
}
//Voice Regions //Voice Regions
/// <summary> /// <summary>
@ -804,6 +804,41 @@ namespace Discord.WebSocket
} }
return null; return null;
} }
internal void PurgeGuildUserCache()
{
var members = Users;
var self = CurrentUser;
_members.Clear();
if (self != null)
_members.TryAdd(self.Id, self);
DownloadedMemberCount = _members.Count;
foreach (var member in members)
{
if (member.Id != self?.Id)
member.GlobalUser.RemoveRef(Discord);
}
}
/// <summary>
/// Gets a collection of all users in this guild.
/// </summary>
/// <remarks>
/// <para>This method retrieves all users found within this guild throught REST.</para>
/// <para>Users returned by this method are not cached.</para>
/// </remarks>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a collection of guild
/// users found within this guild.
/// </returns>
public IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(RequestOptions options = null)
{
if (HasAllMembers)
return ImmutableArray.Create(Users).ToAsyncEnumerable<IReadOnlyCollection<IGuildUser>>();
return GuildHelper.GetUsersAsync(this, Discord, null, null, options);
}
/// <inheritdoc /> /// <inheritdoc />
public async Task DownloadUsersAsync() public async Task DownloadUsersAsync()
@ -815,6 +850,23 @@ namespace Discord.WebSocket
_downloaderPromise.TrySetResultAsync(true); _downloaderPromise.TrySetResultAsync(true);
} }
/// <summary>
/// Gets a collection of users in this guild that the name or nickname starts with the
/// provided <see cref="string"/> at <paramref name="query"/>.
/// </summary>
/// <remarks>
/// The <paramref name="limit"/> can not be higher than <see cref="DiscordConfig.MaxUsersPerBatch"/>.
/// </remarks>
/// <param name="query">The partial name or nickname to search.</param>
/// <param name="limit">The maximum number of users to be gotten.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a collection of guild
/// users that the name or nickname starts with the provided <see cref="string"/> at <paramref name="query"/>.
/// </returns>
public Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null)
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);
//Audit logs //Audit logs
/// <summary> /// <summary>
/// Gets the specified number of audit log entries for this guild. /// Gets the specified number of audit log entries for this guild.
@ -1169,8 +1221,13 @@ namespace Discord.WebSocket
=> await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); {
if (mode == CacheMode.AllowDownload && !HasAllMembers)
return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray();
else
return Users;
}
/// <inheritdoc /> /// <inheritdoc />
async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options) async Task<IGuildUser> IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action<AddGuildUserProperties> func, RequestOptions options)
@ -1184,6 +1241,14 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(Owner); => Task.FromResult<IGuildUser>(Owner);
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildUser>> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await SearchUsersAsync(query, limit, options).ConfigureAwait(false);
else
return ImmutableArray.Create<IGuildUser>();
}
/// <inheritdoc /> /// <inheritdoc />
async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options,

@ -56,11 +56,23 @@ namespace Discord.WebSocket
cachedMessageIds = _orderedMessages; cachedMessageIds = _orderedMessages;
else if (dir == Direction.Before) else if (dir == Direction.Before)
cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value);
else else if (dir == Direction.After)
cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value);
else //Direction.Around
{
if (!_messages.TryGetValue(fromMessageId.Value, out SocketMessage msg))
return ImmutableArray<SocketMessage>.Empty;
int around = limit / 2;
var before = GetMany(fromMessageId, Direction.Before, around);
var after = GetMany(fromMessageId, Direction.After, around).Reverse();
return after.Concat(new SocketMessage[] { msg }).Concat(before).ToImmutableArray();
}
if (dir == Direction.Before) if (dir == Direction.Before)
cachedMessageIds = cachedMessageIds.Reverse(); cachedMessageIds = cachedMessageIds.Reverse();
if (dir == Direction.Around) //Only happens if fromMessageId is null, should only get "around" and itself (+1)
limit = limit / 2 + 1;
return cachedMessageIds return cachedMessageIds
.Select(x => .Select(x =>

@ -46,6 +46,8 @@ namespace Discord.WebSocket
public virtual bool IsSuppressed => false; public virtual bool IsSuppressed => false;
/// <inheritdoc /> /// <inheritdoc />
public virtual DateTimeOffset? EditedTimestamp => null; public virtual DateTimeOffset? EditedTimestamp => null;
/// <inheritdoc />
public virtual bool MentionedEveryone => false;
/// <inheritdoc /> /// <inheritdoc />
public MessageActivity Activity { get; private set; } public MessageActivity Activity { get; private set; }
@ -140,7 +142,7 @@ namespace Discord.WebSocket
Activity = new MessageActivity() Activity = new MessageActivity()
{ {
Type = model.Activity.Value.Type.Value, Type = model.Activity.Value.Type.Value,
PartyId = model.Activity.Value.PartyId.Value PartyId = model.Activity.Value.PartyId.GetValueOrDefault()
}; };
} }
@ -200,6 +202,10 @@ namespace Discord.WebSocket
{ {
_reactions.Clear(); _reactions.Clear();
} }
internal void RemoveReactionsForEmote(IEmote emote)
{
_reactions.RemoveAll(x => x.Emote.Equals(emote));
}
/// <inheritdoc /> /// <inheritdoc />
public Task AddReactionAsync(IEmote emote, RequestOptions options = null) public Task AddReactionAsync(IEmote emote, RequestOptions options = null)
@ -214,6 +220,9 @@ namespace Discord.WebSocket
public Task RemoveAllReactionsAsync(RequestOptions options = null) public Task RemoveAllReactionsAsync(RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsAsync(this, Discord, options); => MessageHelper.RemoveAllReactionsAsync(this, Discord, options);
/// <inheritdoc /> /// <inheritdoc />
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
} }

@ -20,7 +20,9 @@ namespace Discord.WebSocket
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>();
private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>();
/// <inheritdoc /> /// <inheritdoc />
public override bool IsTTS => _isTTS; public override bool IsTTS => _isTTS;
/// <inheritdoc /> /// <inheritdoc />
@ -30,6 +32,8 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks);
/// <inheritdoc /> /// <inheritdoc />
public override bool MentionedEveryone => _isMentioningEveryone;
/// <inheritdoc />
public override IReadOnlyCollection<Attachment> Attachments => _attachments; public override IReadOnlyCollection<Attachment> Attachments => _attachments;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<Embed> Embeds => _embeds; public override IReadOnlyCollection<Embed> Embeds => _embeds;
@ -38,9 +42,9 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags); public override IReadOnlyCollection<SocketGuildChannel> MentionedChannels => MessageHelper.FilterTagsByValue<SocketGuildChannel>(TagType.ChannelMention, _tags);
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketRole> MentionedRoles => MessageHelper.FilterTagsByValue<SocketRole>(TagType.RoleMention, _tags); public override IReadOnlyCollection<SocketRole> MentionedRoles => _roleMentions;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketUser> MentionedUsers => MessageHelper.FilterTagsByValue<SocketUser>(TagType.UserMention, _tags); public override IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions;
internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source)
: base(discord, id, channel, author, source) : base(discord, id, channel, author, source)
@ -57,6 +61,8 @@ namespace Discord.WebSocket
{ {
base.Update(state, model); base.Update(state, model);
SocketGuild guild = (Channel as SocketGuildChannel)?.Guild;
if (model.IsTextToSpeech.IsSpecified) if (model.IsTextToSpeech.IsSpecified)
_isTTS = model.IsTextToSpeech.Value; _isTTS = model.IsTextToSpeech.Value;
if (model.Pinned.IsSpecified) if (model.Pinned.IsSpecified)
@ -69,6 +75,8 @@ namespace Discord.WebSocket
{ {
_isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed);
} }
if (model.RoleMentions.IsSpecified)
_roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray();
if (model.Attachments.IsSpecified) if (model.Attachments.IsSpecified)
{ {
@ -98,32 +106,33 @@ namespace Discord.WebSocket
_embeds = ImmutableArray.Create<Embed>(); _embeds = ImmutableArray.Create<Embed>();
} }
IReadOnlyCollection<IUser> mentions = ImmutableArray.Create<SocketUnknownUser>(); //Is passed to ParseTags to get real mention collection
if (model.UserMentions.IsSpecified) if (model.UserMentions.IsSpecified)
{ {
var value = model.UserMentions.Value; var value = model.UserMentions.Value;
if (value.Length > 0) if (value.Length > 0)
{ {
var newMentions = ImmutableArray.CreateBuilder<SocketUnknownUser>(value.Length); var newMentions = ImmutableArray.CreateBuilder<SocketUser>(value.Length);
for (int i = 0; i < value.Length; i++) for (int i = 0; i < value.Length; i++)
{ {
var val = value[i]; var val = value[i];
if (val.Object != null) var guildUser = guild.GetUser(val.Id);
if (guildUser != null)
newMentions.Add(guildUser);
else if (val.Object != null)
newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object));
} }
mentions = newMentions.ToImmutable(); _userMentions = newMentions.ToImmutable();
} }
} }
if (model.Content.IsSpecified) if (model.Content.IsSpecified)
{ {
var text = model.Content.Value; var text = model.Content.Value;
var guild = (Channel as SocketGuildChannel)?.Guild; _tags = MessageHelper.ParseTags(text, Channel, guild, _userMentions);
_tags = MessageHelper.ParseTags(text, Channel, guild, mentions);
model.Content = text; model.Content = text;
} }
} }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="InvalidOperationException">Only the author of a message may modify the message.</exception> /// <exception cref="InvalidOperationException">Only the author of a message may modify the message.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
@ -147,7 +156,19 @@ namespace Discord.WebSocket
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
=> MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
/// <inheritdoc />
/// <exception cref="InvalidOperationException">This operation may only be called on a <see cref="SocketNewsChannel"/> channel.</exception>
public async Task CrosspostAsync(RequestOptions options = null)
{
if (!(Channel is SocketNewsChannel))
{
throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels.");
}
await MessageHelper.CrosspostAsync(this, Discord, options);
}
private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})";
internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage;
} }

@ -154,6 +154,8 @@ namespace Discord.WebSocket
Nickname = model.Nick.Value; Nickname = model.Nick.Value;
if (model.Roles.IsSpecified) if (model.Roles.IsSpecified)
UpdateRoles(model.Roles.Value); UpdateRoles(model.Roles.Value);
if (model.PremiumSince.IsSpecified)
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks;
} }
private void UpdateRoles(ulong[] roleIds) private void UpdateRoles(ulong[] roleIds)
{ {

@ -18,16 +18,20 @@ namespace Discord.WebSocket
public IActivity Activity { get; } public IActivity Activity { get; }
/// <inheritdoc /> /// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients { get; } public IImmutableSet<ClientType> ActiveClients { get; }
internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet<ClientType> activeClients) /// <inheritdoc />
public IImmutableList<IActivity> Activities { get; }
internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities)
{ {
Status = status; Status = status;
Activity= activity; Activity = activity;
ActiveClients = activeClients; ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.Empty;
} }
internal static SocketPresence Create(Model model) internal static SocketPresence Create(Model model)
{ {
var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault());
return new SocketPresence(model.Status, model.Game?.ToEntity(), clients); var activities = ConvertActivitiesList(model.Activities);
return new SocketPresence(model.Status, model.Game?.ToEntity(), clients, activities);
} }
/// <summary> /// <summary>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types
@ -53,6 +57,25 @@ namespace Discord.WebSocket
} }
return set.ToImmutableHashSet(); return set.ToImmutableHashSet();
} }
/// <summary>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all the activities
/// that a user has from the data supplied in the Presence update frame.
/// </summary>
/// <param name="activities">
/// A list of <see cref="API.Game"/>.
/// </param>
/// <returns>
/// A list of all <see cref="IActivity"/> that this user currently has available.
/// </returns>
private static IImmutableList<IActivity> ConvertActivitiesList(IList<API.Game> activities)
{
if (activities == null || activities.Count == 0)
return ImmutableList<IActivity>.Empty;
var list = new List<IActivity>();
foreach (var activity in activities)
list.Add(activity.ToEntity());
return list.ToImmutableList();
}
/// <summary> /// <summary>
/// Gets the status of the user. /// Gets the status of the user.

@ -25,7 +25,7 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => false; public override bool IsWebhook => false;
/// <inheritdoc /> /// <inheritdoc />
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception> /// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception>
internal override SocketGlobalUser GlobalUser => internal override SocketGlobalUser GlobalUser =>

@ -41,11 +41,16 @@ namespace Discord.WebSocket
public UserStatus Status => Presence.Status; public UserStatus Status => Presence.Status;
/// <inheritdoc /> /// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary> /// <summary>
/// Gets mutual guilds shared with this user. /// Gets mutual guilds shared with this user.
/// </summary> /// </summary>
/// <remarks>
/// This property will only include guilds in the same <see cref="DiscordSocketClient"/>.
/// </remarks>
public IReadOnlyCollection<SocketGuild> MutualGuilds public IReadOnlyCollection<SocketGuild> MutualGuilds
=> Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); => Discord.Guilds.Where(g => g.GetUser(Id) != null).ToImmutableArray();
internal SocketUser(DiscordSocketClient discord, ulong id) internal SocketUser(DiscordSocketClient discord, ulong id)
: base(discord, id) : base(discord, id)

@ -30,7 +30,7 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public override bool IsWebhook => true; public override bool IsWebhook => true;
/// <inheritdoc /> /// <inheritdoc />
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } }
internal override SocketGlobalUser GlobalUser => internal override SocketGlobalUser GlobalUser =>
throw new NotSupportedException(); throw new NotSupportedException();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save