diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index fc5d1247..8b253ed3 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -14,10 +14,10 @@ - - - - + + + + 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); + + } + } +} 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")); diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 136ecd88..90825cf2 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 diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index ab169d60..7a3c152f 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,31 @@ 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 => jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty) + .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 +942,34 @@ internal static bool IsNullableType(this Type type) return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } + 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 + + 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 +1003,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 +1048,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 +1088,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 +1126,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); @@ -1150,7 +1168,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); 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/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..31277b67 --- /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 daily usage summaries. + [JsonPropertyName("dates")] + public DailyUsageSummary[] DailyUsageSummaries { 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..1bff91ab --- /dev/null +++ b/Source/ZoomNet/Models/DailyUsageSummary.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; +using ZoomNet.Json; + +namespace ZoomNet.Models +{ + /// + /// Date Object. + /// + public class DailyUsageSummary + { + /// Gets or sets the date. + [JsonPropertyName("date")] + [JsonConverter(typeof(DateOnlyConverter))] + public (int Year, int Month, int Day) 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/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/IReports.cs b/Source/ZoomNet/Resources/IReports.cs index 965641e6..2e324700 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); } } 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/Reports.cs b/Source/ZoomNet/Resources/Reports.cs index b8dd9ea2..24ab520f 100644 --- a/Source/ZoomNet/Resources/Reports.cs +++ b/Source/ZoomNet/Resources/Reports.cs @@ -87,6 +87,17 @@ public Task> GetHostsAsync(DateTime from, .AsPaginatedResponseWithToken("users"); } + /// + public Task GetDailyUsageReportAsync(int year, int month, string groupId = null, CancellationToken cancellationToken = default) + { + 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) diff --git a/Source/ZoomNet/Resources/Users.cs b/Source/ZoomNet/Resources/Users.cs index 2e0c878d..23627219 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; @@ -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 { 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..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,10 +65,8 @@ public async Task ExecuteAsync(IRequest request, Func - + - + 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 }