diff --git a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
index 4017bb01d..bc346b04d 100644
--- a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
+++ b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
@@ -1,40 +1,39 @@
+
DisCatSharp.ApplicationCommands
DisCatSharp.ApplicationCommands
- Library
- netstandard2.0
DisCatSharp.ApplicationCommands
ApplicationCommands for DisCatSharp
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, slash, slashcommands, contextmenu
LICENSE.md
True
diff --git a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
index 3fd81da16..204439d94 100644
--- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
+++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
@@ -1,44 +1,43 @@
+
DisCatSharp.CommandsNext
DisCatSharp.CommandsNext
- Library
- netstandard2.0
DisCatSharp.CommandsNext
CommandNext extension for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext
LICENSE.md
-
+
True
diff --git a/DisCatSharp.Common/DisCatSharp.Common.csproj b/DisCatSharp.Common/DisCatSharp.Common.csproj
index 2d90828f3..b52099c56 100644
--- a/DisCatSharp.Common/DisCatSharp.Common.csproj
+++ b/DisCatSharp.Common/DisCatSharp.Common.csproj
@@ -1,54 +1,53 @@
+
DisCatSharp.Common
DisCatSharp.Common
9.0
True
True
True
Portable
- Library
- netstandard2.0
DisCatSharp.Common
Assortment of various common types and utilities for DisCatSharp's projects.
common utilities dotnet dotnet-core dotnetfx netfx netcore csharp
LICENSE.md
False
-
- 1701;1702;;DV2001
+
+ 1701;1702;DV2001
-
- 1701;1702;;DV2001
+
+ 1701;1702;DV2001
True
diff --git a/DisCatSharp.Common/Types/SecureRandom.cs b/DisCatSharp.Common/Types/SecureRandom.cs
index e42a11075..9749e3af5 100644
--- a/DisCatSharp.Common/Types/SecureRandom.cs
+++ b/DisCatSharp.Common/Types/SecureRandom.cs
@@ -1,348 +1,348 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace DisCatSharp.Common
{
///
/// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with .
///
public sealed class SecureRandom : Random, IDisposable
{
///
/// Gets the r n g.
///
private RandomNumberGenerator RNG { get; } = RandomNumberGenerator.Create();
private volatile bool _isDisposed = false;
///
/// Creates a new instance of .
///
public SecureRandom()
{ }
///
/// Finalizes this instance by disposing it.
///
~SecureRandom()
{
this.Dispose();
}
///
/// Fills a supplied buffer with random bytes.
///
/// Buffer to fill with random bytes.
public void GetBytes(byte[] buffer)
{
this.RNG.GetBytes(buffer);
}
///
/// Fills a supplied buffer with random nonzero bytes.
///
/// Buffer to fill with random nonzero bytes.
public void GetNonZeroBytes(byte[] buffer)
{
this.RNG.GetNonZeroBytes(buffer);
}
///
/// Fills a supplied memory region with random bytes.
///
/// Memmory region to fill with random bytes.
public void GetBytes(Span buffer)
{
#if NETCOREAPP
this.RNG.GetBytes(buffer);
#else
var buff = ArrayPool.Shared.Rent(buffer.Length);
try
{
var buffSpan = buff.AsSpan(0, buffer.Length);
this.RNG.GetBytes(buff);
buffSpan.CopyTo(buffer);
}
finally
{
ArrayPool.Shared.Return(buff);
}
#endif
}
///
/// Fills a supplied memory region with random nonzero bytes.
///
/// Memmory region to fill with random nonzero bytes.
public void GetNonZeroBytes(Span buffer)
{
#if NETCOREAPP
this.RNG.GetNonZeroBytes(buffer);
#else
var buff = ArrayPool.Shared.Rent(buffer.Length);
try
{
var buffSpan = buff.AsSpan(0, buffer.Length);
this.RNG.GetNonZeroBytes(buff);
buffSpan.CopyTo(buffer);
}
finally
{
ArrayPool.Shared.Return(buff);
}
#endif
}
///
/// Generates a signed 8-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public sbyte GetInt8(sbyte min = 0, sbyte max = sbyte.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
var offset = (sbyte)(min < 0 ? -min : 0);
min += offset;
max += offset;
return (sbyte)(Math.Abs(this.Generate()) % (max - min) + min - offset);
}
///
/// Generates a unsigned 8-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public byte GetUInt8(byte min = 0, byte max = byte.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
return (byte)(this.Generate() % (max - min) + min);
}
///
/// Generates a signed 16-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public short GetInt16(short min = 0, short max = short.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
var offset = (short)(min < 0 ? -min : 0);
min += offset;
max += offset;
return (short)(Math.Abs(this.Generate()) % (max - min) + min - offset);
}
///
/// Generates a unsigned 16-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public ushort GetUInt16(ushort min = 0, ushort max = ushort.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
return (ushort)(this.Generate() % (max - min) + min);
}
///
/// Generates a signed 32-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public int GetInt32(int min = 0, int max = int.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
var offset = min < 0 ? -min : 0;
min += offset;
max += offset;
return Math.Abs(this.Generate()) % (max - min) + min - offset;
}
///
/// Generates a unsigned 32-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public uint GetUInt32(uint min = 0, uint max = uint.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
return this.Generate() % (max - min) + min;
}
///
/// Generates a signed 64-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public long GetInt64(long min = 0, long max = long.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
var offset = min < 0 ? -min : 0;
min += offset;
max += offset;
return Math.Abs(this.Generate()) % (max - min) + min - offset;
}
///
/// Generates a unsigned 64-bit integer within specified range.
///
/// Minimum value to generate. Defaults to 0.
/// Maximum value to generate. Defaults to .
/// Generated random value.
public ulong GetUInt64(ulong min = 0, ulong max = ulong.MaxValue)
{
if (max <= min)
throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max));
return this.Generate() % (max - min) + min;
}
///
/// Generates a 32-bit floating-point number between 0.0 and 1.0.
///
/// Generated 32-bit floating-point number.
public float GetSingle()
{
var (i1, i2) = ((float)this.GetInt32(), (float)this.GetInt32());
return i1 / i2 % 1.0F;
}
///
/// Generates a 64-bit floating-point number between 0.0 and 1.0.
///
/// Generated 64-bit floating-point number.
public double GetDouble()
{
var (i1, i2) = ((double)this.GetInt64(), (double)this.GetInt64());
return i1 / i2 % 1.0;
}
///
/// Generates a 32-bit integer between 0 and . Upper end exclusive.
///
/// Generated 32-bit integer.
public override int Next()
=> this.GetInt32();
///
/// Generates a 32-bit integer between 0 and . Upper end exclusive.
///
/// Maximum value of the generated integer.
/// Generated 32-bit integer.
public override int Next(int maxValue)
=> this.GetInt32(0, maxValue);
///
/// Generates a 32-bit integer between and . Upper end exclusive.
///
/// Minimum value of the generate integer.
/// Maximum value of the generated integer.
/// Generated 32-bit integer.
public override int Next(int minValue, int maxValue)
=> this.GetInt32(minValue, maxValue);
///
/// Generates a 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive.
///
/// Generated 64-bit floating-point number.
public override double NextDouble()
=> this.GetDouble();
///
/// Fills specified buffer with random bytes.
///
/// Buffer to fill with bytes.
public override void NextBytes(byte[] buffer)
=> this.GetBytes(buffer);
///
/// Fills specified memory region with random bytes.
///
/// Memory region to fill with bytes.
- #if NETCOREAPP
+#if NETCOREAPP
override
- #endif
- public void NextBytes(Span buffer)
+#endif
+ public new void NextBytes(Span buffer)
=> this.GetBytes(buffer);
///
/// Disposes this instance and its resources.
///
public void Dispose()
{
if (this._isDisposed)
return;
this._isDisposed = true;
this.RNG.Dispose();
}
///
/// Generates a random 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive.
///
/// Generated 64-bit floating-point number.
protected override double Sample()
=> this.GetDouble();
///
/// Generates the.
///
/// A T.
private T Generate() where T : struct
{
var size = Unsafe.SizeOf();
Span buff = stackalloc byte[size];
this.GetBytes(buff);
return MemoryMarshal.Read(buff);
}
}
}
diff --git a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj
index 336772429..cb1dc6773 100644
--- a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj
+++ b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj
@@ -1,40 +1,39 @@
+
DisCatSharp.Interactivity
DisCatSharp.Interactivity
- Library
- netstandard2.0
DisCatSharp.Interactivity
Interactivity extension for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, interactive, pagination, reactions
LICENSE.md
True
diff --git a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
index 005ea44e3..fdef96a5f 100644
--- a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
+++ b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
@@ -1,319 +1,319 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Interactivity.Enums;
namespace DisCatSharp.Interactivity.EventHandling
{
///
/// The pagination request.
///
internal class PaginationRequest : IPaginationRequest
{
private TaskCompletionSource _tcs;
private readonly CancellationTokenSource _ct;
- private TimeSpan _timeout;
+ private readonly TimeSpan _timeout;
private readonly List _pages;
private readonly PaginationBehaviour _behaviour;
private readonly DiscordMessage _message;
private readonly PaginationEmojis _emojis;
private readonly DiscordUser _user;
private int _index = 0;
///
/// Creates a new Pagination request
///
/// Message to paginate
/// User to allow control for
/// Behaviour during pagination
/// Behavior on pagination end
/// Emojis for this pagination object
/// Timeout time
/// Pagination pages
internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion,
PaginationEmojis emojis, TimeSpan timeout, params Page[] pages)
{
this._tcs = new();
this._ct = new(timeout);
this._ct.Token.Register(() => this._tcs.TrySetResult(true));
this._timeout = timeout;
this._message = message;
this._user = user;
this.PaginationDeletion = deletion;
this._behaviour = behaviour;
this._emojis = emojis;
this._pages = new List();
foreach (var p in pages)
{
this._pages.Add(p);
}
}
///
/// Gets the page count.
///
public int PageCount => this._pages.Count;
///
/// Gets the pagination deletion.
///
public PaginationDeletion PaginationDeletion { get; }
///
/// Gets the page async.
///
/// A Task.
public async Task GetPageAsync()
{
await Task.Yield();
return this._pages[this._index];
}
///
/// Skips the left async.
///
/// A Task.
public async Task SkipLeftAsync()
{
await Task.Yield();
this._index = 0;
}
///
/// Skips the right async.
///
/// A Task.
public async Task SkipRightAsync()
{
await Task.Yield();
this._index = this._pages.Count - 1;
}
///
/// Nexts the page async.
///
/// A Task.
public async Task NextPageAsync()
{
await Task.Yield();
switch (this._behaviour)
{
case PaginationBehaviour.Ignore:
if (this._index == this._pages.Count - 1)
break;
else
this._index++;
break;
case PaginationBehaviour.WrapAround:
if (this._index == this._pages.Count - 1)
this._index = 0;
else
this._index++;
break;
}
}
///
/// Previous the page async.
///
/// A Task.
public async Task PreviousPageAsync()
{
await Task.Yield();
switch (this._behaviour)
{
case PaginationBehaviour.Ignore:
if (this._index == 0)
break;
else
this._index--;
break;
case PaginationBehaviour.WrapAround:
if (this._index == 0)
this._index = this._pages.Count - 1;
else
this._index--;
break;
}
}
///
/// Gets the buttons async.
///
///
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task> GetButtonsAsync()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
=> throw new NotSupportedException("This request does not support buttons.");
///
/// Gets the emojis async.
///
/// A Task.
public async Task GetEmojisAsync()
{
await Task.Yield();
return this._emojis;
}
///
/// Gets the message async.
///
/// A Task.
public async Task GetMessageAsync()
{
await Task.Yield();
return this._message;
}
///
/// Gets the user async.
///
/// A Task.
public async Task GetUserAsync()
{
await Task.Yield();
return this._user;
}
///
/// Dos the cleanup async.
///
/// A Task.
public async Task DoCleanupAsync()
{
switch (this.PaginationDeletion)
{
case PaginationDeletion.DeleteEmojis:
await this._message.DeleteAllReactionsAsync().ConfigureAwait(false);
break;
case PaginationDeletion.DeleteMessage:
await this._message.DeleteAsync().ConfigureAwait(false);
break;
case PaginationDeletion.KeepEmojis:
break;
}
}
///
/// Gets the task completion source async.
///
/// A Task.
public async Task> GetTaskCompletionSourceAsync()
{
await Task.Yield();
return this._tcs;
}
~PaginationRequest()
{
this.Dispose();
}
///
/// Disposes this PaginationRequest.
///
public void Dispose()
{
this._ct.Dispose();
this._tcs = null;
}
}
}
namespace DisCatSharp.Interactivity
{
///
/// The pagination emojis.
///
public class PaginationEmojis
{
public DiscordEmoji SkipLeft;
public DiscordEmoji SkipRight;
public DiscordEmoji Left;
public DiscordEmoji Right;
public DiscordEmoji Stop;
///
/// Initializes a new instance of the class.
///
public PaginationEmojis()
{
this.Left = DiscordEmoji.FromUnicode("◀");
this.Right = DiscordEmoji.FromUnicode("▶");
this.SkipLeft = DiscordEmoji.FromUnicode("⏮");
this.SkipRight = DiscordEmoji.FromUnicode("⏭");
this.Stop = DiscordEmoji.FromUnicode("⏹");
}
}
///
/// The page.
///
public class Page
{
///
/// Gets or sets the content.
///
public string Content { get; set; }
///
/// Gets or sets the embed.
///
public DiscordEmbed Embed { get; set; }
///
/// Initializes a new instance of the class.
///
/// The content.
/// The embed.
public Page(string content = "", DiscordEmbedBuilder embed = null)
{
this.Content = content;
this.Embed = embed?.Build();
}
}
}
diff --git a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj
index e300baed6..8bd945bc7 100644
--- a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj
+++ b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj
@@ -1,40 +1,39 @@
+
DisCatSharp.Lavalink
DisCatSharp.Lavalink
- Library
- netstandard2.0
true
DisCatSharp.Lavalink
Lavalink implementation for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music, lavalink, lavaplayer
LICENSE.md
True
diff --git a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj
index a5254fd92..bcd9473d6 100644
--- a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj
+++ b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj
@@ -1,45 +1,45 @@
- netstandard2.0
+ netstandard2.1
false
win-x86;win-x64
true
false
symbols.nupkg
DisCatSharp.VoiceNext.Natives
Voice Natives for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music
LICENSE.md
true
runtimes
True
diff --git a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj
index ef4888876..1be917a3d 100644
--- a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj
+++ b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj
@@ -1,45 +1,44 @@
+
DisCatSharp.VoiceNext
DisCatSharp.VoiceNext
- Library
- netstandard2.0
true
DisCatSharp.VoiceNext
Voice extension for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music
LICENSE.md
True
diff --git a/DisCatSharp.sln b/DisCatSharp.sln
index 9eb3bc6ad..7c62ed46d 100644
--- a/DisCatSharp.sln
+++ b/DisCatSharp.sln
@@ -1,153 +1,154 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.31911.260
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp", "DisCatSharp\DisCatSharp.csproj", "{EB3D8310-DFAD-4295-97F9-82E253647583}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext", "DisCatSharp.VoiceNext\DisCatSharp.VoiceNext.csproj", "{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4255B64D-92EC-46B3-BC3B-ED2C3A8073EE}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
BUILDING.md = BUILDING.md
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
CONTRIBUTING.md = CONTRIBUTING.md
LICENSE.md = LICENSE.md
README.md = README.md
SECURITY.md = SECURITY.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.CommandsNext", "DisCatSharp.CommandsNext\DisCatSharp.CommandsNext.csproj", "{C8ED55FB-E028-468D-955F-1534C20274EF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Interactivity", "DisCatSharp.Interactivity\DisCatSharp.Interactivity.csproj", "{DD32BEC3-0189-479F-86DC-CCF95E5634A9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F953F5D0-F0C9-41E6-ADBF-60A76D295899}"
ProjectSection(SolutionItems) = preProject
.nuget\NuGet.config = .nuget\NuGet.config
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Items", "Build Items", "{84464D70-687B-40A8-836D-C4F737698969}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\dependabot.yml = .github\dependabot.yml
DisCatSharp.targets = DisCatSharp.targets
docs-oneclick-rebuild.ps1 = docs-oneclick-rebuild.ps1
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
+ Library.targets = Library.targets
NuGet.targets = NuGet.targets
oneclick-rebuild.ps1 = oneclick-rebuild.ps1
Package.targets = Package.targets
rebuild-all.ps1 = rebuild-all.ps1
rebuild-docs.ps1 = rebuild-docs.ps1
rebuild-lib.ps1 = rebuild-lib.ps1
Version.targets = Version.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{430C28D8-5F85-4D6E-AA68-211549435245}"
ProjectSection(SolutionItems) = preProject
.github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md
.github\CODEOWNERS = .github\CODEOWNERS
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\docs-preview.yml = .github\workflows\docs-preview.yml
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
.github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md
.github\FUNDING.yml = .github\FUNDING.yml
.github\pull_request_template.md = .github\pull_request_template.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext.Natives", "DisCatSharp.VoiceNext.Natives\DisCatSharp.VoiceNext.Natives.csproj", "{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Common", "DisCatSharp.Common\DisCatSharp.Common.csproj", "{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.ApplicationCommands", "DisCatSharp.ApplicationCommands\DisCatSharp.ApplicationCommands.csproj", "{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Configuration", "DisCatSharp.Configuration\DisCatSharp.Configuration.csproj", "{603287D3-1EF2-47F1-A611-C7F25869DE14}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Configuration.Tests", "DisCatSharp.Configuration.Tests\DisCatSharp.Configuration.Tests.csproj", "{E15E88B4-63AD-42DE-B685-D31697C62194}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting", "DisCatSharp.Hosting\DisCatSharp.Hosting.csproj", "{72CCE5D5-926B-432A-876A-065FA2BC9B7B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting.Tests", "DisCatSharp.Hosting.Tests\DisCatSharp.Hosting.Tests.csproj", "{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting.DependencyInjection", "DisCatSharp.Hosting.DependencyInjection\DisCatSharp.Hosting.DependencyInjection.csproj", "{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B15E40E0-03FD-4852-B19B-2C50BCC67704}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.Build.0 = Release|Any CPU
{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.Build.0 = Release|Any CPU
{C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.Build.0 = Release|Any CPU
{DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.Build.0 = Release|Any CPU
{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.Build.0 = Release|Any CPU
{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.Build.0 = Release|Any CPU
{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.Build.0 = Release|Any CPU
{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.Build.0 = Release|Any CPU
{603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.Build.0 = Release|Any CPU
{E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.Build.0 = Release|Any CPU
{72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.Build.0 = Release|Any CPU
{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.Build.0 = Release|Any CPU
{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{430C28D8-5F85-4D6E-AA68-211549435245} = {4255B64D-92EC-46B3-BC3B-ED2C3A8073EE}
{E15E88B4-63AD-42DE-B685-D31697C62194} = {B15E40E0-03FD-4852-B19B-2C50BCC67704}
{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94} = {B15E40E0-03FD-4852-B19B-2C50BCC67704}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23F3A981-51B8-4285-A38C-3267F1D25FE7}
EndGlobalSection
EndGlobal
diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj
index ab435d598..fcabf6c9d 100644
--- a/DisCatSharp/DisCatSharp.csproj
+++ b/DisCatSharp/DisCatSharp.csproj
@@ -1,72 +1,71 @@
+
DisCatSharp
DisCatSharp
- Library
- netstandard2.0
DisCatSharp
Another C# API/Framework for Discord Bots.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, webhooks
LICENSE.md
-
+
True
True
True
Resources.resx
ResXFileCodeGenerator
Resources.Designer.cs
diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs
index 9ebe93897..5519ea6f7 100644
--- a/DisCatSharp/Net/Rest/RestClient.cs
+++ b/DisCatSharp/Net/Rest/RestClient.cs
@@ -1,860 +1,860 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Net
{
///
/// Represents a client used to make REST requests.
///
internal sealed class RestClient : IDisposable
{
///
/// Gets the route argument regex.
///
private static Regex RouteArgumentRegex { get; } = new Regex(@":([a-z_]+)");
///
/// Gets the http client.
///
internal HttpClient HttpClient { get; }
///
/// Gets the discord client.
///
private BaseDiscordClient Discord { get; }
///
/// Gets a value indicating whether debug is enabled.
///
internal bool Debug { get; set; }
///
/// Gets the logger.
///
private ILogger Logger { get; }
///
/// Gets the routes to hashes.
///
private ConcurrentDictionary RoutesToHashes { get; }
///
/// Gets the hashes to buckets.
///
private ConcurrentDictionary HashesToBuckets { get; }
///
/// Gets the request queue.
///
private ConcurrentDictionary RequestQueue { get; }
///
/// Gets the global rate limit event.
///
private AsyncManualResetEvent GlobalRateLimitEvent { get; }
///
/// Gets a value indicating whether use reset after.
///
private bool UseResetAfter { get; }
private CancellationTokenSource _bucketCleanerTokenSource;
- private TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60);
+ private readonly TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60);
private volatile bool _cleanerRunning;
private Task _cleanerTask;
private volatile bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The client.
internal RestClient(BaseDiscordClient client)
: this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger)
{
this.Discord = client;
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client));
if (client.Configuration.Override != null)
{
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", client.Configuration.Override);
}
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative ratelimit.
/// The logger.
internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit,
ILogger logger) // This is for meta-clients, such as the webhook client
{
this.Logger = logger;
var httphandler = new HttpClientHandler
{
UseCookies = false,
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseProxy = proxy != null,
Proxy = proxy
};
this.HttpClient = new HttpClient(httphandler)
{
BaseAddress = new Uri(Utilities.GetApiBaseUri(this.Discord?.Configuration)),
Timeout = timeout
};
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
if (this.Discord != null && this.Discord.Configuration != null && this.Discord.Configuration.Override != null)
{
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this.Discord.Configuration.Override);
}
this.RoutesToHashes = new ConcurrentDictionary();
this.HashesToBuckets = new ConcurrentDictionary();
this.RequestQueue = new ConcurrentDictionary();
this.GlobalRateLimitEvent = new AsyncManualResetEvent(true);
this.UseResetAfter = useRelativeRatelimit;
}
///
/// Gets a bucket.
///
/// The method.
/// The route.
/// The route paramaters.
/// The url.
/// A ratelimit bucket.
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url)
{
var rparams_props = route_params.GetType()
.GetTypeInfo()
.DeclaredProperties;
var rparams = new Dictionary();
foreach (var xp in rparams_props)
{
var val = xp.GetValue(route_params);
rparams[xp.Name] = val is string xs
? xs
: val is DateTime dt
? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is DateTimeOffset dto
? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString();
}
var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : "";
var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : "";
var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : "";
// Create a generic route (minus major params) key
// ex: POST:/channels/channel_id/messages
var hashKey = RateLimitBucket.GenerateHashKey(method, route);
// We check if the hash is present, using our generic route (without major params)
// ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33
// If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant
// and assign this to the route to hash cache
// ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited
var hash = this.RoutesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route));
// Next we use the hash to generate the key to obtain the bucket.
// ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id
// or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id
var bucketId = RateLimitBucket.GenerateBucketId(hash, guild_id, channel_id, webhook_id);
// If it's not in cache, create a new bucket and index it by its bucket id.
var bucket = this.HashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guild_id, channel_id, webhook_id));
bucket.LastAttemptAt = DateTimeOffset.UtcNow;
// Cache the routes for each bucket so it can be used for GC later.
if (!bucket.RouteHashes.Contains(bucketId))
bucket.RouteHashes.Add(bucketId);
// Add the current route to the request queue, which indexes the amount
// of requests occurring to the bucket id.
_ = this.RequestQueue.TryGetValue(bucketId, out var count);
// Increment by one atomically due to concurrency
this.RequestQueue[bucketId] = Interlocked.Increment(ref count);
// Start bucket cleaner if not already running.
if (!this._cleanerRunning)
{
this._cleanerRunning = true;
this._bucketCleanerTokenSource = new CancellationTokenSource();
this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token);
this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started.");
}
url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]);
return bucket;
}
///
/// Executes the request async.
///
/// The request to be executed.
public Task ExecuteRequestAsync(BaseRestRequest request)
=> request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null);
///
/// Executes the request async.
/// This is to allow proper rescheduling of the first request from a bucket.
///
/// The request to be executed.
/// The bucket.
/// The ratelimit task completion source.
private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs)
{
if (this._disposed)
return;
HttpResponseMessage res = default;
try
{
await this.GlobalRateLimitEvent.WaitAsync().ConfigureAwait(false);
if (bucket == null)
bucket = request.RateLimitBucket;
if (ratelimitTcs == null)
ratelimitTcs = await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false);
if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request
{
var now = DateTimeOffset.UtcNow;
await bucket.TryResetLimitAsync(now).ConfigureAwait(false);
// Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket
if (Interlocked.Decrement(ref bucket._remaining) < 0)
{
this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is blocked", bucket.ToString());
var delay = bucket.Reset - now;
var resetDate = bucket.Reset;
if (this.UseResetAfter)
{
delay = bucket.ResetAfter.Value;
resetDate = bucket.ResetAfterOffset;
}
if (delay < new TimeSpan(-TimeSpan.TicksPerMinute))
{
this.Logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket");
bucket._remaining = 1;
}
if (delay < TimeSpan.Zero)
delay = TimeSpan.FromMilliseconds(100);
this.Logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Pre-emptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay);
Task.Delay(delay)
.ContinueWith(_ => this.ExecuteRequestAsync(request, null, null))
.LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
return;
}
this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is allowed", bucket.ToString());
}
else
this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {0} is allowed", bucket.ToString());
var req = this.BuildRequest(request);
if(this.Debug)
this.Logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync());
var response = new RestResponse();
try
{
if (this._disposed)
return;
res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false);
var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
var txt = Utilities.UTF8.GetString(bts, 0, bts.Length);
this.Logger.LogTrace(LoggerEvents.RestRx, txt);
response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase);
response.Response = txt;
response.ResponseCode = (int)res.StatusCode;
}
catch (HttpRequestException httpex)
{
this.Logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url);
request.SetFaulted(httpex);
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
this.UpdateBucket(request, response, ratelimitTcs);
Exception ex = null;
switch (response.ResponseCode)
{
case 400:
case 405:
ex = new BadRequestException(request, response);
break;
case 401:
case 403:
ex = new UnauthorizedException(request, response);
break;
case 404:
ex = new NotFoundException(request, response);
break;
case 413:
ex = new RequestSizeException(request, response);
break;
case 429:
ex = new RateLimitException(request, response);
// check the limit info and requeue
this.Handle429(response, out var wait, out var global);
if (wait != null)
{
if (global)
{
bucket.IsGlobal = true;
this.Logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down");
try
{
this.GlobalRateLimitEvent.Reset();
await wait.ConfigureAwait(false);
}
finally
{
// we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here
_ = this.GlobalRateLimitEvent.SetAsync();
}
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
else
{
this.Logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing request to {0}", request.Url);
await wait.ConfigureAwait(false);
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
return;
}
break;
case 500:
case 502:
case 503:
case 504:
ex = new ServerErrorException(request, response);
break;
}
if (ex != null)
request.SetFaulted(ex);
else
request.SetCompleted(response);
}
catch (Exception ex)
{
this.Logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url);
// if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run
if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0)
this.FailInitialRateLimitTest(request, ratelimitTcs);
if (!request.TrySetFaulted(ex))
throw;
}
finally
{
res?.Dispose();
// Get and decrement active requests in this bucket by 1.
_ = this.RequestQueue.TryGetValue(bucket.BucketId, out var count);
this.RequestQueue[bucket.BucketId] = Interlocked.Decrement(ref count);
// If it's 0 or less, we can remove the bucket from the active request queue,
// along with any of its past routes.
if (count <= 0)
{
foreach (var r in bucket.RouteHashes)
{
if (this.RequestQueue.ContainsKey(r))
{
_ = this.RequestQueue.TryRemove(r, out _);
}
}
}
}
}
///
/// Fails the initial rate limit test.
///
/// The request.
/// The ratelimit task completion source.
/// If true, reset to initial.
private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false)
{
if (ratelimitTcs == null && !resetToInitial)
return;
var bucket = request.RateLimitBucket;
bucket._limitValid = false;
bucket._limitTestFinished = null;
bucket._limitTesting = 0;
//Reset to initial values.
if (resetToInitial)
{
this.UpdateHashCaches(request, bucket);
bucket.Maximum = 0;
bucket._remaining = 0;
return;
}
// no need to wait on all the potentially waiting tasks
_ = Task.Run(() => ratelimitTcs.TrySetResult(false));
}
///
/// Waits for the initial rate limit.
///
/// The bucket.
private async Task> WaitForInitialRateLimit(RateLimitBucket bucket)
{
while (!bucket._limitValid)
{
if (bucket._limitTesting == 0)
{
if (Interlocked.CompareExchange(ref bucket._limitTesting, 1, 0) == 0)
{
// if we got here when the first request was just finishing, we must not create the waiter task as it would signel ExecureRequestAsync to bypass rate limiting
if (bucket._limitValid)
return null;
// allow exactly one request to go through without having rate limits available
var ratelimitsTcs = new TaskCompletionSource();
bucket._limitTestFinished = ratelimitsTcs.Task;
return ratelimitsTcs;
}
}
// it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits
Task waitTask = null;
while (bucket._limitTesting != 0 && (waitTask = bucket._limitTestFinished) == null)
await Task.Yield();
if (waitTask != null)
await waitTask.ConfigureAwait(false);
// if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here
}
return null;
}
///
/// Builds the request.
///
/// The request.
/// A http request message.
private HttpRequestMessage BuildRequest(BaseRestRequest request)
{
var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url);
if (request.Headers != null && request.Headers.Any())
foreach (var kvp in request.Headers)
req.Headers.Add(kvp.Key, kvp.Value);
if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload))
{
this.Logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload);
req.Content = new StringContent(nmprequest.Payload);
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
if (request is MultipartWebRequest mprequest)
{
this.Logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var content = new MultipartFormDataContent(boundary);
if (mprequest.Values != null && mprequest.Values.Any())
foreach (var kvp in mprequest.Values)
content.Add(new StringContent(kvp.Value), kvp.Key);
var fileId = mprequest.OverwriteFileIdStart ?? 0;
if (mprequest.Files != null && mprequest.Files.Any())
{
foreach (var f in mprequest.Files)
{
var name = $"files[{fileId.ToString(CultureInfo.InvariantCulture)}]";
content.Add(new StreamContent(f.Value), name, f.Key);
fileId++;
}
}
req.Content = content;
}
if (request is MultipartStickerWebRequest mpsrequest)
{
this.Logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var sc = new StreamContent(mpsrequest.File.Stream);
if (mpsrequest.File.ContentType != null)
sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType);
var fileName = mpsrequest.File.FileName;
if (mpsrequest.File.FileType != null)
fileName += '.' + mpsrequest.File.FileType;
var content = new MultipartFormDataContent(boundary)
{
{ new StringContent(mpsrequest.Name), "name" },
{ new StringContent(mpsrequest.Tags), "tags" },
{ new StringContent(mpsrequest.Description), "description" },
{ sc, "file", fileName }
};
req.Content = content;
}
return req;
}
///
/// Handles the http 429 status.
///
/// The response.
/// The wait task.
/// If true, global.
private void Handle429(RestResponse response, out Task wait_task, out bool global)
{
wait_task = null;
global = false;
if (response.Headers == null)
return;
var hs = response.Headers;
// handle the wait
if (hs.TryGetValue("Retry-After", out var retry_after_raw))
{
var retry_after = TimeSpan.FromSeconds(int.Parse(retry_after_raw, CultureInfo.InvariantCulture));
wait_task = Task.Delay(retry_after);
}
// check if global b1nzy
if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true")
{
// global
global = true;
}
}
///
/// Updates the bucket.
///
/// The request.
/// The response.
/// The ratelimit task completion source.
private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs)
{
var bucket = request.RateLimitBucket;
if (response.Headers == null)
{
if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
var hs = response.Headers;
if (hs.TryGetValue("X-RateLimit-Scope", out var scope))
{
bucket.Scope = scope;
}
if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true")
{
if (response.ResponseCode != 429)
{
bucket.IsGlobal = true;
this.FailInitialRateLimitTest(request, ratelimitTcs);
}
return;
}
var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesmax);
var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesleft);
var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset);
var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter);
var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash);
if (!r1 || !r2 || !r3 || !r4)
{
//If the limits were determined before this request, make the bucket initial again.
if (response.ResponseCode != 429)
this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null);
return;
}
var clienttime = DateTimeOffset.UtcNow;
var resettime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture));
var servertime = clienttime;
if (hs.TryGetValue("Date", out var raw_date))
servertime = DateTimeOffset.Parse(raw_date, CultureInfo.InvariantCulture).ToUniversalTime();
var resetdelta = resettime - servertime;
//var difference = clienttime - servertime;
//if (Math.Abs(difference.TotalSeconds) >= 1)
//// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now);
//else
// difference = TimeSpan.Zero;
if (request.RateLimitWaitOverride.HasValue)
resetdelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value);
var newReset = clienttime + resetdelta;
if (this.UseResetAfter)
{
bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture));
newReset = clienttime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue
? resetdelta
: TimeSpan.Zero);
bucket.ResetAfterOffset = newReset;
}
else
bucket.Reset = newReset;
var maximum = int.Parse(usesmax, CultureInfo.InvariantCulture);
var remaining = int.Parse(usesleft, CultureInfo.InvariantCulture);
if (ratelimitTcs != null)
{
// initial population of the ratelimit data
bucket.SetInitialValues(maximum, remaining, newReset);
_ = Task.Run(() => ratelimitTcs.TrySetResult(true));
}
else
{
// only update the bucket values if this request was for a newer interval than the one
// currently in the bucket, to avoid issues with concurrent requests in one bucket
// remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time
if (bucket._nextReset == 0)
bucket._nextReset = newReset.UtcTicks;
}
this.UpdateHashCaches(request, bucket, hash);
}
///
/// Updates the hash caches.
///
/// The request.
/// The bucket.
/// The new hash.
private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null)
{
var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route);
if (!this.RoutesToHashes.TryGetValue(hashKey, out var oldHash))
return;
// This is an unlimited bucket, which we don't need to keep track of.
if (newHash == null)
{
_ = this.RoutesToHashes.TryRemove(hashKey, out _);
_ = this.HashesToBuckets.TryRemove(bucket.BucketId, out _);
return;
}
// Only update the hash once, due to a bug on Discord's end.
// This will cause issues if the bucket hashes are dynamically changed from the API while running,
// in which case, Dispose will need to be called to clear the caches.
if (bucket._isUnlimited && newHash != oldHash)
{
this.Logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash);
var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
_ = this.RoutesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) =>
{
bucket.Hash = newHash;
var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
// Remove the old unlimited bucket.
_ = this.HashesToBuckets.TryRemove(oldBucketId, out _);
_ = this.HashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket);
return newHash;
});
}
return;
}
///
/// Cleanups the buckets.
///
private async Task CleanupBucketsAsync()
{
while (!this._bucketCleanerTokenSource.IsCancellationRequested)
{
try
{
await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false);
}
catch { }
if (this._disposed)
return;
//Check and clean request queue first in case it wasn't removed properly during requests.
foreach (var key in this.RequestQueue.Keys)
{
var bucket = this.HashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key));
if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow))
_ = this.RequestQueue.TryRemove(key, out _);
}
var removedBuckets = 0;
StringBuilder bucketIdStrBuilder = default;
foreach (var kvp in this.HashesToBuckets)
{
if (bucketIdStrBuilder == null)
bucketIdStrBuilder = new StringBuilder();
var key = kvp.Key;
var value = kvp.Value;
// Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket.
if (this.RequestQueue.ContainsKey(value.BucketId) && !value._isUnlimited)
continue;
var resetOffset = this.UseResetAfter ? value.ResetAfterOffset : value.Reset;
// Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket.
if (resetOffset != null && !value._isUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay))
continue;
_ = this.HashesToBuckets.TryRemove(key, out _);
removedBuckets++;
bucketIdStrBuilder.Append(value.BucketId + ", ");
}
if (removedBuckets > 0)
this.Logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' '));
if (this.HashesToBuckets.Count == 0)
break;
}
if (!this._bucketCleanerTokenSource.IsCancellationRequested)
this._bucketCleanerTokenSource.Cancel();
this._cleanerRunning = false;
this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
~RestClient()
=> this.Dispose();
///
/// Disposes the rest client.
///
public void Dispose()
{
if (this._disposed)
return;
this._disposed = true;
this.GlobalRateLimitEvent.Reset();
if (this._bucketCleanerTokenSource?.IsCancellationRequested == false)
{
this._bucketCleanerTokenSource?.Cancel();
this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
try
{
this._cleanerTask?.Dispose();
this._bucketCleanerTokenSource?.Dispose();
this.HttpClient?.Dispose();
}
catch { }
this.RoutesToHashes.Clear();
this.HashesToBuckets.Clear();
this.RequestQueue.Clear();
}
}
}
diff --git a/Library.targets b/Library.targets
new file mode 100644
index 000000000..2972704cb
--- /dev/null
+++ b/Library.targets
@@ -0,0 +1,7 @@
+
+
+
+ Library
+ netstandard2.1
+
+