| @@ -14,25 +14,20 @@ trigger: | |||||
| jobs: | jobs: | ||||
| - job: Linux | - job: Linux | ||||
| pool: | pool: | ||||
| vmImage: 'ubuntu-16.04' | |||||
| vmImage: 'ubuntu-latest' | |||||
| steps: | steps: | ||||
| - task: UseDotNet@2 | |||||
| displayName: 'Use .NET Core sdk' | |||||
| inputs: | |||||
| packageType: 'sdk' | |||||
| version: '3.x' | |||||
| - template: azure/build.yml | - template: azure/build.yml | ||||
| - job: Windows_build | - job: Windows_build | ||||
| pool: | pool: | ||||
| vmImage: 'windows-2019' | |||||
| vmImage: 'windows-latest' | |||||
| condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev') | condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev') | ||||
| steps: | steps: | ||||
| - template: azure/build.yml | - template: azure/build.yml | ||||
| - job: Windows_deploy | - job: Windows_deploy | ||||
| pool: | pool: | ||||
| vmImage: 'windows-2019' | |||||
| vmImage: 'windows-latest' | |||||
| condition: | | condition: | | ||||
| and ( | and ( | ||||
| succeeded(), | succeeded(), | ||||
| @@ -1,5 +1,10 @@ | |||||
| steps: | steps: | ||||
| - script: dotnet restore -v minimal Discord.Net.sln | |||||
| - task: DotNetCoreCLI@2 | |||||
| inputs: | |||||
| command: 'restore' | |||||
| projects: 'Discord.Net.sln' | |||||
| feedsToUse: 'select' | |||||
| verbosityRestore: 'Minimal' | |||||
| displayName: Restore packages | displayName: Restore packages | ||||
| - script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) | - script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) | ||||
| @@ -2,7 +2,7 @@ | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <TargetFramework>netcoreapp3.0</TargetFramework> | |||||
| <TargetFramework>netcoreapp3.1</TargetFramework> | |||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| @@ -2,11 +2,11 @@ | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <TargetFramework>netcoreapp3.0</TargetFramework> | |||||
| <TargetFramework>netcoreapp3.1</TargetFramework> | |||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" /> | |||||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> | |||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| @@ -2,12 +2,12 @@ | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <TargetFramework>netcoreapp3.0</TargetFramework> | |||||
| <TargetFramework>netcoreapp3.1</TargetFramework> | |||||
| <RootNamespace>_03_sharded_client</RootNamespace> | <RootNamespace>_03_sharded_client</RootNamespace> | ||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" /> | |||||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> | |||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| @@ -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) | ||||
| { | { | ||||
| @@ -1,3 +1,5 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| @@ -30,5 +32,9 @@ namespace Discord | |||||
| /// 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; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -59,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> | ||||
| @@ -88,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. | ||||
| @@ -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. | ||||
| @@ -8,17 +8,27 @@ namespace Discord | |||||
| [Flags] | [Flags] | ||||
| public enum AllowedMentionTypes | 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> | /// <summary> | ||||
| /// Controls role mentions. | /// Controls role mentions. | ||||
| /// </summary> | /// </summary> | ||||
| Roles, | |||||
| Roles = 1, | |||||
| /// <summary> | /// <summary> | ||||
| /// Controls user mentions. | /// Controls user mentions. | ||||
| /// </summary> | /// </summary> | ||||
| Users, | |||||
| Users = 2, | |||||
| /// <summary> | /// <summary> | ||||
| /// Controls <code>@everyone</code> and <code>@here</code> mentions. | /// Controls <code>@everyone</code> and <code>@here</code> mentions. | ||||
| /// </summary> | /// </summary> | ||||
| Everyone, | |||||
| Everyone = 4, | |||||
| } | } | ||||
| } | } | ||||
| @@ -39,7 +39,7 @@ namespace Discord | |||||
| /// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property | /// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property | ||||
| /// must be <c>null</c> or empty. | /// must be <c>null</c> or empty. | ||||
| /// </summary> | /// </summary> | ||||
| public List<ulong> RoleIds { get; set; } | |||||
| public List<ulong> RoleIds { get; set; } = new List<ulong>(); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets or sets the list of all user ids that will be mentioned. | /// Gets or sets the list of all user ids that will be mentioned. | ||||
| @@ -47,7 +47,7 @@ namespace Discord | |||||
| /// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property | /// flag of the <see cref="AllowedTypes"/> property. If the flag is set, the value of this property | ||||
| /// must be <c>null</c> or empty. | /// must be <c>null</c> or empty. | ||||
| /// </summary> | /// </summary> | ||||
| public List<ulong> UserIds { get; set; } | |||||
| public List<ulong> UserIds { get; set; } = new List<ulong>(); | |||||
| /// <summary> | /// <summary> | ||||
| /// Initializes a new instance of the <see cref="AllowedMentions"/> class. | /// Initializes a new instance of the <see cref="AllowedMentions"/> class. | ||||
| @@ -215,6 +215,15 @@ 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. | ||||
| @@ -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> | ||||
| @@ -73,7 +73,7 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| /// <example> | /// <example> | ||||
| /// <para>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)"/>.</para> | |||||
| /// 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; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -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, | |||||
| } | |||||
| } | |||||
| @@ -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 ??= 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(); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,3 +1,4 @@ | |||||
| using Discord.Net; | |||||
| using System.Threading; | using System.Threading; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -57,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; } | ||||
| internal bool IsGatewayBucket { get; set; } | internal bool IsGatewayBucket { get; set; } | ||||
| @@ -41,16 +41,13 @@ namespace Discord | |||||
| { | { | ||||
| public override bool Equals(TEntity x, TEntity y) | public override bool Equals(TEntity x, TEntity y) | ||||
| { | { | ||||
| bool xNull = x == null; | |||||
| bool yNull = y == null; | |||||
| if (xNull && yNull) | |||||
| return true; | |||||
| if (xNull ^ yNull) | |||||
| return false; | |||||
| return x.Id.Equals(y.Id); | |||||
| return (x, y) switch | |||||
| { | |||||
| (null, null) => true, | |||||
| (null, _) => false, | |||||
| (_, null) => false, | |||||
| var (l, r) => l.Id.Equals(r.Id) | |||||
| }; | |||||
| } | } | ||||
| public override int GetHashCode(TEntity obj) | public override int GetHashCode(TEntity obj) | ||||
| @@ -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")] | ||||
| @@ -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(); | ||||
| @@ -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>>(); | ||||
| @@ -80,17 +80,13 @@ namespace Discord.API | |||||
| /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | ||||
| internal static string GetPrefixedToken(TokenType tokenType, string token) | internal static string GetPrefixedToken(TokenType tokenType, string token) | ||||
| { | { | ||||
| switch (tokenType) | |||||
| return tokenType switch | |||||
| { | { | ||||
| case default(TokenType): | |||||
| return token; | |||||
| case TokenType.Bot: | |||||
| return $"Bot {token}"; | |||||
| case TokenType.Bearer: | |||||
| return $"Bearer {token}"; | |||||
| default: | |||||
| throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)); | |||||
| } | |||||
| default(TokenType) => token, | |||||
| TokenType.Bot => $"Bot {token}", | |||||
| TokenType.Bearer => $"Bearer {token}", | |||||
| _ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)), | |||||
| }; | |||||
| } | } | ||||
| internal virtual void Dispose(bool disposing) | internal virtual void Dispose(bool disposing) | ||||
| { | { | ||||
| @@ -133,7 +129,7 @@ namespace Discord.API | |||||
| RestClient.SetCancelToken(_loginCancelToken.Token); | RestClient.SetCancelToken(_loginCancelToken.Token); | ||||
| AuthTokenType = tokenType; | AuthTokenType = tokenType; | ||||
| AuthToken = token; | |||||
| AuthToken = token?.TrimEnd(); | |||||
| if (tokenType != TokenType.Webhook) | if (tokenType != TokenType.Webhook) | ||||
| RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | ||||
| @@ -180,9 +176,9 @@ namespace Discord.API | |||||
| //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; | ||||
| @@ -194,9 +190,9 @@ 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; | ||||
| @@ -209,9 +205,9 @@ 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; | ||||
| @@ -223,9 +219,9 @@ 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 = bucketId; | options.BucketId = bucketId; | ||||
| @@ -236,9 +232,9 @@ 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 = bucketId; | options.BucketId = bucketId; | ||||
| @@ -250,9 +246,9 @@ 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 = bucketId; | ||||
| @@ -524,7 +520,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) | ||||
| @@ -563,7 +560,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) | ||||
| { | { | ||||
| @@ -660,6 +658,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)); | ||||
| @@ -695,6 +705,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) | ||||
| @@ -1127,6 +1146,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) | ||||
| @@ -1137,13 +1172,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) | ||||
| { | { | ||||
| @@ -1433,21 +1468,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 "channelId": return 1; | |||||
| case "httpMethod": return 0; | |||||
| case "guildId": return 1; | |||||
| case "channelId": return 2; | |||||
| case "webhookId": return 3; | |||||
| default: | default: | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -1458,18 +1511,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 ??= 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; | ||||
| @@ -1506,7 +1560,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) | ||||
| @@ -1519,7 +1573,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) | ||||
| { | { | ||||
| @@ -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); | |||||
| var inviter = RestUser.Create(discord, inviterInfo); | |||||
| RestUser inviter = null; | |||||
| 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); | |||||
| var inviter = RestUser.Create(discord, inviterInfo); | |||||
| RestUser inviter = null; | |||||
| 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> | ||||
| @@ -46,6 +46,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); | ||||
| } | } | ||||
| @@ -109,12 +118,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) => | ||||
| @@ -218,18 +234,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); | ||||
| } | } | ||||
| @@ -387,7 +422,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, | ||||
| @@ -34,7 +34,7 @@ namespace Discord.Rest | |||||
| /// </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> | ||||
| @@ -42,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> | ||||
| @@ -60,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. | ||||
| @@ -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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, allowedMentions, options, isSpoiler); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | ||||
| @@ -200,11 +200,11 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, allowedMentions, options, isSpoiler); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
| @@ -178,11 +178,11 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -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(); | ||||
| } | } | ||||
| @@ -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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, allowedMentions, options, isSpoiler); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | ||||
| @@ -266,12 +267,12 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -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 role = RestRole.Create(client, guild, model); | |||||
| await role.ModifyAsync(x => | |||||
| var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams | |||||
| { | { | ||||
| x.Name = name; | |||||
| x.Permissions = (permissions ?? role.Permissions); | |||||
| x.Color = (color ?? Color.Default); | |||||
| x.Hoist = isHoisted; | |||||
| x.Mentionable = isMentionable; | |||||
| }, options).ConfigureAwait(false); | |||||
| Color = color?.RawValue ?? Optional.Create<uint>(), | |||||
| Hoist = isHoisted, | |||||
| Mentionable = isMentionable, | |||||
| Name = name, | |||||
| Permissions = permissions?.RawValue ?? Optional.Create<ulong>() | |||||
| }; | |||||
| 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) | ||||
| @@ -44,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) | ||||
| { | { | ||||
| @@ -76,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) | ||||
| { | { | ||||
| @@ -115,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) | ||||
| { | { | ||||
| @@ -240,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; | ||||
| @@ -253,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 | ||||
| @@ -260,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 | ||||
| @@ -279,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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -165,7 +165,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 +182,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); | ||||
| } | } | ||||
| @@ -148,6 +148,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) | ||||
| @@ -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.SendEdit, "<send_edit>", 10, 10) | |||||
| new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "<unbucketed>", null), 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; | ||||
| @@ -38,7 +38,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(); | ||||
| } | } | ||||
| @@ -94,7 +94,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; | ||||
| @@ -181,12 +181,30 @@ namespace Discord.Net.Queue | |||||
| private RequestBucket GetOrCreateBucket(string id, IRequest request) | private RequestBucket GetOrCreateBucket(string id, IRequest 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() | ||||
| { | { | ||||
| @@ -195,10 +213,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,12 +13,15 @@ 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; } | ||||
| @@ -52,6 +55,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..."); | ||||
| @@ -220,6 +225,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) | ||||
| @@ -235,7 +243,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) | ||||
| { | { | ||||
| @@ -245,10 +254,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)"); | ||||
| @@ -258,18 +268,18 @@ 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; | ||||
| } | } | ||||
| @@ -282,7 +292,39 @@ namespace Discord.Net.Queue | |||||
| 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; | ||||
| @@ -292,7 +334,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 | ||||
| @@ -309,16 +350,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)) | |||||
| { | |||||
| resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); | |||||
| } | |||||
| else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) | |||||
| { | |||||
| 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); | ||||
| */ | */ | ||||
| @@ -328,11 +371,11 @@ 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 | ||||
| } | } | ||||
| else if (request.Options.IsGatewayBucket && request.Options.BucketId != null) | else if (request.Options.IsGatewayBucket && request.Options.BucketId != null) | ||||
| @@ -355,7 +398,7 @@ namespace Discord.Net.Queue | |||||
| 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) && | |||||
| float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; | |||||
| ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && | |||||
| 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; } | |||||
| } | |||||
| } | |||||
| @@ -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> | ||||
| @@ -141,7 +141,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; | ||||
| @@ -333,6 +333,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); | ||||
| @@ -214,7 +214,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> | ||||
| @@ -225,13 +225,18 @@ namespace Discord.API | |||||
| { | { | ||||
| Token = AuthToken, | Token = AuthToken, | ||||
| Properties = props, | Properties = props, | ||||
| LargeThreshold = largeThreshold, | |||||
| GuildSubscriptions = guildSubscriptions | |||||
| LargeThreshold = largeThreshold | |||||
| }; | }; | ||||
| if (totalShards > 1) | if (totalShards > 1) | ||||
| msg.ShardingParams = new int[] { shardID, totalShards }; | msg.ShardingParams = new int[] { shardID, totalShards }; | ||||
| options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id; | options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id; | ||||
| 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(); | ||||
| } | } | ||||
| @@ -243,7 +245,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 | ||||
| @@ -338,7 +340,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; | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -366,7 +368,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var cachedGuilds = guilds.ToImmutableArray(); | var cachedGuilds = guilds.ToImmutableArray(); | ||||
| const short batchSize = 50; | |||||
| const short batchSize = 100; //TODO: Gateway Intents will limit to a maximum of 1 guild_id | |||||
| 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; | ||||
| @@ -374,7 +376,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++) | ||||
| { | { | ||||
| @@ -446,7 +448,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 | ||||
| @@ -515,7 +517,7 @@ 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: | ||||
| @@ -574,6 +576,9 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| else if (_connection.CancelToken.IsCancellationRequested) | else if (_connection.CancelToken.IsCancellationRequested) | ||||
| return; | return; | ||||
| if (BaseConfig.AlwaysDownloadUsers) | |||||
| _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.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); | ||||
| @@ -1389,6 +1394,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); | ||||
| @@ -1740,7 +1773,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); | ||||
| } | } | ||||
| @@ -1769,17 +1802,7 @@ namespace Discord.WebSocket | |||||
| return guild; | return guild; | ||||
| } | } | ||||
| internal SocketGuild RemoveGuild(ulong id) | internal SocketGuild RemoveGuild(ulong 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; | |||||
| } | |||||
| => State.RemoveGuild(id); | |||||
| /// <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) | ||||
| @@ -121,6 +121,7 @@ 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; | ||||
| @@ -132,6 +133,40 @@ namespace Discord.WebSocket | |||||
| /// </remarks> | /// </remarks> | ||||
| public int IdentifyMaxConcurrency { get; set; } = 1; | public int IdentifyMaxConcurrency { get; set; } = 1; | ||||
| /// 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> | ||||
| @@ -42,7 +42,7 @@ namespace Discord.WebSocket | |||||
| /// 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> | ||||
| @@ -51,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> | ||||
| @@ -70,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) | |||||
| { | |||||
| if (messages != null) //Cache enabled | |||||
| cachedMessages = messages.GetMany(fromMessageId, dir, limit); | |||||
| else | |||||
| cachedMessages = ImmutableArray.Create<SocketMessage>(); | |||||
| result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable<IReadOnlyCollection<IMessage>>(); | |||||
| } | |||||
| var cachedMessages = GetCachedMessages(channel, discord, messages, fromMessageId, dir, limit); | |||||
| var 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 | ||||
| @@ -139,12 +139,12 @@ namespace Discord.WebSocket | |||||
| => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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,11 +229,11 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -167,11 +167,11 @@ namespace Discord.WebSocket | |||||
| => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, allowedMentions, options, isSpoiler); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | ||||
| @@ -293,11 +293,11 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -165,13 +165,13 @@ namespace Discord.WebSocket | |||||
| => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); | |||||
| 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, 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) | |||||
| => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); | |||||
| 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, allowedMentions, options, isSpoiler); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
| @@ -302,11 +302,11 @@ 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) | |||||
| => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) | |||||
| => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); | |||||
| 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, allowedMentions).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) | ||||
| => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); | ||||
| @@ -809,17 +809,37 @@ namespace Discord.WebSocket | |||||
| var members = Users; | var members = Users; | ||||
| var self = CurrentUser; | var self = CurrentUser; | ||||
| _members.Clear(); | _members.Clear(); | ||||
| _members.TryAdd(self.Id, self); | |||||
| if (self != null) | |||||
| _members.TryAdd(self.Id, self); | |||||
| DownloadedMemberCount = _members.Count; | DownloadedMemberCount = _members.Count; | ||||
| foreach (var member in members) | foreach (var member in members) | ||||
| { | { | ||||
| if (member.Id != self.Id) | |||||
| if (member.Id != self?.Id) | |||||
| member.GlobalUser.RemoveRef(Discord); | 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() | ||||
| { | { | ||||
| @@ -830,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. | ||||
| @@ -1184,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) | |||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Users); | |||||
| async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) | |||||
| { | |||||
| 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) | ||||
| @@ -1199,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 => | ||||
| @@ -140,7 +140,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 +200,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 +218,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); | ||||
| } | } | ||||
| @@ -123,7 +123,7 @@ namespace Discord.WebSocket | |||||
| 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 +147,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; | |||||
| ActiveClients = activeClients; | |||||
| Activity = activity; | |||||
| 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(); | ||||
| @@ -33,7 +33,7 @@ namespace Discord.Webhook | |||||
| : this(webhookUrl, new DiscordRestConfig()) { } | : this(webhookUrl, new DiscordRestConfig()) { } | ||||
| // regex pattern to match webhook urls | // regex pattern to match webhook urls | ||||
| private static Regex WebhookUrlRegex = new Regex(@"^.*discordapp\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); | |||||
| private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); | |||||
| /// <summary> Creates a new Webhook Discord client. </summary> | /// <summary> Creates a new Webhook Discord client. </summary> | ||||
| public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) | public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) | ||||
| @@ -77,9 +77,9 @@ namespace Discord.Webhook | |||||
| 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); | ||||
| } | } | ||||
| @@ -132,13 +132,13 @@ namespace Discord.Webhook | |||||
| if (match != null) | if (match != null) | ||||
| { | { | ||||
| // ensure that the first group is a ulong, set the _webhookId | // ensure that the first group is a ulong, set the _webhookId | ||||
| // 0th group is always the entire match, so start at index 1 | |||||
| if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) | |||||
| // 0th group is always the entire match, and 1 is the domain; so start at index 2 | |||||
| if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) | |||||
| throw ex("The webhook Id could not be parsed."); | throw ex("The webhook Id could not be parsed."); | ||||
| if (!match.Groups[2].Success) | |||||
| if (!match.Groups[3].Success) | |||||
| throw ex("The webhook token could not be parsed."); | throw ex("The webhook token could not be parsed."); | ||||
| webhookToken = match.Groups[2].Value; | |||||
| webhookToken = match.Groups[3].Value; | |||||
| } | } | ||||
| else | else | ||||
| throw ex(); | throw ex(); | ||||
| @@ -73,12 +73,12 @@ namespace Discord | |||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public 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) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| @@ -81,12 +81,12 @@ namespace Discord | |||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public 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) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| @@ -167,12 +167,12 @@ namespace Discord | |||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||
| public Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) | |||||
| public 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) | |||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| } | } | ||||