From 37d868f9341e0666a9d0f5f55d4206ee015cbb96 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Thu, 26 Jan 2023 03:20:58 +0300 Subject: [PATCH] [Feature] Application Role Connections Metadata support (#2530) * models & internal methods * moar stuff * working `Get` & `ModifyRoleConnectionMetadata` * comments & dots * get & modify user's role connection * Update src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs * Update src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com> --- .../RoleConnection.cs | 43 ++++++ .../RoleConnectionMetadata.cs | 65 ++++++++ .../RoleConnectionMetadataProperties.cs | 138 +++++++++++++++++ .../RoleConnectionMetadataType.cs | 49 ++++++ .../RoleConnectionProperties.cs | 141 ++++++++++++++++++ .../API/Common/RoleConnection.cs | 16 ++ .../API/Common/RoleConnectionMetadata.cs | 25 ++++ src/Discord.Net.Rest/ClientHelper.cs | 72 +++++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 19 +++ src/Discord.Net.Rest/DiscordRestClient.cs | 18 ++- 10 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs create mode 100644 src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadata.cs create mode 100644 src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataProperties.cs create mode 100644 src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataType.cs create mode 100644 src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionProperties.cs create mode 100644 src/Discord.Net.Rest/API/Common/RoleConnection.cs create mode 100644 src/Discord.Net.Rest/API/Common/RoleConnectionMetadata.cs diff --git a/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs new file mode 100644 index 000000000..d2bf54f41 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnection.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents the connection object that the user has attached. +/// +public class RoleConnection +{ + /// + /// Gets the vanity name of the platform a bot has connected to. + /// + public string PlatformName { get; } + + /// + /// Gets the username on the platform a bot has connected to. + /// + public string PlatformUsername { get; } + + /// + /// Gets the object mapping keys to their string-ified values. + /// + public IReadOnlyDictionary Metadata { get; } + + internal RoleConnection(string platformName, string platformUsername, IReadOnlyDictionary metadata) + { + PlatformName = platformName; + PlatformUsername = platformUsername; + Metadata = metadata; + } + + /// + /// Initializes a new with the data from this object. + /// + public RoleConnectionProperties ToRoleConnectionProperties() + => new() + { + PlatformName = PlatformName, + PlatformUsername = PlatformUsername, + Metadata = Metadata.ToDictionary() + }; +} diff --git a/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadata.cs b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadata.cs new file mode 100644 index 000000000..2e5a2ccce --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadata.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord; + +/// +/// Represents the role connection metadata object. +/// +public class RoleConnectionMetadata +{ + /// + /// Gets the of metadata value. + /// + public RoleConnectionMetadataType Type { get; } + + /// + /// Gets the dictionary key for the metadata field. + /// + public string Key { get; } + + /// + /// Gets the name of the metadata field. + /// + public string Name { get; } + + /// + /// Gets the description of the metadata field. + /// + public string Description { get; } + + /// + /// Gets translations of the name. if not set. + /// + public IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets translations of the description. if not set. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; } + + internal RoleConnectionMetadata(RoleConnectionMetadataType type, string key, string name, string description, + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) + { + Type = type; + Key = key; + Name = name; + Description = description; + NameLocalizations = nameLocalizations?.ToImmutableDictionary(); + DescriptionLocalizations = descriptionLocalizations?.ToImmutableDictionary(); + } + + /// + /// Initializes a new with the data from this object. + /// + public RoleConnectionMetadataProperties ToRoleConnectionMetadataProperties() + => new() + { + Name = Name, + Description = Description, + Type = Type, + Key = Key, + NameLocalizations = NameLocalizations, + DescriptionLocalizations = DescriptionLocalizations + }; +} diff --git a/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataProperties.cs b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataProperties.cs new file mode 100644 index 000000000..b88e57ccc --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataProperties.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System; +using System.Collections.Immutable; + +namespace Discord; + +/// +/// Properties object used to create or modify object. +/// +public class RoleConnectionMetadataProperties +{ + private const int MaxKeyLength = 50; + private const int MaxNameLength = 100; + private const int MaxDescriptionLength = 200; + + private string _key; + private string _name; + private string _description; + + private IReadOnlyDictionary _nameLocalizations; + private IReadOnlyDictionary _descriptionLocalizations; + + /// + /// Gets or sets the of metadata value. + /// + public RoleConnectionMetadataType Type { get; set; } + + /// + /// Gets or sets the dictionary key for the metadata field. + /// + public string Key + { + get => _key; + set + { + Preconditions.AtMost(value.Length, MaxKeyLength, nameof(Key), $"Key length must be less than or equal to {MaxKeyLength}"); + _key = value; + } + } + + /// + /// Gets or sets the name of the metadata field. + /// + public string Name + { + get => _name; + set + { + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name), $"Name length must be less than or equal to {MaxNameLength}"); + _name = value; + } + } + + /// + /// Gets or sets the description of the metadata field. + /// + public string Description + { + get => _description; + set + { + Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description), $"Description length must be less than or equal to {MaxDescriptionLength}"); + _description = value; + } + } + + /// + /// Gets or sets translations of the name. if not set. + /// + public IReadOnlyDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + if (value is not null) + foreach (var localization in value) + if (localization.Value.Length > MaxNameLength) + throw new ArgumentException($"Name localization length must be less than or equal to {MaxNameLength}. Locale '{localization}'"); + _nameLocalizations = value; + } + } + + /// + /// Gets or sets translations of the description. if not set. + /// + public IReadOnlyDictionary DescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + if (value is not null) + foreach (var localization in value) + if (localization.Value.Length > MaxDescriptionLength) + throw new ArgumentException($"Description localization length must be less than or equal to {MaxDescriptionLength}. Locale '{localization}'"); + _descriptionLocalizations = value; + } + } + + /// + /// Initializes a new instance of . + /// + /// The type of the metadata value. + /// The dictionary key for the metadata field. Max 50 characters. + /// The name of the metadata visible in user profile. Max 100 characters. + /// The description of the metadata visible in user profile. Max 200 characters. + /// Translations for the name. + /// Translations for the description. + public RoleConnectionMetadataProperties(RoleConnectionMetadataType type, string key, string name, string description, + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) + { + Type = type; + Key = key; + Name = name; + Description = description; + NameLocalizations = nameLocalizations?.ToImmutableDictionary(); + DescriptionLocalizations = descriptionLocalizations?.ToImmutableDictionary(); + } + + /// + /// Initializes a new instance of . + /// + public RoleConnectionMetadataProperties() { } + + /// + /// Initializes a new with the data from provided . + /// + public static RoleConnectionMetadataProperties FromRoleConnectionMetadata(RoleConnectionMetadata metadata) + => new() + { + Name = metadata.Name, + Description = metadata.Description, + Type = metadata.Type, + Key = metadata.Key, + NameLocalizations = metadata.NameLocalizations, + DescriptionLocalizations = metadata.DescriptionLocalizations + }; +} + diff --git a/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataType.cs b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataType.cs new file mode 100644 index 000000000..50b851ae9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionMetadataType.cs @@ -0,0 +1,49 @@ +using System; + +namespace Discord; + +/// +/// Represents the type of Application Role Connection Metadata. +/// +public enum RoleConnectionMetadataType +{ + /// + /// The metadata's integer value is less than or equal to the guild's configured value. + /// + IntegerLessOrEqual = 1, + + /// + /// The metadata's integer value is greater than or equal to the guild's configured value. + /// + IntegerGreaterOrEqual = 2, + + /// + /// The metadata's integer value is equal to the guild's configured value. + /// + IntegerEqual = 3, + + /// + /// The metadata's integer value is not equal to the guild's configured value. + /// + IntegerNotEqual = 4, + + /// + /// The metadata's ISO8601 string value is less or equal to the guild's configured value. + /// + DateTimeLessOrEqual = 5, + + /// + /// The metadata's ISO8601 string value is greater to the guild's configured value. + /// + DateTimeGreaterOrEqual = 6, + + /// + /// The metadata's integer value is equal to the guild's configured value. + /// + BoolEqual = 7, + + /// + /// The metadata's integer value is equal to the guild's configured value. + /// + BoolNotEqual = 8, +} diff --git a/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionProperties.cs b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionProperties.cs new file mode 100644 index 000000000..db68fada5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationRoleConnection/RoleConnectionProperties.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents the properties used to modify user's . +/// +public class RoleConnectionProperties +{ + private const int MaxPlatformNameLength = 50; + private const int MaxPlatformUsernameLength = 100; + private const int MaxMetadataRecords = 100; + + private string _platformName; + private string _platformUsername; + private Dictionary _metadata; + + /// + /// Gets or sets the vanity name of the platform a bot has connected. Max 50 characters. + /// + public string PlatformName + { + get => _platformName; + set + { + if (value is not null) + Preconditions.AtMost(value.Length, MaxPlatformNameLength, nameof(PlatformName), $"Platform name length must be less or equal to {MaxPlatformNameLength}"); + _platformName = value; + } + } + + /// + /// Gets or sets the username on the platform a bot has connected. Max 100 characters. + /// + public string PlatformUsername + { + get => _platformUsername; + set + { + if(value is not null) + Preconditions.AtMost(value.Length, MaxPlatformUsernameLength, nameof(PlatformUsername), $"Platform username length must be less or equal to {MaxPlatformUsernameLength}"); + _platformUsername = value; + } + } + + /// + /// Gets or sets object mapping keys to their string-ified values. + /// + public Dictionary Metadata + { + get => _metadata; + set + { + if (value is not null) + Preconditions.AtMost(value.Count, MaxPlatformUsernameLength, nameof(Metadata), $"Metadata records count must be less or equal to {MaxMetadataRecords}"); + _metadata = value; + } + } + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithDate(string key, DateTimeOffset value) + => AddMetadataRecord(key, value.ToString("O")); + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithBool(string key, bool value) + => AddMetadataRecord(key, value ? "1" : "0"); + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithNumber(string key, int value) + => AddMetadataRecord(key, value.ToString()); + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithNumber(string key, uint value) + => AddMetadataRecord(key, value.ToString()); + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithNumber(string key, long value) + => AddMetadataRecord(key, value.ToString()); + + /// + /// Adds a metadata record with the provided key and value. + /// + /// The current . + public RoleConnectionProperties WithNumber(string key, ulong value) + => AddMetadataRecord(key, value.ToString()); + + internal RoleConnectionProperties AddMetadataRecord(string key, string value) + { + Metadata ??= new Dictionary(); + if(!Metadata.ContainsKey(key)) + Preconditions.AtMost(Metadata.Count + 1, MaxPlatformUsernameLength, nameof(Metadata), $"Metadata records count must be less or equal to {MaxMetadataRecords}"); + + _metadata[key] = value; + return this; + } + + /// + /// Initializes a new instance of . + /// + /// The name of the platform a bot has connected.s + /// Gets the username on the platform a bot has connected. + /// Object mapping keys to their values. + public RoleConnectionProperties(string platformName, string platformUsername, IDictionary metadata = null) + { + PlatformName = platformName; + PlatformUsername = platformUsername; + Metadata = metadata.ToDictionary(); + } + + /// + /// Initializes a new instance of . + /// + public RoleConnectionProperties() {} + + /// + /// Initializes a new with the data from provided . + /// + public static RoleConnectionProperties FromRoleConnection(RoleConnection roleConnection) + => new() + { + PlatformName = roleConnection.PlatformName, + PlatformUsername = roleConnection.PlatformUsername, + Metadata = roleConnection.Metadata.ToDictionary() + }; +} diff --git a/src/Discord.Net.Rest/API/Common/RoleConnection.cs b/src/Discord.Net.Rest/API/Common/RoleConnection.cs new file mode 100644 index 000000000..f8c26a6b6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/RoleConnection.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API; + +public class RoleConnection +{ + [JsonProperty("platform_name")] + public Optional PlatformName { get; set; } + + [JsonProperty("platform_username")] + public Optional PlatformUsername { get; set; } + + [JsonProperty("metadata")] + public Optional> Metadata { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/RoleConnectionMetadata.cs b/src/Discord.Net.Rest/API/Common/RoleConnectionMetadata.cs new file mode 100644 index 000000000..9ce36a05d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/RoleConnectionMetadata.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API; + +public class RoleConnectionMetadata +{ + [JsonProperty("type")] + public RoleConnectionMetadataType Type { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } +} diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 0c8f8c42f..61766131c 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -264,5 +264,77 @@ namespace Discord.Rest public static Task RemoveRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) => client.ApiClient.RemoveRoleAsync(guildId, userId, roleId, options); #endregion + + #region Role Connection Metadata + + public static async Task> GetRoleConnectionMetadataRecordsAsync(BaseDiscordClient client, RequestOptions options = null) + => (await client.ApiClient.GetApplicationRoleConnectionMetadataRecordsAsync(options)) + .Select(model + => new RoleConnectionMetadata( + model.Type, + model.Key, + model.Name, + model.Description, + model.NameLocalizations.IsSpecified + ? model.NameLocalizations.Value?.ToImmutableDictionary() + : null, + model.DescriptionLocalizations.IsSpecified + ? model.DescriptionLocalizations.Value?.ToImmutableDictionary() + : null)) + .ToImmutableArray(); + + public static async Task> ModifyRoleConnectionMetadataRecordsAsync(ICollection metadata, BaseDiscordClient client, RequestOptions options = null) + => (await client.ApiClient.UpdateApplicationRoleConnectionMetadataRecordsAsync(metadata + .Select(x => new API.RoleConnectionMetadata + { + Name = x.Name, + Description = x.Description, + Key = x.Key, + Type = x.Type, + NameLocalizations = x.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = x.DescriptionLocalizations?.ToDictionary() + }).ToArray())) + .Select(model + => new RoleConnectionMetadata( + model.Type, + model.Key, + model.Name, + model.Description, + model.NameLocalizations.IsSpecified + ? model.NameLocalizations.Value?.ToImmutableDictionary() + : null, + model.DescriptionLocalizations.IsSpecified + ? model.DescriptionLocalizations.Value?.ToImmutableDictionary() + : null)) + .ToImmutableArray(); + + public static async Task GetUserRoleConnectionAsync(ulong applicationId, BaseDiscordClient client, RequestOptions options = null) + { + var roleConnection = await client.ApiClient.GetUserApplicationRoleConnectionAsync(applicationId, options); + + return new RoleConnection(roleConnection.PlatformName.GetValueOrDefault(null), + roleConnection.PlatformUsername.GetValueOrDefault(null), + roleConnection.Metadata.GetValueOrDefault()); + } + + public static async Task ModifyUserRoleConnectionAsync(ulong applicationId, RoleConnectionProperties roleConnection, BaseDiscordClient client, RequestOptions options = null) + { + var updatedConnection = await client.ApiClient.ModifyUserApplicationRoleConnectionAsync(applicationId, + new API.RoleConnection + { + PlatformName = roleConnection.PlatformName, + PlatformUsername = roleConnection.PlatformUsername, + Metadata = roleConnection.Metadata + }, options); + + return new RoleConnection( + updatedConnection.PlatformName.GetValueOrDefault(null), + updatedConnection.PlatformUsername.GetValueOrDefault(null), + updatedConnection.Metadata.GetValueOrDefault()?.ToImmutableDictionary() + ); + } + + + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 8b2128a0d..7d015d912 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -4,12 +4,15 @@ using Discord.Net; using Discord.Net.Converters; using Discord.Net.Queue; using Discord.Net.Rest; + using Newtonsoft.Json; + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.Design; using System.Diagnostics; +using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; @@ -2507,5 +2510,21 @@ namespace Discord.API } #endregion + + #region Application Role Connections Metadata + + public async Task GetApplicationRoleConnectionMetadataRecordsAsync(RequestOptions options = null) + => await SendAsync("GET", () => $"applications/{CurrentApplicationId}/role-connections/metadata", new BucketIds(), options: options).ConfigureAwait(false); + + public async Task UpdateApplicationRoleConnectionMetadataRecordsAsync(RoleConnectionMetadata[] roleConnections, RequestOptions options = null) + => await SendJsonAsync ("PUT", () => $"applications/{CurrentApplicationId}/role-connections/metadata", roleConnections, new BucketIds(), options: options).ConfigureAwait(false); + + public async Task GetUserApplicationRoleConnectionAsync(ulong applicationId, RequestOptions options = null) + => await SendAsync("GET", () => $"users/@me/applications/{applicationId}/role-connection", new BucketIds(), options: options); + + public async Task ModifyUserApplicationRoleConnectionAsync(ulong applicationId, RoleConnection connection, RequestOptions options = null) + => await SendJsonAsync("PUT", () => $"users/@me/applications/{applicationId}/role-connection", connection, new BucketIds(), options: options); + + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index ddd38c5be..0778e69ad 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -231,7 +231,23 @@ namespace Discord.Rest => MessageHelper.RemoveAllReactionsAsync(channelId, messageId, this, options); public Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) => MessageHelper.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote, this, options); -#endregion + + public Task> GetRoleConnectionMetadataRecordsAsync(RequestOptions options = null) + => ClientHelper.GetRoleConnectionMetadataRecordsAsync(this, options); + + public Task> ModifyRoleConnectionMetadataRecordsAsync(ICollection metadata, RequestOptions options = null) + { + Preconditions.AtMost(metadata.Count, 5, nameof(metadata), "An application can have a maximum of 5 metadata records."); + return ClientHelper.ModifyRoleConnectionMetadataRecordsAsync(metadata, this, options); + } + + public Task GetUserApplicationRoleConnectionAsync(ulong applicationId, RequestOptions options = null) + => ClientHelper.GetUserRoleConnectionAsync(applicationId, this, options); + + public Task ModifyUserApplicationRoleConnectionAsync(ulong applicationId, RoleConnectionProperties roleConnection, RequestOptions options = null) + => ClientHelper.ModifyUserRoleConnectionAsync(applicationId, roleConnection, this, options); + + #endregion #region IDiscordClient ///