diff --git a/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs index 410d8b2a..2e3149f2 100644 --- a/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs @@ -73,7 +73,9 @@ public void Write_multiple() [InlineData("H.323/SIP", ParticipantDevice.Sip)] [InlineData("Windows", ParticipantDevice.Windows)] [InlineData("WIN", ParticipantDevice.Windows)] + [InlineData("win 11", ParticipantDevice.Windows)] [InlineData("Zoom Rooms", ParticipantDevice.ZoomRoom)] + [InlineData("win 10+ 17763", ParticipantDevice.Windows)] public void Read_single(string value, ParticipantDevice expectedValue) { // Arrange diff --git a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs index 2752e270..498d8e59 100644 --- a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs @@ -1,6 +1,7 @@ using RichardSzalay.MockHttp; using Shouldly; using System; +using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -531,5 +532,41 @@ public async Task GetRecordingsForUserAsync() result.Records.ShouldNotBeNull(); result.Records.Length.ShouldBe(10); } + + /// + /// This unit test simulates a scenario where we attempt to download a file but our oAuth token has expired. + /// In this situation, we expect the token to be refreshed and the download request to be reissued. + /// See Issue 348 for more details. + /// + [Fact] + public async Task DownloadFileAsync_with_expired_token() + { + // Arrange + var downloadUrl = "http://dummywebsite.com/dummyfile.txt"; + + var mockTokenHttp = new MockHttpMessageHandler(); + mockTokenHttp // Issue a new token + .When(HttpMethod.Post, "https://api.zoom.us/oauth/token") + .Respond(HttpStatusCode.OK, "application/json", "{\"refresh_token\":\"new refresh token\",\"access_token\":\"new access 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\"}")); + 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")); + + var client = Utils.GetFluentClient(mockHttp, mockTokenHttp); + var recordings = new CloudRecordings(client); + + // Act + var result = await recordings.DownloadFileAsync(downloadUrl, CancellationToken.None).ConfigureAwait(true); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + } } } diff --git a/Source/ZoomNet/Extensions/Public.cs b/Source/ZoomNet/Extensions/Public.cs index b9266054..237e9e2b 100644 --- a/Source/ZoomNet/Extensions/Public.cs +++ b/Source/ZoomNet/Extensions/Public.cs @@ -312,5 +312,43 @@ public static Task InviteParticipantAsync(this IMeetings meetingsResource, long { return meetingsResource.InviteParticipantsAsync(meetingId, new[] { emailAddress }, cancellationToken); } + + /// + /// Retrieve the details of a meeting. + /// + /// The meeting resource. + /// The meeting ID. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The . + /// + /// + /// Please note that when retrieving a recurring meeting, this method will omit previous occurrences. + /// Use if you want past occurrences to be included. + /// + public static Task GetAsync(this IMeetings meetingResource, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default) + { + return meetingResource.GetAsync(meetingId, occurrenceId, false, cancellationToken); + } + + /// + /// Retrieve the details of a webinar. + /// + /// The webinar resource. + /// The webinar ID. + /// The webinar occurrence id. + /// The cancellation token. + /// + /// The . + /// + /// + /// Please note that when retrieving a recurring meeting, this method will omit previous occurrences. + /// Use if you want past occurrences to be included. + /// + public static Task GetAsync(this IWebinars webinarResource, long webinarId, string occurrenceId = null, CancellationToken cancellationToken = default) + { + return webinarResource.GetAsync(webinarId, occurrenceId, false, cancellationToken); + } } } diff --git a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs index ff26e552..a3ddd9fe 100644 --- a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs @@ -23,6 +23,7 @@ public override ParticipantDevice[] Read(ref Utf8JsonReader reader, Type typeToC var stringValue = reader.GetString(); var items = stringValue .Split(new[] { '+' }) + .Where(item => !long.TryParse(item, out _)) // Filter out values like "17763" which is a Windows build number. See https://github.com/Jericho/ZoomNet/issues/354 for details .Select(item => Convert(item)) .ToArray(); diff --git a/Source/ZoomNet/Models/DashboardParticipant.cs b/Source/ZoomNet/Models/DashboardParticipant.cs index e5bec08c..e0ca14ef 100644 --- a/Source/ZoomNet/Models/DashboardParticipant.cs +++ b/Source/ZoomNet/Models/DashboardParticipant.cs @@ -46,6 +46,18 @@ public class DashboardParticipant [JsonPropertyName("device")] public ParticipantDevice[] Devices { get; set; } + /// + /// Gets or sets the device operation system. + /// + [JsonPropertyName("os")] + public string OperatingSystem { get; set; } + + /// + /// Gets or sets the device operation system version. + /// + [JsonPropertyName("os_version")] + public string OperatingSystemVersion { get; set; } + /// /// Gets or sets the participant's IP address. /// diff --git a/Source/ZoomNet/Models/ParticipantDevice.cs b/Source/ZoomNet/Models/ParticipantDevice.cs index 8e6f7b09..4f59c8e7 100644 --- a/Source/ZoomNet/Models/ParticipantDevice.cs +++ b/Source/ZoomNet/Models/ParticipantDevice.cs @@ -29,7 +29,7 @@ public enum ParticipantDevice /// /// The participant joined via VoIP using a Windows device. /// - [MultipleValuesEnumMember(DefaultValue = "Windows", OtherValues = new[] { "WIN" })] + [MultipleValuesEnumMember(DefaultValue = "Windows", OtherValues = new[] { "WIN", "win 10", "win 11" })] Windows, /// diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs index e977e858..1895ad03 100644 --- a/Source/ZoomNet/Resources/CloudRecordings.cs +++ b/Source/ZoomNet/Resources/CloudRecordings.cs @@ -55,7 +55,7 @@ public Task> GetRecordingsForU return _client .GetAsync($"users/{userId}/recordings") - .WithArgument("trash", queryTrash.ToString().ToLower()) + .WithArgument("trash", queryTrash.ToString().ToLowerInvariant()) .WithArgument("from", from?.ToZoomFormat(dateOnly: true)) .WithArgument("to", to?.ToZoomFormat(dateOnly: true)) .WithArgument("page_size", recordsPerPage) @@ -86,7 +86,7 @@ public Task> GetRecordingsForU return _client .GetAsync($"users/{userId}/recordings") - .WithArgument("trash", queryTrash.ToString().ToLower()) + .WithArgument("trash", queryTrash.ToString().ToLowerInvariant()) .WithArgument("from", from?.ToZoomFormat(dateOnly: true)) .WithArgument("to", to?.ToZoomFormat(dateOnly: true)) .WithArgument("page_size", recordsPerPage) diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index d575fd6f..3b291b56 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -26,7 +26,7 @@ public interface IMeetings /// An array of meeting summaries. /// /// - /// To obtain the full details about a given meeting you must invoke . + /// To obtain the full details about a given meeting you must invoke . /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); @@ -43,7 +43,7 @@ public interface IMeetings /// An array of meeting summaries. /// /// - /// To obtain the full details about a given meeting you must invoke . + /// To obtain the full details about a given meeting you must invoke . /// Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); @@ -165,11 +165,16 @@ public interface IMeetings /// /// The meeting ID. /// The meeting occurrence id. + /// Set this parameter to true to view meeting details of all previous occurrences of a recurring meeting. /// The cancellation token. /// /// The . /// - Task GetAsync(long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default); + /// + /// Please note that 'includePreviousOccurences' is applicable only when fetching a recurring meeting. + /// It will be ignored if you are fetching any other type of meeting such as scheduled, personal or instant for example. + /// + Task GetAsync(long meetingId, string occurrenceId = null, bool includePreviousOccurrences = false, CancellationToken cancellationToken = default); /// /// Delete a meeting. diff --git a/Source/ZoomNet/Resources/IWebinars.cs b/Source/ZoomNet/Resources/IWebinars.cs index 58b56b73..c53d0848 100644 --- a/Source/ZoomNet/Resources/IWebinars.cs +++ b/Source/ZoomNet/Resources/IWebinars.cs @@ -25,7 +25,7 @@ public interface IWebinars /// An array of webinar summaries. /// /// - /// To obtain the full details about a given webinar you must invoke . + /// To obtain the full details about a given webinar you must invoke . /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] Task> GetAllAsync(string userId, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); @@ -41,7 +41,7 @@ public interface IWebinars /// An array of webinar summaries. /// /// - /// To obtain the full details about a given webinar you must invoke . + /// To obtain the full details about a given webinar you must invoke . /// Task> GetAllAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); @@ -144,11 +144,16 @@ public interface IWebinars /// /// The webinar ID. /// The webinar occurrence id. + /// Set this parameter to true to view meeting details of all previous occurrences of a recurring meeting. /// The cancellation token. /// /// The . /// - Task GetAsync(long webinarId, string occurrenceId = null, CancellationToken cancellationToken = default); + /// + /// Please note that 'includePreviousOccurences' is applicable only when fetching a recurring webinar. + /// It will be ignored if you are fetching any other type of meeting such as scheduled, personal or instant for example. + /// + Task GetAsync(long webinarId, string occurrenceId = null, bool includePreviousOccurrences = false, CancellationToken cancellationToken = default); /// /// Delete a webinar. diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index cdd52983..dae44e89 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -214,20 +214,13 @@ public Task CreateRecurringMeetingAsync( .AsObject(); } - /// - /// Retrieve the details of a meeting. - /// - /// The meeting ID. - /// The meeting occurrence id. - /// The cancellation token. - /// - /// The . - /// - public Task GetAsync(long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default) + /// + public Task GetAsync(long meetingId, string occurrenceId = null, bool includePreviousOccurrences = false, CancellationToken cancellationToken = default) { return _client .GetAsync($"meetings/{meetingId}") .WithArgument("occurrence_id", occurrenceId) + .WithArgument("show_previous_occurrences", includePreviousOccurrences.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsObject(); } @@ -361,8 +354,8 @@ public Task DeleteAsync(long meetingId, string occurrenceId = null, bool notifyH return _client .DeleteAsync($"meetings/{meetingId}") .WithArgument("occurrence_id", occurrenceId) - .WithArgument("schedule_for_reminder", notifyHost.ToString().ToLower()) - .WithArgument("cancel_meeting_reminder", notifyRegistrants.ToString().ToLower()) + .WithArgument("schedule_for_reminder", notifyHost.ToString().ToLowerInvariant()) + .WithArgument("cancel_meeting_reminder", notifyRegistrants.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsMessage(); } diff --git a/Source/ZoomNet/Resources/Users.cs b/Source/ZoomNet/Resources/Users.cs index b31f8eb4..2e0c878d 100644 --- a/Source/ZoomNet/Resources/Users.cs +++ b/Source/ZoomNet/Resources/Users.cs @@ -192,9 +192,9 @@ public Task DeleteAsync(string userId, string transferEmail, bool transferMeetin .DeleteAsync($"users/{userId}") .WithArgument("action", "delete") .WithArgument("transfer_email", transferEmail) - .WithArgument("transfer_meetings", transferMeetings.ToString().ToLower()) - .WithArgument("transfer_webinars", transferWebinars.ToString().ToLower()) - .WithArgument("transfer_recordings", transferRecordings.ToString().ToLower()) + .WithArgument("transfer_meetings", transferMeetings.ToString().ToLowerInvariant()) + .WithArgument("transfer_webinars", transferWebinars.ToString().ToLowerInvariant()) + .WithArgument("transfer_recordings", transferRecordings.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsMessage(); } @@ -217,9 +217,9 @@ public Task DisassociateAsync(string userId, string transferEmail, bool transfer .DeleteAsync($"users/{userId}") .WithArgument("action", "disassociate") .WithArgument("transfer_email", transferEmail) - .WithArgument("transfer_meetings", transferMeetings.ToString().ToLower()) - .WithArgument("transfer_webinars", transferWebinars.ToString().ToLower()) - .WithArgument("transfer_recordings", transferRecordings.ToString().ToLower()) + .WithArgument("transfer_meetings", transferMeetings.ToString().ToLowerInvariant()) + .WithArgument("transfer_webinars", transferWebinars.ToString().ToLowerInvariant()) + .WithArgument("transfer_recordings", transferRecordings.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsMessage(); } diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index f4cd1244..efe88e22 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -257,20 +257,13 @@ public Task UpdateRecurringWebinarAsync(long webinarId, string topic = null, str .AsMessage(); } - /// - /// Retrieve the details of a webinar. - /// - /// The webinar ID. - /// The webinar occurrence id. - /// The cancellation token. - /// - /// The . - /// - public Task GetAsync(long webinarId, string occurrenceId = null, CancellationToken cancellationToken = default) + /// + public Task GetAsync(long webinarId, string occurrenceId = null, bool includePreviousOccurrences = false, CancellationToken cancellationToken = default) { return _client .GetAsync($"webinars/{webinarId}") .WithArgument("occurrence_id", occurrenceId) + .WithArgument("show_previous_occurrences", includePreviousOccurrences.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsObject(); } @@ -290,7 +283,7 @@ public Task DeleteAsync(long webinarId, string occurrenceId = null, bool sendNot return _client .DeleteAsync($"webinars/{webinarId}") .WithArgument("occurrence_id", occurrenceId) - .WithArgument("cancel_webinar_reminder", sendNotification.ToString().ToLower()) + .WithArgument("cancel_webinar_reminder", sendNotification.ToString().ToLowerInvariant()) .WithCancellationToken(cancellationToken) .AsMessage(); }