Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ public static class ServiceCollectionExtensions
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="tokenFactory">A function that retrieves the bot token.</param>
/// <param name="apiBasePath">The base API endpoint.</param>
/// <param name="cdnBasePath">The base path to the CDN.</param>
/// <param name="tokenType">The type of token to register.</param>
/// <param name="buildClient">Extra options to configure the rest client.</param>
/// <returns>The service collection, with the services added.</returns>
public static IServiceCollection AddDiscordGateway
(
this IServiceCollection serviceCollection,
Func<IServiceProvider, string> tokenFactory,
Uri apiBasePath,
Uri cdnBasePath,
DiscordTokenType tokenType = DiscordTokenType.Bot,
Action<IHttpClientBuilder>? buildClient = null
)
{
serviceCollection.AddSingleton<IAsyncTokenStore>
(
ctx => new StaticTokenStore(tokenFactory(ctx), DiscordTokenType.Bot)
ctx => new StaticTokenStore(tokenFactory(ctx), tokenType, apiBasePath, cdnBasePath)
);

return serviceCollection.AddDiscordGateway(buildClient);
Expand Down
2 changes: 1 addition & 1 deletion Backend/Remora.Discord.Rest/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static class Constants
/// <summary>
/// Gets the base API URL.
/// </summary>
public static Uri BaseURL { get; } = new($"https://discord.com/api/v{(int)DiscordAPIVersion.V10}/");
public static Uri DiscordBaseURL { get; } = new($"https://discord.com/api/v{(int)DiscordAPIVersion.V10}/");

/// <summary>
/// Gets the name of the audit log reason header.
Expand Down
11 changes: 10 additions & 1 deletion Backend/Remora.Discord.Rest/DiscordTokenType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,14 @@ public enum DiscordTokenType
/// <summary>
/// The token gained through OAuth2 API.
/// </summary>
Bearer
Bearer,

/// <summary>
/// The token gained by authenticating as a user.
/// </summary>
/// <remarks>
/// This kind of token doesn't actually exist. The type is simply not transmitted in this case.
/// This value is provided only for use internal to Remora.Discord.
/// </remarks>
User
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,23 @@ public static class ServiceCollectionExtensions
/// <param name="tokenFactory">
/// A function that creates or retrieves the authorization token and its token type.
/// </param>
/// /// <param name="apiBasePath">The base API endpoint.</param>
/// <param name="cdnBasePath">The base path to the CDN.</param>
/// <param name="buildClient">Extra client building operations.</param>
/// <returns>The service collection, with the services added.</returns>
public static IServiceCollection AddDiscordRest
(
this IServiceCollection serviceCollection,
Func<IServiceProvider, (string Token, DiscordTokenType TokenType)> tokenFactory,
Uri apiBasePath,
Uri cdnBasePath,
Action<IHttpClientBuilder>? buildClient = null
)
{
serviceCollection.AddSingleton<IAsyncTokenStore>(ctx =>
{
var (token, type) = tokenFactory(ctx);
return new StaticTokenStore(token, type);
return new StaticTokenStore(token, type, apiBasePath, cdnBasePath);
});

return serviceCollection.AddDiscordRest(buildClient);
Expand Down Expand Up @@ -243,13 +247,14 @@ public static IServiceCollection AddDiscordRest
var retryDelay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 5);
var clientBuilder = serviceCollection
.AddRestHttpClient<RestError>("Discord")
.ConfigureHttpClient((_, client) =>
.ConfigureHttpClient((services, client) =>
{
IAsyncTokenStore tokenStore = services.GetRequiredService<IAsyncTokenStore>();
var assemblyName = Assembly.GetExecutingAssembly().GetName();
var name = assemblyName.Name ?? "Remora.Discord";
var version = assemblyName.Version ?? new Version(1, 0, 0);

client.BaseAddress = Constants.BaseURL;
client.BaseAddress = tokenStore.BaseApiUri;
client.DefaultRequestHeaders.UserAgent.Add
(
new ProductInfoHeaderValue(name, version.ToString())
Expand Down
11 changes: 11 additions & 0 deletions Backend/Remora.Discord.Rest/IAsyncTokenStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
Expand All @@ -43,4 +44,14 @@ public interface IAsyncTokenStore
/// Gets the type of the token.
/// </summary>
DiscordTokenType TokenType { get; }

/// <summary>
/// Gets the base uri to the Discord API.
/// </summary>
Uri BaseApiUri { get; }

/// <summary>
/// Gets the base uri to the Discord CDN.
/// </summary>
Uri BaseCDNUri { get; }
}
26 changes: 13 additions & 13 deletions Backend/Remora.Discord.Rest/StaticTokenStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
Expand All @@ -29,25 +30,24 @@ namespace Remora.Discord.Rest;
/// <summary>
/// Represents a storage class for a static token.
/// </summary>
/// <param name="token">The token to store.</param>
/// <param name="tokenType">The type of token to store.</param>
/// <param name="baseAPIUri">The base uri for the Discord API.</param>
/// <param name="baseCDNUri">The base uri for the Discord CDN.</param>
[PublicAPI]
public class StaticTokenStore : IAsyncTokenStore
public class StaticTokenStore(string token, DiscordTokenType tokenType, Uri baseAPIUri, Uri baseCDNUri) : IAsyncTokenStore
{
private readonly string _token;
private readonly string _token = token;

/// <inheritdoc />
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken) => new(_token);

/// <inheritdoc />
public DiscordTokenType TokenType { get; }
public DiscordTokenType TokenType { get; } = tokenType;

/// <summary>
/// Initializes a new instance of the <see cref="StaticTokenStore"/> class.
/// </summary>
/// <param name="token">The token to store.</param>
/// <param name="tokenType">The type of token to store.</param>
public StaticTokenStore(string token, DiscordTokenType tokenType)
{
_token = token;
this.TokenType = tokenType;
}
/// <inheritdoc/>
public Uri BaseApiUri { get; } = baseAPIUri;

/// <inheritdoc/>
public Uri BaseCDNUri { get; } = baseCDNUri;
}
29 changes: 24 additions & 5 deletions Remora.Discord.Hosting/Extensions/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using Remora.Discord.Gateway.Extensions;
using Remora.Discord.Hosting.Options;
using Remora.Discord.Hosting.Services;
using Remora.Discord.Rest;
using Remora.Extensions.Options.Immutable;

namespace Remora.Discord.Hosting.Extensions;
Expand All @@ -51,12 +52,26 @@ public static IHostBuilder AddDiscordService
this IHostBuilder hostBuilder,
Func<IServiceProvider, string> tokenFactory,
Action<IHttpClientBuilder>? buildClient = null
)
=> AddDiscordService(hostBuilder, tokenFactory, DiscordServiceOptions.Discord, buildClient);

/// <inheritdoc cref="AddDiscordService(IHostBuilder, Func{IServiceProvider, string}, Action{IHttpClientBuilder}?)"/>
/// <param name="hostbuilder"/>
/// <param name="tokenFactory"/>
/// <param name="discordServiceOptions">The <paramref name="discordServiceOptions"/> used to configure this service.</param>
/// <param name="buildClient"/>
public static IHostBuilder AddDiscordService
(
this IHostBuilder hostbuilder,
Func<IServiceProvider, string> tokenFactory,
DiscordServiceOptions discordServiceOptions,
Action<IHttpClientBuilder>? buildClient = null
)
{
hostBuilder.ConfigureServices((_, serviceCollection) =>
serviceCollection.AddDiscordService(tokenFactory, buildClient));
hostbuilder.ConfigureServices((_, serviceCollection) =>
serviceCollection.AddDiscordService(tokenFactory, discordServiceOptions, buildClient));

return hostBuilder;
return hostbuilder;
}

/// <summary>
Expand All @@ -65,19 +80,23 @@ public static IHostBuilder AddDiscordService
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="tokenFactory">A function that retrieves the bot token.</param>
/// <param name="discordServiceOptions">The <see cref="DiscordServiceOptions"/> used to configure this service.</param>
/// <param name="buildClient">Extra options to configure the rest client.</param>
/// <returns>The service collection, with the services added.</returns>
public static IServiceCollection AddDiscordService
(
this IServiceCollection serviceCollection,
Func<IServiceProvider, string> tokenFactory,
DiscordServiceOptions discordServiceOptions,
Action<IHttpClientBuilder>? buildClient = null
)
{
serviceCollection.Configure(() => new DiscordServiceOptions());
discordServiceOptions.Verify();

serviceCollection.Configure(() => discordServiceOptions);

serviceCollection
.AddDiscordGateway(tokenFactory, buildClient);
.AddDiscordGateway(tokenFactory, discordServiceOptions.APIBasePath, discordServiceOptions.CDNBasePath, discordServiceOptions.TokenType, buildClient);

serviceCollection
.TryAddSingleton<DiscordService>();
Expand Down
99 changes: 98 additions & 1 deletion Remora.Discord.Hosting/Options/DiscordServiceOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,112 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using OneOf;
using Remora.Discord.Rest;

using ApiConstants = Remora.Discord.API.Constants;
using RestConstants = Remora.Discord.Rest.Constants;

namespace Remora.Discord.Hosting.Options;

/// <summary>
/// Defines a set of options used by the background gateway service.
/// </summary>
/// <param name="APIBasePath">The base path to the API.</param>
/// <param name="CDNBasePath">The base path to the CDN.</param>
/// <param name="TokenType">The type of the token to use.
/// <param name="TerminateApplicationOnCriticalGatewayErrors">
/// Whether the service should stop the application if a critical gateway error is encountered.
/// </param>
[PublicAPI]

Check warning on line 43 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'Expected an end tag for element 'param'.'

Check warning on line 43 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'Expected an end tag for element 'param'.'

Check warning on line 43 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'Expected an end tag for element 'param'.'
public record DiscordServiceOptions(bool TerminateApplicationOnCriticalGatewayErrors = true);
public sealed record DiscordServiceOptions
(
Uri APIBasePath,
Uri CDNBasePath,
DiscordTokenType TokenType,
bool TerminateApplicationOnCriticalGatewayErrors = true
)
{
/// <summary>
/// Gets a <see cref="DiscordServiceOptions"/> intended for use with the official Discord API.
/// </summary>
public static DiscordServiceOptions Discord => new(RestConstants.DiscordBaseURL, ApiConstants.CDNBaseURL, DiscordTokenType.Bot, true);

/// <summary>
/// Initializes a new instance of the <see cref="DiscordServiceOptions"/> class.
/// </summary>
/// <param name="apiBasePath">The api base path, as either a <see cref="string"/> or a <see cref="Uri"/>.</param>
/// <param name="cdnBasePath">The cdn base path, as either a <see cref="string"/> or a <see cref="Uri"/>.</param>
public DiscordServiceOptions(OneOf<string, Uri> apiBasePath, OneOf<string, Uri> cdnBasePath)
: this(apiBasePath, DiscordTokenType.Bot, true)

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 3: cannot convert from 'bool' to 'Remora.Discord.Rest.DiscordTokenType'

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 2: cannot convert from 'Remora.Discord.Rest.DiscordTokenType' to 'System.Uri'

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'OneOf.OneOf<string, System.Uri>' to 'System.Uri'

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 3: cannot convert from 'bool' to 'Remora.Discord.Rest.DiscordTokenType'

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 2: cannot convert from 'Remora.Discord.Rest.DiscordTokenType' to 'System.Uri'

Check failure on line 63 in Remora.Discord.Hosting/Options/DiscordServiceOptions.cs

View workflow job for this annotation

GitHub Actions / build

Argument 1: cannot convert from 'OneOf.OneOf<string, System.Uri>' to 'System.Uri'
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DiscordServiceOptions"/> class.
/// </summary>
/// <param name="apiBasePath">The base path, either as a <see cref="string"/> or a <see cref="Uri"/>.</param>
/// <param name="cdnBasePath">The cdn base path, as either a <see cref="string"/> or a <see cref="Uri"/>.</param>
/// <param name="tokenType">The type of the token to use.</param>
/// <param name="terminateApplicationOnCriticalGatewayErrors">Whether the service should stop the application if a critical gateway error is encountered.</param>
public DiscordServiceOptions(OneOf<string, Uri> apiBasePath, OneOf<string, Uri> cdnBasePath, DiscordTokenType tokenType, bool terminateApplicationOnCriticalGatewayErrors)
: this(apiBasePath.Match(path => new Uri(path), uri => uri), cdnBasePath.Match(path => new Uri(path), uri => uri), tokenType, true)
{
}

/// <summary>
/// Verifies this instance to ensure a user is not trying to connect to Discord via a user token.
/// </summary>
/// <exception cref="InvalidOperationException">The API path is pointing to official Discord servers
/// and a connection attempt was made with a <see cref="DiscordTokenType.User"/> token.</exception>
public void Verify()
{
if (!TryVerify())
{
throw GetUserTokenException();
}
}

/// <summary>
/// Verifies this instance to ensure a user is not trying to connect to Discord via a user token.
/// </summary>
/// <returns><see langword="true"/> if the instance passes verification; otherwise, <see langword="false"/>.</returns>
public bool TryVerify()
=> DiscordServiceOptions.TryVerify(this);

/// <summary>
/// Verifies the provided <paramref name="serviceOptions"/> to ensure a user is not trying to connect to Discord via a user token.
/// </summary>
/// <param name="serviceOptions">The <see cref="DiscordServiceOptions"/> instance to verify.</param>
/// <exception cref="InvalidOperationException">The API path is pointing to official Discord servers
/// and a connection attempt was made with a <see cref="DiscordTokenType.User"/> token.</exception>
public static void Verify(DiscordServiceOptions serviceOptions)
{
if (!DiscordServiceOptions.TryVerify(serviceOptions))
{
throw GetUserTokenException();
}
}

/// <summary>
/// Verifies the provided <paramref name="serviceOptions"/> to ensure a user is not trying to connect to Discord via a user token.
/// </summary>
/// <param name="serviceOptions">The <see cref="DiscordServiceOptions"/> instance to verify.</param>
/// <returns><see langword="true"/> if the instance passes verification; otherwise, <see langword="false"/>.</returns>
public static bool TryVerify(DiscordServiceOptions serviceOptions)
{
if (serviceOptions.APIBasePath == RestConstants.DiscordBaseURL)
{
return serviceOptions.TokenType != DiscordTokenType.User;
}

// If we're not using the Discord API, allow any token type.
return true;
}

private static InvalidOperationException GetUserTokenException()
=> new InvalidOperationException("You MUST NOT use a user token with the official Discord API.");
}
Loading
Loading