From 101bc63a45068781a90416ad9bd2a45af40a28d1 Mon Sep 17 00:00:00 2001 From: Volodymyr Dombrovskyi Date: Mon, 26 Dec 2022 20:58:26 +0100 Subject: [PATCH 1/4] (#258) Added support for active/inactive host reports --- .../ZoomNet.IntegrationTests/Tests/Reports.cs | 12 ++- .../Json/ZoomNetJsonSerializerContext.cs | 2 + Source/ZoomNet/Models/ReportHost.cs | 45 +++++++++++ Source/ZoomNet/Models/ReportHostType.cs | 21 +++++ Source/ZoomNet/Resources/IReports.cs | 26 ++++++- Source/ZoomNet/Resources/Reports.cs | 77 ++++++++++--------- 6 files changed, 145 insertions(+), 38 deletions(-) create mode 100644 Source/ZoomNet/Models/ReportHost.cs create mode 100644 Source/ZoomNet/Models/ReportHostType.cs diff --git a/Source/ZoomNet.IntegrationTests/Tests/Reports.cs b/Source/ZoomNet.IntegrationTests/Tests/Reports.cs index 0c407a27..26c7672c 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Reports.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Reports.cs @@ -15,9 +15,17 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync("\n***** REPORTS *****\n").ConfigureAwait(false); - //GET ALL MEETINGS var now = DateTime.UtcNow; - var pastMeetings = await client.Reports.GetMeetingsAsync(myUser.Id, now.Subtract(TimeSpan.FromDays(30)), now, ReportMeetingType.Past, 30, null, cancellationToken); + var from = now.Subtract(TimeSpan.FromDays(30)); + var to = now; + + //GET ALL HOSTS + var activeHosts = await client.Reports.GetHostsAsync(from, to, ReportHostType.Active, 30, null, cancellationToken); + var inactiveHosts = await client.Reports.GetHostsAsync(from, to, ReportHostType.Inactive, 30, null, cancellationToken); + await log.WriteLineAsync($"Active Hosts: {activeHosts.Records.Length}. Inactive Hosts: {inactiveHosts.Records.Length}").ConfigureAwait(false); + + //GET ALL MEETINGS + var pastMeetings = await client.Reports.GetMeetingsAsync(myUser.Id, from, to, ReportMeetingType.Past, 30, null, cancellationToken); int totalParticipants = 0; diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index ef60d8b8..5347015d 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -136,6 +136,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.RegistrationQuestionsForMeeting))] [JsonSerializable(typeof(ZoomNet.Models.RegistrationQuestionsForWebinar))] [JsonSerializable(typeof(ZoomNet.Models.RegistrationType))] + [JsonSerializable(typeof(ZoomNet.Models.ReportHost))] [JsonSerializable(typeof(ZoomNet.Models.ReportMeetingParticipant))] [JsonSerializable(typeof(ZoomNet.Models.ReportMeetingType))] [JsonSerializable(typeof(ZoomNet.Models.ReportParticipant))] @@ -361,6 +362,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.RegistrationQuestionsForMeeting[]))] [JsonSerializable(typeof(ZoomNet.Models.RegistrationQuestionsForWebinar[]))] [JsonSerializable(typeof(ZoomNet.Models.RegistrationType[]))] + [JsonSerializable(typeof(ZoomNet.Models.ReportHost[]))] [JsonSerializable(typeof(ZoomNet.Models.ReportMeetingParticipant[]))] [JsonSerializable(typeof(ZoomNet.Models.ReportMeetingType[]))] [JsonSerializable(typeof(ZoomNet.Models.ReportParticipant[]))] diff --git a/Source/ZoomNet/Models/ReportHost.cs b/Source/ZoomNet/Models/ReportHost.cs new file mode 100644 index 00000000..1c017c26 --- /dev/null +++ b/Source/ZoomNet/Models/ReportHost.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models; + +/// +/// Host report item. +/// +public class ReportHost +{ + /// Gets or sets the user id. + [JsonPropertyName("id")] + public string Id { get; set; } + + /// Gets or sets the department. + [JsonPropertyName("dept")] + public string Department { get; set; } + + /// Gets or sets a valid email address. + [JsonPropertyName("email")] + public string Email { get; set; } + + /// Gets or sets the type. + [JsonPropertyName("type")] + public UserType Type { get; set; } + + /// Gets or sets display name. + [JsonPropertyName("user_name")] + public string DisplayName { get; set; } + + /// Gets or sets the custom attributes. + [JsonPropertyName("custom_attributes")] + public CustomAttribute[] CustomAttributes { get; set; } + + /// Gets or sets the number of participants in meetings for user. + [JsonPropertyName("participants")] + public int TotalParticipants { get; set; } + + /// Gets or sets the number of meetings for user. + [JsonPropertyName("meetings")] + public int TotalMeetings { get; set; } + + /// Gets or sets the number of meeting minutes for user. + [JsonPropertyName("meeting_minutes")] + public int TotalMeetingMinutes { get; set; } +} diff --git a/Source/ZoomNet/Models/ReportHostType.cs b/Source/ZoomNet/Models/ReportHostType.cs new file mode 100644 index 00000000..4d8de196 --- /dev/null +++ b/Source/ZoomNet/Models/ReportHostType.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models; + +/// +/// Enumeration to indicate the type of a host. +/// +public enum ReportHostType +{ + /// + /// Active. + /// + [EnumMember(Value = "active")] + Active, + + /// + /// Inactive. + /// + [EnumMember(Value = "inactive")] + Inactive, +} diff --git a/Source/ZoomNet/Resources/IReports.cs b/Source/ZoomNet/Resources/IReports.cs index 9ee0a431..a04cf519 100644 --- a/Source/ZoomNet/Resources/IReports.cs +++ b/Source/ZoomNet/Resources/IReports.cs @@ -44,8 +44,32 @@ public interface IReports /// /// The cancellation token. /// - /// An array of meetings.. + /// An array of meetings. /// Task> GetMeetingsAsync(string userId, DateTime from, DateTime to, ReportMeetingType type = ReportMeetingType.Past, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default); + + /// + /// Gets active/inactive host reports. + /// + /// + /// A user is considered to be an active host during the month specified in the "from" and "to" range, if the user has hosted at least one meeting during this period. If the user didn't host any meetings during this period, the user is considered to be inactive. + /// The Active Hosts report displays a list of meetings, participants, and meeting minutes for a specific time range, up to one month. The month should fall within the last six months. + /// The Inactive Hosts report pulls a list of users who were not active during a specific period of time. + /// Use this method to retrieve an active or inactive host report for a specified period of time. The time range for the report is limited to a month and the month should fall under the past six months. + /// + /// Start date. + /// End date. + /// Type of report. + /// The number of records returned within a single API call. + /// + /// The next page token is used to paginate through large result sets. + /// A next page token will be returned whenever the set of available results exceeds the current page size. + /// The expiration period for this token is 15 minutes. + /// + /// The cancellation token. + /// + /// An array of report items. + /// + Task> GetHostsAsync(DateTime from, DateTime to, ReportHostType type = ReportHostType.Active, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/Reports.cs b/Source/ZoomNet/Resources/Reports.cs index f1df1cc0..92c72966 100644 --- a/Source/ZoomNet/Resources/Reports.cs +++ b/Source/ZoomNet/Resources/Reports.cs @@ -41,10 +41,7 @@ internal Reports(IClient client) /// public Task> GetMeetingParticipantsAsync(string meetingId, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default) { - if (pageSize < 1 || pageSize > 300) - { - throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be between 1 and 300"); - } + VerifyPageSize(pageSize); return _client .GetAsync($"report/meetings/{meetingId}/participants") @@ -55,39 +52,11 @@ public Task> GetMeetingPart .AsPaginatedResponseWithToken("participants"); } - /// - /// Get a list past meetings and webinars for a specified time period. The time range for the report is limited to a month and the month must fall within the past six months. - /// - /// The user ID or email address of the user. - /// Start date. - /// End date. - /// The meeting type to query for. - /// The number of records returned within a single API call. - /// - /// The next page token is used to paginate through large result sets. - /// A next page token will be returned whenever the set of available results exceeds the current page size. - /// The expiration period for this token is 15 minutes. - /// - /// The cancellation token. - /// - /// An array of meetings.. - /// + /// public Task> GetMeetingsAsync(string userId, DateTime from, DateTime to, ReportMeetingType type, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default) { - if (to < from) - { - throw new ArgumentOutOfRangeException(nameof(to), $"Should be greater then or equal to {nameof(from)}."); - } - - if (to - from > TimeSpan.FromDays(30)) - { - throw new ArgumentOutOfRangeException(nameof(to), "The date range should not exceed one month."); - } - - if (pageSize < 1 || pageSize > 300) - { - throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be between 1 and 300"); - } + VerifyReportDatesRange(from, to); + VerifyPageSize(pageSize); return _client .GetAsync($"report/users/{userId}/meetings") @@ -99,5 +68,43 @@ public Task> GetMeetingsAsync(string use .WithCancellationToken(cancellationToken) .AsPaginatedResponseWithToken("meetings"); } + + /// + public Task> GetHostsAsync(DateTime from, DateTime to, ReportHostType type = ReportHostType.Active, int pageSize = 30, string pageToken = null, CancellationToken cancellationToken = default) + { + VerifyReportDatesRange(from, to); + VerifyPageSize(pageSize); + + return _client + .GetAsync("report/users") + .WithArgument("from", from.ToZoomFormat(dateOnly: true)) + .WithArgument("to", to.ToZoomFormat(dateOnly: true)) + .WithArgument("type", type.ToEnumString()) + .WithArgument("page_size", pageSize) + .WithArgument("next_page_token", pageToken) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponseWithToken("users"); + } + + private static void VerifyPageSize(int pageSize) + { + if (pageSize < 1 || pageSize > 300) + { + throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be between 1 and 300"); + } + } + + private static void VerifyReportDatesRange(DateTime from, DateTime to) + { + if (to < from) + { + throw new ArgumentOutOfRangeException(nameof(to), $"Should be greater then or equal to {nameof(from)}."); + } + + if (to - from > TimeSpan.FromDays(30)) + { + throw new ArgumentOutOfRangeException(nameof(to), "The date range should not exceed one month."); + } + } } } From b65b89785088a3d34e80fb94bef433d0e696479b Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 30 Dec 2022 11:26:25 -0500 Subject: [PATCH 2/4] Use `Contains(char)` --- Source/ZoomNet/Resources/Roles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Roles.cs b/Source/ZoomNet/Resources/Roles.cs index 69cf64fa..f8efcba0 100644 --- a/Source/ZoomNet/Resources/Roles.cs +++ b/Source/ZoomNet/Resources/Roles.cs @@ -133,7 +133,7 @@ public Task AssignUsersAsync(string roleId, IEnumerable userIds, Cancell { // Zoom requires either the "id" field or the "email" field. // If both are provided, "id" takes precedence. - { "members", userIds.Select(id => new JsonObject { { (id ?? string.Empty).Contains("@") ? "email" : "id", id } }).ToArray() } + { "members", userIds.Select(id => new JsonObject { { (id ?? string.Empty).Contains('@') ? "email" : "id", id } }).ToArray() } }; return _client From 6b0e149eeb8ebd72057a9761dad86c3f480cf1b7 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 11 Jan 2023 10:02:47 -0500 Subject: [PATCH 3/4] Serialize the `Type` as an integer rather than a string when updating a user Resolves #260 --- 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 03566a30..b31f8eb4 100644 --- a/Source/ZoomNet/Resources/Users.cs +++ b/Source/ZoomNet/Resources/Users.cs @@ -145,7 +145,7 @@ public Task UpdateAsync(string userId, string firstName = null, string lastName { "pronouns", pronouns }, { "pronouns_option", pronounsDisplay?.ToEnumString() }, { "timezone", timezone?.ToEnumString() }, - { "type", type?.ToEnumString() }, + { "type", type }, { "use_pmi", usePmi }, { "vanity_name", personalMeetingRoomName }, { "custom_attributes", customAttributes?.ToArray() } From c12ecaa5c6bf5fb555232c1442b2ccacfb1c28e2 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 11 Jan 2023 10:05:02 -0500 Subject: [PATCH 4/4] Zoom's documentation mentions a 'No Meetings License' user type The user type is not mentioned in the "How to identify your Zoom user type" section here: https://support.zoom.us/hc/en-us/articles/201363173-Zoom-user-types-roles but it is documented here: https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/userUpdate --- Source/ZoomNet/Models/UserType.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/ZoomNet/Models/UserType.cs b/Source/ZoomNet/Models/UserType.cs index 8f42b03d..629bab39 100644 --- a/Source/ZoomNet/Models/UserType.cs +++ b/Source/ZoomNet/Models/UserType.cs @@ -14,6 +14,9 @@ public enum UserType /// On-premise. OnPremise = 3, + /// No Meetings License. + NoMeetingsLicense = 4, + /// None. /// This can only be set with . None = 99,