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();
}