diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs
index f851048e..fcd9b8c5 100644
--- a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs
+++ b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs
@@ -11,7 +11,7 @@ namespace ZoomNet.IntegrationTests.Tests;
public class Chatbot : IIntegrationTest
{
- ///
+ ///
public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, CancellationToken cancellationToken)
{
var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User);
diff --git a/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs b/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs
index c64ea1a9..77fdfb4b 100644
--- a/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs
+++ b/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs
@@ -1,5 +1,5 @@
-using System.Text.Json;
-using Shouldly;
+using Shouldly;
+using System.Text.Json;
using Xunit;
using ZoomNet.Json;
using ZoomNet.Models;
diff --git a/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs b/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs
new file mode 100644
index 00000000..35eceacf
--- /dev/null
+++ b/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs
@@ -0,0 +1,84 @@
+using Shouldly;
+using System.Text.Json;
+using Xunit;
+using ZoomNet.Json;
+using ZoomNet.Models;
+
+namespace ZoomNet.UnitTests.Models
+{
+ public class PhoneUserTests
+ {
+ #region constants
+
+ internal const string PHONE_USER = @"{
+ ""id"": ""NL3cEpSdRc-c2t8aLoZqiw"",
+ ""phone_user_id"": ""u7pnC468TaS46OuNoEw6GA"",
+ ""email"": ""test_phone_user@testapi.com"",
+ ""name"": ""test phone user"",
+ ""extension_id"": ""CcrEGgmeQem1uyJsuIRKwA"",
+ ""extension_number"": 123,
+ ""status"": ""activate"",
+ ""calling_plans"": [
+ {
+ ""type"": 600,
+ ""name"": ""Delhi billing"",
+ ""billing_account_id"": ""3WWAEiEjTj2IQuyDiKMd_A"",
+ ""billing_account_name"": ""Delhi billing""
+ }
+ ],
+ ""phone_numbers"": [
+ {
+ ""id"": ""---M1padRvSUtw7YihN7sA"",
+ ""number"": ""14232058798""
+ }
+ ],
+ ""site"": {
+ ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"",
+ ""name"": ""Test Site""
+ },
+ ""department"": ""Test"",
+ ""cost_center"": ""Cost Test Center""
+ }";
+
+ #endregion
+
+ #region tests
+
+ [Fact]
+ public void Parse_Json_PhoneUserTests()
+ {
+ // Arrange
+
+ // Act
+ var result = JsonSerializer.Deserialize(
+ PHONE_USER, JsonFormatter.SerializerOptions);
+
+ // Assert
+ result.Id.ShouldBe("NL3cEpSdRc-c2t8aLoZqiw");
+ result.PhoneUserId.ShouldBe("u7pnC468TaS46OuNoEw6GA");
+ result.Email.ShouldBe("test_phone_user@testapi.com");
+ result.Name.ShouldBe("test phone user");
+ result.ExtensionId.ShouldBe("CcrEGgmeQem1uyJsuIRKwA");
+ result.ExtensionNumber.ShouldBe(123);
+ result.Status.ShouldBe(PhoneCallUserStatus.Active);
+ result.CallingPlans.ShouldNotBeNull();
+ result.CallingPlans.Length.ShouldBe(1);
+ result.PhoneNumbers.ShouldNotBeNull();
+ result.PhoneNumbers.Length.ShouldBe(1);
+ result.Department.ShouldBe("Test");
+ result.CostCenter.ShouldBe("Cost Test Center");
+ result.Site.Id.ShouldBe("8f71O6rWT8KFUGQmJIFAdQ");
+ result.Site.Name.ShouldBe("Test Site");
+
+ var callingPlan = result.CallingPlans[0];
+ callingPlan.BillingAccountId.ShouldBe("3WWAEiEjTj2IQuyDiKMd_A");
+ callingPlan.BillingAccountName.ShouldBe("Delhi billing");
+
+ var phoneNumber = result.PhoneNumbers[0];
+ phoneNumber.PhoneNumberId.ShouldBe("---M1padRvSUtw7YihN7sA");
+ phoneNumber.PhoneNumber.ShouldBe("14232058798");
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs b/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs
index c1e44131..5beae3c9 100644
--- a/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs
+++ b/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs
@@ -1,8 +1,8 @@
-using System.Net.Http;
+using RichardSzalay.MockHttp;
+using Shouldly;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using RichardSzalay.MockHttp;
-using Shouldly;
using Xunit;
using ZoomNet.Resources;
diff --git a/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs b/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs
new file mode 100644
index 00000000..20906c0b
--- /dev/null
+++ b/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs
@@ -0,0 +1,114 @@
+using RichardSzalay.MockHttp;
+using Shouldly;
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+using ZoomNet.Resources;
+
+namespace ZoomNet.UnitTests.Resources
+{
+ public class PhoneUserTests
+ {
+ #region constants
+
+ internal const string PHONE_USERS_PAGINATED_OBJECT = @"{
+ ""next_page_token"": ""F2qwertyg5eIqRRgC2YMauur8ZHUaJqtS3i"",
+ ""page_size"": 1,
+ ""total_records"": 10,
+ ""users"": [
+ {
+ ""id"": ""NL3cEpSdRc-c2t8aLoZqiw"",
+ ""phone_user_id"": ""u7pnC468TaS46OuNoEw6GA"",
+ ""email"": ""test_phone_user@testapi.com"",
+ ""name"": ""test phone user"",
+ ""extension_id"": ""CcrEGgmeQem1uyJsuIRKwA"",
+ ""extension_number"": 123,
+ ""status"": ""activate"",
+ ""calling_plans"": [
+ {
+ ""type"": 600,
+ ""name"": ""Delhi billing"",
+ ""billing_account_id"": ""3WWAEiEjTj2IQuyDiKMd_A"",
+ ""billing_account_name"": ""Delhi billing""
+ }
+ ],
+ ""phone_numbers"": [
+ {
+ ""id"": ""---M1padRvSUtw7YihN7sA"",
+ ""number"": ""14232058798""
+ }
+ ],
+ ""site"": {
+ ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"",
+ ""name"": ""Test Site""
+ },
+ ""department"": ""Test"",
+ ""cost_center"": ""Cost Test Center""
+ }
+ ]
+ }";
+
+ #endregion
+
+ #region tests
+
+ [Fact]
+ public async Task GetPhoneUsersPaginatedResponseTestsAsync()
+ {
+ // Arrange
+ var pageSize = 1;
+
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(
+ HttpMethod.Get,
+ Utils.GetZoomApiUri("phone/users"))
+ .Respond(
+ "application/json",
+ PHONE_USERS_PAGINATED_OBJECT);
+
+ var client = Utils.GetFluentClient(mockHttp);
+ var phone = new Phone(client);
+
+ // Act
+ var result = await phone
+ .ListPhoneUsersAsync(pageSize: pageSize)
+ .ConfigureAwait(true);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+ mockHttp.VerifyNoOutstandingRequest();
+ result.NextPageToken.ShouldNotBeNullOrEmpty();
+ result.PageSize.ShouldBe(1);
+ result.TotalRecords.ShouldBe(10);
+ result.Records.ShouldNotBeNull();
+ result.Records.Length.ShouldBe(1);
+ result.Records[0].Id.ShouldBe("NL3cEpSdRc-c2t8aLoZqiw");
+ result.Records[0].PhoneNumbers[0].PhoneNumberId.ShouldBe("---M1padRvSUtw7YihN7sA");
+ result.Records[0].PhoneNumbers[0].PhoneNumber.ShouldBe("14232058798");
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(101)]
+ public void InvalidPageSize_GetPhoneUsersPaginatedResponseTests(int pageSize)
+ {
+ // Arrange
+ var mockHttp = new MockHttpMessageHandler();
+
+ var client = Utils.GetFluentClient(mockHttp);
+ var phone = new Phone(client);
+
+ // Act and Assert
+ var exception = Assert.Throws(() => phone
+ .ListPhoneUsersAsync(pageSize: pageSize)
+ .ConfigureAwait(true));
+
+ exception.ParamName.ShouldBe(nameof(pageSize));
+ exception.Message.ShouldStartWith("Records per page must be between 1 and 100");
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs
index b7f1ebb5..12dc1853 100644
--- a/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs
+++ b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs
@@ -1,7 +1,7 @@
-using System.Net.Http;
-using System.Threading.Tasks;
using RichardSzalay.MockHttp;
using Shouldly;
+using System.Net.Http;
+using System.Threading.Tasks;
using Xunit;
using ZoomNet.Resources;
diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs
index 79ee3779..61b62734 100644
--- a/Source/ZoomNet/Extensions/Internal.cs
+++ b/Source/ZoomNet/Extensions/Internal.cs
@@ -500,22 +500,22 @@ internal static string EnsureEndsWith(this string value, string suffix)
internal static T GetPropertyValue(this JsonElement element, string name, T defaultValue)
{
- return GetPropertyValue(element, new[] { name }, defaultValue, false);
+ return element.GetPropertyValue(new[] { name }, defaultValue, false);
}
internal static T GetPropertyValue(this JsonElement element, string[] names, T defaultValue)
{
- return GetPropertyValue(element, names, defaultValue, false);
+ return element.GetPropertyValue(names, defaultValue, false);
}
internal static T GetPropertyValue(this JsonElement element, string name)
{
- return GetPropertyValue(element, new[] { name }, default, true);
+ return element.GetPropertyValue(new[] { name }, default, true);
}
internal static T GetPropertyValue(this JsonElement element, string[] names)
{
- return GetPropertyValue(element, names, default, true);
+ return element.GetPropertyValue(names, default, true);
}
internal static async Task ForEachAsync(this IEnumerable items, Func> action, int maxDegreeOfParalellism)
@@ -644,8 +644,8 @@ internal static IEnumerable> ParseQuerystring(this
internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
{
- var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
- DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
+ var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME);
+ DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
return diagnosticInfo;
}
@@ -687,8 +687,8 @@ internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
if (rootJsonElement.ValueKind == JsonValueKind.Object)
{
- errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null;
- errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage);
+ 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(
@@ -749,7 +749,7 @@ internal static async Task DecompressAsync(this Stream source)
internal static string ToEnumString(this T enumValue)
where T : Enum
{
- if (TryToEnumString(enumValue, out string stringValue)) return stringValue;
+ if (enumValue.TryToEnumString(out string stringValue)) return stringValue;
return enumValue.ToString();
}
@@ -796,7 +796,7 @@ internal static bool TryToEnumString(this T enumValue, out string stringValue
internal static T ToEnum(this string str)
where T : Enum
{
- if (TryToEnum(str, out T enumValue)) return enumValue;
+ if (str.TryToEnum(out T enumValue)) return enumValue;
throw new ArgumentException($"There is no value in the {typeof(T).Name} enum that corresponds to '{str}'.");
}
@@ -907,7 +907,7 @@ private static async Task AsObject(this HttpContent httpContent, string pr
return JsonSerializer.Deserialize(responseContent, options ?? JsonFormatter.DeserializerOptions);
}
- var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
+ var jsonDoc = JsonDocument.Parse(responseContent, default);
if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
{
return property.ToObject(options);
@@ -933,7 +933,7 @@ private static async Task AsRawJsonDocument(this HttpContent httpC
{
var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
- var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
+ var jsonDoc = JsonDocument.Parse(responseContent, default);
if (string.IsNullOrEmpty(propertyName))
{
@@ -943,7 +943,7 @@ private static async Task AsRawJsonDocument(this HttpContent httpC
if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
{
var propertyContent = property.GetRawText();
- return JsonDocument.Parse(propertyContent, (JsonDocumentOptions)default);
+ return JsonDocument.Parse(propertyContent, default);
}
else if (throwIfPropertyIsMissing)
{
@@ -1106,7 +1106,7 @@ private static T GetPropertyValue(this JsonElement element, string[] names, T
{
var underlyingType = Nullable.GetUnderlyingType(typeOfT);
var getElementValue = typeof(Internal)
- .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
+ .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(underlyingType);
return (T)getElementValue.Invoke(null, new object[] { property.Value });
@@ -1116,7 +1116,7 @@ private static T GetPropertyValue(this JsonElement element, string[] names, T
{
var elementType = typeOfT.GetElementType();
var getElementValue = typeof(Internal)
- .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
+ .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(elementType);
var arrayList = new ArrayList(property.Value.GetArrayLength());
diff --git a/Source/ZoomNet/Json/BooleanConverter.cs b/Source/ZoomNet/Json/BooleanConverter.cs
index 5b0b5944..e63fb15b 100644
--- a/Source/ZoomNet/Json/BooleanConverter.cs
+++ b/Source/ZoomNet/Json/BooleanConverter.cs
@@ -14,11 +14,17 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer
{
switch (reader.TokenType)
{
+ case JsonTokenType.None:
+ case JsonTokenType.Null:
+ throw new JsonException($"Unable to convert a null value into a boolean value");
+
case JsonTokenType.True:
case JsonTokenType.False:
return reader.GetBoolean();
+
case JsonTokenType.Number:
return reader.GetByte() == 1;
+
default:
throw new JsonException($"Unable to convert the content of {reader.TokenType.ToEnumString()} JSON node into a boolean value");
}
diff --git a/Source/ZoomNet/Json/DateTimeConverter.cs b/Source/ZoomNet/Json/DateTimeConverter.cs
index 593963fa..9eebc008 100644
--- a/Source/ZoomNet/Json/DateTimeConverter.cs
+++ b/Source/ZoomNet/Json/DateTimeConverter.cs
@@ -13,10 +13,16 @@ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
{
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 DateTime");
+
case JsonTokenType.String:
return reader.GetDateTime();
+
default:
- throw new Exception($"Unable to convert {reader.TokenType.ToEnumString()} to DateTime");
+ throw new JsonException($"Unable to convert {reader.TokenType.ToEnumString()} to DateTime");
}
}
diff --git a/Source/ZoomNet/Json/KeyValuePairConverter.cs b/Source/ZoomNet/Json/KeyValuePairConverter.cs
index b055092a..bc50af19 100644
--- a/Source/ZoomNet/Json/KeyValuePairConverter.cs
+++ b/Source/ZoomNet/Json/KeyValuePairConverter.cs
@@ -59,7 +59,7 @@ public override KeyValuePair[] Read(ref Utf8JsonReader reader, T
return values.ToArray();
}
- throw new Exception("Unable to read Key/Value pair");
+ throw new JsonException("Unable to read Key/Value pair");
}
public override void Write(Utf8JsonWriter writer, KeyValuePair[] value, JsonSerializerOptions options)
diff --git a/Source/ZoomNet/Json/MeetingConverter.cs b/Source/ZoomNet/Json/MeetingConverter.cs
index aba36e7f..01e31066 100644
--- a/Source/ZoomNet/Json/MeetingConverter.cs
+++ b/Source/ZoomNet/Json/MeetingConverter.cs
@@ -28,7 +28,7 @@ public override Meeting Read(ref Utf8JsonReader reader, Type typeToConvert, Json
case MeetingType.RecurringNoFixedTime:
return rootElement.ToObject(options);
default:
- throw new Exception($"{meetingType} is an unknown meeting type");
+ throw new JsonException($"{meetingType} is an unknown meeting type");
}
}
}
diff --git a/Source/ZoomNet/Json/NullableDateTimeConverter.cs b/Source/ZoomNet/Json/NullableDateTimeConverter.cs
index 53410a9b..d3faf33a 100644
--- a/Source/ZoomNet/Json/NullableDateTimeConverter.cs
+++ b/Source/ZoomNet/Json/NullableDateTimeConverter.cs
@@ -9,6 +9,8 @@ namespace ZoomNet.Json
///
internal class NullableDateTimeConverter : ZoomNetJsonConverter
{
+ private readonly DateTimeConverter _dateTimeConverter = new DateTimeConverter();
+
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
@@ -17,17 +19,16 @@ internal class NullableDateTimeConverter : ZoomNetJsonConverter
case JsonTokenType.Null:
case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()):
return null;
- case JsonTokenType.String:
- return reader.GetDateTime();
+
default:
- throw new Exception($"Unable to convert {reader.TokenType.ToEnumString()} to nullable DateTime");
+ return _dateTimeConverter.Read(ref reader, typeToConvert, options);
}
}
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
- if (value.HasValue) writer.WriteStringValue(value.Value.ToZoomFormat());
- else writer.WriteNullValue();
+ if (!value.HasValue) writer.WriteNullValue();
+ else _dateTimeConverter.Write(writer, value.Value, options);
}
}
}
diff --git a/Source/ZoomNet/Json/NullableHexColorConverter.cs b/Source/ZoomNet/Json/NullableHexColorConverter.cs
index 8c2f3c02..73308e09 100644
--- a/Source/ZoomNet/Json/NullableHexColorConverter.cs
+++ b/Source/ZoomNet/Json/NullableHexColorConverter.cs
@@ -11,35 +11,37 @@ namespace ZoomNet.Json;
///
public class NullableHexColorConverter : JsonConverter
{
- ///
+ ///
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(Color?);
}
- ///
+ ///
public override Color? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- var str = reader.GetString();
- if (string.IsNullOrEmpty(str))
- return null;
- str = str.Replace("#", string.Empty);
- if (str.Length == 6)
- str = $"FF{str}";
- return int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argB)
- ? Color.FromArgb(argB)
- : null;
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.None:
+ case JsonTokenType.Null:
+ case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()):
+ return null;
+
+ default:
+ {
+ var str = reader.GetString().Replace("#", string.Empty);
+ if (str.Length == 6) str = $"FF{str}";
+ return int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argB)
+ ? Color.FromArgb(argB)
+ : null;
+ }
+ }
}
- ///
+ ///
public override void Write(Utf8JsonWriter writer, Color? value, JsonSerializerOptions options)
{
- if (value == null)
- {
- writer.WriteNullValue();
- return;
- }
-
- writer.WriteStringValue($"#{value.Value.R:X2}{value.Value.G:X2}{value.Value.B:X2}");
+ if (!value.HasValue) writer.WriteNullValue();
+ else writer.WriteStringValue($"#{value.Value.R:X2}{value.Value.G:X2}{value.Value.B:X2}");
}
}
diff --git a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs
index 869f1cc3..ff26e552 100644
--- a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs
+++ b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs
@@ -28,7 +28,7 @@ public override ParticipantDevice[] Read(ref Utf8JsonReader reader, Type typeToC
return items;
default:
- throw new Exception("Unable to convert to ParticipantDevice");
+ throw new JsonException("Unable to convert to ParticipantDevice");
}
}
diff --git a/Source/ZoomNet/Json/WebhookEventConverter.cs b/Source/ZoomNet/Json/WebhookEventConverter.cs
index 9a1f5dff..a881c475 100644
--- a/Source/ZoomNet/Json/WebhookEventConverter.cs
+++ b/Source/ZoomNet/Json/WebhookEventConverter.cs
@@ -280,7 +280,7 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe
webHookEvent = endpointUrlValidationEvent;
break;
default:
- throw new Exception($"{eventType} is an unknown event type");
+ throw new JsonException($"{eventType} is an unknown event type");
}
webHookEvent.EventType = eventType;
@@ -306,7 +306,7 @@ private static KeyValuePair ConvertJsonPropertyToKeyValuePair(Js
if (value.TryGetInt64(out var longValue)) return new KeyValuePair(key, longValue);
if (value.TryGetInt32(out var intValue)) return new KeyValuePair(key, intValue);
if (value.TryGetInt16(out var shortValue)) return new KeyValuePair(key, shortValue);
- throw new Exception($"Property {key} appears to contain a numerical value but we are unable to determine to exact type");
+ throw new JsonException($"Property {key} appears to contain a numerical value but we are unable to determine to exact type");
default: return new KeyValuePair(key, value.GetRawText());
}
}
diff --git a/Source/ZoomNet/Json/WebinarConverter.cs b/Source/ZoomNet/Json/WebinarConverter.cs
index bf84d616..5ae880e2 100644
--- a/Source/ZoomNet/Json/WebinarConverter.cs
+++ b/Source/ZoomNet/Json/WebinarConverter.cs
@@ -25,7 +25,7 @@ public override Webinar Read(ref Utf8JsonReader reader, Type typeToConvert, Json
case WebinarType.RecurringNoFixedTime:
return rootElement.ToObject(options);
default:
- throw new Exception($"{webinarType} is an unknown webinar type");
+ throw new JsonException($"{webinarType} is an unknown webinar type");
}
}
}
diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs
index 418aaf6d..42f4c6df 100644
--- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs
+++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs
@@ -58,7 +58,10 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection), TypeInfoPropertyName = "ChatbotMessageChatbotSection")]
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannel))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelSettings))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelType))]
@@ -152,6 +155,7 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus))]
[JsonSerializable(typeof(ZoomNet.Models.PhoneNumber))]
[JsonSerializable(typeof(ZoomNet.Models.PhoneType))]
+ [JsonSerializable(typeof(ZoomNet.Models.PhoneUser))]
[JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType))]
[JsonSerializable(typeof(ZoomNet.Models.Poll))]
[JsonSerializable(typeof(ZoomNet.Models.PollAnswer))]
@@ -208,6 +212,7 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails))]
[JsonSerializable(typeof(ZoomNet.Models.SecuritySettings))]
[JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail))]
+ [JsonSerializable(typeof(ZoomNet.Models.Site))]
[JsonSerializable(typeof(ZoomNet.Models.SmsAttachment))]
[JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType))]
[JsonSerializable(typeof(ZoomNet.Models.SmsDirection))]
@@ -354,7 +359,10 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection[]), TypeInfoPropertyName = "ChatbotMessageChatbotSectionArray")]
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation[]))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannel[]))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions[]))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember[]))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions[]))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions[]))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole[]))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelSettings[]))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelType[]))]
@@ -448,6 +456,7 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus[]))]
[JsonSerializable(typeof(ZoomNet.Models.PhoneNumber[]))]
[JsonSerializable(typeof(ZoomNet.Models.PhoneType[]))]
+ [JsonSerializable(typeof(ZoomNet.Models.PhoneUser[]))]
[JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType[]))]
[JsonSerializable(typeof(ZoomNet.Models.Poll[]))]
[JsonSerializable(typeof(ZoomNet.Models.PollAnswer[]))]
@@ -504,6 +513,7 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails[]))]
[JsonSerializable(typeof(ZoomNet.Models.SecuritySettings[]))]
[JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail[]))]
+ [JsonSerializable(typeof(ZoomNet.Models.Site[]))]
[JsonSerializable(typeof(ZoomNet.Models.SmsAttachment[]))]
[JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType[]))]
[JsonSerializable(typeof(ZoomNet.Models.SmsDirection[]))]
@@ -619,6 +629,9 @@ namespace ZoomNet.Json
[JsonSerializable(typeof(ZoomNet.Models.CallLogType?))]
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle?), TypeInfoPropertyName = "ChatbotMessageChatbotActionStyleNullable")]
[JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource?), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownStaticSourceNullable")]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions?))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions?))]
+ [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions?))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole?))]
[JsonSerializable(typeof(ZoomNet.Models.ChatChannelType?))]
[JsonSerializable(typeof(ZoomNet.Models.ChatMentionType?))]
diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs
index 209358d5..ab2a7b32 100644
--- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs
+++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs
@@ -14,7 +14,7 @@ public class ChatbotFormFields : IChatbotBody, IChatbotSection, IChatbotValidate
[JsonPropertyName("items")]
public ICollection Items { get; set; }
- ///
+ ///
public void Validate(bool enableMarkdownSupport)
{
foreach (var item in Items)
diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs
index 50c7150d..676dbe32 100644
--- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs
+++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs
@@ -54,7 +54,7 @@ public ChatbotMessageLine(string text)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Link { get; set; }
- ///
+ ///
public void Validate(bool enableMarkdownSupport)
{
if (enableMarkdownSupport && Link != null)
diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs
index c80badc2..2230c100 100644
--- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs
+++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs
@@ -64,7 +64,7 @@ public DateTime TimeStampFromDateTime
set => TimeStamp = value.ToUnixTime(Internal.UnixTimePrecision.Milliseconds);
}
- ///
+ ///
public void Validate(bool enableMarkdownSupport)
{
foreach (var section in Sections.OfType())
diff --git a/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs
index 02b9278d..bd931fd6 100644
--- a/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs
+++ b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs
@@ -1,15 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Text.Json.Serialization;
-using System.Threading.Tasks;
namespace ZoomNet.Models
{
///
/// Information about the Enable continuous meeting chat feature.
- ///
+ ///
public class ContinuousMeetingChatSettings
{
///
diff --git a/Source/ZoomNet/Models/PhoneCallUserProfile.cs b/Source/ZoomNet/Models/PhoneCallUserProfile.cs
index de9ffd70..095fe729 100644
--- a/Source/ZoomNet/Models/PhoneCallUserProfile.cs
+++ b/Source/ZoomNet/Models/PhoneCallUserProfile.cs
@@ -1,4 +1,4 @@
-using System.Text.Json.Serialization;
+using System.Text.Json.Serialization;
namespace ZoomNet.Models;
diff --git a/Source/ZoomNet/Models/PhoneUser.cs b/Source/ZoomNet/Models/PhoneUser.cs
new file mode 100644
index 00000000..58c5b313
--- /dev/null
+++ b/Source/ZoomNet/Models/PhoneUser.cs
@@ -0,0 +1,81 @@
+using System.Text.Json.Serialization;
+
+namespace ZoomNet.Models;
+
+///
+/// Zoom Phone user information.
+///
+public class PhoneUser
+{
+ ///
+ /// Gets or sets the calling plans.
+ ///
+ [JsonPropertyName("calling_plans")]
+ public CallingPlan[] CallingPlans { get; set; }
+
+ ///
+ /// Gets or sets the cost center.
+ ///
+ [JsonPropertyName("cost_center")]
+ public string CostCenter { get; set; }
+
+ ///
+ /// Gets or sets the department of the object.
+ ///
+ [JsonPropertyName("department")]
+ public string Department { get; set; }
+
+ ///
+ /// Gets or sets the email address.
+ ///
+ [JsonPropertyName("email")]
+ public string Email { get; set; }
+
+ ///
+ /// Gets or sets the extension ID.
+ ///
+ [JsonPropertyName("extension_id")]
+ public string ExtensionId { get; set; }
+
+ ///
+ /// Gets or sets the extension number.
+ ///
+ [JsonPropertyName("extension_number")]
+ public int ExtensionNumber { get; set; }
+
+ ///
+ /// Gets or sets the Zoom user ID.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Gets or sets the Zoom user name.
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the phone numbers.
+ ///
+ [JsonPropertyName("phone_numbers")]
+ public PhoneCallPhoneNumber[] PhoneNumbers { get; set; }
+
+ ///
+ /// Gets or sets the Zoom phone user id.
+ ///
+ [JsonPropertyName("phone_user_id")]
+ public string PhoneUserId { get; set; }
+
+ ///
+ /// Gets or sets a site.
+ ///
+ [JsonPropertyName("site")]
+ public Site Site { get; set; }
+
+ ///
+ /// Gets or sets the status of the user.
+ ///
+ [JsonPropertyName("status")]
+ public PhoneCallUserStatus Status { get; set; }
+}
diff --git a/Source/ZoomNet/Models/Site.cs b/Source/ZoomNet/Models/Site.cs
new file mode 100644
index 00000000..1375b493
--- /dev/null
+++ b/Source/ZoomNet/Models/Site.cs
@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+
+namespace ZoomNet.Models;
+
+///
+/// Site information to which a user belongs to.
+///
+public class Site
+{
+ ///
+ /// Gets or sets the Id.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ ///
+ /// Gets or sets the name.
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+}
diff --git a/Source/ZoomNet/Resources/CallLogs.cs b/Source/ZoomNet/Resources/CallLogs.cs
index 9f2f4441..e9857153 100644
--- a/Source/ZoomNet/Resources/CallLogs.cs
+++ b/Source/ZoomNet/Resources/CallLogs.cs
@@ -76,7 +76,7 @@ public Task> GetAsync(string userId, Dat
/// An array of .
///
public Task> GetAsync(DateTime? from = null, DateTime? to = null, CallLogType type = CallLogType.All, CallLogPathType? pathType = null, CallLogTimeType? timeType = CallLogTimeType.StartTime, string siteId = null, bool chargedCallLogs = false, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default)
- {
+ {
if (recordsPerPage < 1 || recordsPerPage > 300)
{
throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300");
diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs
index 5fbe2e5e..66204e7e 100644
--- a/Source/ZoomNet/Resources/CloudRecordings.cs
+++ b/Source/ZoomNet/Resources/CloudRecordings.cs
@@ -4,10 +4,12 @@
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using ZoomNet.Models;
+using ZoomNet.Utilities;
namespace ZoomNet.Resources
{
@@ -356,20 +358,44 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable registran
return UpdateRegistrantsStatusAsync(meetingId, registrantIds, "deny", cancellationToken);
}
- ///
- /// Download the recording file.
- ///
- /// The URL of the recording file to download.
- /// Cancellation token.
- ///
- /// The containing the file.
- ///
- public Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default)
+ ///
+ public async Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default)
{
- return _client
+ /*
+ * PLEASE NOTE:
+ *
+ * The HttpRequestMessage in this method is dispatched with its completion option set to "ResponseHeadersRead".
+ * This ensures the content of the response is streamed rather than buffered in memory.
+ * This is important in cases where the downloaded file is quite large.
+ * In this scenario, we don't want the entirety of the file to be buffered in a MemoryStream because
+ * it could lead to "out of memory" exceptions if the file is large enough.
+ * See https://github.com/Jericho/ZoomNet/pull/342 for a discussion on this topic.
+ *
+ * Forthermore, as of this writing, the FluentHttp library does not allow us to stream the content of responses
+ * which means that the code in this method cannot be simplified like so:
+ return _client
.GetAsync(downloadUrl)
.WithCancellationToken(cancellationToken)
.AsStream();
+ *
+ * The downside of not using the FluentHttp library to dispatch the request is that we lose automatic retries,
+ * error handling, logging, etc.
+ */
+
+ using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl))
+ {
+ var tokenHandler = _client.Filters.OfType().SingleOrDefault();
+ if (tokenHandler != null)
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token);
+ }
+
+ var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadAsStreamAsync();
+ }
}
private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable registrantIds, string status, CancellationToken cancellationToken = default)
diff --git a/Source/ZoomNet/Resources/IPhone.cs b/Source/ZoomNet/Resources/IPhone.cs
index fac8a3f8..14702d94 100644
--- a/Source/ZoomNet/Resources/IPhone.cs
+++ b/Source/ZoomNet/Resources/IPhone.cs
@@ -58,6 +58,36 @@ Task GetRecordingTranscriptAsync(
Task GetPhoneCallUserProfileAsync(
string userId, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves a list of all of an account's users who are assigned a Zoom Phone license.
+ ///
+ /// The number of records returned from a single API call. Default is 30.
+ ///
+ /// The next page token paginates through a large set of results.
+ /// A next page token is returned whenever the set of available results exceeds the current page size.
+ /// The expiration period for this token is 15 minutes.
+ ///
+ /// The unique identifier of the site.
+ /// The type of calling plan.
+ /// The status of the Zoom Phone user.
+ /// The department where the user belongs.
+ /// The cost center where the user belongs.
+ /// The partial string of user's name, extension number, or phone number.
+ /// A cancellation token that can be used to cancel the asynchronous operation.
+ ///
+ /// A task representing the asynchronous operation. The task result contains an array of Zoom Phone users in type of .
+ ///
+ Task> ListPhoneUsersAsync(
+ int pageSize = 30,
+ string nextPageToken = null,
+ string siteId = null,
+ int? callingType = null,
+ PhoneCallUserStatus? status = null,
+ string department = null,
+ string costCenter = null,
+ string keyword = null,
+ CancellationToken cancellationToken = default);
+
#endregion
}
}
diff --git a/Source/ZoomNet/Resources/Phone.cs b/Source/ZoomNet/Resources/Phone.cs
index fa9e3ea8..f5fdade5 100644
--- a/Source/ZoomNet/Resources/Phone.cs
+++ b/Source/ZoomNet/Resources/Phone.cs
@@ -1,11 +1,12 @@
using Pathoschild.Http.Client;
+using System;
using System.Threading;
using System.Threading.Tasks;
using ZoomNet.Models;
namespace ZoomNet.Resources
{
- ///
+ ///
public class Phone : IPhone
{
#region private fields
@@ -29,7 +30,7 @@ internal Phone(IClient client)
#region Recordings endpoints
- ///
+ ///
public Task GetRecordingAsync(
string callId, CancellationToken cancellationToken = default)
{
@@ -39,7 +40,7 @@ public Task GetRecordingAsync(
.AsObject();
}
- ///
+ ///
public Task GetRecordingTranscriptAsync(
string recordingId, CancellationToken cancellationToken = default)
{
@@ -53,7 +54,7 @@ public Task GetRecordingTranscriptAsync(
#region Users Endpoints
- ///
+ ///
public Task GetPhoneCallUserProfileAsync(string userId, CancellationToken cancellationToken = default)
{
return _client
@@ -62,6 +63,37 @@ public Task GetPhoneCallUserProfileAsync(string userId, Ca
.AsObject();
}
+ ///
+ public Task> ListPhoneUsersAsync(
+ int pageSize = 30,
+ string nextPageToken = null,
+ string siteId = null,
+ int? callingType = null,
+ PhoneCallUserStatus? status = null,
+ string department = null,
+ string costCenter = null,
+ string keyword = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (pageSize < 1 || pageSize > 100)
+ {
+ throw new ArgumentOutOfRangeException(nameof(pageSize), "Records per page must be between 1 and 100");
+ }
+
+ return _client
+ .GetAsync($"phone/users")
+ .WithArgument("page_size", pageSize)
+ .WithArgument("next_page_token", nextPageToken)
+ .WithArgument("site_id", siteId)
+ .WithArgument("calling_type", callingType)
+ .WithArgument("status", status)
+ .WithArgument("department", department)
+ .WithArgument("cost_center", costCenter)
+ .WithArgument("keyword", keyword)
+ .WithCancellationToken(cancellationToken)
+ .AsPaginatedResponseWithToken("users");
+ }
+
#endregion
}
}
diff --git a/Source/ZoomNet/Resources/Sms.cs b/Source/ZoomNet/Resources/Sms.cs
index d3600ed8..19e4a18c 100644
--- a/Source/ZoomNet/Resources/Sms.cs
+++ b/Source/ZoomNet/Resources/Sms.cs
@@ -6,7 +6,7 @@
namespace ZoomNet.Resources
{
- ///
+ ///
public class Sms : ISms
{
private readonly IClient _client;
@@ -20,7 +20,7 @@ internal Sms(IClient client)
_client = client;
}
- ///
+ ///
public Task> GetSmsSessionDetailsAsync(
string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default)
{