From 77d4af37ec6c9608ac154a965e3f06d9ecf9f7e9 Mon Sep 17 00:00:00 2001 From: Jericho Date: Mon, 3 Oct 2022 13:53:06 -0400 Subject: [PATCH 01/12] Sort permissions alphabetically for convenience --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 5d024013..5d707c93 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -126,6 +126,7 @@ public async Task RunAsync() // Get my user and permisisons var myUser = await client.Users.GetCurrentAsync(source.Token).ConfigureAwait(false); var myPermissions = await client.Users.GetCurrentPermissionsAsync(source.Token).ConfigureAwait(false); + Array.Sort(myPermissions); // Sort permissions alphabetically for convenience // Execute the async tests in parallel (with max degree of parallelism) var results = await integrationTests.ForEachAsync( From d38a72b056cce14a05112ee5831bbca7f1782ba2 Mon Sep 17 00:00:00 2001 From: Jericho Date: Wed, 5 Oct 2022 12:03:40 -0400 Subject: [PATCH 02/12] Switch to FormUrlEncodedContent when requesting or refreshing oauth tokens Resolves #243 --- Source/ZoomNet/Utilities/OAuthTokenHandler.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 26a249f3..6d274e75 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -75,25 +75,27 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - var grantType = _connectionInfo.GrantType.ToEnumString(); - var requestUrl = $"https://api.zoom.us/oauth/token?grant_type={grantType}"; + var contentValues = new Dictionary(); + contentValues.Add("grant_type", _connectionInfo.GrantType.ToEnumString()); + switch (_connectionInfo.GrantType) { case OAuthGrantType.AccountCredentials: - requestUrl += $"&account_id={_connectionInfo.AccountId}"; + contentValues.Add("account_id", _connectionInfo.AccountId); break; case OAuthGrantType.AuthorizationCode: - requestUrl += $"&code={_connectionInfo.AuthorizationCode}"; - if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) requestUrl += $"&redirect_uri={_connectionInfo.RedirectUri}"; + contentValues.Add("code", _connectionInfo.AuthorizationCode); + if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) contentValues.Add("redirect_uri", _connectionInfo.RedirectUri); break; case OAuthGrantType.RefreshToken: - requestUrl += $"&refresh_token={_connectionInfo.RefreshToken}"; + contentValues.Add("refresh_token", _connectionInfo.RefreshToken); break; } var requestTime = DateTime.UtcNow; - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.zoom.us/oauth/token"); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_connectionInfo.ClientId}:{_connectionInfo.ClientSecret}"))); + request.Content = new FormUrlEncodedContent(contentValues); var response = _httpClient.SendAsync(request).ConfigureAwait(false).GetAwaiter().GetResult(); var responseContent = response.Content.ReadAsStringAsync(null).ConfigureAwait(false).GetAwaiter().GetResult(); From ed7e22a06bb5d69edb191f6b69be14a3d4e6358b Mon Sep 17 00:00:00 2001 From: Jericho Date: Wed, 5 Oct 2022 12:16:14 -0400 Subject: [PATCH 03/12] Support PKCE for authorization_code grant_type Resolves #244 --- Source/ZoomNet/OAuthConnectionInfo.cs | 9 ++++++++- Source/ZoomNet/Utilities/OAuthTokenHandler.cs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/ZoomNet/OAuthConnectionInfo.cs b/Source/ZoomNet/OAuthConnectionInfo.cs index 9178c13c..063ae402 100644 --- a/Source/ZoomNet/OAuthConnectionInfo.cs +++ b/Source/ZoomNet/OAuthConnectionInfo.cs @@ -72,6 +72,11 @@ public class OAuthConnectionInfo : IConnectionInfo /// public string RedirectUri { get; } + /// + /// Gets the cryptographically random string used to correlate the authorization request to the token request. + /// + public string CodeVerifier { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -118,7 +123,8 @@ public OAuthConnectionInfo(string clientId, string clientSecret) /// The authorization code. /// The delegate invoked when the token is refreshed. /// The Redirect Uri. - public OAuthConnectionInfo(string clientId, string clientSecret, string authorizationCode, OnTokenRefreshedDelegate onTokenRefreshed, string redirectUri = null) + /// The cryptographically random string used to correlate the authorization request to the token request. + public OAuthConnectionInfo(string clientId, string clientSecret, string authorizationCode, OnTokenRefreshedDelegate onTokenRefreshed, string redirectUri = null, string codeVerifier = null) { if (string.IsNullOrEmpty(clientId)) throw new ArgumentNullException(nameof(clientId)); if (string.IsNullOrEmpty(clientSecret)) throw new ArgumentNullException(nameof(clientSecret)); @@ -131,6 +137,7 @@ public OAuthConnectionInfo(string clientId, string clientSecret, string authoriz TokenExpiration = DateTime.MinValue; GrantType = OAuthGrantType.AuthorizationCode; OnTokenRefreshed = onTokenRefreshed; + CodeVerifier = codeVerifier; } /// diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 6d274e75..867e288e 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -86,6 +86,7 @@ public string RefreshTokenIfNecessary(bool forceRefresh) case OAuthGrantType.AuthorizationCode: contentValues.Add("code", _connectionInfo.AuthorizationCode); if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) contentValues.Add("redirect_uri", _connectionInfo.RedirectUri); + if (!string.IsNullOrEmpty(_connectionInfo.CodeVerifier)) contentValues.Add("code_verifier", _connectionInfo.CodeVerifier); break; case OAuthGrantType.RefreshToken: contentValues.Add("refresh_token", _connectionInfo.RefreshToken); From a52f53c241a640f607fa50dbccd38fb4f32974db Mon Sep 17 00:00:00 2001 From: Jericho Date: Sat, 8 Oct 2022 16:19:13 -0400 Subject: [PATCH 04/12] Upgrade Http Fluent Client to 4.2.0 --- Source/ZoomNet/Extensions/Internal.cs | 71 +++++++-------------------- Source/ZoomNet/ZoomNet.csproj | 2 +- 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 483b3584..7df01411 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -1,5 +1,4 @@ using Pathoschild.Http.Client; -using Pathoschild.Http.Client.Extensibility; using System; using System.Collections; using System.Collections.Generic; @@ -371,42 +370,6 @@ internal static IRequest WithJsonBody(this IRequest request, T body) return request.WithBody(bodyBuilder => bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"))); } - /// Add a filter to a request. - /// The type of filter. - /// The request. - /// The filter. - /// - /// When true, the first filter of matching type is replaced with the new filter (thereby preserving the position of the filter in the list of filters) and any other filter of matching type is removed. - /// When false, the filter is simply added to the list of filters. - /// - /// Returns the request builder for chaining. - internal static IRequest WithFilter(this IRequest request, TFilter filter, bool replaceExisting = true) - where TFilter : IHttpFilter - { - var matchingFilters = request.Filters.OfType().ToArray(); - - if (matchingFilters.Length == 0 || !replaceExisting) - { - request.Filters.Add(filter); - } - else - { - // Replace the first matching filter with the new filter - var collectionAsList = request.Filters as IList; - var indexOfMatchingFilter = collectionAsList.IndexOf(matchingFilters[0]); - collectionAsList.RemoveAt(indexOfMatchingFilter); - collectionAsList.Insert(indexOfMatchingFilter, filter); - - // Remove any other matching filter - for (int i = 1; i < matchingFilters.Length; i++) - { - request.Filters.Remove(matchingFilters[i]); - } - } - - return request; - } - /// Asynchronously retrieve the response body as a . /// The response. /// The encoding. You can leave this parameter null and the encoding will be @@ -696,23 +659,23 @@ internal static (WeakReference RequestReference, string Diag try { var rootJsonElement = JsonDocument.Parse(responseContent).RootElement; - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; - errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); - if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) - { - var errorDetails = string.Join( - " ", - jsonErrorDetails - .EnumerateArray() - .Select(jsonErrorDetail => - { - var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; - return errorDetail; - }) - .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); - - if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; - } + errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; + errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); + if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + { + var errorDetails = string.Join( + " ", + jsonErrorDetails + .EnumerateArray() + .Select(jsonErrorDetail => + { + var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; + return errorDetail; + }) + .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); + + if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; + } isError = errorCode.HasValue; } diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 4f989b34..382f2cd1 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -40,7 +40,7 @@ - + From e97587020d577af01685ae0dcde6901023e51844 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sat, 8 Oct 2022 16:20:39 -0400 Subject: [PATCH 05/12] (GH-243) Fix unit test --- Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs index 40be9691..88cab27d 100644 --- a/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs +++ b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs @@ -35,7 +35,7 @@ public void Attempt_to_refresh_token_multiple_times_despite_exception() var mockHttp = new MockHttpMessageHandler(); mockHttp - .When(HttpMethod.Post, $"https://api.zoom.us/oauth/token?grant_type=authorization_code&code={authorizationCode}") + .When(HttpMethod.Post, $"https://api.zoom.us/oauth/token") .Respond(HttpStatusCode.BadRequest, "application/json", apiResponse); var handler = new OAuthTokenHandler(connectionInfo, mockHttp.ToHttpClient(), null); From 1e7278b836e117eb536438bca5c26fa7337df316 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sat, 8 Oct 2022 18:09:28 -0400 Subject: [PATCH 06/12] Utilize HTTP Fluent client's new 'WithFilter/WithoutFilter' methods to add the custom zoom Error Handler --- Source/ZoomNet/Extensions/Internal.cs | 38 ++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 7df01411..de469ca8 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -351,7 +351,9 @@ internal static async Task AsRawJsonDocument(this IRequest request /// Returns the request builder for chaining. internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, string customExceptionMessage = null) { - return request.WithFilter(new ZoomErrorHandler(true, customExceptionMessage)); + return request + .WithoutFilter() + .WithFilter(new ZoomErrorHandler(true, customExceptionMessage)); } /// Set the body content of the HTTP request. @@ -659,23 +661,23 @@ internal static (WeakReference RequestReference, string Diag try { var rootJsonElement = JsonDocument.Parse(responseContent).RootElement; - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; - errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); - if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) - { - var errorDetails = string.Join( - " ", - jsonErrorDetails - .EnumerateArray() - .Select(jsonErrorDetail => - { - var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; - return errorDetail; - }) - .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); - - if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; - } + errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; + errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); + if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + { + var errorDetails = string.Join( + " ", + jsonErrorDetails + .EnumerateArray() + .Select(jsonErrorDetail => + { + var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; + return errorDetail; + }) + .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); + + if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; + } isError = errorCode.HasValue; } From ee296e208b99409fe5a36a9402d1695c4db50385 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sat, 8 Oct 2022 19:15:17 -0400 Subject: [PATCH 07/12] Standardize with my other projects --- .../Models/DashboardParticipant.cs | 2 +- Source/ZoomNet.UnitTests/Models/Registrant.cs | 2 +- .../Resources/CloudRecordingsTests.cs | 2 +- .../Utilities/ParticipantDeviceConverter.cs | 4 +- Source/ZoomNet.UnitTests/Utils.cs | 2 +- Source/ZoomNet/Extensions/Internal.cs | 110 +++++++++++++----- ...omNetJsonFormatter.cs => JsonFormatter.cs} | 6 +- Source/ZoomNet/Utilities/ZoomErrorHandler.cs | 2 +- Source/ZoomNet/WebhookParser.cs | 2 +- Source/ZoomNet/ZoomClient.cs | 2 +- 10 files changed, 92 insertions(+), 42 deletions(-) rename Source/ZoomNet/Json/{ZoomNetJsonFormatter.cs => JsonFormatter.cs} (96%) diff --git a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs index d1dfab76..0805ccc2 100644 --- a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs +++ b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs @@ -50,7 +50,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_DASHBOARDPARTICIPANT_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_DASHBOARDPARTICIPANT_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Models/Registrant.cs b/Source/ZoomNet.UnitTests/Models/Registrant.cs index 2620f552..c0881e36 100644 --- a/Source/ZoomNet.UnitTests/Models/Registrant.cs +++ b/Source/ZoomNet.UnitTests/Models/Registrant.cs @@ -47,7 +47,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_REGISTRANT_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_REGISTRANT_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs index 15381420..45124c2c 100644 --- a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs @@ -471,7 +471,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_CLOUD_RECORDING_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_CLOUD_RECORDING_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs index 23f155d6..3b1f459a 100644 --- a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs @@ -84,7 +84,7 @@ public void Read_single(string value, ParticipantDevice expectedValue) // Act jsonReader.Read(); - var result = converter.Read(ref jsonReader, objectType, ZoomNetJsonFormatter.DeserializerOptions); + var result = converter.Read(ref jsonReader, objectType, JsonFormatter.DeserializerOptions); // Assert result.ShouldNotBeNull(); @@ -109,7 +109,7 @@ public void Read_multiple() // Act jsonReader.Read(); - var result = converter.Read(ref jsonReader, objectType, ZoomNetJsonFormatter.DeserializerOptions); + var result = converter.Read(ref jsonReader, objectType, JsonFormatter.DeserializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Utils.cs b/Source/ZoomNet.UnitTests/Utils.cs index ac314c82..8b038da0 100644 --- a/Source/ZoomNet.UnitTests/Utils.cs +++ b/Source/ZoomNet.UnitTests/Utils.cs @@ -22,7 +22,7 @@ public static Pathoschild.Http.Client.IClient GetFluentClient(MockHttpMessageHan // Remove all the built-in formatters and replace them with our custom JSON formatter client.Formatters.Clear(); - client.Formatters.Add(new ZoomNetJsonFormatter()); + client.Formatters.Add(new JsonFormatter()); // Order is important: DiagnosticHandler must be first. // Also, the list of filters must be kept in sync with the filters in ZoomClient in the ZoomNet project. diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index de469ca8..0dd8072d 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -1,3 +1,4 @@ +using HttpMultipartParser; using Pathoschild.Http.Client; using System; using System.Collections; @@ -5,6 +6,7 @@ using System.ComponentModel; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -226,6 +228,19 @@ internal static Encoding GetEncoding(this HttpContent content, Encoding defaultE return encoding; } + /// + /// Returns the value of a parameter or the default value if it doesn't exist. + /// + /// The parser. + /// The name of the parameter. + /// The default value. + /// The value of the parameter. + internal static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue) + { + if (parser.HasParameter(name)) return parser.GetParameterValue(name); + else return defaultValue; + } + /// Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type. /// The response model to deserialize into. /// The response. @@ -360,6 +375,7 @@ internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, stri /// The type of object to serialize into a JSON string. /// The request. /// The value to serialize into the HTTP body content. + /// Indicates if the charset should be omitted from the 'Content-Type' request header. /// Returns the request builder for chaining. /// /// This method is equivalent to IRequest.AsBody<T>(T body) because omitting the media type @@ -367,9 +383,19 @@ internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, stri /// formatter happens to be the JSON formatter. However, I don't feel good about relying on the /// default ordering of the items in the MediaTypeFormatterCollection. /// - internal static IRequest WithJsonBody(this IRequest request, T body) + internal static IRequest WithJsonBody(this IRequest request, T body, bool omitCharSet = false) { - return request.WithBody(bodyBuilder => bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"))); + return request.WithBody(bodyBuilder => + { + var httpContent = bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json")); + + if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet)) + { + httpContent.Headers.ContentType.CharSet = string.Empty; + } + + return httpContent; + }); } /// Asynchronously retrieve the response body as a . @@ -625,9 +651,6 @@ internal static (WeakReference RequestReference, string Diag internal static async Task<(bool, string, int?)> GetErrorMessageAsync(this HttpResponseMessage message) { - // Assume there is no error - var isError = false; - // Default error code int? errorCode = null; @@ -661,25 +684,29 @@ internal static (WeakReference RequestReference, string Diag try { var rootJsonElement = JsonDocument.Parse(responseContent).RootElement; - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; - errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); - if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + + if (rootJsonElement.ValueKind == JsonValueKind.Object) { - var errorDetails = string.Join( - " ", - jsonErrorDetails - .EnumerateArray() - .Select(jsonErrorDetail => - { - var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; - return errorDetail; - }) - .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); - - if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; - } + errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; + errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); + if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + { + var errorDetails = string.Join( + " ", + jsonErrorDetails + .EnumerateArray() + .Select(jsonErrorDetail => + { + var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; + return errorDetail; + }) + .Where(message => !string.IsNullOrEmpty(message))); - isError = errorCode.HasValue; + if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; + } + + return (errorCode.HasValue, errorMessage, errorCode); + } } catch { @@ -687,7 +714,31 @@ internal static (WeakReference RequestReference, string Diag } } - return (isError, errorMessage, errorCode); + return (!message.IsSuccessStatusCode, errorMessage, errorCode); + } + + internal static async Task CompressAsync(this Stream source) + { + var compressedStream = new MemoryStream(); + using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true)) + { + await source.CopyToAsync(gzip).ConfigureAwait(false); + } + + compressedStream.Position = 0; + return compressedStream; + } + + internal static async Task DecompressAsync(this Stream source) + { + var decompressedStream = new MemoryStream(); + using (var gzip = new GZipStream(source, CompressionMode.Decompress, true)) + { + await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false); + } + + decompressedStream.Position = 0; + return decompressedStream; } /// Convert an enum to its string representation. @@ -786,7 +837,7 @@ internal static bool TryToEnum(this string str, out T enumValue) internal static T ToObject(this JsonElement element, JsonSerializerOptions options = null) { - return JsonSerializer.Deserialize(element, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return JsonSerializer.Deserialize(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions); } internal static void Add(this JsonObject jsonObject, string propertyName, T value) @@ -830,14 +881,13 @@ private static async Task AsObject(this HttpContent httpContent, string pr if (string.IsNullOrEmpty(propertyName)) { - return JsonSerializer.Deserialize(responseContent, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return JsonSerializer.Deserialize(responseContent, options ?? JsonFormatter.DeserializerOptions); } var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default); if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property)) { - var propertyContent = property.GetRawText(); - return JsonSerializer.Deserialize(propertyContent, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return property.ToObject(options); } else if (throwIfPropertyIsMissing) { @@ -916,7 +966,7 @@ private static async Task> AsPaginatedResponse(this Http PageCount = pageCount, PageNumber = pageNumber, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; @@ -955,7 +1005,7 @@ private static async Task> AsPaginatedResponseWith { NextPageToken = nextPageToken, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; @@ -998,7 +1048,7 @@ private static async Task> AsPaginated To = to, NextPageToken = nextPageToken, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; diff --git a/Source/ZoomNet/Json/ZoomNetJsonFormatter.cs b/Source/ZoomNet/Json/JsonFormatter.cs similarity index 96% rename from Source/ZoomNet/Json/ZoomNetJsonFormatter.cs rename to Source/ZoomNet/Json/JsonFormatter.cs index 3e29dca5..721364e1 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonFormatter.cs +++ b/Source/ZoomNet/Json/JsonFormatter.cs @@ -12,7 +12,7 @@ namespace ZoomNet.Json { - internal class ZoomNetJsonFormatter : MediaTypeFormatterBase + internal class JsonFormatter : MediaTypeFormatterBase { internal static readonly JsonSerializerOptions SerializerOptions; internal static readonly JsonSerializerOptions DeserializerOptions; @@ -22,7 +22,7 @@ internal class ZoomNetJsonFormatter : MediaTypeFormatterBase private const int DefaultBufferSize = 1024; - static ZoomNetJsonFormatter() + static JsonFormatter() { SerializerOptions = new JsonSerializerOptions() { @@ -64,7 +64,7 @@ static ZoomNetJsonFormatter() DeserializationContext = new ZoomNetJsonSerializerContext(DeserializerOptions); } - public ZoomNetJsonFormatter() + public JsonFormatter() { this.AddMediaType("application/json"); } diff --git a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs index 6fac193b..b87b275e 100644 --- a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs +++ b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs @@ -62,7 +62,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException) throw new ZoomException(errorMessage, response.Message, diagnosticLog); } - else if (!isError && response.IsSuccessStatusCode) + else if (!isError) { return; } diff --git a/Source/ZoomNet/WebhookParser.cs b/Source/ZoomNet/WebhookParser.cs index 9a17514c..974e7e87 100644 --- a/Source/ZoomNet/WebhookParser.cs +++ b/Source/ZoomNet/WebhookParser.cs @@ -31,7 +31,7 @@ public class WebhookParser : IWebhookParser /// An . public Event ParseEventWebhook(string requestBody) { - var webHookEvent = JsonSerializer.Deserialize(requestBody, ZoomNetJsonFormatter.DeserializerOptions); + var webHookEvent = JsonSerializer.Deserialize(requestBody, JsonFormatter.DeserializerOptions); return webHookEvent; } } diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index 73cb9d23..bfebc42c 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -220,7 +220,7 @@ private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool d // Remove all the built-in formatters and replace them with our custom JSON formatter _fluentClient.Formatters.Clear(); - _fluentClient.Formatters.Add(new ZoomNetJsonFormatter()); + _fluentClient.Formatters.Add(new JsonFormatter()); // Order is important: the token handler (either JWT or OAuth) must be first, followed by DiagnosticHandler and then by ErrorHandler. if (connectionInfo is JwtConnectionInfo jwtConnectionInfo) From 91bec9710c34f6607c9f2238753594cd847ad10a Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 16 Oct 2022 11:34:30 -0400 Subject: [PATCH 08/12] Filter out unmodified when parsing payload for MeetingUpdated and WebinarUpdated webhook events Resolves #245 --- Source/ZoomNet.UnitTests/WebhookParserTests.cs | 17 +++++++++-------- Source/ZoomNet/Json/WebhookEventConverter.cs | 16 ++++++++++++++-- .../Models/Webhooks/MeetingUpdatedEvent.cs | 6 ++++++ .../Models/Webhooks/WebinarUpdatedEvent.cs | 6 ++++++ 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/Source/ZoomNet.UnitTests/WebhookParserTests.cs b/Source/ZoomNet.UnitTests/WebhookParserTests.cs index db131a0d..de9d116e 100644 --- a/Source/ZoomNet.UnitTests/WebhookParserTests.cs +++ b/Source/ZoomNet.UnitTests/WebhookParserTests.cs @@ -243,14 +243,15 @@ public void MeetingUpdated() parsedEvent.Operator.ShouldBe("someone@example.com"); parsedEvent.OperatorId.ShouldBe("8lzIwvZTSOqjndWPbPqzuA"); parsedEvent.ModifiedFields.ShouldNotBeNull(); - parsedEvent.ModifiedFields.Length.ShouldBe(3); - parsedEvent.ModifiedFields[0].FieldName.ShouldBe("id"); - parsedEvent.ModifiedFields[0].OldValue.ShouldBe(94890226305); - parsedEvent.ModifiedFields[0].NewValue.ShouldBe(94890226305); - parsedEvent.ModifiedFields[1].FieldName.ShouldBe("topic"); - parsedEvent.ModifiedFields[1].OldValue.ShouldBe("ZoomNet Unit Testing: scheduled meeting"); - parsedEvent.ModifiedFields[1].NewValue.ShouldBe("ZoomNet Unit Testing: UPDATED scheduled meeting"); - parsedEvent.ModifiedFields[2].FieldName.ShouldBe("settings"); + parsedEvent.ModifiedFields.Length.ShouldBe(2); + parsedEvent.ModifiedFields[0].FieldName.ShouldBe("topic"); + parsedEvent.ModifiedFields[0].OldValue.ShouldBe("ZoomNet Unit Testing: scheduled meeting"); + parsedEvent.ModifiedFields[0].NewValue.ShouldBe("ZoomNet Unit Testing: UPDATED scheduled meeting"); + parsedEvent.ModifiedFields[1].FieldName.ShouldBe("settings"); + parsedEvent.MeetingFields.ShouldNotBeNull(); + parsedEvent.MeetingFields.Length.ShouldBe(1); + parsedEvent.MeetingFields[0].FieldName.ShouldBe("id"); + parsedEvent.MeetingFields[0].Value.ShouldBe(94890226305); } [Fact] diff --git a/Source/ZoomNet/Json/WebhookEventConverter.cs b/Source/ZoomNet/Json/WebhookEventConverter.cs index 06c3a1c8..560438d6 100644 --- a/Source/ZoomNet/Json/WebhookEventConverter.cs +++ b/Source/ZoomNet/Json/WebhookEventConverter.cs @@ -58,7 +58,13 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); meetingUpdatedEvent.ModifiedFields = oldMeetingValues.Keys - .Select(key => (key, oldMeetingValues[key], newMeetingValues[key])) + .Where(key => !oldMeetingValues[key].Equals(newMeetingValues[key])) + .Select(key => (FieldName: key, OldValue: oldMeetingValues[key], NewValue: newMeetingValues[key])) + .ToArray(); + + meetingUpdatedEvent.MeetingFields = oldMeetingValues.Keys + .Where(key => oldMeetingValues[key].Equals(newMeetingValues[key])) + .Select(key => (FieldName: key, Value: oldMeetingValues[key])) .ToArray(); webHookEvent = meetingUpdatedEvent; @@ -195,7 +201,13 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); webinarUpdatedEvent.ModifiedFields = oldWebinarValues.Keys - .Select(key => (key, oldWebinarValues[key], newWebinarValues[key])) + .Where(key => !oldWebinarValues[key].Equals(newWebinarValues[key])) + .Select(key => (FieldName: key, OldValue: oldWebinarValues[key], NewValue: newWebinarValues[key])) + .ToArray(); + + webinarUpdatedEvent.WebinarFields = oldWebinarValues.Keys + .Where(key => oldWebinarValues[key].Equals(newWebinarValues[key])) + .Select(key => (FieldName: key, Value: oldWebinarValues[key])) .ToArray(); webHookEvent = webinarUpdatedEvent; diff --git a/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs b/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs index c4d4cb8f..73eeade6 100644 --- a/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs @@ -29,5 +29,11 @@ public class MeetingUpdatedEvent : Event /// Gets or sets the fields that have been modified. /// public (string FieldName, object OldValue, object NewValue)[] ModifiedFields { get; set; } + + /// + /// Gets or sets the fields about the meeting. + /// + /// Typically, this array will contain fields such as Id, Uuid, etc. + public (string FieldName, object Value)[] MeetingFields { get; set; } } } diff --git a/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs b/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs index bd0703e3..d7238386 100644 --- a/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs @@ -23,5 +23,11 @@ public class WebinarUpdatedEvent : Event /// Gets or sets the fields that have been modified. /// public (string FieldName, object OldValue, object NewValue)[] ModifiedFields { get; set; } + + /// + /// Gets or sets the fields about the webinar. + /// + /// Typically, this array will contain fields such as Id, Uuid, etc. + public (string FieldName, object Value)[] WebinarFields { get; set; } } } From 1e3bb703f5bf52602e4a968e013e7f4df9ef16bc Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 16 Oct 2022 11:42:18 -0400 Subject: [PATCH 09/12] Upgrade nuget package references --- Source/ZoomNet/ZoomNet.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 382f2cd1..1a70284a 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -36,13 +36,13 @@ - - + + - + From 4711863a6b88355f0a09abcdeb9facc4ecc93d54 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 16 Oct 2022 11:42:43 -0400 Subject: [PATCH 10/12] Formatting --- Source/ZoomNet/Utilities/OAuthTokenHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 867e288e..59e6db61 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -75,8 +75,10 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - var contentValues = new Dictionary(); - contentValues.Add("grant_type", _connectionInfo.GrantType.ToEnumString()); + var contentValues = new Dictionary() + { + { "grant_type", _connectionInfo.GrantType.ToEnumString() }, + }; switch (_connectionInfo.GrantType) { From 2602692addf3b1900c6c5afc5c568c71c1f7c345 Mon Sep 17 00:00:00 2001 From: Jericho Date: Thu, 27 Oct 2022 10:16:21 -0400 Subject: [PATCH 11/12] Add missing values to the RecordingContentType and RecordingFileType enumerations Resolves #246 --- Source/ZoomNet/Models/RecordingContentType.cs | 30 +++++++++++++++++-- Source/ZoomNet/Models/RecordingFileType.cs | 10 ++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Source/ZoomNet/Models/RecordingContentType.cs b/Source/ZoomNet/Models/RecordingContentType.cs index 2109bafa..ec460711 100644 --- a/Source/ZoomNet/Models/RecordingContentType.cs +++ b/Source/ZoomNet/Models/RecordingContentType.cs @@ -47,12 +47,36 @@ public enum RecordingContentType [EnumMember(Value = "chat_file")] ChatFile, + /// Active speaker. + [EnumMember(Value = "active_speaker")] + ActiveSpeaker, + + /// Poll. + [EnumMember(Value = "poll")] + Poll, + /// timeline. [EnumMember(Value = "timeline")] Timeline, - /// Active speaker. - [EnumMember(Value = "active_speaker")] - ActiveSpeaker + /// closed_caption. + [EnumMember(Value = "closed_caption")] + ClosedCaption, + + /// Audio interpretation. + [EnumMember(Value = "audio_interpretation")] + AudioInterpretation, + + /// Summary. + [EnumMember(Value = "summary")] + Summary, + + /// Summary next steps. + [EnumMember(Value = "summary_next_steps")] + SummaryNextSteps, + + /// Summary smart chapters. + [EnumMember(Value = "summary_smart_chapters")] + SummarySmartChapters, } } diff --git a/Source/ZoomNet/Models/RecordingFileType.cs b/Source/ZoomNet/Models/RecordingFileType.cs index df78d7da..447b4e7d 100644 --- a/Source/ZoomNet/Models/RecordingFileType.cs +++ b/Source/ZoomNet/Models/RecordingFileType.cs @@ -34,6 +34,14 @@ public enum RecordingFileType /// File contains closed captions of the recording in VTT file format. [EnumMember(Value = "cc")] - ClosedCaptioning + ClosedCaptioning, + + /// File containing polling data in csv format. + [EnumMember(Value = "csv")] + PollingData, + + /// Summary file of the recording in JSON file format + [EnumMember(Value = "summary")] + Summary, } } From e663442fd5f46bb820598881fd6d378b18b67c37 Mon Sep 17 00:00:00 2001 From: Jericho Date: Thu, 27 Oct 2022 10:45:22 -0400 Subject: [PATCH 12/12] Upgrade to Cake 2.3.0 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7bd5f9f5..dd76b6d1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "2.2.0", + "version": "2.3.0", "commands": [ "dotnet-cake" ]