From f1c112e17c8996e3193daeae469e5000a394b781 Mon Sep 17 00:00:00 2001 From: MazeXP <26042705+MazeXP@users.noreply.github.com> Date: Mon, 12 Dec 2022 19:59:15 +0100 Subject: [PATCH 1/3] Implement role connections API endpoints and objects --- .../ApplicationRoleConnectionMetadataType.cs | 72 ++++ .../API/Objects/Applications/IApplication.cs | 16 +- .../IApplicationRoleConnectionMetadata.cs | 67 ++++ .../Applications/IPartialApplication.cs | 3 + .../API/Rest/IDiscordRestApplicationAPI.cs | 29 ++ .../API/Objects/Applications/Application.cs | 3 +- .../ApplicationRoleConnectionMetadata.cs | 40 +++ .../Applications/PartialApplication.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 18 +- .../Applications/DiscordRestApplicationAPI.cs | 60 ++++ .../ApplicationRoleConnectionMetadataTests.cs | 39 ++ .../DiscordRestApplicationAPITests.cs | 338 ++++++++++++++++++ .../APPLICATION_ROLE_CONNECTION_METADATA.json | 6 + 13 files changed, 688 insertions(+), 6 deletions(-) create mode 100644 Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/ApplicationRoleConnectionMetadataType.cs create mode 100644 Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs create mode 100644 Backend/Remora.Discord.API/API/Objects/Applications/ApplicationRoleConnectionMetadata.cs create mode 100644 Tests/Remora.Discord.API.Tests/API/Objects/ApplicationRoleConnections/ApplicationRoleConnectionMetadataTests.cs create mode 100644 Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION_METADATA/APPLICATION_ROLE_CONNECTION_METADATA.json diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/ApplicationRoleConnectionMetadataType.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/ApplicationRoleConnectionMetadataType.cs new file mode 100644 index 0000000000..48f742e452 --- /dev/null +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/ApplicationRoleConnectionMetadataType.cs @@ -0,0 +1,72 @@ +// +// ApplicationRoleConnectionMetadataType.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using JetBrains.Annotations; + +namespace Remora.Discord.API.Abstractions.Objects; + +/// +/// Enumerates various application role connection metadata types. +/// +[PublicAPI] +public enum ApplicationRoleConnectionMetadataType +{ + /// + /// The metadata value (integer) is less than or equal to the guild's configured value (integer). + /// + IntegerLessThanOrEqual = 1, + + /// + /// The metadata value (integer) is greater than or equal to the guild's configured value (integer). + /// + IntegerGreaterThanOrEqual = 2, + + /// + /// The metadata value (integer) is equal to the guild's configured value (integer). + /// + IntegerEqual = 3, + + /// + /// The metadata value (integer) is not equal to the guild's configured value (integer). + /// + IntegerNotEqual = 4, + + /// + /// The metadata value (ISO8601 string) is less than or equal to the guild's configured value (integer; days before current date). + /// + DateTimeLessThanOrEqual = 5, + + /// + /// The metadata value (ISO8601 string) is greater than or equal to the guild's configured value (integer; days before current date). + /// + DateTimeGreaterThanOrEqual = 6, + + /// + /// The metadata value (integer) is equal to the guild's configured value (integer; 1). + /// + BooleanEqual = 7, + + /// + /// The metadata value (integer) is not equal to the guild's configured value (integer; 1). + /// + BooleanNotEqual = 8, +} diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplication.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplication.cs index b0c0f4bfbd..cf6230063d 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplication.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplication.cs @@ -133,6 +133,13 @@ public interface IApplication : IPartialApplication /// new Optional CustomInstallUrl { get; } + /// + /// Gets the application's role connection verification entry point, + /// which when configured will render the app as a verification method + /// in the guild role verification configuration. + /// + new Optional RoleConnectionsVerificationUrl { get; } + /// Optional IPartialApplication.ID => this.ID; @@ -185,11 +192,14 @@ public interface IApplication : IPartialApplication Optional IPartialApplication.Flags => this.Flags; /// - Optional> IPartialApplication.Tags => throw new NotImplementedException(); + Optional> IPartialApplication.Tags => this.Tags; + + /// + Optional IPartialApplication.InstallParams => this.InstallParams; /// - Optional IPartialApplication.InstallParams => throw new NotImplementedException(); + Optional IPartialApplication.CustomInstallUrl => this.CustomInstallUrl; /// - Optional IPartialApplication.CustomInstallUrl => throw new NotImplementedException(); + Optional IPartialApplication.RoleConnectionsVerificationUrl => this.RoleConnectionsVerificationUrl; } diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs new file mode 100644 index 0000000000..41dde9efcf --- /dev/null +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs @@ -0,0 +1,67 @@ +// +// IApplicationRoleConnectionMetadata.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Collections.Generic; +using JetBrains.Annotations; +using Remora.Rest.Core; + +namespace Remora.Discord.API.Abstractions.Objects; + +/// +/// Represents an application role connection metadata field. +/// +[PublicAPI] +public interface IApplicationRoleConnectionMetadata +{ + /// + /// Gets the type of metadata value. + /// + ApplicationRoleConnectionMetadataType Type { get; } + + /// + /// Gets the dictionary key for the metadata field. + /// + /// The key must only consist of a-z, 0-9 or _ and must have a length between 1 and 50 characters. + string Key { get; } + + /// + /// Gets the name of the metadata field. + /// + /// The length of the name must be between 1 and 100 characters. + string Name { get; } + + /// + /// Gets the localized names of the metadata field. + /// + Optional> NameLocalizations { get; } + + /// + /// Gets the description of the metadata field. + /// + /// The length of the description must be between 1 and 200 characters. + string Description { get; } + + /// + /// Gets the localized descriptions of the metadata field. + /// + Optional> DescriptionLocalizations { get; } +} diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IPartialApplication.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IPartialApplication.cs index 270c23941f..3574b70e4e 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IPartialApplication.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IPartialApplication.cs @@ -92,4 +92,7 @@ public interface IPartialApplication /// Optional CustomInstallUrl { get; } + + /// + Optional RoleConnectionsVerificationUrl { get; } } diff --git a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs index be920d94cc..e227156a3b 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs @@ -331,4 +331,33 @@ Task> EditApplicationCommandPermissi IReadOnlyList permissions, CancellationToken ct = default ); + + /// + /// Gets the application role connection metadata records for the given application.. + /// + /// The ID of the bot application. + /// The cancellation token for this operation. + /// A retrieval result which may or may not have succeeded. + Task>> GetApplicationRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationID, + CancellationToken ct = default + ); + + /// + /// Updates the application role connection metadata records for the given application.. + /// + /// + /// An application can have a maximum of 5 metadata records. + /// + /// The ID of the bot application. + /// The metadata records to overwrite the existing ones. + /// The cancellation token for this operation. + /// An creation result which may or may not have succeeded. + Task>> UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationID, + IReadOnlyList records, + CancellationToken ct = default + ); } diff --git a/Backend/Remora.Discord.API/API/Objects/Applications/Application.cs b/Backend/Remora.Discord.API/API/Objects/Applications/Application.cs index 7d3691bee3..0646c90693 100644 --- a/Backend/Remora.Discord.API/API/Objects/Applications/Application.cs +++ b/Backend/Remora.Discord.API/API/Objects/Applications/Application.cs @@ -53,5 +53,6 @@ public record Application Optional Flags = default, Optional> Tags = default, Optional InstallParams = default, - Optional CustomInstallUrl = default + Optional CustomInstallUrl = default, + Optional RoleConnectionsVerificationUrl = default ) : IApplication; diff --git a/Backend/Remora.Discord.API/API/Objects/Applications/ApplicationRoleConnectionMetadata.cs b/Backend/Remora.Discord.API/API/Objects/Applications/ApplicationRoleConnectionMetadata.cs new file mode 100644 index 0000000000..87e1f3db43 --- /dev/null +++ b/Backend/Remora.Discord.API/API/Objects/Applications/ApplicationRoleConnectionMetadata.cs @@ -0,0 +1,40 @@ +// +// ApplicationRoleConnectionMetadata.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Collections.Generic; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Rest.Core; + +namespace Remora.Discord.API.Objects; + +/// +[PublicAPI] +public record ApplicationRoleConnectionMetadata +( + string Key, + string Name, + string Description, + ApplicationRoleConnectionMetadataType Type, + Optional> NameLocalizations = default, + Optional> DescriptionLocalizations = default +) : IApplicationRoleConnectionMetadata; diff --git a/Backend/Remora.Discord.API/API/Objects/Applications/PartialApplication.cs b/Backend/Remora.Discord.API/API/Objects/Applications/PartialApplication.cs index ed579dd858..77c5f0a883 100644 --- a/Backend/Remora.Discord.API/API/Objects/Applications/PartialApplication.cs +++ b/Backend/Remora.Discord.API/API/Objects/Applications/PartialApplication.cs @@ -53,5 +53,6 @@ public record PartialApplication Optional Flags = default, Optional> Tags = default, Optional InstallParams = default, - Optional CustomInstallUrl = default + Optional CustomInstallUrl = default, + Optional RoleConnectionsVerificationUrl = default ) : IPartialApplication; diff --git a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs index 02a76c29e3..3765507338 100644 --- a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs +++ b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs @@ -110,7 +110,8 @@ public static IServiceCollection ConfigureDiscordJsonConverters .AddOAuth2ObjectConverters() .AddTeamObjectConverters() .AddStageInstanceObjectConverters() - .AddStickerObjectConverters(); + .AddStickerObjectConverters() + .AddApplicationRoleConnectionObjectConverters(); options.AddDataObjectConverter(); options.AddConverter(); @@ -1126,4 +1127,19 @@ private static JsonSerializerOptions AddStickerObjectConverters(this JsonSeriali return options; } + + /// + /// Adds the JSON converters that handle application role connection objects. + /// + /// The serializer options. + /// The options, with the converters added. + private static JsonSerializerOptions AddApplicationRoleConnectionObjectConverters + ( + this JsonSerializerOptions options + ) + { + options.AddDataObjectConverter(); + + return options; + } } diff --git a/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs b/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs index 80f6bbe85a..587bf44500 100644 --- a/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs +++ b/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs @@ -565,4 +565,64 @@ public virtual Task> EditApplication ct: ct ); } + + /// + public virtual Task>> GetApplicationRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationID, + CancellationToken ct = default + ) + { + return this.RestHttpClient.GetAsync> + ( + $"applications/{applicationID}/role-connections/metadata", + b => b.WithRateLimitContext(this.RateLimitCache), + ct: ct + ); + } + + /// + public virtual async Task>> UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationID, + IReadOnlyList records, + CancellationToken ct = default + ) + { + if (records.Any(r => r.Key.Length is < 1 or > 50)) + { + return new ArgumentOutOfRangeError + ( + nameof(records), + "Role connection metadata keys must be between 1 and 50 characters." + ); + } + + if (records.Any(r => r.Name.Length is < 1 or > 100)) + { + return new ArgumentOutOfRangeError + ( + nameof(records), + "Role connection metadata names must be between 1 and 100 characters." + ); + } + + if (records.Any(r => r.Description.Length is < 1 or > 200)) + { + return new ArgumentOutOfRangeError + ( + nameof(records), + "Role connection metadata descriptions must be between 1 and 200 characters." + ); + } + + return await this.RestHttpClient.PutAsync> + ( + $"applications/{applicationID}/role-connections/metadata", + b => b + .WithJsonArray(json => JsonSerializer.Serialize(json, records, this.JsonOptions), false) + .WithRateLimitContext(this.RateLimitCache), + ct: ct + ); + } } diff --git a/Tests/Remora.Discord.API.Tests/API/Objects/ApplicationRoleConnections/ApplicationRoleConnectionMetadataTests.cs b/Tests/Remora.Discord.API.Tests/API/Objects/ApplicationRoleConnections/ApplicationRoleConnectionMetadataTests.cs new file mode 100644 index 0000000000..023d0c1c6d --- /dev/null +++ b/Tests/Remora.Discord.API.Tests/API/Objects/ApplicationRoleConnections/ApplicationRoleConnectionMetadataTests.cs @@ -0,0 +1,39 @@ +// +// ApplicationRoleConnectionMetadataTests.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Tests.TestBases; + +namespace Remora.Discord.API.Tests.Objects; + +/// +public class ApplicationRoleConnectionMetadataTests : ObjectTestBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public ApplicationRoleConnectionMetadataTests(JsonBackedTypeTestFixture fixture) + : base(fixture) + { + } +} diff --git a/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs b/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs index 08e3ce93b2..4d6883fcf2 100644 --- a/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs +++ b/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs @@ -2092,4 +2092,342 @@ public async Task PerformsRequestCorrectly() ResultAssert.Successful(result); } } + + /// + /// Tests the method. + /// + public class GetApplicationRoleConnectionMetadataRecordsAsync : RestAPITestBase + { + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public GetApplicationRoleConnectionMetadataRecordsAsync(RestAPITestFixture fixture) + : base(fixture) + { + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task PerformsRequestCorrectly() + { + var applicationID = DiscordSnowflake.New(0); + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Get, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .WithNoContent() + .Respond("application/json", "[ ]") + ); + + var result = await api.GetApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID + ); + + ResultAssert.Successful(result); + } + } + + /// + /// Tests the method. + /// + public class UpdateApplicationRoleConnectionMetadataRecordsAsync : RestAPITestBase + { + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public UpdateApplicationRoleConnectionMetadataRecordsAsync(RestAPITestFixture fixture) + : base(fixture) + { + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task PerformsRequestCorrectly() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + "a", + "b", + "c", + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ), + new ApplicationRoleConnectionMetadata + ( + new string('d', 50), + new string('e', 100), + new string('f', 200), + ApplicationRoleConnectionMetadataType.IntegerGreaterThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .WithJson + ( + json => json.IsArray + ( + a => a + .WithElement + ( + 0, + e => e.IsObject + ( + o => o + .WithProperty("key", p => p.Is(records[0].Key)) + .WithProperty("name", p => p.Is(records[0].Name)) + .WithProperty("description", p => p.Is(records[0].Description)) + .WithProperty("type", p => p.Is((int)records[0].Type)) + ) + ) + .WithElement + ( + 1, + e => e.IsObject + ( + o => o + .WithProperty("key", p => p.Is(records[1].Key)) + .WithProperty("name", p => p.Is(records[1].Name)) + .WithProperty("description", p => p.Is(records[1].Description)) + .WithProperty("type", p => p.Is((int)records[1].Type)) + ) + ) + ) + ) + .Respond("application/json", "[]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Successful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfKeyIsTooShort() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + string.Empty, + "b", + "c", + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfKeyIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + new string('a', 51), + "b", + "c", + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfNameIsTooShort() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + "a", + string.Empty, + "c", + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfNameIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + "a", + new string('b', 101), + "c", + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfDescriptionIsTooShort() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + "a", + "b", + string.Empty, + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method returns a client-side error if a failure condition is met. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfDescriptionIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var records = new[] + { + new ApplicationRoleConnectionMetadata + ( + "a", + "b", + new string('c', 201), + ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual + ) + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") + .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") + ); + + var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync + ( + applicationID, + records + ); + + ResultAssert.Unsuccessful(result); + } + } } diff --git a/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION_METADATA/APPLICATION_ROLE_CONNECTION_METADATA.json b/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION_METADATA/APPLICATION_ROLE_CONNECTION_METADATA.json new file mode 100644 index 0000000000..15b0918c67 --- /dev/null +++ b/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION_METADATA/APPLICATION_ROLE_CONNECTION_METADATA.json @@ -0,0 +1,6 @@ +{ + "type": 1, + "key": "none", + "name": "none", + "description": "none" +} \ No newline at end of file From b74f6d43c294afc17867cdf7b0fe2d10c1856a30 Mon Sep 17 00:00:00 2001 From: MazeXP <26042705+MazeXP@users.noreply.github.com> Date: Mon, 12 Dec 2022 22:02:56 +0100 Subject: [PATCH 2/3] Add remaining role connection endpoints and objects --- .../IApplicationRoleConnectionMetadata.cs | 6 +- .../Users/IApplicationRoleConnection.cs | 53 +++++ .../API/Rest/IDiscordRestUserAPI.cs | 42 ++++ .../Users/ApplicationRoleConnection.cs | 37 ++++ .../Extensions/ServiceCollectionExtensions.cs | 1 + .../API/CachingDiscordRestUserAPI.cs | 60 ++++++ Backend/Remora.Discord.Caching/KeyHelpers.cs | 10 + .../Applications/DiscordRestApplicationAPI.cs | 12 +- .../API/Users/DiscordRestUserAPI.cs | 70 ++++++ .../Users/ApplicationRoleConnectionTests.cs | 39 ++++ .../DiscordRestApplicationAPITests.cs | 119 +---------- .../API/Users/DiscordRestUserAPITests.cs | 202 ++++++++++++++++++ .../APPLICATION_ROLE_CONNECTION.json | 7 + 13 files changed, 537 insertions(+), 121 deletions(-) create mode 100644 Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs create mode 100644 Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs create mode 100644 Tests/Remora.Discord.API.Tests/API/Objects/Users/ApplicationRoleConnectionTests.cs create mode 100644 Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION/APPLICATION_ROLE_CONNECTION.json diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs index 41dde9efcf..69027a6f0e 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Applications/IApplicationRoleConnectionMetadata.cs @@ -40,13 +40,13 @@ public interface IApplicationRoleConnectionMetadata /// /// Gets the dictionary key for the metadata field. /// - /// The key must only consist of a-z, 0-9 or _ and must have a length between 1 and 50 characters. + /// The key must only consist of a-z, 0-9 or _ and must have a length of max. 50 characters. string Key { get; } /// /// Gets the name of the metadata field. /// - /// The length of the name must be between 1 and 100 characters. + /// The length of the name must be max. 100 characters. string Name { get; } /// @@ -57,7 +57,7 @@ public interface IApplicationRoleConnectionMetadata /// /// Gets the description of the metadata field. /// - /// The length of the description must be between 1 and 200 characters. + /// The length of the description must be max. 200 characters. string Description { get; } /// diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs new file mode 100644 index 0000000000..42c8813ace --- /dev/null +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs @@ -0,0 +1,53 @@ +// +// IApplicationRoleConnection.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Collections.Generic; +using JetBrains.Annotations; +using Remora.Rest.Core; + +namespace Remora.Discord.API.Abstractions.Objects; + +/// +/// Represents a role connection that an application has attached to an user. +/// +[PublicAPI] +public interface IApplicationRoleConnection +{ + /// + /// Gets the vanity name of the platform a bot has connected. + /// + /// The length of the platform username must be max. 50 characters. + Optional PlatformName { get; } + + /// + /// Gets the username on the platform a bot has connected. + /// + /// The length of the platform username must be max. 100 characters. + Optional PlatformUsername { get; } + + /// + /// Gets the object mapping of to their stringified value + /// for the user on the platform a bot has connected. + /// + /// The length of the stringified value must max. 100 characters. + Optional> Metadata { get; } +} diff --git a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestUserAPI.cs b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestUserAPI.cs index 32894ad912..157af32f28 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestUserAPI.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestUserAPI.cs @@ -130,10 +130,52 @@ Task> CreateDMAsync /// /// Gets a list of connection objects. /// + /// + /// Requires the "connections" OAuth" scope. + /// /// The cancellation token for this operation. /// A retrieval result which may or may not have succeeded. Task>> GetUserConnectionsAsync ( CancellationToken ct = default ); + + /// + /// Gets the application role connection for the user. + /// + /// + /// Requires an OAuth2 access token with role_connections.write scope for the specified . + /// + /// The ID of the application. + /// The cancellation token for this operation. + /// A retrieval result which may or may not have succeeded. + Task> GetUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + CancellationToken ct = default + ); + + /// + /// Updates and returns the application role connection for the user. + /// + /// + /// Requires an OAuth2 access token with role_connections.write scope for the specified . + /// + /// The ID of the application. + /// The vanity name of the platform a bot has connected (max 50 characters). + /// The username on the platform a bot has connected (max 100 characters). + /// + /// The object mapping application role connection metadata keys to their stringified value (max 100 characters) for + /// the user on the platform a bot has connected. + /// + /// The cancellation token for this operation. + /// A retrieval result which may or may not have succeeded. + Task> UpdateUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + Optional platformName = default, + Optional platformUsername = default, + Optional> metadata = default, + CancellationToken ct = default + ); } diff --git a/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs b/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs new file mode 100644 index 0000000000..f7afc88139 --- /dev/null +++ b/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs @@ -0,0 +1,37 @@ +// +// ApplicationRoleConnection.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Collections.Generic; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Rest.Core; + +namespace Remora.Discord.API.Objects; + +/// +[PublicAPI] +public record ApplicationRoleConnection +( + Optional PlatformName = default, + Optional PlatformUsername = default, + Optional> Metadata = default +) : IApplicationRoleConnection; diff --git a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs index 3765507338..cbeae52db7 100644 --- a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs +++ b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs @@ -1139,6 +1139,7 @@ this JsonSerializerOptions options ) { options.AddDataObjectConverter(); + options.AddDataObjectConverter(); return options; } diff --git a/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs b/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs index c6f373e862..c5ea2ea1fa 100644 --- a/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs +++ b/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs @@ -244,4 +244,64 @@ public async Task> GetCurrentUserGuildMemberAsync return result; } + + /// + public async Task> GetUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + CancellationToken ct = default + ) + { + var key = KeyHelpers.CreateCurrentUserApplicationRoleConnectionCacheKey(applicationID); + var cacheResult = await _cacheService.TryGetValueAsync(key, ct); + + if (cacheResult.IsSuccess) + { + return cacheResult; + } + + var getUserApplicationRoleConnection = await _actual.GetUserApplicationRoleConnectionAsync + ( + applicationID, + ct + ); + if (!getUserApplicationRoleConnection.IsDefined(out var userApplicationRoleConnection)) + { + return getUserApplicationRoleConnection; + } + + await _cacheService.CacheAsync(key, userApplicationRoleConnection, ct); + + return getUserApplicationRoleConnection; + } + + /// + public async Task> UpdateUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + Optional platformName = default, + Optional platformUsername = default, + Optional> metadata = default, + CancellationToken ct = default + ) + { + var result = await _actual.UpdateUserApplicationRoleConnectionAsync + ( + applicationID, + platformName, + platformUsername, + metadata, + ct + ); + + if (!result.IsDefined(out var userApplicationRoleConnection)) + { + return result; + } + + var key = KeyHelpers.CreateCurrentUserApplicationRoleConnectionCacheKey(applicationID); + await _cacheService.CacheAsync(key, userApplicationRoleConnection, ct); + + return result; + } } diff --git a/Backend/Remora.Discord.Caching/KeyHelpers.cs b/Backend/Remora.Discord.Caching/KeyHelpers.cs index d2dd28bf6a..5d4a8b1a3e 100644 --- a/Backend/Remora.Discord.Caching/KeyHelpers.cs +++ b/Backend/Remora.Discord.Caching/KeyHelpers.cs @@ -298,6 +298,16 @@ public static string CreateCurrentUserDMsCacheKey() return $"{CreateCurrentUserCacheKey()}:Channels"; } + /// + /// Creates a cache key for an instance of the current instance. + /// + /// The ID of the application. + /// The cache key. + public static string CreateCurrentUserApplicationRoleConnectionCacheKey(in Snowflake applicationID) + { + return $"{CreateCurrentUserCacheKey()}:Application:{applicationID}:RoleConnection"; + } + /// /// Creates a cache key for an instance. /// diff --git a/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs b/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs index 587bf44500..525199a3ac 100644 --- a/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs +++ b/Backend/Remora.Discord.Rest/API/Applications/DiscordRestApplicationAPI.cs @@ -589,30 +589,30 @@ public virtual async Task r.Key.Length is < 1 or > 50)) + if (records.Any(r => r.Key.Length > 50)) { return new ArgumentOutOfRangeError ( nameof(records), - "Role connection metadata keys must be between 1 and 50 characters." + "Role connection metadata keys must be max. 50 characters." ); } - if (records.Any(r => r.Name.Length is < 1 or > 100)) + if (records.Any(r => r.Name.Length > 100)) { return new ArgumentOutOfRangeError ( nameof(records), - "Role connection metadata names must be between 1 and 100 characters." + "Role connection metadata names must be max. 100 characters." ); } - if (records.Any(r => r.Description.Length is < 1 or > 200)) + if (records.Any(r => r.Description.Length > 200)) { return new ArgumentOutOfRangeError ( nameof(records), - "Role connection metadata descriptions must be between 1 and 200 characters." + "Role connection metadata descriptions must be max. 200 characters." ); } diff --git a/Backend/Remora.Discord.Rest/API/Users/DiscordRestUserAPI.cs b/Backend/Remora.Discord.Rest/API/Users/DiscordRestUserAPI.cs index 0d8031c06d..004aa28408 100644 --- a/Backend/Remora.Discord.Rest/API/Users/DiscordRestUserAPI.cs +++ b/Backend/Remora.Discord.Rest/API/Users/DiscordRestUserAPI.cs @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -230,4 +231,73 @@ public virtual Task>> GetUserConnectionsAsync ct: ct ); } + + /// + public virtual Task> GetUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + CancellationToken ct = default + ) + { + return this.RestHttpClient.GetAsync + ( + $"users/@me/applications/{applicationID}/role-connection", + b => b.WithRateLimitContext(this.RateLimitCache), + ct: ct + ); + } + + /// + public virtual async Task> UpdateUserApplicationRoleConnectionAsync + ( + Snowflake applicationID, + Optional platformName = default, + Optional platformUsername = default, + Optional> metadata = default, + CancellationToken ct = default + ) + { + if (platformName.HasValue && platformName.Value.Length > 50) + { + return new ArgumentOutOfRangeError + ( + nameof(platformName), + "The platform name must be max. 50 characters." + ); + } + + if (platformUsername.HasValue && platformUsername.Value.Length > 100) + { + return new ArgumentOutOfRangeError + ( + nameof(platformUsername), + "The platform username must be max. 100 characters." + ); + } + + if (metadata.HasValue && metadata.Value.Values.Any(m => m.Length > 100)) + { + return new ArgumentOutOfRangeError + ( + nameof(metadata), + "The metadata values must be max. 100 characters." + ); + } + + return await this.RestHttpClient.PutAsync + ( + $"users/@me/applications/{applicationID}/role-connection", + b => b.WithJson + ( + json => + { + json.Write("platform_name", platformName, this.JsonOptions); + json.Write("platform_username", platformUsername, this.JsonOptions); + json.Write("metadata", metadata, this.JsonOptions); + } + ) + .WithRateLimitContext(this.RateLimitCache), + ct: ct + ); + } } diff --git a/Tests/Remora.Discord.API.Tests/API/Objects/Users/ApplicationRoleConnectionTests.cs b/Tests/Remora.Discord.API.Tests/API/Objects/Users/ApplicationRoleConnectionTests.cs new file mode 100644 index 0000000000..6b9eb2fee2 --- /dev/null +++ b/Tests/Remora.Discord.API.Tests/API/Objects/Users/ApplicationRoleConnectionTests.cs @@ -0,0 +1,39 @@ +// +// ApplicationRoleConnectionTests.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Tests.TestBases; + +namespace Remora.Discord.API.Tests.Objects; + +/// +public class ApplicationRoleConnectionTests : ObjectTestBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public ApplicationRoleConnectionTests(JsonBackedTypeTestFixture fixture) + : base(fixture) + { + } +} diff --git a/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs b/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs index 4d6883fcf2..82c8589a88 100644 --- a/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs +++ b/Tests/Remora.Discord.Rest.Tests/API/Applications/DiscordRestApplicationAPITests.cs @@ -2159,9 +2159,9 @@ public async Task PerformsRequestCorrectly() { new ApplicationRoleConnectionMetadata ( - "a", - "b", - "c", + string.Empty, + string.Empty, + string.Empty, ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual ), new ApplicationRoleConnectionMetadata @@ -2220,41 +2220,6 @@ public async Task PerformsRequestCorrectly() ResultAssert.Successful(result); } - /// - /// Tests whether the API method returns a client-side error if a failure condition is met. - /// - /// A representing the result of the asynchronous operation. - [Fact] - public async Task ReturnsUnsuccessfulIfKeyIsTooShort() - { - var applicationID = DiscordSnowflake.New(0); - var records = new[] - { - new ApplicationRoleConnectionMetadata - ( - string.Empty, - "b", - "c", - ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual - ) - }; - - var api = CreateAPI - ( - b => b - .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") - .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") - ); - - var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync - ( - applicationID, - records - ); - - ResultAssert.Unsuccessful(result); - } - /// /// Tests whether the API method returns a client-side error if a failure condition is met. /// @@ -2268,43 +2233,8 @@ public async Task ReturnsUnsuccessfulIfKeyIsTooLong() new ApplicationRoleConnectionMetadata ( new string('a', 51), - "b", - "c", - ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual - ) - }; - - var api = CreateAPI - ( - b => b - .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") - .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") - ); - - var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync - ( - applicationID, - records - ); - - ResultAssert.Unsuccessful(result); - } - - /// - /// Tests whether the API method returns a client-side error if a failure condition is met. - /// - /// A representing the result of the asynchronous operation. - [Fact] - public async Task ReturnsUnsuccessfulIfNameIsTooShort() - { - var applicationID = DiscordSnowflake.New(0); - var records = new[] - { - new ApplicationRoleConnectionMetadata - ( - "a", string.Empty, - "c", + string.Empty, ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual ) }; @@ -2337,43 +2267,8 @@ public async Task ReturnsUnsuccessfulIfNameIsTooLong() { new ApplicationRoleConnectionMetadata ( - "a", + string.Empty, new string('b', 101), - "c", - ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual - ) - }; - - var api = CreateAPI - ( - b => b - .Expect(HttpMethod.Put, $"{Constants.BaseURL}applications/{applicationID}/role-connections/metadata") - .Respond("application/json", $"[ {SampleRepository.Samples[typeof(IApplicationRoleConnectionMetadata)]} ]") - ); - - var result = await api.UpdateApplicationRoleConnectionMetadataRecordsAsync - ( - applicationID, - records - ); - - ResultAssert.Unsuccessful(result); - } - - /// - /// Tests whether the API method returns a client-side error if a failure condition is met. - /// - /// A representing the result of the asynchronous operation. - [Fact] - public async Task ReturnsUnsuccessfulIfDescriptionIsTooShort() - { - var applicationID = DiscordSnowflake.New(0); - var records = new[] - { - new ApplicationRoleConnectionMetadata - ( - "a", - "b", string.Empty, ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual ) @@ -2407,8 +2302,8 @@ public async Task ReturnsUnsuccessfulIfDescriptionIsTooLong() { new ApplicationRoleConnectionMetadata ( - "a", - "b", + string.Empty, + string.Empty, new string('c', 201), ApplicationRoleConnectionMetadataType.IntegerLessThanOrEqual ) diff --git a/Tests/Remora.Discord.Rest.Tests/API/Users/DiscordRestUserAPITests.cs b/Tests/Remora.Discord.Rest.Tests/API/Users/DiscordRestUserAPITests.cs index e7cd57c98b..c377781c60 100644 --- a/Tests/Remora.Discord.Rest.Tests/API/Users/DiscordRestUserAPITests.cs +++ b/Tests/Remora.Discord.Rest.Tests/API/Users/DiscordRestUserAPITests.cs @@ -479,4 +479,206 @@ public async Task PerformsRequestCorrectly() ResultAssert.Successful(result); } } + + /// + /// Tests the method. + /// + public class GetUserApplicationRoleConnectionAsync : RestAPITestBase + { + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public GetUserApplicationRoleConnectionAsync(RestAPITestFixture fixture) + : base(fixture) + { + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task PerformsRequestCorrectly() + { + var applicationID = DiscordSnowflake.New(0); + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Get, $"{Constants.BaseURL}users/@me/applications/{applicationID}/role-connection") + .Respond("application/json", SampleRepository.Samples[typeof(IApplicationRoleConnection)]) + ); + + var result = await api.GetUserApplicationRoleConnectionAsync(applicationID); + ResultAssert.Successful(result); + } + } + + /// + /// Tests the method. + /// + public class UpdateUserApplicationRoleConnectionAsync : RestAPITestBase + { + /// + /// Initializes a new instance of the class. + /// + /// The test fixture. + public UpdateUserApplicationRoleConnectionAsync(RestAPITestFixture fixture) + : base(fixture) + { + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task PerformsRequestCorrectly() + { + var applicationID = DiscordSnowflake.New(0); + var platformName = string.Empty; + var platformUsername = string.Empty; + var metadata = new Dictionary + { + { "a", string.Empty }, + { "b", new string('1', 100) } + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}users/@me/applications/{applicationID}/role-connection") + .WithJson + ( + json => json.IsObject + ( + o => o + .WithProperty("platform_name", p => p.Is(platformName)) + .WithProperty("platform_username", p => p.Is(platformUsername)) + .WithProperty + ( + "metadata", + p => p.IsObject + ( + m => + { + foreach (var metaPair in metadata) + { + m.WithProperty(metaPair.Key, p => p.Is(metaPair.Value)); + } + } + ) + ) + ) + ) + .Respond("application/json", SampleRepository.Samples[typeof(IApplicationRoleConnection)]) + ); + + var result = await api.UpdateUserApplicationRoleConnectionAsync + ( + applicationID, + platformName, + platformUsername, + metadata + ); + ResultAssert.Successful(result); + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfPlatformNameIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var platformName = new string('a', 51); + var platformUsername = string.Empty; + var metadata = new Dictionary + { + { "a", "true" } + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}users/@me/applications/{applicationID}/role-connection") + .Respond("application/json", SampleRepository.Samples[typeof(IApplicationRoleConnection)]) + ); + + var result = await api.UpdateUserApplicationRoleConnectionAsync + ( + applicationID, + platformName, + platformUsername, + metadata + ); + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfPlatformUsernameIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var platformName = string.Empty; + var platformUsername = new string('a', 101); + var metadata = new Dictionary + { + { "a", "true" } + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}users/@me/applications/{applicationID}/role-connection") + .Respond("application/json", SampleRepository.Samples[typeof(IApplicationRoleConnection)]) + ); + + var result = await api.UpdateUserApplicationRoleConnectionAsync + ( + applicationID, + platformName, + platformUsername, + metadata + ); + ResultAssert.Unsuccessful(result); + } + + /// + /// Tests whether the API method performs its request correctly. + /// + /// A representing the result of the asynchronous operation. + [Fact] + public async Task ReturnsUnsuccessfulIfMetadataValueIsTooLong() + { + var applicationID = DiscordSnowflake.New(0); + var platformName = string.Empty; + var platformUsername = string.Empty; + var metadata = new Dictionary + { + { "a", new string('1', 101) } + }; + + var api = CreateAPI + ( + b => b + .Expect(HttpMethod.Put, $"{Constants.BaseURL}users/@me/applications/{applicationID}/role-connection") + .Respond("application/json", SampleRepository.Samples[typeof(IApplicationRoleConnection)]) + ); + + var result = await api.UpdateUserApplicationRoleConnectionAsync + ( + applicationID, + platformName, + platformUsername, + metadata + ); + ResultAssert.Unsuccessful(result); + } + } } diff --git a/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION/APPLICATION_ROLE_CONNECTION.json b/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION/APPLICATION_ROLE_CONNECTION.json new file mode 100644 index 0000000000..ad0b41b711 --- /dev/null +++ b/Tests/Remora.Discord.Tests/Samples/Objects/APPLICATION_ROLE_CONNECTION/APPLICATION_ROLE_CONNECTION.json @@ -0,0 +1,7 @@ +{ + "platform_name": "none", + "platform_username": "none", + "metadata": { + "dummy": "none" + } +} \ No newline at end of file From ecf36834635b34f3c2b32db054c4b3a34e46d67b Mon Sep 17 00:00:00 2001 From: MazeXP <26042705+MazeXP@users.noreply.github.com> Date: Tue, 13 Dec 2022 22:08:26 +0000 Subject: [PATCH 3/3] Fix nullability for IApplicationRoleConnection --- .../API/Objects/Users/IApplicationRoleConnection.cs | 7 +++---- .../API/Rest/IDiscordRestApplicationAPI.cs | 2 +- .../API/Objects/Users/ApplicationRoleConnection.cs | 7 +++---- .../Extensions/ServiceCollectionExtensions.cs | 5 +---- .../API/CachingDiscordRestUserAPI.cs | 1 + 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs b/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs index 42c8813ace..84768413d2 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Objects/Users/IApplicationRoleConnection.cs @@ -22,7 +22,6 @@ using System.Collections.Generic; using JetBrains.Annotations; -using Remora.Rest.Core; namespace Remora.Discord.API.Abstractions.Objects; @@ -36,18 +35,18 @@ public interface IApplicationRoleConnection /// Gets the vanity name of the platform a bot has connected. /// /// The length of the platform username must be max. 50 characters. - Optional PlatformName { get; } + string? PlatformName { get; } /// /// Gets the username on the platform a bot has connected. /// /// The length of the platform username must be max. 100 characters. - Optional PlatformUsername { get; } + string? PlatformUsername { get; } /// /// Gets the object mapping of to their stringified value /// for the user on the platform a bot has connected. /// /// The length of the stringified value must max. 100 characters. - Optional> Metadata { get; } + IReadOnlyDictionary Metadata { get; } } diff --git a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs index e227156a3b..cdf1090db8 100644 --- a/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs +++ b/Backend/Remora.Discord.API.Abstractions/API/Rest/IDiscordRestApplicationAPI.cs @@ -353,7 +353,7 @@ Task>> GetApplicationRo /// The ID of the bot application. /// The metadata records to overwrite the existing ones. /// The cancellation token for this operation. - /// An creation result which may or may not have succeeded. + /// An update result which may or may not have succeeded. Task>> UpdateApplicationRoleConnectionMetadataRecordsAsync ( Snowflake applicationID, diff --git a/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs b/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs index f7afc88139..57227ea394 100644 --- a/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs +++ b/Backend/Remora.Discord.API/API/Objects/Users/ApplicationRoleConnection.cs @@ -23,7 +23,6 @@ using System.Collections.Generic; using JetBrains.Annotations; using Remora.Discord.API.Abstractions.Objects; -using Remora.Rest.Core; namespace Remora.Discord.API.Objects; @@ -31,7 +30,7 @@ namespace Remora.Discord.API.Objects; [PublicAPI] public record ApplicationRoleConnection ( - Optional PlatformName = default, - Optional PlatformUsername = default, - Optional> Metadata = default + string? PlatformName, + string? PlatformUsername, + IReadOnlyDictionary Metadata ) : IApplicationRoleConnection; diff --git a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs index cbeae52db7..5f04fdbd9e 100644 --- a/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs +++ b/Backend/Remora.Discord.API/Extensions/ServiceCollectionExtensions.cs @@ -1133,10 +1133,7 @@ private static JsonSerializerOptions AddStickerObjectConverters(this JsonSeriali /// /// The serializer options. /// The options, with the converters added. - private static JsonSerializerOptions AddApplicationRoleConnectionObjectConverters - ( - this JsonSerializerOptions options - ) + private static JsonSerializerOptions AddApplicationRoleConnectionObjectConverters(this JsonSerializerOptions options) { options.AddDataObjectConverter(); options.AddDataObjectConverter(); diff --git a/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs b/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs index c5ea2ea1fa..6859bfcaaf 100644 --- a/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs +++ b/Backend/Remora.Discord.Caching/API/CachingDiscordRestUserAPI.cs @@ -265,6 +265,7 @@ public async Task> GetUserApplicationRoleConn applicationID, ct ); + if (!getUserApplicationRoleConnection.IsDefined(out var userApplicationRoleConnection)) { return getUserApplicationRoleConnection;