mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-09 11:51:59 -04:00
Add a setting to control whether to respect advisory rate limits
This commit is contained in:
parent
f31e73bb7a
commit
b34ab51b86
7 changed files with 149 additions and 27 deletions
|
@ -27,8 +27,19 @@ public abstract class DiscordCommandBase : ICommand
|
|||
)]
|
||||
public bool IsBotToken { get; init; } = false;
|
||||
|
||||
[CommandOption(
|
||||
"respect-rate-limits",
|
||||
Description = "Whether to respect advisory rate limits. "
|
||||
+ "If disabled, only hard rate limits (i.e. 429 responses) will be respected."
|
||||
)]
|
||||
public bool ShouldRespectRateLimits { get; init; } = true;
|
||||
|
||||
private DiscordClient? _discordClient;
|
||||
protected DiscordClient Discord => _discordClient ??= new DiscordClient(Token);
|
||||
protected DiscordClient Discord =>
|
||||
_discordClient ??= new DiscordClient(
|
||||
Token,
|
||||
ShouldRespectRateLimits ? RateLimitPreference.RespectAll : RateLimitPreference.IgnoreAll
|
||||
);
|
||||
|
||||
public virtual ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
|
|
|
@ -18,7 +18,10 @@ using JsonExtensions.Reading;
|
|||
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
public class DiscordClient(string token)
|
||||
public class DiscordClient(
|
||||
string token,
|
||||
RateLimitPreference rateLimitPreference = RateLimitPreference.RespectAll
|
||||
)
|
||||
{
|
||||
private readonly Uri _baseUri = new("https://discord.com/api/v10/", UriKind.Absolute);
|
||||
private TokenKind? _resolvedTokenKind;
|
||||
|
@ -47,33 +50,43 @@ public class DiscordClient(string token)
|
|||
innerCancellationToken
|
||||
);
|
||||
|
||||
// If this was the last request available before hitting the rate limit,
|
||||
// wait out the reset time so that future requests can succeed.
|
||||
// This may add an unnecessary delay in case the user doesn't intend to
|
||||
// make any more requests, but implementing a smarter solution would
|
||||
// require properly keeping track of Discord's global/per-route/per-resource
|
||||
// rate limits and that's just way too much effort.
|
||||
// https://discord.com/developers/docs/topics/rate-limits
|
||||
var remainingRequestCount = response
|
||||
.Headers.TryGetValue("X-RateLimit-Remaining")
|
||||
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||
|
||||
var resetAfterDelay = response
|
||||
.Headers.TryGetValue("X-RateLimit-Reset-After")
|
||||
?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))
|
||||
.Pipe(TimeSpan.FromSeconds);
|
||||
|
||||
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
|
||||
// Discord has advisory rate limits (communicated via response headers), but they are typically
|
||||
// way stricter than the actual rate limits enforced by the server.
|
||||
// The user may choose to ignore the advisory rate limits and only retry on hard rate limits,
|
||||
// if they want to prioritize speed over compliance (and safety of their account).
|
||||
// This is especially relevant for user accounts, as the advisory rate limits sometimes don't
|
||||
// make any sense there.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1021
|
||||
if (rateLimitPreference.ShouldRespect(tokenKind))
|
||||
{
|
||||
var delay =
|
||||
// Adding a small buffer to the reset time reduces the chance of getting
|
||||
// rate limited again, because it allows for more requests to be released.
|
||||
(resetAfterDelay.Value + TimeSpan.FromSeconds(1))
|
||||
// Sometimes Discord returns an absurdly high value for the reset time, which
|
||||
// is not actually enforced by the server. So we cap it at a reasonable value.
|
||||
.Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));
|
||||
var remainingRequestCount = response
|
||||
.Headers.TryGetValue("X-RateLimit-Remaining")
|
||||
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||
|
||||
await Task.Delay(delay, innerCancellationToken);
|
||||
var resetAfterDelay = response
|
||||
.Headers.TryGetValue("X-RateLimit-Reset-After")
|
||||
?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))
|
||||
.Pipe(TimeSpan.FromSeconds);
|
||||
|
||||
// If this was the last request available before hitting the rate limit,
|
||||
// wait out the reset time so that future requests can succeed.
|
||||
// This may add an unnecessary delay in case the user doesn't intend to
|
||||
// make any more requests, but implementing a smarter solution would
|
||||
// require properly keeping track of Discord's global/per-route/per-resource
|
||||
// rate limits and that's just way too much effort.
|
||||
// https://discord.com/developers/docs/topics/rate-limits
|
||||
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
|
||||
{
|
||||
var delay =
|
||||
// Adding a small buffer to the reset time reduces the chance of getting
|
||||
// rate limited again, because it allows for more requests to be released.
|
||||
(resetAfterDelay.Value + TimeSpan.FromSeconds(1))
|
||||
// Sometimes Discord returns an absurdly high value for the reset time, which
|
||||
// is not actually enforced by the server. So we cap it at a reasonable value.
|
||||
.Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));
|
||||
|
||||
await Task.Delay(delay, innerCancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
|
|
33
DiscordChatExporter.Core/Discord/RateLimitPreference.cs
Normal file
33
DiscordChatExporter.Core/Discord/RateLimitPreference.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
[Flags]
|
||||
public enum RateLimitPreference
|
||||
{
|
||||
IgnoreAll = 0,
|
||||
RespectForUserRequests = 0b1,
|
||||
RespectForBotRequests = 0b10,
|
||||
RespectAll = RespectForUserRequests | RespectForBotRequests,
|
||||
}
|
||||
|
||||
public static class RateLimitPreferenceExtensions
|
||||
{
|
||||
internal static bool ShouldRespect(
|
||||
this RateLimitPreference rateLimitPreference,
|
||||
TokenKind tokenKind
|
||||
) =>
|
||||
tokenKind == TokenKind.Bot
|
||||
? (rateLimitPreference & RateLimitPreference.RespectForBotRequests) != 0
|
||||
: (rateLimitPreference & RateLimitPreference.RespectForUserRequests) != 0;
|
||||
|
||||
public static string GetDisplayName(this RateLimitPreference rateLimitPreference) =>
|
||||
rateLimitPreference switch
|
||||
{
|
||||
RateLimitPreference.IgnoreAll => "Always ignore",
|
||||
RateLimitPreference.RespectForUserRequests => "Respect for user requests",
|
||||
RateLimitPreference.RespectForBotRequests => "Respect for bot requests",
|
||||
RateLimitPreference.RespectAll => "Always respect",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(rateLimitPreference)),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
public class RateLimitPreferenceToStringConverter : IValueConverter
|
||||
{
|
||||
public static RateLimitPreferenceToStringConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) =>
|
||||
value is RateLimitPreference rateLimitPreference
|
||||
? rateLimitPreference.GetDisplayName()
|
||||
: default;
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => throw new NotSupportedException();
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using System.Text.Json.Serialization;
|
||||
using Cogwheel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Models;
|
||||
|
@ -46,6 +47,13 @@ public partial class SettingsService()
|
|||
set => SetProperty(ref _isTokenPersisted, value);
|
||||
}
|
||||
|
||||
private RateLimitPreference _rateLimitPreference = RateLimitPreference.RespectAll;
|
||||
public RateLimitPreference RateLimitPreference
|
||||
{
|
||||
get => _rateLimitPreference;
|
||||
set => SetProperty(ref _rateLimitPreference, value);
|
||||
}
|
||||
|
||||
private ThreadInclusionMode _threadInclusionMode;
|
||||
public ThreadInclusionMode ThreadInclusionMode
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Models;
|
||||
|
@ -42,6 +43,15 @@ public class SettingsViewModel : DialogViewModelBase
|
|||
set => _settingsService.IsTokenPersisted = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<RateLimitPreference> AvailableRateLimitPreferences { get; } =
|
||||
Enum.GetValues<RateLimitPreference>();
|
||||
|
||||
public RateLimitPreference RateLimitPreference
|
||||
{
|
||||
get => _settingsService.RateLimitPreference;
|
||||
set => _settingsService.RateLimitPreference = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusionModes { get; } =
|
||||
Enum.GetValues<ThreadInclusionMode>();
|
||||
|
||||
|
|
|
@ -54,6 +54,25 @@
|
|||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsTokenPersisted}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Rate limit preference -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Whether to respect advisory rate limits. If disabled, only hard rate limits (i.e. 429 responses) will be respected.">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Rate limit preference" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
DockPanel.Dock="Right"
|
||||
ItemsSource="{Binding AvailableRateLimitPreferences}"
|
||||
SelectedItem="{Binding RateLimitPreference}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:RateLimitPreferenceToStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Thread inclusion mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue