mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-02 07:39:50 -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
DiscordChatExporter.Cli/Commands/Base
DiscordChatExporter.Core/Discord
DiscordChatExporter.Gui
Converters
Services
ViewModels/Dialogs
Views/Dialogs
|
@ -27,8 +27,19 @@ public abstract class DiscordCommandBase : ICommand
|
||||||
)]
|
)]
|
||||||
public bool IsBotToken { get; init; } = false;
|
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;
|
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)
|
public virtual ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,7 +18,10 @@ using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord;
|
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 readonly Uri _baseUri = new("https://discord.com/api/v10/", UriKind.Absolute);
|
||||||
private TokenKind? _resolvedTokenKind;
|
private TokenKind? _resolvedTokenKind;
|
||||||
|
@ -47,33 +50,43 @@ public class DiscordClient(string token)
|
||||||
innerCancellationToken
|
innerCancellationToken
|
||||||
);
|
);
|
||||||
|
|
||||||
// If this was the last request available before hitting the rate limit,
|
// Discord has advisory rate limits (communicated via response headers), but they are typically
|
||||||
// wait out the reset time so that future requests can succeed.
|
// way stricter than the actual rate limits enforced by the server.
|
||||||
// This may add an unnecessary delay in case the user doesn't intend to
|
// The user may choose to ignore the advisory rate limits and only retry on hard rate limits,
|
||||||
// make any more requests, but implementing a smarter solution would
|
// if they want to prioritize speed over compliance (and safety of their account).
|
||||||
// require properly keeping track of Discord's global/per-route/per-resource
|
// This is especially relevant for user accounts, as the advisory rate limits sometimes don't
|
||||||
// rate limits and that's just way too much effort.
|
// make any sense there.
|
||||||
// https://discord.com/developers/docs/topics/rate-limits
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1021
|
||||||
var remainingRequestCount = response
|
if (rateLimitPreference.ShouldRespect(tokenKind))
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
var delay =
|
var remainingRequestCount = response
|
||||||
// Adding a small buffer to the reset time reduces the chance of getting
|
.Headers.TryGetValue("X-RateLimit-Remaining")
|
||||||
// rate limited again, because it allows for more requests to be released.
|
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||||
(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);
|
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;
|
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 System.Text.Json.Serialization;
|
||||||
using Cogwheel;
|
using Cogwheel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Gui.Framework;
|
using DiscordChatExporter.Gui.Framework;
|
||||||
using DiscordChatExporter.Gui.Models;
|
using DiscordChatExporter.Gui.Models;
|
||||||
|
@ -46,6 +47,13 @@ public partial class SettingsService()
|
||||||
set => SetProperty(ref _isTokenPersisted, value);
|
set => SetProperty(ref _isTokenPersisted, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RateLimitPreference _rateLimitPreference = RateLimitPreference.RespectAll;
|
||||||
|
public RateLimitPreference RateLimitPreference
|
||||||
|
{
|
||||||
|
get => _rateLimitPreference;
|
||||||
|
set => SetProperty(ref _rateLimitPreference, value);
|
||||||
|
}
|
||||||
|
|
||||||
private ThreadInclusionMode _threadInclusionMode;
|
private ThreadInclusionMode _threadInclusionMode;
|
||||||
public ThreadInclusionMode ThreadInclusionMode
|
public ThreadInclusionMode ThreadInclusionMode
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Gui.Framework;
|
using DiscordChatExporter.Gui.Framework;
|
||||||
using DiscordChatExporter.Gui.Models;
|
using DiscordChatExporter.Gui.Models;
|
||||||
|
@ -42,6 +43,15 @@ public class SettingsViewModel : DialogViewModelBase
|
||||||
set => _settingsService.IsTokenPersisted = value;
|
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; } =
|
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusionModes { get; } =
|
||||||
Enum.GetValues<ThreadInclusionMode>();
|
Enum.GetValues<ThreadInclusionMode>();
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,25 @@
|
||||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsTokenPersisted}" />
|
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsTokenPersisted}" />
|
||||||
</DockPanel>
|
</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 -->
|
<!-- Thread inclusion mode -->
|
||||||
<DockPanel
|
<DockPanel
|
||||||
Margin="16,8"
|
Margin="16,8"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue