From cb1935e360a93e213309e4519de77c763e9ef3ab Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 7 Aug 2024 12:11:47 -0400 Subject: [PATCH 01/16] Fix package versioning when publishing --- build.cake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.cake b/build.cake index f5742da2..0a7dbf1a 100644 --- a/build.cake +++ b/build.cake @@ -364,6 +364,7 @@ Task("Create-NuGet-Package") .Does(() => { var releaseNotesUrl = @$"https://github.com/{gitHubRepoOwner}/{gitHubRepo}/releases/tag/{milestone}"; + var packageVersion = (isMainRepo && isMainBranch && isTagged) ? versionInfo.MajorMinorPatch : versionInfo.FullSemVer.Replace('+', '-'); var settings = new DotNetPackSettings { @@ -378,7 +379,7 @@ Task("Create-NuGet-Package") MSBuildSettings = new DotNetMSBuildSettings { PackageReleaseNotes = releaseNotesUrl, - PackageVersion = versionInfo.FullSemVer.Replace('+', '-') + PackageVersion = packageVersion } }; From d53fc870c2d3030fe30e99e9c8cb90994dc268e7 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 7 Aug 2024 12:21:10 -0400 Subject: [PATCH 02/16] Turns out, we didn't need the package versioning fix --- build.cake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.cake b/build.cake index 0a7dbf1a..f5742da2 100644 --- a/build.cake +++ b/build.cake @@ -364,7 +364,6 @@ Task("Create-NuGet-Package") .Does(() => { var releaseNotesUrl = @$"https://github.com/{gitHubRepoOwner}/{gitHubRepo}/releases/tag/{milestone}"; - var packageVersion = (isMainRepo && isMainBranch && isTagged) ? versionInfo.MajorMinorPatch : versionInfo.FullSemVer.Replace('+', '-'); var settings = new DotNetPackSettings { @@ -379,7 +378,7 @@ Task("Create-NuGet-Package") MSBuildSettings = new DotNetMSBuildSettings { PackageReleaseNotes = releaseNotesUrl, - PackageVersion = packageVersion + PackageVersion = versionInfo.FullSemVer.Replace('+', '-') } }; From bb70f693e9108d2c1e76ca823438fa30b5a23d14 Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 9 Aug 2024 11:14:48 -0400 Subject: [PATCH 03/16] Ensure the GetPropertyValue extension method respects the throwIfMissing parameter Resolves #357 --- Source/ZoomNet/Extensions/Internal.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index ab169d60..25436d58 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -1150,7 +1150,11 @@ private static T GetPropertyValue(this JsonElement element, string[] names, T if (property.HasValue) break; } - if (!property.HasValue) return defaultValue; + if (!property.HasValue) + { + if (throwIfMissing) throw new Exception($"Unable to find {string.Join(", ", names)} in the Json document"); + else return defaultValue; + } var typeOfT = typeof(T); From 593694bfabcfe15581cf4c8159ccb4a101255602 Mon Sep 17 00:00:00 2001 From: Jericho Date: Wed, 25 Sep 2024 10:19:16 -0400 Subject: [PATCH 04/16] Refresh build script --- build.cake | 6 +++--- global.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.cake b/build.cake index f5742da2..d3c1785b 100644 --- a/build.cake +++ b/build.cake @@ -1,9 +1,9 @@ // Install tools. -#tool dotnet:?package=GitVersion.Tool&version=6.0.1 +#tool dotnet:?package=GitVersion.Tool&version=6.0.2 #tool dotnet:?package=coveralls.net&version=4.0.1 #tool nuget:https://f.feedz.io/jericho/jericho/nuget/?package=GitReleaseManager&version=0.17.0-collaborators0008 -#tool nuget:?package=ReportGenerator&version=5.3.8 -#tool nuget:?package=xunit.runner.console&version=2.9.0 +#tool nuget:?package=ReportGenerator&version=5.3.9 +#tool nuget:?package=xunit.runner.console&version=2.9.1 #tool nuget:?package=CodecovUploader&version=0.8.0 // Install addins. diff --git a/global.json b/global.json index 175f404f..caa3335a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.303", + "version": "8.0.402", "rollForward": "patch", "allowPrerelease": false } From c9fff40da435560366ceab91b7cd356869f76dc4 Mon Sep 17 00:00:00 2001 From: Jericho Date: Wed, 25 Sep 2024 11:36:13 -0400 Subject: [PATCH 05/16] Upgrade nuget packages --- .../ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj | 2 +- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index fc5d1247..22a1e038 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -17,7 +17,7 @@ - + diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 136ecd88..779a9dfe 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers From 14b3f63bacaa25a5c03e79832211d849c8817cdb Mon Sep 17 00:00:00 2001 From: Jericho Date: Wed, 25 Sep 2024 11:44:10 -0400 Subject: [PATCH 06/16] Handle responses from the Zoom API containing unescaped double-quotes Resolves #361 --- Source/ZoomNet/Extensions/Internal.cs | 130 ++++++++++-------- Source/ZoomNet/Resources/Accounts.cs | 16 +-- Source/ZoomNet/Resources/Meetings.cs | 6 +- Source/ZoomNet/Resources/Users.cs | 12 +- Source/ZoomNet/Resources/Webinars.cs | 6 +- .../ZoomNet/Utilities/ZoomRetryCoordinator.cs | 14 +- 6 files changed, 104 insertions(+), 80 deletions(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 25436d58..90290091 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -16,6 +16,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using ZoomNet.Json; @@ -347,19 +348,19 @@ internal static async Task> AsPaginate return await response.AsPaginatedResponseWithTokenAndDateRange(propertyName, options).ConfigureAwait(false); } - /// Get a raw JSON document representation of the response. + /// Get a JSON representation of the response. /// An error occurred processing the response. - internal static Task AsRawJsonDocument(this IResponse response, string propertyName = null, bool throwIfPropertyIsMissing = true) + internal static Task AsJson(this IResponse response, string propertyName = null, bool throwIfPropertyIsMissing = true) { - return response.Message.Content.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing); + return response.Message.Content.AsJson(propertyName, throwIfPropertyIsMissing); } - /// Get a raw JSON document representation of the response. + /// Get a JSON representation of the response. /// An error occurred processing the response. - internal static async Task AsRawJsonDocument(this IRequest request, string propertyName = null, bool throwIfPropertyIsMissing = true) + internal static async Task AsJson(this IRequest request, string propertyName = null, bool throwIfPropertyIsMissing = true) { var response = await request.AsResponse().ConfigureAwait(false); - return await response.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing).ConfigureAwait(false); + return await response.AsJson(propertyName, throwIfPropertyIsMissing).ConfigureAwait(false); } /// @@ -734,41 +735,35 @@ internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response) } */ - var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(responseContent)) + try { - try + var jsonResponse = await message.Content.ParseZoomResponseAsync().ConfigureAwait(false); + if (jsonResponse.ValueKind == JsonValueKind.Object) { - var rootJsonElement = JsonDocument.Parse(responseContent).RootElement; - - if (rootJsonElement.ValueKind == JsonValueKind.Object) + errorCode = jsonResponse.TryGetProperty("code", out JsonElement jsonErrorCode) ? jsonErrorCode.GetInt32() : null; + errorMessage = jsonResponse.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage; + if (jsonResponse.TryGetProperty("errors", out JsonElement jsonErrorDetails)) { - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? jsonErrorCode.GetInt32() : 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))); - - if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; - } - - return (errorCode.HasValue, errorMessage, errorCode); + 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))); + + if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; } + + return (errorCode.HasValue, errorMessage, errorCode); } - catch - { - // Intentionally ignore parsing errors - } + } + catch + { + // Intentionally ignore parsing errors } return (!message.IsSuccessStatusCode, errorMessage, errorCode); @@ -951,6 +946,34 @@ internal static bool IsNullableType(this Type type) return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } + internal static async Task ParseZoomResponseAsync(this HttpContent responseFromZoomApi, CancellationToken cancellationToken = default) + { + var responseContent = await responseFromZoomApi.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(responseContent)) return default; // the 'ValueKind' property of the default JsonElement is JsonValueKind.Undefined + + const string pattern = @"(.*?)(?<=""message"":"")(.*?)(?=""})(.*?$)"; + var matches = Regex.Match(responseContent, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + var prefix = matches.Groups[1].Value; + var message = matches.Groups[2].Value; + var postfix = matches.Groups[3].Value; + + if (string.IsNullOrEmpty(message)) return JsonDocument.Parse(responseContent).RootElement; + + /* + Sometimes the error message is malformed due to the presence of double quotes that are not properly escaped. + See: https://devforum.zoom.us/t/list-events-endpoint-returns-invalid-json-in-the-payload/115792 for more info. + One instance where this problem was observed is when retrieving the list of events without having the necessary permissions to do so. + The result is the following response with unescaped double-quotes in the error message: + { + "code": 104, + "message": "Invalid access token, does not contain scopes:["zoom_events_basic:read","zoom_events_basic:read:admin"]" + } + */ + var escapedMessage = Regex.Replace(message, @"(?Asynchronously converts the JSON encoded content and convert it to an object of the desired type. /// The response model to deserialize into. /// The content. @@ -984,28 +1007,30 @@ private static async Task AsObject(this HttpContent httpContent, string pr } } - /// Get a raw JSON object representation of the response. + /// Get a JSON representation of the response. /// The content. /// The name of the JSON property (or null if not applicable) where the desired data is stored. /// Indicates if an exception should be thrown when the specified JSON property is missing from the response. /// The cancellation token. - /// Returns the response body, or null if the response has no body. + /// Returns the response body, or a JsonElement with its 'ValueKind' set to 'Undefined' if the response has no body. /// An error occurred processing the response. - private static async Task AsRawJsonDocument(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default) + private static async Task AsJson(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default) { - var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false); - - var jsonDoc = JsonDocument.Parse(responseContent, default); + var jsonResponse = await httpContent.ParseZoomResponseAsync(cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(propertyName)) { - return jsonDoc; + return jsonResponse; } - if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property)) + if (jsonResponse.ValueKind != JsonValueKind.Object) + { + throw new Exception("The response from the Zomm API does not contain a valid JSON string"); + } + else if (jsonResponse.TryGetProperty(propertyName, out JsonElement property)) { var propertyContent = property.GetRawText(); - return JsonDocument.Parse(propertyContent, default); + return JsonDocument.Parse(propertyContent, default).RootElement; } else if (throwIfPropertyIsMissing) { @@ -1027,9 +1052,8 @@ private static async Task AsRawJsonDocument(this HttpContent httpC /// An error occurred processing the response. private static async Task> AsPaginatedResponse(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default) { - // Get the content as a queryable json document - var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false); - var rootElement = doc.RootElement; + // Get the content as a JSON element + var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false); // Get the various metadata properties var pageCount = rootElement.GetPropertyValue("page_count", 0); @@ -1068,9 +1092,8 @@ private static async Task> AsPaginatedResponse(this Http /// An error occurred processing the response. private static async Task> AsPaginatedResponseWithToken(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default) { - // Get the content as a queryable json document - var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false); - var rootElement = doc.RootElement; + // Get the content as a JSON element + var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false); // Get the various metadata properties var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty); @@ -1107,9 +1130,8 @@ private static async Task> AsPaginatedResponseWith /// An error occurred processing the response. private static async Task> AsPaginatedResponseWithTokenAndDateRange(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default) { - // Get the content as a queryable json document - var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false); - var rootElement = doc.RootElement; + // Get the content as a JSON element + var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false); // Get the various metadata properties var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture); diff --git a/Source/ZoomNet/Resources/Accounts.cs b/Source/ZoomNet/Resources/Accounts.cs index 63b72b58..90ba601b 100644 --- a/Source/ZoomNet/Resources/Accounts.cs +++ b/Source/ZoomNet/Resources/Accounts.cs @@ -203,13 +203,13 @@ public async Task GetMeetingAuthenticationSettingsAsync( .GetAsync($"accounts/{accountId}/settings") .WithArgument("option", "meeting_authentication") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); var settings = new AuthenticationSettings() { - RequireAuthentication = response.RootElement.GetPropertyValue("meeting_authentication", false), - AuthenticationOptions = response.RootElement.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() + RequireAuthentication = response.GetPropertyValue("meeting_authentication", false), + AuthenticationOptions = response.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() }; return settings; @@ -229,13 +229,13 @@ public async Task GetRecordingAuthenticationSettingsAsyn .GetAsync($"accounts/{accountId}/settings") .WithArgument("option", "recording_authentication") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); var settings = new AuthenticationSettings() { - RequireAuthentication = response.RootElement.GetPropertyValue("recording_authentication", false), - AuthenticationOptions = response.RootElement.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() + RequireAuthentication = response.GetPropertyValue("recording_authentication", false), + AuthenticationOptions = response.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() }; return settings; @@ -254,10 +254,10 @@ public async Task GetRecordingAuthenticationSettingsAsyn var response = await _client .GetAsync($"accounts/{accountId}/managed_domains") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument("domains") + .AsJson("domains") .ConfigureAwait(false); - var managedDomains = response.RootElement + var managedDomains = response .EnumerateArray() .Select(jsonElement => { diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index 0c51d9cc..118fbb29 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -795,10 +795,10 @@ public async Task GetRegistrationQuestionsAsync var response = await _client .GetAsync($"meetings/{meetingId}/registrants/questions") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); - var allFields = response.RootElement.GetProperty("questions").EnumerateArray() + var allFields = response.GetProperty("questions").EnumerateArray() .Select(item => (Field: item.GetPropertyValue("field_name").ToEnum(), IsRequired: item.GetPropertyValue("required"))); var requiredFields = allFields.Where(f => f.IsRequired).Select(f => f.Field).ToArray(); @@ -808,7 +808,7 @@ public async Task GetRegistrationQuestionsAsync { RequiredFields = requiredFields, OptionalFields = optionalFields, - Questions = response.RootElement.GetProperty("custom_questions", false)?.ToObject() ?? Array.Empty() + Questions = response.GetProperty("custom_questions", false)?.ToObject() ?? Array.Empty() }; return registrationQuestions; } diff --git a/Source/ZoomNet/Resources/Users.cs b/Source/ZoomNet/Resources/Users.cs index 2e0c878d..4771905d 100644 --- a/Source/ZoomNet/Resources/Users.cs +++ b/Source/ZoomNet/Resources/Users.cs @@ -445,13 +445,13 @@ public async Task GetMeetingAuthenticationSettingsAsync( .GetAsync($"users/{userId}/settings") .WithArgument("option", "meeting_authentication") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); var settings = new AuthenticationSettings() { - RequireAuthentication = response.RootElement.GetPropertyValue("meeting_authentication", false), - AuthenticationOptions = response.RootElement.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() + RequireAuthentication = response.GetPropertyValue("meeting_authentication", false), + AuthenticationOptions = response.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() }; return settings; @@ -471,13 +471,13 @@ public async Task GetRecordingAuthenticationSettingsAsyn .GetAsync($"users/{userId}/settings") .WithArgument("option", "recording_authentication") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); var settings = new AuthenticationSettings() { - RequireAuthentication = response.RootElement.GetPropertyValue("recording_authentication", false), - AuthenticationOptions = response.RootElement.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() + RequireAuthentication = response.GetPropertyValue("recording_authentication", false), + AuthenticationOptions = response.GetProperty("authentication_options", false)?.ToObject() ?? Array.Empty() }; return settings; diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index efe88e22..54c3b8e0 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -800,10 +800,10 @@ public async Task GetRegistrationQuestionsAsync var response = await _client .GetAsync($"webinars/{webinarId}/registrants/questions") .WithCancellationToken(cancellationToken) - .AsRawJsonDocument() + .AsJson() .ConfigureAwait(false); - var allFields = response.RootElement.GetProperty("questions").EnumerateArray() + var allFields = response.GetProperty("questions").EnumerateArray() .Select(item => (Field: item.GetPropertyValue("field_name").ToEnum(), IsRequired: item.GetPropertyValue("required"))); var requiredFields = allFields.Where(f => f.IsRequired).Select(f => f.Field).ToArray(); @@ -813,7 +813,7 @@ public async Task GetRegistrationQuestionsAsync { RequiredFields = requiredFields, OptionalFields = optionalFields, - Questions = response.RootElement.GetProperty("custom_questions", false)?.ToObject() ?? Array.Empty() + Questions = response.GetProperty("custom_questions", false)?.ToObject() ?? Array.Empty() }; return registrationQuestions; } diff --git a/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs b/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs index 29dc87c0..8d8000da 100644 --- a/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs +++ b/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs @@ -66,13 +66,15 @@ public async Task ExecuteAsync(IRequest request, Func Date: Wed, 25 Sep 2024 11:45:23 -0400 Subject: [PATCH 07/16] Fix typo when throwing a ArgumentOutOfRange exception --- Source/ZoomNet/Resources/Users.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Users.cs b/Source/ZoomNet/Resources/Users.cs index 4771905d..23627219 100644 --- a/Source/ZoomNet/Resources/Users.cs +++ b/Source/ZoomNet/Resources/Users.cs @@ -718,7 +718,7 @@ public Task DeleteVirtualBackgroundAsync(string userId, string fileId, Cancellat /// public Task UpdatePresenceStatusAsync(string userId, PresenceStatus status, int? duration = null, CancellationToken cancellationToken = default) { - if (status == PresenceStatus.Unknown) throw new ArgumentOutOfRangeException("You can not change a user's status to Unknown.", nameof(status)); + if (status == PresenceStatus.Unknown) throw new ArgumentOutOfRangeException(nameof(status), "You can not change a user's status to Unknown."); var data = new JsonObject { From 519dd3253b1d5365f78169369c773efb33cfc4c2 Mon Sep 17 00:00:00 2001 From: Jericho Date: Thu, 26 Sep 2024 10:41:33 -0400 Subject: [PATCH 08/16] Retry coordinator should utilize GetErrorMessageAsync() rather than manually parse the response --- Source/ZoomNet/Extensions/Internal.cs | 8 ++------ Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs | 13 ++++--------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 90290091..7a3c152f 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -748,11 +748,7 @@ internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response) " ", jsonErrorDetails .EnumerateArray() - .Select(jsonErrorDetail => - { - var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; - return errorDetail; - }) + .Select(jsonErrorDetail => jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty) .Where(message => !string.IsNullOrEmpty(message))); if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; @@ -946,7 +942,7 @@ internal static bool IsNullableType(this Type type) return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } - internal static async Task ParseZoomResponseAsync(this HttpContent responseFromZoomApi, CancellationToken cancellationToken = default) + private static async Task ParseZoomResponseAsync(this HttpContent responseFromZoomApi, CancellationToken cancellationToken = default) { var responseContent = await responseFromZoomApi.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(responseContent)) return default; // the 'ValueKind' property of the default JsonElement is JsonValueKind.Undefined diff --git a/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs b/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs index 8d8000da..8947b3c8 100644 --- a/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs +++ b/Source/ZoomNet/Utilities/ZoomRetryCoordinator.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -66,15 +65,11 @@ public async Task ExecuteAsync(IRequest request, Func Date: Wed, 2 Oct 2024 13:59:25 -0400 Subject: [PATCH 09/16] Fix the DownloadFileAsync_with_expired_token unit test --- Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs index 2f9a0fd2..b7f77ebc 100644 --- a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs @@ -552,7 +552,7 @@ public async Task DownloadFileAsync_with_expired_token() var mockHttp = new MockHttpMessageHandler(); mockHttp // The first time the file is requested, we return "401 Unauthorized" to simulate an expired token. .Expect(HttpMethod.Get, downloadUrl) - .Respond(HttpStatusCode.Unauthorized, new StringContent("{\"message\":\"access token is expired\"}")); + .Respond(HttpStatusCode.Unauthorized, new StringContent("{\"code\":123456,\"message\":\"access token is expired\"}")); mockHttp // The second time the file is requested, we return "200 OK" with the file content. .Expect(HttpMethod.Get, downloadUrl) .Respond(HttpStatusCode.OK, new StringContent("This is the content of the file")); From 54a6f6402367ebd0ad92c779ce2abccc3ecbf87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emin=20Me=C5=A1i=C4=87?= Date: Sun, 13 Oct 2024 18:38:05 +0200 Subject: [PATCH 10/16] Add daily usage report. (#362) * Add daily usage report. * Rename DateObject to DailyUsageSummary. * Change type for date to be DateOnly. --- .../Json/ZoomNetJsonSerializerContext.cs | 2 ++ Source/ZoomNet/Models/DailyUsageReport.cs | 22 +++++++++++++ Source/ZoomNet/Models/DailyUsageSummary.cs | 31 +++++++++++++++++++ Source/ZoomNet/Resources/IReports.cs | 12 +++++++ Source/ZoomNet/Resources/Reports.cs | 9 ++++++ 5 files changed, 76 insertions(+) create mode 100644 Source/ZoomNet/Models/DailyUsageReport.cs create mode 100644 Source/ZoomNet/Models/DailyUsageSummary.cs diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index 42f4c6df..0c70f0cb 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -385,6 +385,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CrcPortsHourUsage[]))] [JsonSerializable(typeof(ZoomNet.Models.CrcPortsUsage[]))] [JsonSerializable(typeof(ZoomNet.Models.CustomAttribute[]))] + [JsonSerializable(typeof(ZoomNet.Models.DailyUsageReport[]))] [JsonSerializable(typeof(ZoomNet.Models.DashboardMeetingMetrics[]))] [JsonSerializable(typeof(ZoomNet.Models.DashboardMeetingMetricsPaginationObject[]))] [JsonSerializable(typeof(ZoomNet.Models.DashboardMeetingParticipant[]))] @@ -394,6 +395,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.DashboardParticipant[]))] [JsonSerializable(typeof(ZoomNet.Models.DashboardParticipantQos[]))] [JsonSerializable(typeof(ZoomNet.Models.DataCenterRegion[]))] + [JsonSerializable(typeof(ZoomNet.Models.DailyUsageSummary[]))] [JsonSerializable(typeof(ZoomNet.Models.EmailNotificationUserSettings[]))] [JsonSerializable(typeof(ZoomNet.Models.EmergencyAddress[]))] [JsonSerializable(typeof(ZoomNet.Models.EncryptionType[]))] diff --git a/Source/ZoomNet/Models/DailyUsageReport.cs b/Source/ZoomNet/Models/DailyUsageReport.cs new file mode 100644 index 00000000..ef7e3c10 --- /dev/null +++ b/Source/ZoomNet/Models/DailyUsageReport.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Daily Usage Report. + /// + public class DailyUsageReport + { + /// Gets or sets the dates. + [JsonPropertyName("dates")] + public DailyUsageSummary[] Dates { get; set; } + + /// Gets or sets the month. + [JsonPropertyName("month")] + public int Month { get; set; } + + /// Gets or sets the year. + [JsonPropertyName("year")] + public int Year { get; set; } + } +} diff --git a/Source/ZoomNet/Models/DailyUsageSummary.cs b/Source/ZoomNet/Models/DailyUsageSummary.cs new file mode 100644 index 00000000..a0dd0890 --- /dev/null +++ b/Source/ZoomNet/Models/DailyUsageSummary.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Date Object. + /// + public class DailyUsageSummary + { + /// Gets or sets the date. + [JsonPropertyName("date")] + public DateOnly Date { get; set; } + + /// Gets or sets number of meeting minutes. + [JsonPropertyName("meeting_minutes")] + public int MeetingMinutes { get; set; } + + /// Gets or sets number of meetings. + [JsonPropertyName("meetings")] + public int Meetings { get; set; } + + /// Gets or sets number of new users. + [JsonPropertyName("new_users")] + public int NewUsers { get; set; } + + /// Gets or sets number of participants. + [JsonPropertyName("participants")] + public int Participants { get; set; } + } +} diff --git a/Source/ZoomNet/Resources/IReports.cs b/Source/ZoomNet/Resources/IReports.cs index 965641e6..8832a7af 100644 --- a/Source/ZoomNet/Resources/IReports.cs +++ b/Source/ZoomNet/Resources/IReports.cs @@ -87,5 +87,17 @@ public interface IReports /// An array of report items. /// Task> GetHostsAsync(DateTime from, DateTime to, ReportHostType type = ReportHostType.Active, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default); + + /// + /// Gets daily report to access the account-wide usage of Zoom services for each day in a given month. It lists the number of new users, meetings, participants, and meeting minutes. + /// + /// Year for this report. + /// Month for this report. + /// The group ID. + /// The cancellation token. + /// + /// The object of . + /// + public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Source/ZoomNet/Resources/Reports.cs b/Source/ZoomNet/Resources/Reports.cs index b8dd9ea2..8801b630 100644 --- a/Source/ZoomNet/Resources/Reports.cs +++ b/Source/ZoomNet/Resources/Reports.cs @@ -87,6 +87,15 @@ public Task> GetHostsAsync(DateTime from, .AsPaginatedResponseWithToken("users"); } + /// + public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return _client.GetAsync("report/daily").WithArgument("year", year).WithArgument("month", month) + .WithArgument("groupId", groupId) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + private static void VerifyPageSize(int pageSize) { if (pageSize < 1 || pageSize > 300) From e6c13f5bd1eb962a604beba4ff82e9df9250cdb8 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:39:42 -0400 Subject: [PATCH 11/16] (GH-362) Fix formatting --- Source/ZoomNet/Resources/IReports.cs | 2 +- Source/ZoomNet/Resources/Reports.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/Resources/IReports.cs b/Source/ZoomNet/Resources/IReports.cs index 8832a7af..2e324700 100644 --- a/Source/ZoomNet/Resources/IReports.cs +++ b/Source/ZoomNet/Resources/IReports.cs @@ -98,6 +98,6 @@ public interface IReports /// /// The object of . /// - public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default(CancellationToken)); + public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/Reports.cs b/Source/ZoomNet/Resources/Reports.cs index 8801b630..24ab520f 100644 --- a/Source/ZoomNet/Resources/Reports.cs +++ b/Source/ZoomNet/Resources/Reports.cs @@ -88,9 +88,11 @@ public Task> GetHostsAsync(DateTime from, } /// - public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default) { - return _client.GetAsync("report/daily").WithArgument("year", year).WithArgument("month", month) + return _client.GetAsync("report/daily") + .WithArgument("year", year) + .WithArgument("month", month) .WithArgument("groupId", groupId) .WithCancellationToken(cancellationToken) .AsObject(); From 106f060bc3c9073f4a5cc0af7325ee3126106894 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:41:50 -0400 Subject: [PATCH 12/16] (GH-362) Change the name of the 'Dates' property in the DailyUsageReport model to 'DailyUsageSummaries' --- Source/ZoomNet/Models/DailyUsageReport.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet/Models/DailyUsageReport.cs b/Source/ZoomNet/Models/DailyUsageReport.cs index ef7e3c10..31277b67 100644 --- a/Source/ZoomNet/Models/DailyUsageReport.cs +++ b/Source/ZoomNet/Models/DailyUsageReport.cs @@ -7,9 +7,9 @@ namespace ZoomNet.Models /// public class DailyUsageReport { - /// Gets or sets the dates. + /// Gets or sets the daily usage summaries. [JsonPropertyName("dates")] - public DailyUsageSummary[] Dates { get; set; } + public DailyUsageSummary[] DailyUsageSummaries { get; set; } /// Gets or sets the month. [JsonPropertyName("month")] From 847b0fb7b5a1f6bb8ac97c53f54d0464d21887a0 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:55:01 -0400 Subject: [PATCH 13/16] (GH-362) Replace DateOnly with (int Year, int Month, int Day)) --- Source/ZoomNet/Json/DateOnlyConverter.cs | 37 ++++++++++++++++++++++ Source/ZoomNet/Models/DailyUsageSummary.cs | 5 +-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Source/ZoomNet/Json/DateOnlyConverter.cs diff --git a/Source/ZoomNet/Json/DateOnlyConverter.cs b/Source/ZoomNet/Json/DateOnlyConverter.cs new file mode 100644 index 00000000..96570405 --- /dev/null +++ b/Source/ZoomNet/Json/DateOnlyConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Text.Json; + +namespace ZoomNet.Json +{ + /// + /// Converts a DateOnly (which is represented by 3 integer values: year, month and day) to or from JSON. + /// + /// + internal class DateOnlyConverter : ZoomNetJsonConverter<(int Year, int Month, int Day)> + { + public override (int Year, int Month, int Day) Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.None: + case JsonTokenType.Null: + case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()): + throw new JsonException("Unable to convert a null value to DateOnly"); + + case JsonTokenType.String: + var rawValue = reader.GetString(); + var parts = rawValue.Split('-'); + if (parts.Length != 3) throw new JsonException($"Unable to convert {rawValue} to DateOnly"); + return (int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2])); + + default: + throw new JsonException($"Unable to convert {reader.TokenType.ToEnumString()} to DateOnly"); + } + } + + public override void Write(Utf8JsonWriter writer, (int Year, int Month, int Day) value, JsonSerializerOptions options) + { + writer.WriteStringValue($"{value.Year:D4}-{value.Month:D2}-{value.Day:D2}"); + } + } +} diff --git a/Source/ZoomNet/Models/DailyUsageSummary.cs b/Source/ZoomNet/Models/DailyUsageSummary.cs index a0dd0890..1bff91ab 100644 --- a/Source/ZoomNet/Models/DailyUsageSummary.cs +++ b/Source/ZoomNet/Models/DailyUsageSummary.cs @@ -1,5 +1,5 @@ -using System; using System.Text.Json.Serialization; +using ZoomNet.Json; namespace ZoomNet.Models { @@ -10,7 +10,8 @@ public class DailyUsageSummary { /// Gets or sets the date. [JsonPropertyName("date")] - public DateOnly Date { get; set; } + [JsonConverter(typeof(DateOnlyConverter))] + public (int Year, int Month, int Day) Date { get; set; } /// Gets or sets number of meeting minutes. [JsonPropertyName("meeting_minutes")] From 9e736d2fe321901ff11e54b49a44c59e4606be1e Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:55:27 -0400 Subject: [PATCH 14/16] Unit test to ensure DailyUsageReport is property deserialized --- .../Models/DailyUsageReportTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Source/ZoomNet.UnitTests/Models/DailyUsageReportTests.cs diff --git a/Source/ZoomNet.UnitTests/Models/DailyUsageReportTests.cs b/Source/ZoomNet.UnitTests/Models/DailyUsageReportTests.cs new file mode 100644 index 00000000..0cd29319 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/DailyUsageReportTests.cs @@ -0,0 +1,53 @@ +using Shouldly; +using System.Text.Json; +using Xunit; +using ZoomNet.Json; +using ZoomNet.Models; + +namespace ZoomNet.UnitTests.Models +{ + public class DailyUsageReportTests + { + #region FIELDS + + internal const string DAILY_USAGE_REPORT_JSON = @"{ + ""dates"": [ + { + ""date"": ""2022-03-01"", + ""meeting_minutes"": 34, + ""meetings"": 2, + ""new_users"": 3, + ""participants"": 4 + } + ], + ""month"": 3, + ""year"": 2022 + }"; + + #endregion + + [Fact] + public void Parse_json() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize(DAILY_USAGE_REPORT_JSON, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Month.ShouldBe(3); + result.Year.ShouldBe(2022); + result.DailyUsageSummaries.ShouldNotBeEmpty(); + result.DailyUsageSummaries.Length.ShouldBe(1); + result.DailyUsageSummaries[0].Date.Year.ShouldBe(2022); + result.DailyUsageSummaries[0].Date.Month.ShouldBe(3); + result.DailyUsageSummaries[0].Date.Day.ShouldBe(1); + result.DailyUsageSummaries[0].MeetingMinutes.ShouldBe(34); + result.DailyUsageSummaries[0].Meetings.ShouldBe(2); + result.DailyUsageSummaries[0].NewUsers.ShouldBe(3); + result.DailyUsageSummaries[0].Participants.ShouldBe(4); + + } + } +} From 90f62a7634f257d54b9f4103ebce665d8f3c03b8 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:58:12 -0400 Subject: [PATCH 15/16] Upgrade System.Text.Json to 8.0.5 to resolve CVE-2024-43485 --- Source/ZoomNet/ZoomNet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 2a9015d3..355d53e5 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -42,7 +42,7 @@ - + From b1c75609ea1030011480add816d4464553028b39 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 13 Oct 2024 12:58:52 -0400 Subject: [PATCH 16/16] Upgrade nuget packages --- .../ZoomNet.IntegrationTests.csproj | 6 +++--- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 2 +- Source/ZoomNet/ZoomNet.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index 22a1e038..8b253ed3 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 779a9dfe..90825cf2 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 355d53e5..24a6c71d 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -38,7 +38,7 @@ - +