From e37c5b0930f2f32a13c872571e466b6e1f1b773e Mon Sep 17 00:00:00 2001 From: pvgritsenko-ansible <165666478+pvgritsenko-ansible@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:07:42 +0300 Subject: [PATCH 01/19] Adding support to retrieving sms session by session id (#336) * adding support to retrieving sms session by session id * Summary fix * Sms resource added; Models renaming; Sms resource test added. * extra using removed --------- Co-authored-by: P. Gritsenko --- .../Models/SmsSessionTests.cs | 106 ++++++++++++++++++ .../Resources/SmsSessionTests.cs | 44 ++++++++ .../Json/ZoomNetJsonSerializerContext.cs | 8 ++ Source/ZoomNet/Models/SmsAttachment.cs | 40 +++++++ Source/ZoomNet/Models/SmsAttachmentType.cs | 46 ++++++++ Source/ZoomNet/Models/SmsDirection.cs | 22 ++++ .../ZoomNet/Models/SmsHistoryParticipant.cs | 28 +++++ Source/ZoomNet/Models/SmsMessage.cs | 59 ++++++++++ Source/ZoomNet/Models/SmsMessageType.cs | 38 +++++++ Source/ZoomNet/Models/SmsParticipantOwner.cs | 22 ++++ .../ZoomNet/Models/SmsParticipantOwnerType.cs | 34 ++++++ Source/ZoomNet/Resources/ISms.cs | 35 ++++++ Source/ZoomNet/Resources/Sms.cs | 55 +++++++++ 13 files changed, 537 insertions(+) create mode 100644 Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs create mode 100644 Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs create mode 100644 Source/ZoomNet/Models/SmsAttachment.cs create mode 100644 Source/ZoomNet/Models/SmsAttachmentType.cs create mode 100644 Source/ZoomNet/Models/SmsDirection.cs create mode 100644 Source/ZoomNet/Models/SmsHistoryParticipant.cs create mode 100644 Source/ZoomNet/Models/SmsMessage.cs create mode 100644 Source/ZoomNet/Models/SmsMessageType.cs create mode 100644 Source/ZoomNet/Models/SmsParticipantOwner.cs create mode 100644 Source/ZoomNet/Models/SmsParticipantOwnerType.cs create mode 100644 Source/ZoomNet/Resources/ISms.cs create mode 100644 Source/ZoomNet/Resources/Sms.cs diff --git a/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs b/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs new file mode 100644 index 00000000..f2a160e9 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Text.Json; +using Shouldly; +using Xunit; +using ZoomNet.Json; +using ZoomNet.Models; + +namespace ZoomNet.UnitTests.Models +{ + public class SmsSessionTests + { + #region constants + + internal const string SMS_HISTORY = @"{ + ""attachments"": [ + { + ""download_url"": ""https://exampleurl.us/file/download/x18dcVWxTcCzbp4zr2AT3A?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjcm9zc2ZpbGUiLCJhdWQiOiJmaWxlIiwiZGlnIjoiYTZkODE4NzQ2MDNmN2UzZWM4OThkNDMxM2IxNjNhNTQ4NGI4MjkxMTA0ZmQyYzc4MTg1NmY0MGUxY2FlOTI3YyIsImV4cCI6MTY0ODE5NDA1NH0.eCURcan9QOOw9wvBdSn_-TBzgT5HWBzp04IfsK19Oto"", + ""id"": ""x18dcVWxTcCzbp4zr2AT3A"", + ""name"": ""FWDHOMaNRaqIvNc3aIdisg.jpg"", + ""size"": 225740, + ""type"": ""JPG"" + }, + { + ""download_url"": ""https://exampleurl.us/file/download/TZkODE4NzQ2MDNmN2UzZWM?jwt=rGJXbGciOiJIUzI1NiJ9.eyJpc3MiOiJjcm9zc2ZpbGUiLCJhdWQiOiJmaWxlIiwiZGlnIjASf7fsJsfhl88jf02fLgyuM4OThkNDMxM2IxNjNhNTQ4NGI4MjkxMTA0ZmQyYzc4MTg1NmY0MGUxY2FlOTI3YyIsImV4cCI6MTY0ODE5NDA1NH0.eCURcan9QOOw9wvBdSn_-TBzgT5HWBzp04IfsK19Oti"", + ""id"": ""TZkODE4NzQ2MDNmN2UzZWM"", + ""name"": ""ASf7fsJsfhl88jf02fLgyu.mp3"", + ""size"": 303890, + ""type"": ""AUDIO"" + } + ], + ""date_time"": ""2024-03-23T02:58:01Z"", + ""direction"": ""In"", + ""message"": ""welcome"", + ""message_id"": ""IQ-cRH5P5EiTWCwpNzScnECJw"", + ""message_type"": 2, + ""sender"": { + ""display_name"": ""test api"", + ""owner"": { + ""id"": ""DnEopNmXQEGU2uvvzjgojw"", + ""type"": ""user"" + }, + ""phone_number"": ""18108001001"" + }, + ""to_members"": [ + { + ""display_name"": ""ezreal mao"", + ""owner"": { + ""id"": ""WeD59Hn7SvqNRB9jcxz5NQ"", + ""type"": ""user"" + }, + ""phone_number"": ""12092693625"" + } + ] + }"; + + #endregion + + #region Tests + + [Fact] + public void Parse_Json_SmsHistory() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize( + SMS_HISTORY, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Attachments.ShouldNotBeNull(); + result.Attachments.Length.ShouldBe(2); + result.Attachments[0].DownloadUrl.ShouldBe("https://exampleurl.us/file/download/x18dcVWxTcCzbp4zr2AT3A?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjcm9zc2ZpbGUiLCJhdWQiOiJmaWxlIiwiZGlnIjoiYTZkODE4NzQ2MDNmN2UzZWM4OThkNDMxM2IxNjNhNTQ4NGI4MjkxMTA0ZmQyYzc4MTg1NmY0MGUxY2FlOTI3YyIsImV4cCI6MTY0ODE5NDA1NH0.eCURcan9QOOw9wvBdSn_-TBzgT5HWBzp04IfsK19Oto"); + result.Attachments[0].Id.ShouldBe("x18dcVWxTcCzbp4zr2AT3A"); + result.Attachments[0].Name.ShouldBe("FWDHOMaNRaqIvNc3aIdisg.jpg"); + result.Attachments[0].Size.ShouldBe(225740); + result.Attachments[0].SmsAttachmentType.ShouldBe(SmsAttachmentType.JPG); + result.Attachments[1].DownloadUrl.ShouldBe("https://exampleurl.us/file/download/TZkODE4NzQ2MDNmN2UzZWM?jwt=rGJXbGciOiJIUzI1NiJ9.eyJpc3MiOiJjcm9zc2ZpbGUiLCJhdWQiOiJmaWxlIiwiZGlnIjASf7fsJsfhl88jf02fLgyuM4OThkNDMxM2IxNjNhNTQ4NGI4MjkxMTA0ZmQyYzc4MTg1NmY0MGUxY2FlOTI3YyIsImV4cCI6MTY0ODE5NDA1NH0.eCURcan9QOOw9wvBdSn_-TBzgT5HWBzp04IfsK19Oti"); + result.Attachments[1].Id.ShouldBe("TZkODE4NzQ2MDNmN2UzZWM"); + result.Attachments[1].Name.ShouldBe("ASf7fsJsfhl88jf02fLgyu.mp3"); + result.Attachments[1].Size.ShouldBe(303890); + result.Attachments[1].SmsAttachmentType.ShouldBe(SmsAttachmentType.AUDIO); + + result.CreationTime.ShouldBe(new DateTime(2024, 3, 23, 2, 58, 1, DateTimeKind.Utc)); + result.Direction.ShouldBe(SmsDirection.Inbound); + result.Message.ShouldBe("welcome"); + result.MessageId.ShouldBe("IQ-cRH5P5EiTWCwpNzScnECJw"); + result.MessageType.ShouldBe(SmsMessageType.Mms); + result.Sender.ShouldNotBeNull(); + result.Sender.PhoneNumber.ShouldBe("18108001001"); + result.Sender.DisplayName.ShouldBe("test api"); + result.Sender.Owner.ShouldNotBeNull(); + result.Sender.Owner.Id.ShouldBe("DnEopNmXQEGU2uvvzjgojw"); + result.Sender.Owner.Type.ShouldBe(SmsParticipantOwnerType.User); + result.ToMembers.ShouldNotBeNull(); + result.ToMembers.Length.ShouldBe(1); + result.ToMembers[0].DisplayName.ShouldBe("ezreal mao"); + result.ToMembers[0].PhoneNumber.ShouldBe("12092693625"); + result.ToMembers[0].Owner.ShouldNotBeNull(); + result.ToMembers[0].Owner.Id.ShouldBe("WeD59Hn7SvqNRB9jcxz5NQ"); + result.ToMembers[0].Owner.Type.ShouldBe(SmsParticipantOwnerType.User); + } + + #endregion + } +} diff --git a/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs new file mode 100644 index 00000000..b7f1ebb5 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs @@ -0,0 +1,44 @@ +using System.Net.Http; +using System.Threading.Tasks; +using RichardSzalay.MockHttp; +using Shouldly; +using Xunit; +using ZoomNet.Resources; + +namespace ZoomNet.UnitTests.Resources +{ + public class SmsSessionTests + { + [Fact] + public async Task GetSmsSessionAsync() + { + // Arrange + string sessionId = "aa74aeea445022779e375d2de1e193c5c131e7347283e3a6f308637cdd2a6832dc2cd7d4694c4a3316b010048373ad14174d6a8e0c548860dc2f3586e0de7fde8d2e097badf450605424521e2523139ddc2cd7d4694c4a3316b010048373ad141305076fc94fe1a4c8c52f47d6c3dcc72c74aeea445022779e375d2de1e193c5"; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect( + HttpMethod.Get, + Utils.GetZoomApiUri("phone/sms/sessions", sessionId)) + .Respond( + "application/json", + Models.SmsSessionTests.SMS_HISTORY); + + var client = Utils.GetFluentClient(mockHttp); + var sms = new Sms(client); + + // Act + var result = await sms + .GetSmsSessionDetailsAsync( + sessionId, + from: null, + to: null) + .ConfigureAwait(true); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + } + } +} diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index e9c2959e..89494d15 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -203,6 +203,14 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.SecuritySettings))] [JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail))] [JsonSerializable(typeof(ZoomNet.Models.StreamingService))] + [JsonSerializable(typeof(ZoomNet.Models.SmsAttachment))] + [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType))] + [JsonSerializable(typeof(ZoomNet.Models.SmsDirection))] + [JsonSerializable(typeof(ZoomNet.Models.SmsHistoryParticipant))] + [JsonSerializable(typeof(ZoomNet.Models.SmsMessage))] + [JsonSerializable(typeof(ZoomNet.Models.SmsMessageType))] + [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwner))] + [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwnerType))] [JsonSerializable(typeof(ZoomNet.Models.Survey))] [JsonSerializable(typeof(ZoomNet.Models.SurveyDetails))] [JsonSerializable(typeof(ZoomNet.Models.SurveyQuestion))] diff --git a/Source/ZoomNet/Models/SmsAttachment.cs b/Source/ZoomNet/Models/SmsAttachment.cs new file mode 100644 index 00000000..a50e7662 --- /dev/null +++ b/Source/ZoomNet/Models/SmsAttachment.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// SMS attachment file information. + /// + public class SmsAttachment + { + /// + /// Gets or sets the download link for the media file. + /// + [JsonPropertyName("download_url")] + public string DownloadUrl { get; set; } + + /// + /// Gets or sets the media file ID. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the file name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the file size. + /// + [JsonPropertyName("size")] + public int Size { get; set; } + + /// + /// Gets or sets the file type. + /// + [JsonPropertyName("type")] + public SmsAttachmentType SmsAttachmentType { get; set; } + } +} diff --git a/Source/ZoomNet/Models/SmsAttachmentType.cs b/Source/ZoomNet/Models/SmsAttachmentType.cs new file mode 100644 index 00000000..b923a441 --- /dev/null +++ b/Source/ZoomNet/Models/SmsAttachmentType.cs @@ -0,0 +1,46 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Allowed SMS attachment types. + /// + public enum SmsAttachmentType + { + /// + /// OTHER. + /// + [EnumMember(Value = "OTHER")] + OTHER, + + /// + /// PNG. + /// + [EnumMember(Value = "PNG")] + PNG, + + /// + /// GIF. + /// + [EnumMember(Value = "GIF")] + GIF, + + /// + /// JPG. + /// + [EnumMember(Value = "JPG")] + JPG, + + /// + /// AUDIO. + /// + [EnumMember(Value = "AUDIO")] + AUDIO, + + /// + /// VIDEO. + /// + [EnumMember(Value = "VIDEO")] + VIDEO + } +} diff --git a/Source/ZoomNet/Models/SmsDirection.cs b/Source/ZoomNet/Models/SmsDirection.cs new file mode 100644 index 00000000..84724fbe --- /dev/null +++ b/Source/ZoomNet/Models/SmsDirection.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Direction of SMS. + /// + public enum SmsDirection + { + /// + /// Inbound SMS. + /// + [EnumMember(Value = "in")] + Inbound, + + /// + /// Outbound SMS. + /// + [EnumMember(Value = "out")] + Outbound + } +} diff --git a/Source/ZoomNet/Models/SmsHistoryParticipant.cs b/Source/ZoomNet/Models/SmsHistoryParticipant.cs new file mode 100644 index 00000000..44538e3b --- /dev/null +++ b/Source/ZoomNet/Models/SmsHistoryParticipant.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// The user who sent it/who received the SMS. + /// + public class SmsHistoryParticipant + { + /// + /// Gets or sets sender's/receiver's name. + /// + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } + + /// + /// Gets or sets sender's/receiver's phone number. + /// + [JsonPropertyName("phone_number")] + public string PhoneNumber { get; set; } + + /// + /// Gets or sets owner. + /// + [JsonPropertyName("owner")] + public SmsParticipantOwner Owner { get; set; } + } +} diff --git a/Source/ZoomNet/Models/SmsMessage.cs b/Source/ZoomNet/Models/SmsMessage.cs new file mode 100644 index 00000000..b98d0f83 --- /dev/null +++ b/Source/ZoomNet/Models/SmsMessage.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// SMS message information. + /// + public class SmsMessage + { + /// + /// Gets or sets the SMS attachment array. + /// + [JsonPropertyName("attachments")] + public SmsAttachment[] Attachments { get; set; } + + /// + /// Gets or sets the UTC time when the message was created. + /// + [JsonPropertyName("date_time")] + public DateTime CreationTime { get; set; } + + /// + /// Gets or sets the SMS direction. + /// + [JsonPropertyName("direction")] + public SmsDirection Direction { get; set; } + + /// + /// Gets or sets the SMS text contents. + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// Gets or sets the SMS message ID. + /// + [JsonPropertyName("message_id")] + public string MessageId { get; set; } + + /// + /// Gets or sets the SMS message type. + /// + [JsonPropertyName("message_type")] + public SmsMessageType MessageType { get; set; } + + /// + /// Gets or sets the SMS sender. + /// + [JsonPropertyName("sender")] + public SmsHistoryParticipant Sender { get; set; } + + /// + /// Gets or sets the SMS receivers. + /// + [JsonPropertyName("to_members")] + public SmsHistoryParticipant[] ToMembers { get; set; } + } +} diff --git a/Source/ZoomNet/Models/SmsMessageType.cs b/Source/ZoomNet/Models/SmsMessageType.cs new file mode 100644 index 00000000..7ef41b3d --- /dev/null +++ b/Source/ZoomNet/Models/SmsMessageType.cs @@ -0,0 +1,38 @@ +namespace ZoomNet.Models +{ + /// + /// SMS message types. + /// + public enum SmsMessageType + { + /// + /// SMS. + /// + Sms = 1, + + /// + /// MMS. + /// + Mms = 2, + + /// + /// Group SMS. + /// + GroupSms = 3, + + /// + /// Group MMS. + /// + GroupMms = 4, + + /// + /// International SMS. + /// + InternationalSms = 5, + + /// + /// MSG_ON_NET. + /// + MsgOnNet = 6 + } +} diff --git a/Source/ZoomNet/Models/SmsParticipantOwner.cs b/Source/ZoomNet/Models/SmsParticipantOwner.cs new file mode 100644 index 00000000..0999da72 --- /dev/null +++ b/Source/ZoomNet/Models/SmsParticipantOwner.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// SMS history owner. + /// + public class SmsParticipantOwner + { + /// + /// Gets or sets the owner ID. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the owner type. + /// + [JsonPropertyName("type")] + public SmsParticipantOwnerType Type { get; set; } + } +} diff --git a/Source/ZoomNet/Models/SmsParticipantOwnerType.cs b/Source/ZoomNet/Models/SmsParticipantOwnerType.cs new file mode 100644 index 00000000..23e7de96 --- /dev/null +++ b/Source/ZoomNet/Models/SmsParticipantOwnerType.cs @@ -0,0 +1,34 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// SMS history owner type. + /// + public enum SmsParticipantOwnerType + { + /// + /// user. + /// + [EnumMember(Value = "user")] + User, + + /// + /// callQueue. + /// + [EnumMember(Value = "callQueue")] + CallQueue, + + /// + /// autoReceptionist. + /// + [EnumMember(Value = "autoReceptionist")] + AutoReceptionist, + + /// + /// sharedLineGroup. + /// + [EnumMember(Value = "sharedLineGroup")] + SharedLineGroup + } +} diff --git a/Source/ZoomNet/Resources/ISms.cs b/Source/ZoomNet/Resources/ISms.cs new file mode 100644 index 00000000..97812626 --- /dev/null +++ b/Source/ZoomNet/Resources/ISms.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to access Zoom Phone SMS API endpoints. + /// + /// + /// See + /// Zoom API documentation for more information. + /// + public interface ISms + { + /// + /// Get details about an SMS session. + /// + /// SMS session ID. + /// The start time and date. The date range defined by the and parameters should be a month as the response only includes one month's worth of data at once. If unspecified, returns data from the past 30 days. + /// Required only when the from parameter is specified. + /// Order of SMS to return based on creation time. True - ascending, false - descending, null - doesn't sort. + /// The number of records returned within a single API call. Max value 100. + /// The page identifier of returned records. + /// The cancellation token. + /// An array of sms histories. + /// + /// See + /// Zoom endpoint documentation for more information. + /// + Task> GetSmsSessionDetailsAsync( + string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); + } +} diff --git a/Source/ZoomNet/Resources/Sms.cs b/Source/ZoomNet/Resources/Sms.cs new file mode 100644 index 00000000..572f9784 --- /dev/null +++ b/Source/ZoomNet/Resources/Sms.cs @@ -0,0 +1,55 @@ +using Pathoschild.Http.Client; +using System; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + public class Sms : ISms + { + #region private fields + + private readonly IClient _client; + + #endregion + + #region constructor + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + internal Sms(IClient client) + { + _client = client; + } + + #endregion + + #region SMS endpoints + + /// + public Task> GetSmsSessionDetailsAsync( + string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) + { + if (recordsPerPage < 1 || recordsPerPage > 100) + { + throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 100"); + } + + return _client + .GetAsync($"phone/sms/sessions/{sessionId}") + .WithArgument("sort", orderAscending.HasValue ? orderAscending.Value ? 1 : 2 : null) + .WithArgument("from", from?.ToZoomFormat(dateOnly: false)) + .WithArgument("to", to?.ToZoomFormat(dateOnly: false)) + .WithArgument("page_size", recordsPerPage) + .WithArgument("next_page_token", pagingToken) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponseWithToken("sms_histories"); + } + + #endregion + } +} From 447d10459efcd6691991a262e9426c7d92c71ec2 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 10:29:34 -0400 Subject: [PATCH 02/19] (GH-336) Rename a few properties to more closely match our naming convention --- .../Models/SmsSessionTests.cs | 24 +++++++++---------- Source/ZoomNet/Models/SmsAttachment.cs | 2 +- Source/ZoomNet/Models/SmsAttachmentType.cs | 24 +++++++++---------- Source/ZoomNet/Models/SmsMessage.cs | 6 ++--- Source/ZoomNet/Resources/ISms.cs | 3 +-- Source/ZoomNet/Resources/Sms.cs | 12 ---------- 6 files changed, 29 insertions(+), 42 deletions(-) diff --git a/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs b/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs index f2a160e9..a2421b5e 100644 --- a/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs +++ b/Source/ZoomNet.UnitTests/Models/SmsSessionTests.cs @@ -1,6 +1,6 @@ +using Shouldly; using System; using System.Text.Json; -using Shouldly; using Xunit; using ZoomNet.Json; using ZoomNet.Models; @@ -74,31 +74,31 @@ public void Parse_Json_SmsHistory() result.Attachments[0].Id.ShouldBe("x18dcVWxTcCzbp4zr2AT3A"); result.Attachments[0].Name.ShouldBe("FWDHOMaNRaqIvNc3aIdisg.jpg"); result.Attachments[0].Size.ShouldBe(225740); - result.Attachments[0].SmsAttachmentType.ShouldBe(SmsAttachmentType.JPG); + result.Attachments[0].Type.ShouldBe(SmsAttachmentType.Jpg); result.Attachments[1].DownloadUrl.ShouldBe("https://exampleurl.us/file/download/TZkODE4NzQ2MDNmN2UzZWM?jwt=rGJXbGciOiJIUzI1NiJ9.eyJpc3MiOiJjcm9zc2ZpbGUiLCJhdWQiOiJmaWxlIiwiZGlnIjASf7fsJsfhl88jf02fLgyuM4OThkNDMxM2IxNjNhNTQ4NGI4MjkxMTA0ZmQyYzc4MTg1NmY0MGUxY2FlOTI3YyIsImV4cCI6MTY0ODE5NDA1NH0.eCURcan9QOOw9wvBdSn_-TBzgT5HWBzp04IfsK19Oti"); result.Attachments[1].Id.ShouldBe("TZkODE4NzQ2MDNmN2UzZWM"); result.Attachments[1].Name.ShouldBe("ASf7fsJsfhl88jf02fLgyu.mp3"); result.Attachments[1].Size.ShouldBe(303890); - result.Attachments[1].SmsAttachmentType.ShouldBe(SmsAttachmentType.AUDIO); + result.Attachments[1].Type.ShouldBe(SmsAttachmentType.Audio); - result.CreationTime.ShouldBe(new DateTime(2024, 3, 23, 2, 58, 1, DateTimeKind.Utc)); + result.CreatedOn.ShouldBe(new DateTime(2024, 3, 23, 2, 58, 1, DateTimeKind.Utc)); result.Direction.ShouldBe(SmsDirection.Inbound); result.Message.ShouldBe("welcome"); result.MessageId.ShouldBe("IQ-cRH5P5EiTWCwpNzScnECJw"); - result.MessageType.ShouldBe(SmsMessageType.Mms); + result.Type.ShouldBe(SmsMessageType.Mms); result.Sender.ShouldNotBeNull(); result.Sender.PhoneNumber.ShouldBe("18108001001"); result.Sender.DisplayName.ShouldBe("test api"); result.Sender.Owner.ShouldNotBeNull(); result.Sender.Owner.Id.ShouldBe("DnEopNmXQEGU2uvvzjgojw"); result.Sender.Owner.Type.ShouldBe(SmsParticipantOwnerType.User); - result.ToMembers.ShouldNotBeNull(); - result.ToMembers.Length.ShouldBe(1); - result.ToMembers[0].DisplayName.ShouldBe("ezreal mao"); - result.ToMembers[0].PhoneNumber.ShouldBe("12092693625"); - result.ToMembers[0].Owner.ShouldNotBeNull(); - result.ToMembers[0].Owner.Id.ShouldBe("WeD59Hn7SvqNRB9jcxz5NQ"); - result.ToMembers[0].Owner.Type.ShouldBe(SmsParticipantOwnerType.User); + result.Recipients.ShouldNotBeNull(); + result.Recipients.Length.ShouldBe(1); + result.Recipients[0].DisplayName.ShouldBe("ezreal mao"); + result.Recipients[0].PhoneNumber.ShouldBe("12092693625"); + result.Recipients[0].Owner.ShouldNotBeNull(); + result.Recipients[0].Owner.Id.ShouldBe("WeD59Hn7SvqNRB9jcxz5NQ"); + result.Recipients[0].Owner.Type.ShouldBe(SmsParticipantOwnerType.User); } #endregion diff --git a/Source/ZoomNet/Models/SmsAttachment.cs b/Source/ZoomNet/Models/SmsAttachment.cs index a50e7662..e562c1f7 100644 --- a/Source/ZoomNet/Models/SmsAttachment.cs +++ b/Source/ZoomNet/Models/SmsAttachment.cs @@ -35,6 +35,6 @@ public class SmsAttachment /// Gets or sets the file type. /// [JsonPropertyName("type")] - public SmsAttachmentType SmsAttachmentType { get; set; } + public SmsAttachmentType Type { get; set; } } } diff --git a/Source/ZoomNet/Models/SmsAttachmentType.cs b/Source/ZoomNet/Models/SmsAttachmentType.cs index b923a441..d471b963 100644 --- a/Source/ZoomNet/Models/SmsAttachmentType.cs +++ b/Source/ZoomNet/Models/SmsAttachmentType.cs @@ -8,39 +8,39 @@ namespace ZoomNet.Models public enum SmsAttachmentType { /// - /// OTHER. + /// Other. /// [EnumMember(Value = "OTHER")] - OTHER, + Other, /// - /// PNG. + /// Png. /// [EnumMember(Value = "PNG")] - PNG, + Png, /// - /// GIF. + /// Gif. /// [EnumMember(Value = "GIF")] - GIF, + Gif, /// - /// JPG. + /// Jpg. /// [EnumMember(Value = "JPG")] - JPG, + Jpg, /// - /// AUDIO. + /// Audio. /// [EnumMember(Value = "AUDIO")] - AUDIO, + Audio, /// - /// VIDEO. + /// Video. /// [EnumMember(Value = "VIDEO")] - VIDEO + Video } } diff --git a/Source/ZoomNet/Models/SmsMessage.cs b/Source/ZoomNet/Models/SmsMessage.cs index b98d0f83..05a96e90 100644 --- a/Source/ZoomNet/Models/SmsMessage.cs +++ b/Source/ZoomNet/Models/SmsMessage.cs @@ -18,7 +18,7 @@ public class SmsMessage /// Gets or sets the UTC time when the message was created. /// [JsonPropertyName("date_time")] - public DateTime CreationTime { get; set; } + public DateTime CreatedOn { get; set; } /// /// Gets or sets the SMS direction. @@ -42,7 +42,7 @@ public class SmsMessage /// Gets or sets the SMS message type. /// [JsonPropertyName("message_type")] - public SmsMessageType MessageType { get; set; } + public SmsMessageType Type { get; set; } /// /// Gets or sets the SMS sender. @@ -54,6 +54,6 @@ public class SmsMessage /// Gets or sets the SMS receivers. /// [JsonPropertyName("to_members")] - public SmsHistoryParticipant[] ToMembers { get; set; } + public SmsHistoryParticipant[] Recipients { get; set; } } } diff --git a/Source/ZoomNet/Resources/ISms.cs b/Source/ZoomNet/Resources/ISms.cs index 97812626..2b266398 100644 --- a/Source/ZoomNet/Resources/ISms.cs +++ b/Source/ZoomNet/Resources/ISms.cs @@ -29,7 +29,6 @@ public interface ISms /// See /// Zoom endpoint documentation for more information. /// - Task> GetSmsSessionDetailsAsync( - string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); + Task> GetSmsSessionDetailsAsync(string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/Sms.cs b/Source/ZoomNet/Resources/Sms.cs index 572f9784..85c4ddbe 100644 --- a/Source/ZoomNet/Resources/Sms.cs +++ b/Source/ZoomNet/Resources/Sms.cs @@ -9,14 +9,8 @@ namespace ZoomNet.Resources /// public class Sms : ISms { - #region private fields - private readonly IClient _client; - #endregion - - #region constructor - /// /// Initializes a new instance of the class. /// @@ -26,10 +20,6 @@ internal Sms(IClient client) _client = client; } - #endregion - - #region SMS endpoints - /// public Task> GetSmsSessionDetailsAsync( string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) @@ -49,7 +39,5 @@ public Task> GetSmsSessionDetailsAsync( .WithCancellationToken(cancellationToken) .AsPaginatedResponseWithToken("sms_histories"); } - - #endregion } } From c81db02d140a9902ff4aa902a2494221646c3302 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 10:32:10 -0400 Subject: [PATCH 03/19] Fix parameters out of order --- Source/ZoomNet/Resources/Webinars.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index 2e15e4af..8624659d 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -953,7 +953,7 @@ public Task GetLiveStreamSettingsAsync(long webinarId, Ca /// public Task CreateInviteLinksAsync(long webinarId, IEnumerable names, long timeToLive = 7200, CancellationToken cancellationToken = default) { - if (names == null || !names.Any()) throw new ArgumentNullException("You must provide at least one name", nameof(names)); + if (names == null || !names.Any()) throw new ArgumentNullException(nameof(names), "You must provide at least one name"); var data = new JsonObject { From d192ff5e160999cd5287042c4c9f4c8b66f590c7 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 10:34:36 -0400 Subject: [PATCH 04/19] Collection initialization can be simplified --- Source/ZoomNet/Resources/Webinars.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index 8624659d..1a4a5bcd 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -346,7 +346,7 @@ public Task GetPanelistsAsync(long webinarId, CancellationToken canc /// public Task AddPanelistAsync(long webinarId, string email, string fullName, string virtualBackgroundId = null, CancellationToken cancellationToken = default) { - return AddPanelistsAsync(webinarId, new (string Email, string FullName, string VirtualBackgroundId)[] { (email, fullName, virtualBackgroundId) }, cancellationToken); + return AddPanelistsAsync(webinarId, [(email, fullName, virtualBackgroundId)], cancellationToken); } /// From 40d885943cc18cfb991bd6199db70718583cbe79 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 10:37:07 -0400 Subject: [PATCH 05/19] Sync serializer context with model classes --- .../Json/ZoomNetJsonSerializerContext.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index 89494d15..418aaf6d 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -22,6 +22,8 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.BatchRegistrant))] [JsonSerializable(typeof(ZoomNet.Models.BatchRegistrantInfo))] [JsonSerializable(typeof(ZoomNet.Models.CalendarIntegrationType))] + [JsonSerializable(typeof(ZoomNet.Models.CallingPlan))] + [JsonSerializable(typeof(ZoomNet.Models.CallingPlanType))] [JsonSerializable(typeof(ZoomNet.Models.CallLog))] [JsonSerializable(typeof(ZoomNet.Models.CallLogCalleeNumberType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogCallerNumberType))] @@ -73,6 +75,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ConcurrentMeetingType))] [JsonSerializable(typeof(ZoomNet.Models.Contact))] [JsonSerializable(typeof(ZoomNet.Models.ContactType))] + [JsonSerializable(typeof(ZoomNet.Models.ContinuousMeetingChatSettings))] [JsonSerializable(typeof(ZoomNet.Models.Country))] [JsonSerializable(typeof(ZoomNet.Models.CrcPortMetrics))] [JsonSerializable(typeof(ZoomNet.Models.CrcPortsHourUsage))] @@ -132,6 +135,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PastInstance))] [JsonSerializable(typeof(ZoomNet.Models.PastMeeting))] [JsonSerializable(typeof(ZoomNet.Models.PayMode))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallPhoneNumber))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecording))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCalleeNumberType))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCallerNumberType))] @@ -144,6 +148,8 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscriptTimelineUser))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingType))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUser))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserProfile))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType))] @@ -202,7 +208,6 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails))] [JsonSerializable(typeof(ZoomNet.Models.SecuritySettings))] [JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail))] - [JsonSerializable(typeof(ZoomNet.Models.StreamingService))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachment))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType))] [JsonSerializable(typeof(ZoomNet.Models.SmsDirection))] @@ -211,6 +216,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.SmsMessageType))] [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwner))] [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwnerType))] + [JsonSerializable(typeof(ZoomNet.Models.StreamingService))] [JsonSerializable(typeof(ZoomNet.Models.Survey))] [JsonSerializable(typeof(ZoomNet.Models.SurveyDetails))] [JsonSerializable(typeof(ZoomNet.Models.SurveyQuestion))] @@ -365,6 +371,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ConcurrentMeetingType[]))] [JsonSerializable(typeof(ZoomNet.Models.Contact[]))] [JsonSerializable(typeof(ZoomNet.Models.ContactType[]))] + [JsonSerializable(typeof(ZoomNet.Models.ContinuousMeetingChatSettings[]))] [JsonSerializable(typeof(ZoomNet.Models.Country[]))] [JsonSerializable(typeof(ZoomNet.Models.CrcPortMetrics[]))] [JsonSerializable(typeof(ZoomNet.Models.CrcPortsHourUsage[]))] @@ -380,6 +387,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.DashboardParticipantQos[]))] [JsonSerializable(typeof(ZoomNet.Models.DataCenterRegion[]))] [JsonSerializable(typeof(ZoomNet.Models.EmailNotificationUserSettings[]))] + [JsonSerializable(typeof(ZoomNet.Models.EmergencyAddress[]))] [JsonSerializable(typeof(ZoomNet.Models.EncryptionType[]))] [JsonSerializable(typeof(ZoomNet.Models.FeatureUserSettings[]))] [JsonSerializable(typeof(ZoomNet.Models.ImMetric[]))] @@ -423,6 +431,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PastInstance[]))] [JsonSerializable(typeof(ZoomNet.Models.PastMeeting[]))] [JsonSerializable(typeof(ZoomNet.Models.PayMode[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallPhoneNumber[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecording[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCalleeNumberType[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCallerNumberType[]))] @@ -437,7 +446,6 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUser[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserProfile[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus[]))] - [JsonSerializable(typeof(ZoomNet.Models.PhoneCallPhoneNumber[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType[]))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType[]))] @@ -496,6 +504,14 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails[]))] [JsonSerializable(typeof(ZoomNet.Models.SecuritySettings[]))] [JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsAttachment[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsDirection[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsHistoryParticipant[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsMessage[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsMessageType[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwner[]))] + [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwnerType[]))] [JsonSerializable(typeof(ZoomNet.Models.StreamingService[]))] [JsonSerializable(typeof(ZoomNet.Models.Survey[]))] [JsonSerializable(typeof(ZoomNet.Models.SurveyDetails[]))] @@ -588,6 +604,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.AuthenticationType?))] [JsonSerializable(typeof(ZoomNet.Models.AutoRecordingType?))] [JsonSerializable(typeof(ZoomNet.Models.CalendarIntegrationType?))] + [JsonSerializable(typeof(ZoomNet.Models.CallingPlanType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogCalleeNumberType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogCallerNumberType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogCallType?))] @@ -636,6 +653,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerExtensionStatus?))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerType?))] [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingType?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus?))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType?))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType?))] [JsonSerializable(typeof(ZoomNet.Models.PollQuestionType?))] @@ -661,6 +679,10 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ReportMeetingType?))] [JsonSerializable(typeof(ZoomNet.Models.RoleInPurchaseProcess?))] [JsonSerializable(typeof(ZoomNet.Models.ScreenshareContentType?))] + [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType?))] + [JsonSerializable(typeof(ZoomNet.Models.SmsDirection?))] + [JsonSerializable(typeof(ZoomNet.Models.SmsMessageType?))] + [JsonSerializable(typeof(ZoomNet.Models.SmsParticipantOwnerType?))] [JsonSerializable(typeof(ZoomNet.Models.StreamingService?))] [JsonSerializable(typeof(ZoomNet.Models.SurveyQuestionType?))] [JsonSerializable(typeof(ZoomNet.Models.TimeZones?))] From 0fa91714803279a21c3b763a1255812501e02de1 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 11:06:23 -0400 Subject: [PATCH 06/19] Remove duplicated XML comments --- Source/ZoomNet/Resources/Chat.cs | 217 +++---------------------------- 1 file changed, 20 insertions(+), 197 deletions(-) diff --git a/Source/ZoomNet/Resources/Chat.cs b/Source/ZoomNet/Resources/Chat.cs index 379764bf..d0d1017c 100644 --- a/Source/ZoomNet/Resources/Chat.cs +++ b/Source/ZoomNet/Resources/Chat.cs @@ -12,13 +12,7 @@ namespace ZoomNet.Resources { - /// - /// Allows you to manage sub accounts under the master account. - /// - /// - /// - /// See Zoom documentation for more information. - /// + /// public class Chat : IChat { private readonly Pathoschild.Http.Client.IClient _client; @@ -32,16 +26,7 @@ internal Chat(Pathoschild.Http.Client.IClient client) _client = client; } - /// - /// Retrieve a user's chat channels. - /// - /// The user Id or email address. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of channels. - /// + /// public Task> GetAccountChannelsForUserAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) @@ -57,15 +42,7 @@ public Task> GetAccountChannelsForUserAs .AsPaginatedResponseWithToken("channels"); } - /// - /// Retrieve information about a specific chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The cancellation token. - /// - /// A . - /// + /// public Task GetAccountChannelAsync(string userId, string channelId, CancellationToken cancellationToken = default) { return _client @@ -74,18 +51,7 @@ public Task GetAccountChannelAsync(string userId, string channelId, .AsObject(); } - /// - /// Create a chat channel that allows users to communicate via chat in private or public groups. - /// - /// The user Id or email address. - /// The name of the channel. - /// The type of channel. - /// The email addresses of the members to include in the channel. - /// The cancellation token. - /// Zoom allows a maximum of 5 members to be added at once. - /// - /// The new . - /// + /// public Task CreateAccountChannelAsync(string userId, string name, ChatChannelType type, IEnumerable emails = null, CancellationToken cancellationToken = default) { if (emails != null && emails.Count() > 5) throw new ArgumentOutOfRangeException(nameof(emails), "You can invite up to 5 members at once"); @@ -104,16 +70,7 @@ public Task CreateAccountChannelAsync(string userId, string name, C .AsObject(); } - /// - /// Update a chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The name of the channel. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task UpdateAccountChannelAsync(string userId, string channelId, string name, CancellationToken cancellationToken = default) { var data = new JsonObject @@ -128,15 +85,7 @@ public Task UpdateAccountChannelAsync(string userId, string channelId, string na .AsMessage(); } - /// - /// Delete a chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task DeleteAccountChannelAsync(string userId, string channelId, CancellationToken cancellationToken = default) { return _client @@ -145,17 +94,7 @@ public Task DeleteAccountChannelAsync(string userId, string channelId, Cancellat .AsMessage(); } - /// - /// Retrieve the members of a chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of channel members. - /// + /// public Task> GetAccountChannelMembersAsync(string userId, string channelId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) @@ -171,17 +110,7 @@ public Task> GetAccountChannelMemb .AsPaginatedResponseWithToken("members"); } - /// - /// Invite members to join a chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The email addresses of the members to include in the channel. - /// The cancellation token. - /// Zoom allows a maximum of 5 members to be added at once. - /// - /// An array containing the unique identifiers of the new members. - /// + /// public Task InviteMembersToAccountChannelAsync(string userId, string channelId, IEnumerable emails, CancellationToken cancellationToken = default) { if (emails == null || !emails.Any()) throw new ArgumentNullException(nameof(emails), "You must specify at least one member to invite"); @@ -199,16 +128,7 @@ public Task InviteMembersToAccountChannelAsync(string userId, string c .AsObject("ids"); } - /// - /// Remove a member from a chat channel. - /// - /// The user Id or email address. - /// The channel Id. - /// The member Id. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task RemoveMemberFromAccountChannelAsync(string userId, string channelId, string memberId, CancellationToken cancellationToken = default) { return _client @@ -217,14 +137,7 @@ public Task RemoveMemberFromAccountChannelAsync(string userId, string channelId, .AsMessage(); } - /// - /// Retrieve information about a specific chat channel. - /// - /// The channel Id. - /// The cancellation token. - /// - /// A . - /// + /// public Task GetChannelAsync(string channelId, CancellationToken cancellationToken = default) { return _client @@ -233,15 +146,7 @@ public Task GetChannelAsync(string channelId, CancellationToken can .AsObject(); } - /// - /// Update a chat channel. - /// - /// The channel Id. - /// The name of the channel. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task UpdateChannelAsync(string channelId, string name, CancellationToken cancellationToken = default) { var data = new JsonObject @@ -256,14 +161,7 @@ public Task UpdateChannelAsync(string channelId, string name, CancellationToken .AsMessage(); } - /// - /// Delete a chat channel. - /// - /// The channel Id. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task DeleteChannelAsync(string channelId, CancellationToken cancellationToken = default) { return _client @@ -272,15 +170,7 @@ public Task DeleteChannelAsync(string channelId, CancellationToken cancellationT .AsMessage(); } - /// - /// Remove a member from a chat channel. - /// - /// The channel Id. - /// The member Id. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task RemoveMemberFromChannelAsync(string channelId, string memberId, CancellationToken cancellationToken = default) { return _client @@ -289,14 +179,7 @@ public Task RemoveMemberFromChannelAsync(string channelId, string memberId, Canc .AsMessage(); } - /// - /// Join a chat channel. - /// - /// The channel Id. - /// The cancellation token. - /// - /// The member Id. - /// + /// public Task JoinChannelAsync(string channelId, CancellationToken cancellationToken = default) { return _client @@ -317,97 +200,37 @@ public Task SendMessageToChannelAsync(string userId, string channelId, s return SendMessageAsync(userId, null, channelId, message, replyMessageId, fileIds, mentions, cancellationToken); } - /// - /// Retrieve the chat messages sent/received to/from a contact. - /// - /// The user Id or email address. - /// The email address of the contact to whom the user conversed. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of chat messages. - /// + /// public Task> GetMessagesToContactAsync(string userId, string recipientEmail, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { return GetMessagesAsync(userId, recipientEmail, null, recordsPerPage, pagingToken, cancellationToken); } - /// - /// Retrieve the chat messages sent/received in a channel of which the user is a member. - /// - /// The user Id or email address. - /// The channel Id. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of chat messages. - /// + /// public Task> GetMessagesToChannelAsync(string userId, string channelId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { return GetMessagesAsync(userId, null, channelId, recordsPerPage, pagingToken, cancellationToken); } - /// - /// Update a message that was previously sent to a user on on the sender's contact list. - /// - /// The unique identifier of the message. - /// The unique identifier of the sender. - /// The email address of the contact to whom you would like to send the message. - /// The message. - /// Mentions. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task UpdateMessageToContactAsync(string messageId, string userId, string recipientEmail, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) { return UpdateMessageAsync(messageId, userId, recipientEmail, null, message, mentions, cancellationToken); } - /// - /// Update a message that was previously sent to a channel of which the sender is a member. - /// - /// The unique identifier of the message. - /// The unique identifier of the sender. - /// The channel Id. - /// The message. - /// Mentions. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task UpdateMessageToChannelAsync(string messageId, string userId, string channelId, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) { return UpdateMessageAsync(messageId, userId, null, channelId, message, mentions, cancellationToken); } - /// - /// Delete a message that was previously sent to a user on on the sender's contact list. - /// - /// The unique identifier of the message. - /// The unique identifier of the sender. - /// The email address of the contact to whom you would like to send the message. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task DeleteMessageToContactAsync(string messageId, string userId, string recipientEmail, CancellationToken cancellationToken = default) { return DeleteMessageAsync(messageId, userId, recipientEmail, null, cancellationToken); } - /// - /// Delete a message that was previously sent to a channel of which the sender is a member. - /// - /// The unique identifier of the message. - /// The unique identifier of the sender. - /// The channel Id. - /// The cancellation token. - /// - /// The async task. - /// + /// public Task DeleteMessageToChannelAsync(string messageId, string userId, string channelId, CancellationToken cancellationToken = default) { return DeleteMessageAsync(messageId, userId, null, channelId, cancellationToken); From 9272b518ab47c26bb483af4aedf2fa2c0042b40e Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 13:06:42 -0400 Subject: [PATCH 07/19] (GH-337) Ensure that we fetch the channel before we attempt to update it because we need to have its settings --- Source/ZoomNet.IntegrationTests/Tests/Chat.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs index 3a5eb970..c40ad337 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs @@ -34,14 +34,14 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie var channel = await client.Chat.CreateAccountChannelAsync(myUser.Id, "ZoomNet Integration Testing: new channel", ChatChannelType.Public, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Account channel \"{channel.Name}\" created (Id={channel.Id}").ConfigureAwait(false); - // UPDATE THE CHANNEL - await client.Chat.UpdateAccountChannelAsync(myUser.Id, channel.Id, "ZoomNet Integration Testing: updated channel", cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"Account channel \"{channel.Id}\" updated").ConfigureAwait(false); - // RETRIEVE THE CHANNEL channel = await client.Chat.GetAccountChannelAsync(myUser.Id, channel.Id, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Account channel \"{channel.Id}\" retrieved").ConfigureAwait(false); + // UPDATE THE CHANNEL + await client.Chat.UpdateAccountChannelAsync(myUser.Id, channel.Id, "ZoomNet Integration Testing: updated channel", channel.Settings, channel.Type, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Account channel \"{channel.Id}\" updated").ConfigureAwait(false); + // RETRIEVE THE CHANNEL MEMBERS var paginatedMembers = await client.Chat.GetAccountChannelMembersAsync(myUser.Id, channel.Id, 10, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Account channel \"{channel.Id}\" has {paginatedMembers.TotalRecords} members").ConfigureAwait(false); From 6f1a8da80fa47a4b21e4aa19df16ae8d9e5f3464 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 16:45:20 -0400 Subject: [PATCH 08/19] Structured logs Resolves #338 --- Source/ZoomNet/Utilities/DiagnosticHandler.cs | 242 +++++++++++------- Source/ZoomNet/Utilities/ZoomErrorHandler.cs | 5 +- 2 files changed, 145 insertions(+), 102 deletions(-) diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index 6ec357af..fb2da82c 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -4,11 +4,12 @@ using Pathoschild.Http.Client.Extensibility; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; +using System.Text.RegularExpressions; namespace ZoomNet.Utilities { @@ -22,19 +23,143 @@ internal class DiagnosticInfo { public WeakReference RequestReference { get; set; } - public string Diagnostic { get; set; } - public long RequestTimestamp { get; set; } + public WeakReference ResponseReference { get; set; } + public long ResponseTimestamp { get; set; } - public DiagnosticInfo(WeakReference requestReference, string diagnostic, long requestTimestamp, long responseTimestamp) + public DiagnosticInfo(WeakReference requestReference, long requestTimestamp, WeakReference responseReference, long responseTimestamp) { RequestReference = requestReference; - Diagnostic = diagnostic; RequestTimestamp = requestTimestamp; + ResponseReference = responseReference; ResponseTimestamp = responseTimestamp; } + + public string GetLoggingTemplate() + { + RequestReference.TryGetTarget(out HttpRequestMessage request); + ResponseReference.TryGetTarget(out HttpResponseMessage response); + + var logTemplate = new StringBuilder(); + + if (request != null) + { + logTemplate.AppendLine("REQUEST SENT BY ZOOMNET: {Request_HttpMethod} {Request_Uri} HTTP/{Request_HttpVersion}"); + logTemplate.AppendLine("REQUEST HEADERS:"); + + var requestHeaders = response?.RequestMessage?.Headers ?? Enumerable.Empty>>(); + if (!requestHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))) + { + requestHeaders = requestHeaders.Append(new KeyValuePair>("Content-Length", new[] { "0" })); + } + + foreach (var header in requestHeaders.OrderBy(kvp => kvp.Key)) + { + logTemplate.AppendLine(" " + header.Key + ": {Request_Header_" + header.Key + "}"); + } + + logTemplate.AppendLine("REQUEST: {Request_Content}"); + logTemplate.AppendLine(); + } + + if (response != null) + { + logTemplate.AppendLine("RESPONSE FROM ZOOM: HTTP/{Response_HttpVersion} {Response_StatusCode} {Response_ReasonPhrase}"); + logTemplate.AppendLine("RESPONSE HEADERS:"); + + var responseHeaders = response?.Headers ?? Enumerable.Empty>>(); + if (!responseHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))) + { + responseHeaders = responseHeaders.Append(new KeyValuePair>("Content-Length", new[] { "0" })); + } + + foreach (var header in responseHeaders.OrderBy(kvp => kvp.Key)) + { + logTemplate.AppendLine(" " + header.Key + ": {Response_Header_" + header.Key + "}"); + } + + logTemplate.AppendLine("RESPONSE: {Response_Content}"); + logTemplate.AppendLine(); + } + + logTemplate.AppendLine("DIAGNOSTIC: The request took {Diagnostic_Elapsed:N} milliseconds"); + + return logTemplate.ToString(); + } + + public object[] GetLoggingParameters() + { + RequestReference.TryGetTarget(out HttpRequestMessage request); + ResponseReference.TryGetTarget(out HttpResponseMessage response); + + // Get the content to the request/response and calculate how long it took to get the response + var elapsed = TimeSpan.FromTicks(ResponseTimestamp - RequestTimestamp); + var requestContent = request?.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult(); + var responseContent = response?.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult(); + + // Calculate the content size + var requestContentLength = requestContent?.Length ?? 0; + var responseContentLength = responseContent?.Length ?? 0; + + // Get the request headers + var requestHeaders = response?.RequestMessage?.Headers ?? Enumerable.Empty>>(); + if (!requestHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))) + { + requestHeaders = requestHeaders.Append(new KeyValuePair>("Content-Length", new[] { requestContentLength.ToString() })); + } + + // Get the response headers + var responseHeaders = response?.Headers ?? Enumerable.Empty>>(); + if (!responseHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))) + { + responseHeaders = responseHeaders.Append(new KeyValuePair>("Content-Length", new[] { responseContentLength.ToString() })); + } + + // The order of these values must match the order in which they appear in the logging template + var logParams = new List(); + + if (request != null) + { + logParams.AddRange([request.Method.Method, request.RequestUri, request.Version]); + logParams.AddRange(requestHeaders + .OrderBy(kvp => kvp.Key) + .Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value)) + .ToArray()); + logParams.Add(requestContent?.TrimEnd('\r', '\n') ?? ""); + } + + if (response != null) + { + logParams.AddRange([response.Version, (int)response.StatusCode, response.ReasonPhrase]); + logParams.AddRange(responseHeaders + .OrderBy(kvp => kvp.Key) + .Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value)) + .ToList()); + logParams.Add(responseContent?.TrimEnd('\r', '\n') ?? ""); + } + + logParams.Add(elapsed.TotalMilliseconds); + + return logParams.ToArray(); + } + + public string GetFormattedLog() + { + var formattedLog = GetLoggingTemplate(); + var args = GetLoggingParameters(); + + var pattern = @"(.*?{)(\w+?.+?)(}.*)"; + for (var i = 0; i < args.Length; i++) + { + formattedLog = Regex.Replace(formattedLog, pattern, $"$1 {i} $3", RegexOptions.None); + } + + formattedLog = formattedLog.Replace("{ ", "{").Replace(" }", "}"); + + return string.Format(formattedLog, args); + } } #region FIELDS @@ -73,17 +198,8 @@ public void OnRequest(IRequest request) var diagnosticId = Guid.NewGuid().ToString("N"); request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, diagnosticId); - // Log the request - var httpRequest = request.Message; - var diagnostic = new StringBuilder(); - - diagnostic.AppendLine("REQUEST:"); - diagnostic.AppendLine($" {httpRequest.Method.Method} {httpRequest.RequestUri} HTTP/{httpRequest.Version}"); - LogHeaders(diagnostic, httpRequest.Headers); - LogContent(diagnostic, httpRequest.Content); - // Add the diagnostic info to our cache - DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); + DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference(request.Message), Stopwatch.GetTimestamp(), null, long.MinValue)); } /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. @@ -92,105 +208,33 @@ public void OnRequest(IRequest request) public void OnResponse(IResponse response, bool httpErrorAsException) { var responseTimestamp = Stopwatch.GetTimestamp(); - var httpResponse = response.Message; var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME); if (DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo)) { - var updatedDiagnostic = new StringBuilder(diagnosticInfo.Diagnostic); - try + // Update the cached diagnostic info + diagnosticInfo.ResponseReference = new WeakReference(response.Message); + diagnosticInfo.ResponseTimestamp = responseTimestamp; + DiagnosticsInfo[diagnosticId] = diagnosticInfo; + + // Log + var logLevel = response.IsSuccessStatusCode ? _logLevelSuccessfulCalls : _logLevelFailedCalls; + if (_logger?.IsEnabled(logLevel) ?? false) { - // Log the response - updatedDiagnostic.AppendLine(); - updatedDiagnostic.AppendLine("RESPONSE:"); - updatedDiagnostic.AppendLine($" HTTP/{httpResponse.Version} {(int)httpResponse.StatusCode} {httpResponse.ReasonPhrase}"); - LogHeaders(updatedDiagnostic, httpResponse.Headers); - LogContent(updatedDiagnostic, httpResponse.Content); - - // Calculate how much time elapsed between request and response - var elapsed = TimeSpan.FromTicks(responseTimestamp - diagnosticInfo.RequestTimestamp); - - // Log diagnostic - updatedDiagnostic.AppendLine(); - updatedDiagnostic.AppendLine("DIAGNOSTIC:"); - updatedDiagnostic.AppendLine($" The request took {elapsed.ToDurationString()}"); - } - catch (Exception e) - { - Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURRED: {1}\r\n{0}", new string('=', 50), e.GetBaseException().Message); - updatedDiagnostic.AppendLine($"AN EXCEPTION OCCURRED: {e.GetBaseException().Message}"); + var template = diagnosticInfo.GetLoggingTemplate(); + var parameters = diagnosticInfo.GetLoggingParameters(); - if (_logger.IsEnabled(LogLevel.Error)) - { - _logger.LogError(e, "An exception occurred when inspecting the response from Zoom"); - } + _logger.Log(logLevel, template, parameters); } - finally - { - var logLevel = response.IsSuccessStatusCode ? _logLevelSuccessfulCalls : _logLevelFailedCalls; - if (_logger.IsEnabled(logLevel)) - { - _logger.Log(logLevel, updatedDiagnostic.ToString() - .Replace("{", "{{") - .Replace("}", "}}")); - } - DiagnosticsInfo.TryUpdate( - diagnosticId, - new DiagnosticInfo(diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp), - diagnosticInfo); - } + Cleanup(); } - - Cleanup(); } #endregion #region PRIVATE METHODS - private static void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders) - { - if (httpHeaders != null) - { - foreach (var header in httpHeaders) - { - if (header.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase)) - { - diagnostic.AppendLine($" {header.Key}: ...redacted for security reasons..."); - } - else - { - diagnostic.AppendLine($" {header.Key}: {string.Join(", ", header.Value)}"); - } - } - } - } - - private static void LogContent(StringBuilder diagnostic, HttpContent httpContent) - { - if (httpContent == null) - { - diagnostic.AppendLine(" Content-Length: 0"); - } - else - { - LogHeaders(diagnostic, httpContent.Headers); - - var contentLength = httpContent.Headers?.ContentLength.GetValueOrDefault(0) ?? 0; - if (!httpContent.Headers?.Contains("Content-Length") ?? false) - { - diagnostic.AppendLine($" Content-Length: {contentLength}"); - } - - if (contentLength > 0) - { - diagnostic.AppendLine(); - diagnostic.AppendLine(httpContent.ReadAsStringAsync(null).ConfigureAwait(false).GetAwaiter().GetResult() ?? ""); - } - } - } - private static void Cleanup() { try diff --git a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs index b87b275e..42697a7e 100644 --- a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs +++ b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs @@ -50,9 +50,8 @@ public void OnRequest(IRequest request) { } /// Whether HTTP error responses should be raised as exceptions. public void OnResponse(IResponse response, bool httpErrorAsException) { - var (isError, errorMessage, errorCode) = response.Message.GetErrorMessageAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - var diagnosticInfo = response.GetDiagnosticInfo(); - var diagnosticLog = diagnosticInfo.Diagnostic ?? "Diagnostic log unavailable"; + var (isError, errorMessage, errorCode) = response.Message.GetErrorMessageAsync().GetAwaiter().GetResult(); + var diagnosticLog = response.GetDiagnosticInfo()?.GetFormattedLog() ?? "Diagnostic log unavailable"; if (TreatHttp200AsException && response.Status == HttpStatusCode.OK) { From eb29152b2e44fc9f9b1d38d21f82d98821648b49 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 16:49:45 -0400 Subject: [PATCH 09/19] Use .net8 HostedApplication to run integration tests Resolves #339 --- .../ZoomNet.IntegrationTests/ConsoleUtils.cs | 43 --------------- .../ZoomNet.IntegrationTests/NativeMethods.cs | 52 ------------------- Source/ZoomNet.IntegrationTests/Program.cs | 41 ++++++++++----- Source/ZoomNet.IntegrationTests/TestSuite.cs | 22 ++------ .../TestSuites/WebSocketsTestSuite.cs | 6 +-- .../ZoomNet.IntegrationTests/TestsRunner.cs | 18 +++---- .../ZoomNet.IntegrationTests.csproj | 10 +++- 7 files changed, 50 insertions(+), 142 deletions(-) delete mode 100644 Source/ZoomNet.IntegrationTests/ConsoleUtils.cs delete mode 100644 Source/ZoomNet.IntegrationTests/NativeMethods.cs diff --git a/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs b/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs deleted file mode 100644 index b7e0668e..00000000 --- a/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace ZoomNet.IntegrationTests -{ - public static class ConsoleUtils - { - public static void CenterConsole() - { - var hWin = NativeMethods.GetConsoleWindow(); - if (hWin == IntPtr.Zero) return; - - var monitor = NativeMethods.MonitorFromWindow(hWin, NativeMethods.MONITOR_DEFAULT_TO_NEAREST); - if (monitor == IntPtr.Zero) return; - - var monitorInfo = new NativeMethods.NativeMonitorInfo(); - NativeMethods.GetMonitorInfo(monitor, monitorInfo); - - NativeMethods.GetWindowRect(hWin, out NativeMethods.NativeRectangle consoleInfo); - - var monitorWidth = monitorInfo.Monitor.Right - monitorInfo.Monitor.Left; - var monitorHeight = monitorInfo.Monitor.Bottom - monitorInfo.Monitor.Top; - - var consoleWidth = consoleInfo.Right - consoleInfo.Left; - var consoleHeight = consoleInfo.Bottom - consoleInfo.Top; - - var left = monitorInfo.Monitor.Left + ((monitorWidth - consoleWidth) / 2); - var top = monitorInfo.Monitor.Top + ((monitorHeight - consoleHeight) / 2); - - NativeMethods.MoveWindow(hWin, left, top, consoleWidth, consoleHeight, false); - } - - public static char Prompt(string prompt) - { - while (Console.KeyAvailable) - { - Console.ReadKey(false); - } - Console.Out.WriteLine(prompt); - var result = Console.ReadKey(); - return result.KeyChar; - } - } -} diff --git a/Source/ZoomNet.IntegrationTests/NativeMethods.cs b/Source/ZoomNet.IntegrationTests/NativeMethods.cs deleted file mode 100644 index b572fc0c..00000000 --- a/Source/ZoomNet.IntegrationTests/NativeMethods.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace ZoomNet.IntegrationTests -{ - public static class NativeMethods - { - public const Int32 MONITOR_DEFAULT_TO_PRIMARY = 0x00000001; - public const Int32 MONITOR_DEFAULT_TO_NEAREST = 0x00000002; - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr GetConsoleWindow(); - - [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetWindowRect(IntPtr hWnd, out NativeRectangle rc); - - [DllImport("user32.dll", SetLastError = true)] - public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint); - - [DllImport("user32.dll")] - public static extern IntPtr MonitorFromWindow(IntPtr handle, Int32 flags); - - [DllImport("user32.dll")] - public static extern Boolean GetMonitorInfo(IntPtr hMonitor, NativeMonitorInfo lpmi); - - [Serializable, StructLayout(LayoutKind.Sequential)] - public struct NativeRectangle - { - public Int32 Left; - public Int32 Top; - public Int32 Right; - public Int32 Bottom; - - public NativeRectangle(Int32 left, Int32 top, Int32 right, Int32 bottom) - { - this.Left = left; - this.Top = top; - this.Right = right; - this.Bottom = bottom; - } - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public sealed class NativeMonitorInfo - { - public Int32 Size = Marshal.SizeOf(typeof(NativeMonitorInfo)); - public NativeRectangle Monitor; - public NativeRectangle Work; - public Int32 Flags; - } - } -} diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs index 235c3094..0f8c99be 100644 --- a/Source/ZoomNet.IntegrationTests/Program.cs +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -1,25 +1,45 @@ using Logzio.DotNet.NLog; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NLog; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace ZoomNet.IntegrationTests { public class Program { - public static async Task Main(string[] args) + public static async Task Main(string[] args) { //var serializerContext = GenerateAttributesForSerializerContext(); - var services = new ServiceCollection(); - ConfigureServices(services); - await using var serviceProvider = services.BuildServiceProvider(); - var app = serviceProvider.GetService(); - return await app.RunAsync().ConfigureAwait(false); + var builder = Host.CreateApplicationBuilder(); + builder.Services.AddHostedService(); + + // Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the integration tests) + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + // Configure logging + builder.Logging.ClearProviders(); // Remove the built-in providers (which include the Console) + builder.Logging.AddNLog(GetNLogConfiguration()); // Add our desired custom providers (which include the Colored Console) + + // Run the tests + var host = builder.Build(); + await host.StartAsync(cts.Token).ConfigureAwait(false); + + // Stop NLog (which has the desirable side-effect of flushing any pending logs) + LogManager.Shutdown(); } private static string GenerateAttributesForSerializerContext() @@ -59,17 +79,10 @@ private static string GenerateAttributesForSerializerContext() var arrayAttributes = string.Join("\r\n", typesSortedAlphabetically.Where(t => !string.IsNullOrEmpty(t.JsonSerializeAttributeArray)).Select(t => t.JsonSerializeAttributeArray)); var nullableAttributes = string.Join("\r\n", typesSortedAlphabetically.Where(t => !string.IsNullOrEmpty(t.JsonSerializeAttributeNullable)).Select(t => t.JsonSerializeAttributeNullable)); - var result = string.Join("\r\n\r\n", new[] { simpleAttributes, arrayAttributes, nullableAttributes }); + var result = string.Join("\r\n\r\n", [simpleAttributes, arrayAttributes, nullableAttributes]); return result; } - private static void ConfigureServices(ServiceCollection services) - { - services - .AddLogging(loggingBuilder => loggingBuilder.AddNLog(GetNLogConfiguration())) - .AddTransient(); - } - private static LoggingConfiguration GetNLogConfiguration() { // Configure logging diff --git a/Source/ZoomNet.IntegrationTests/TestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuite.cs index eaaafd3b..847b6e8b 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuite.cs @@ -34,16 +34,8 @@ public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory FetchCurrentUserInfo = fetchCurrentUserInfo; } - public virtual async Task RunTestsAsync() + public virtual async Task RunTestsAsync(CancellationToken cancellationToken) { - // Configure cancellation - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - }; - // Configure ZoomNet client var client = new ZoomClient(ConnectionInfo, Proxy, null, LoggerFactory.CreateLogger()); @@ -53,8 +45,8 @@ public virtual async Task RunTestsAsync() if (FetchCurrentUserInfo) { - currentUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); - currentUserPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); + currentUser = await client.Users.GetCurrentAsync(cancellationToken).ConfigureAwait(false); + currentUserPermissions = await client.Users.GetCurrentPermissionsAsync(cancellationToken).ConfigureAwait(false); Array.Sort(currentUserPermissions); // Sort permissions alphabetically for convenience } @@ -67,7 +59,7 @@ public virtual async Task RunTestsAsync() try { var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(currentUser, currentUserPermissions, client, log, cts.Token).ConfigureAwait(false); + await integrationTest.RunAsync(currentUser, currentUserPermissions, client, log, cancellationToken).ConfigureAwait(false); return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); } catch (OperationCanceledException) @@ -106,12 +98,6 @@ public virtual async Task RunTestsAsync() await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); - // Prompt user to press a key in order to allow reading the log in the console - var promptLog = new StringWriter(); - await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); - ConsoleUtils.Prompt(promptLog.ToString()); - // Return code indicating success/failure var resultCode = ResultCodes.Success; if (results.Any(result => result.ResultCode != ResultCodes.Success)) diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs index c050a1e9..11554e3b 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs @@ -17,7 +17,7 @@ public WebSocketsTestSuite(IConnectionInfo connectionInfo, string subscriptionId _subscriptionId = subscriptionId; } - public override async Task RunTestsAsync() + public override async Task RunTestsAsync(CancellationToken cancellationToken) { var logger = base.LoggerFactory.CreateLogger(); var eventProcessor = new Func(async (webhookEvent, cancellationToken) => @@ -27,18 +27,16 @@ public override async Task RunTestsAsync() }); // Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the websocket client) - var cts = new CancellationTokenSource(); var exitEvent = new ManualResetEvent(false); Console.CancelKeyPress += (s, e) => { e.Cancel = true; - cts.Cancel(); exitEvent.Set(); }; // Start the websocket client using var client = new ZoomWebSocketClient(base.ConnectionInfo, _subscriptionId, eventProcessor, base.Proxy, logger); - await client.StartAsync(cts.Token).ConfigureAwait(false); + await client.StartAsync(cancellationToken).ConfigureAwait(false); exitEvent.WaitOne(); return ResultCodes.Success; diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index e4f2d2c2..5acecd81 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -1,12 +1,14 @@ +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Net; +using System.Threading; using System.Threading.Tasks; using ZoomNet.IntegrationTests.TestSuites; namespace ZoomNet.IntegrationTests { - internal class TestsRunner + internal class TestsRunner : IHostedService { private enum TestType { @@ -31,7 +33,7 @@ public TestsRunner(ILoggerFactory loggerFactory) _loggerFactory = loggerFactory; } - public async Task RunAsync() + public async Task StartAsync(CancellationToken cancellationToken) { // ----------------------------------------------------------------------------- // Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. @@ -47,10 +49,6 @@ public async Task RunAsync() var connectionType = ConnectionType.OAuthServerToServer; // ----------------------------------------------------------------------------- - // Ensure the Console is tall enough and centered on the screen - if (OperatingSystem.IsWindows()) Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); - ConsoleUtils.CenterConsole(); - // Configure the proxy if desired var proxy = useProxy ? new WebProxy($"http://localhost:{proxyPort}") : null; @@ -59,10 +57,12 @@ public async Task RunAsync() var testSuite = GetTestSuite(connectionInfo, testType, proxy, _loggerFactory); // Run the tests - var resultCode = await testSuite.RunTestsAsync().ConfigureAwait(false); + await testSuite.RunTestsAsync(cancellationToken).ConfigureAwait(false); + } - // Return result - return (int)resultCode; + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; } private static IConnectionInfo GetConnectionInfo(ConnectionType connectionType) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index 657fd7f0..4619172e 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -13,8 +13,14 @@ - - + + + + + + + + From 75e84cdfee7b3c343d530e8ef46f9d54045872ec Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 18:23:24 -0400 Subject: [PATCH 10/19] Fix the misconfigured LogzioTarget logging target --- Source/ZoomNet.IntegrationTests/Program.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs index 0f8c99be..ee2ada97 100644 --- a/Source/ZoomNet.IntegrationTests/Program.cs +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -92,18 +92,25 @@ private static LoggingConfiguration GetNLogConfiguration() var logzioToken = Environment.GetEnvironmentVariable("LOGZIO_TOKEN"); if (!string.IsNullOrEmpty(logzioToken)) { - var logzioTarget = new LogzioTarget { Token = logzioToken }; - logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("source", "ZoomNet_integration_tests")); + var logzioTarget = new LogzioTarget + { + Name = "Logzio", + Token = logzioToken, + LogzioType = "nlog", + JsonKeysCamelCase = true, + // ProxyAddress = "http://localhost:8888", + }; + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("Source", "ZoomNet_integration_tests")); logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("ZoomNet-Version", ZoomNet.ZoomClient.Version)); nLogConfig.AddTarget("Logzio", logzioTarget); - nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, "Logzio", "*"); + nLogConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logzioTarget, "*"); } // Send logs to console var consoleTarget = new ColoredConsoleTarget(); nLogConfig.AddTarget("ColoredConsole", consoleTarget); - nLogConfig.AddRule(NLog.LogLevel.Warn, NLog.LogLevel.Fatal, consoleTarget, "*"); + nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, consoleTarget, "*"); nLogConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, consoleTarget, "ZoomNet.ZoomWebSocketClient"); return nLogConfig; From da95fcbb9bbdb3b1ab03afac9b384a8a4092fd07 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 18:24:57 -0400 Subject: [PATCH 11/19] Increase the delay after deleting previously created channels because Zoom throws a "channel with same name already exist" when we subsequently attempt to create a new channel during integration testing --- Source/ZoomNet.IntegrationTests/Tests/Chat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs index c40ad337..2991d628 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs @@ -26,7 +26,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie { await client.Chat.DeleteChannelAsync(oldChannel.Id, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Channel {oldChannel.Id} deleted").ConfigureAwait(false); - await Task.Delay(250, cancellationToken).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up }); await Task.WhenAll(cleanUpTasks).ConfigureAwait(false); From c8cd905dd9d86f1d5b1f2ca8930833e32de4c944 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 18:26:30 -0400 Subject: [PATCH 12/19] Fix the bug that prevents updating a channel Resolves #337 --- .../Models/ChatChannelAddMemberPermissions.cs | 18 ++++++++++++++ .../ChatChannelMentionAllPermissions.cs | 23 ++++++++++++++++++ .../Models/ChatChannelPostingPermissions.cs | 23 ++++++++++++++++++ Source/ZoomNet/Models/ChatChannelSettings.cs | 18 +++++++++++--- Source/ZoomNet/Resources/Chat.cs | 24 +++++++++++++++++-- Source/ZoomNet/Resources/IChat.cs | 4 +++- 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 Source/ZoomNet/Models/ChatChannelAddMemberPermissions.cs create mode 100644 Source/ZoomNet/Models/ChatChannelMentionAllPermissions.cs create mode 100644 Source/ZoomNet/Models/ChatChannelPostingPermissions.cs diff --git a/Source/ZoomNet/Models/ChatChannelAddMemberPermissions.cs b/Source/ZoomNet/Models/ChatChannelAddMemberPermissions.cs new file mode 100644 index 00000000..dd23a35f --- /dev/null +++ b/Source/ZoomNet/Models/ChatChannelAddMemberPermissions.cs @@ -0,0 +1,18 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate who can add new channel members. + /// + public enum ChatChannelAddMemberPermissions + { + /// + /// All channel members can add new members. + /// + Everyone = 1, + + /// + /// Only channel owners and administrators can add new members. + /// + Administrators = 2 + } +} diff --git a/Source/ZoomNet/Models/ChatChannelMentionAllPermissions.cs b/Source/ZoomNet/Models/ChatChannelMentionAllPermissions.cs new file mode 100644 index 00000000..76271b6e --- /dev/null +++ b/Source/ZoomNet/Models/ChatChannelMentionAllPermissions.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate who can can use @all. + /// + public enum ChatChannelMentionAllPermissions + { + /// + /// Everyone can use @all. + /// + Everyone = 1, + + /// + /// Only channel owners and administrators can use @all. + /// + Administrators = 2, + + /// + /// Nobody can use @all. + /// + Nobody = 3 + } +} diff --git a/Source/ZoomNet/Models/ChatChannelPostingPermissions.cs b/Source/ZoomNet/Models/ChatChannelPostingPermissions.cs new file mode 100644 index 00000000..e0a27e24 --- /dev/null +++ b/Source/ZoomNet/Models/ChatChannelPostingPermissions.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate who can post to a chat channel. + /// + public enum ChatChannelPostingPermissions + { + /// + /// All chat channel members can post to the channel. + /// + Everyone = 1, + + /// + /// Only channel owners and administrators can post to the channel. + /// + Administrators = 2, + + /// + /// Only channel owners, administrators, and certain members can post to the channel. + /// + Restricted = 3 + } +} diff --git a/Source/ZoomNet/Models/ChatChannelSettings.cs b/Source/ZoomNet/Models/ChatChannelSettings.cs index 4fecfda4..2bd1ee96 100644 --- a/Source/ZoomNet/Models/ChatChannelSettings.cs +++ b/Source/ZoomNet/Models/ChatChannelSettings.cs @@ -11,7 +11,7 @@ public class ChatChannelSettings /// Gets or sets a value indicating whether members can see files previously posted to the channel. /// [JsonPropertyName("new_members_can_see_previous_messages_files")] - public bool CanSeePreviousMessageFiles { get; set; } + public bool NewMembersCanSeePreviousMessageFiles { get; set; } /// /// Gets or sets a value indicating whether external users can be added to the channel. @@ -20,9 +20,21 @@ public class ChatChannelSettings public bool CanAddExternalUsers { get; set; } /// - /// Gets or sets the permissions. + /// Gets or sets the value indicating who can post to a channel. /// [JsonPropertyName("posting_permissions")] - public int Permissions { get; set; } + public ChatChannelPostingPermissions PostingPermissions { get; set; } + + /// + /// Gets or sets the value indicating who can add new channel members. + /// + [JsonPropertyName("add_member_permissions")] + public ChatChannelAddMemberPermissions AddMemberPermissions { get; set; } + + /// + /// Gets or sets the value indicating who can use @all. + /// + [JsonPropertyName("mention_all_permissions")] + public ChatChannelMentionAllPermissions MentionAllPermissions { get; set; } } } diff --git a/Source/ZoomNet/Resources/Chat.cs b/Source/ZoomNet/Resources/Chat.cs index d0d1017c..b148ad9b 100644 --- a/Source/ZoomNet/Resources/Chat.cs +++ b/Source/ZoomNet/Resources/Chat.cs @@ -71,11 +71,31 @@ public Task CreateAccountChannelAsync(string userId, string name, C } /// - public Task UpdateAccountChannelAsync(string userId, string channelId, string name, CancellationToken cancellationToken = default) + public Task UpdateAccountChannelAsync(string userId, string channelId, string name, ChatChannelSettings settings, ChatChannelType? type = null, CancellationToken cancellationToken = default) { var data = new JsonObject { - { "name", name } + { "name", name }, + { "type", type }, + { + // I am hard-coding the properties of the channel_settings rather than simply passing + // a reference to the settings object because the ChatChannelSettings model class has + // additional properties and Zoom does not allow us to update these additional + // properties (e.g.: "allow_to_add_external_users"). + // + // If we include the addtional properties, the Zoom API responds with a misleading + // error message: {"code":300,"message":"Request Body should be a valid JSON object."} + // + // Contrary to what the error message says, the body contains a valid JSON object but + // it includes properties that should not be included. + "channel_settings", new JsonObject + { + { "add_member_permissions", settings.AddMemberPermissions }, + { "new_members_can_see_previous_messages_files", settings.NewMembersCanSeePreviousMessageFiles }, + { "posting_permissions", settings.PostingPermissions }, + { "mention_all_permissions", settings.MentionAllPermissions }, + } + } }; return _client diff --git a/Source/ZoomNet/Resources/IChat.cs b/Source/ZoomNet/Resources/IChat.cs index f5b9c87f..1de45b8e 100644 --- a/Source/ZoomNet/Resources/IChat.cs +++ b/Source/ZoomNet/Resources/IChat.cs @@ -60,11 +60,13 @@ public interface IChat /// The user Id or email address. /// The channel Id. /// The name of the channel. + /// The settings of the chat channel. + /// This field changes the channel type. /// The cancellation token. /// /// The async task. /// - Task UpdateAccountChannelAsync(string userId, string channelId, string name, CancellationToken cancellationToken = default); + Task UpdateAccountChannelAsync(string userId, string channelId, string name, ChatChannelSettings settings, ChatChannelType? type = null, CancellationToken cancellationToken = default); /// /// Delete a chat channel. From adc9c17dfd7e125acf271184ba4d50e25c642b40 Mon Sep 17 00:00:00 2001 From: jericho Date: Wed, 3 Apr 2024 18:26:59 -0400 Subject: [PATCH 13/19] Improve comment in DiagnosticHandler --- Source/ZoomNet/Utilities/DiagnosticHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index fb2da82c..06b3daf0 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -103,7 +103,7 @@ public object[] GetLoggingParameters() var requestContentLength = requestContent?.Length ?? 0; var responseContentLength = responseContent?.Length ?? 0; - // Get the request headers + // Get the request headers (please note: intentionally getting headers from "response.RequestMessage" rather than "request") var requestHeaders = response?.RequestMessage?.Headers ?? Enumerable.Empty>>(); if (!requestHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))) { @@ -127,7 +127,7 @@ public object[] GetLoggingParameters() .OrderBy(kvp => kvp.Key) .Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value)) .ToArray()); - logParams.Add(requestContent?.TrimEnd('\r', '\n') ?? ""); + logParams.Add(requestContent?.TrimEnd('\r', '\n')); } if (response != null) @@ -137,7 +137,7 @@ public object[] GetLoggingParameters() .OrderBy(kvp => kvp.Key) .Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value)) .ToList()); - logParams.Add(responseContent?.TrimEnd('\r', '\n') ?? ""); + logParams.Add(responseContent?.TrimEnd('\r', '\n')); } logParams.Add(elapsed.TotalMilliseconds); From 93e6690672bd7b945cd7bbb4b4018f6f9129a353 Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 4 Apr 2024 13:40:20 -0400 Subject: [PATCH 14/19] Add a remark to explain why Contacts.GetAllAsync doesn't always work --- Source/ZoomNet/Resources/Contacts.cs | 41 +++------------------------ Source/ZoomNet/Resources/IContacts.cs | 5 ++++ 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/Source/ZoomNet/Resources/Contacts.cs b/Source/ZoomNet/Resources/Contacts.cs index e554de4f..d641d2d6 100644 --- a/Source/ZoomNet/Resources/Contacts.cs +++ b/Source/ZoomNet/Resources/Contacts.cs @@ -6,13 +6,7 @@ namespace ZoomNet.Resources { - /// - /// Allows you to manage contacts. - /// - /// - /// - /// See Zoom documentation for more information. - /// + /// public class Contacts : IContacts { private readonly Pathoschild.Http.Client.IClient _client; @@ -26,16 +20,7 @@ internal Contacts(Pathoschild.Http.Client.IClient client) _client = client; } - /// - /// Retrieve the current user's contacts. - /// - /// The type of contacts. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of contacts. - /// + /// public Task> GetAllAsync(ContactType type = ContactType.Internal, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 50) @@ -52,17 +37,7 @@ public Task> GetAllAsync(ContactType type = .AsPaginatedResponseWithToken("contacts"); } - /// - /// Search contacts. - /// - /// The search keyword: either first name, last na,me or email of the contact. - /// Indicate whether you want the status pf a contact to be included in the response. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of contacts. - /// + /// public Task> SearchAsync(string keyword, bool queryPresenceStatus = true, int recordsPerPage = 1, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 25) @@ -80,15 +55,7 @@ public Task> SearchAsync(string keyword, boo .AsPaginatedResponseWithToken("contacts"); } - /// - /// Retrieve information about a specific contact of the current Zoom user. - /// - /// The unique identifier or email address of the contact. - /// Indicate whether you want the status of a contact to be included in the response. - /// The cancellation token. - /// - /// The . - /// + /// public Task GetAsync(string contactId, bool queryPresenceStatus = true, CancellationToken cancellationToken = default) { return _client diff --git a/Source/ZoomNet/Resources/IContacts.cs b/Source/ZoomNet/Resources/IContacts.cs index 91053097..30c27394 100644 --- a/Source/ZoomNet/Resources/IContacts.cs +++ b/Source/ZoomNet/Resources/IContacts.cs @@ -22,6 +22,11 @@ public interface IContacts /// /// An array of contacts. /// + /// + /// There's a note in the Zoom API documentation that say: "This API only supports user-managed OAuth app." + /// When invoking this method and using any other type of app, such as a Server-to-server oauth app for example, + /// the Zoom api returns the following message: Invalid access token, does not contain scopes:[chat_contact:read]. + /// Task> GetAllAsync(ContactType type = ContactType.Internal, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); /// From f00eaaa624908c2aa193b531276ee2ed9c65b293 Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 4 Apr 2024 13:41:19 -0400 Subject: [PATCH 15/19] Change the order of the Contacts integration tests because some of them are known to fail --- Source/ZoomNet.IntegrationTests/Tests/Contacts.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs b/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs index 0e1b1cf7..aa4ab1df 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs @@ -13,18 +13,17 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync("\n***** CONTACTS *****\n").ConfigureAwait(false); - // GET THE CONTACTS FOR THIS USER + var contact = await client.Contacts.GetAsync(myUser.Id, true, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"{contact.EmailAddress} is {contact.PresenceStatus}").ConfigureAwait(false); + + var paginatedSearchedContacts = await client.Contacts.SearchAsync("zzz", true, 1, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Found {paginatedSearchedContacts.TotalRecords} contacts").ConfigureAwait(false); + var paginatedInternalContacts = await client.Contacts.GetAllAsync(ContactType.Internal, 50, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"There are {paginatedInternalContacts.TotalRecords} internal contacts for user {myUser.Id}").ConfigureAwait(false); var paginatedExternalContacts = await client.Contacts.GetAllAsync(ContactType.External, 50, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"There are {paginatedExternalContacts.TotalRecords} external contacts for user {myUser.Id}").ConfigureAwait(false); - - var paginatedSearchedContacts = await client.Contacts.SearchAsync("zzz", true, 1, null, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"Found {paginatedSearchedContacts.TotalRecords} contacts").ConfigureAwait(false); - - var contact = await client.Contacts.GetAsync(myUser.Id, true, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"{contact.EmailAddress} is {contact.PresenceStatus}").ConfigureAwait(false); } } } From fe4023d114bdc0ea704f5a59866655ac310bbb9b Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 4 Apr 2024 13:41:40 -0400 Subject: [PATCH 16/19] Upgrade nuget packages --- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 10 +++++----- Source/ZoomNet/ZoomNet.csproj | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 1f1dee94..a641f25a 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -8,20 +8,20 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 076cda39..3520de29 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -37,12 +37,12 @@ - - - + + + - + From 423b929f1dfd4c9661e6afcca7478b0bafdf7a99 Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 5 Apr 2024 15:08:37 -0400 Subject: [PATCH 17/19] Better XML comment --- Source/ZoomNet.IntegrationTests/Tests/Contacts.cs | 6 +++--- Source/ZoomNet/Resources/IContacts.cs | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs b/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs index aa4ab1df..cb683eab 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Contacts.cs @@ -13,12 +13,12 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync("\n***** CONTACTS *****\n").ConfigureAwait(false); - var contact = await client.Contacts.GetAsync(myUser.Id, true, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"{contact.EmailAddress} is {contact.PresenceStatus}").ConfigureAwait(false); - var paginatedSearchedContacts = await client.Contacts.SearchAsync("zzz", true, 1, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Found {paginatedSearchedContacts.TotalRecords} contacts").ConfigureAwait(false); + var contact = await client.Contacts.GetAsync(myUser.Id, true, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"{contact.EmailAddress} is {contact.PresenceStatus}").ConfigureAwait(false); + var paginatedInternalContacts = await client.Contacts.GetAllAsync(ContactType.Internal, 50, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"There are {paginatedInternalContacts.TotalRecords} internal contacts for user {myUser.Id}").ConfigureAwait(false); diff --git a/Source/ZoomNet/Resources/IContacts.cs b/Source/ZoomNet/Resources/IContacts.cs index 30c27394..507b4dee 100644 --- a/Source/ZoomNet/Resources/IContacts.cs +++ b/Source/ZoomNet/Resources/IContacts.cs @@ -51,6 +51,11 @@ public interface IContacts /// /// The . /// + /// + /// There's a note in the Zoom API documentation that say: "This API only supports user-managed OAuth app." + /// When invoking this method and using any other type of app, such as a Server-to-server oauth app for example, + /// the Zoom api returns the following message: Invalid access token, does not contain scopes:[chat_contact:read]. + /// Task GetAsync(string contactId, bool queryPresenceStatus = true, CancellationToken cancellationToken = default); } } From 2152560b0a9877d2a4243901bf862736d0831153 Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 5 Apr 2024 15:09:38 -0400 Subject: [PATCH 18/19] Restore old array initialization syntax --- Source/ZoomNet/Resources/Webinars.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index 1a4a5bcd..f4cd1244 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -346,7 +346,7 @@ public Task GetPanelistsAsync(long webinarId, CancellationToken canc /// public Task AddPanelistAsync(long webinarId, string email, string fullName, string virtualBackgroundId = null, CancellationToken cancellationToken = default) { - return AddPanelistsAsync(webinarId, [(email, fullName, virtualBackgroundId)], cancellationToken); + return AddPanelistsAsync(webinarId, new[] { (email, fullName, virtualBackgroundId) }, cancellationToken); } /// From 5f01f03527f177e9e09fb870443a1aab15ae7dd6 Mon Sep 17 00:00:00 2001 From: jericho Date: Fri, 5 Apr 2024 19:23:07 -0400 Subject: [PATCH 19/19] Refresh build script --- build.cake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.cake b/build.cake index d0329230..afaf2846 100644 --- a/build.cake +++ b/build.cake @@ -2,13 +2,13 @@ #tool dotnet:?package=GitVersion.Tool&version=5.12.0 #tool dotnet:?package=coveralls.net&version=4.0.1 #tool nuget:https://f.feedz.io/jericho/jericho/nuget/?package=GitReleaseManager&version=0.17.0-collaborators0003 -#tool nuget:?package=ReportGenerator&version=5.2.1 +#tool nuget:?package=ReportGenerator&version=5.2.4 #tool nuget:?package=xunit.runner.console&version=2.7.0 -#tool nuget:?package=CodecovUploader&version=0.7.1 +#tool nuget:?package=CodecovUploader&version=0.7.2 // Install addins. #addin nuget:?package=Cake.Coveralls&version=1.1.0 -#addin nuget:?package=Cake.Git&version=3.0.0 +#addin nuget:?package=Cake.Git&version=4.0.0 #addin nuget:?package=Cake.Codecov&version=1.0.1