From 811d419069c464b357a44b50431b263b8c00cb01 Mon Sep 17 00:00:00 2001 From: theit8514 Date: Sat, 5 Aug 2023 14:07:49 -0400 Subject: [PATCH 01/16] (#20) Add Chatbot resource (#312) * Add Chatbot resource * Fix documentation * Fix build error --- .../ZoomNet.IntegrationTests/Tests/Chatbot.cs | 234 ++++++++++++++++++ .../ZoomNet.IntegrationTests/TestsRunner.cs | 3 +- .../Models/ChatbotValidateTests.cs | 142 +++++++++++ Source/ZoomNet/IZoomClient.cs | 5 + .../ZoomNet/Json/NullableHexColorConverter.cs | 45 ++++ .../Json/ZoomNetJsonSerializerContext.cs | 17 ++ .../Models/ChatbotMessage/ChatbotAction.cs | 30 +++ .../ChatbotMessage/ChatbotActionStyle.cs | 38 +++ .../Models/ChatbotMessage/ChatbotActions.cs | 24 ++ .../ChatbotMessage/ChatbotAttachment.cs | 39 +++ .../ChatbotAttachmentInformation.cs | 21 ++ .../Models/ChatbotMessage/ChatbotContent.cs | 42 ++++ .../ChatbotMessage/ChatbotDropdownItem.cs | 21 ++ .../ChatbotMessage/ChatbotDropdownList.cs | 40 +++ .../ChatbotDropdownStaticSource.cs | 26 ++ .../Models/ChatbotMessage/ChatbotFormField.cs | 47 ++++ .../ChatbotMessage/ChatbotFormFields.cs | 25 ++ .../Models/ChatbotMessage/ChatbotHeader.cs | 44 ++++ .../ChatbotMessage/ChatbotMessageLine.cs | 65 +++++ .../ChatbotMessage/ChatbotMessageStyle.cs | 31 +++ .../ChatbotMessage/ChatbotMessageText.cs | 38 +++ .../Models/ChatbotMessage/ChatbotSection.cs | 75 ++++++ .../Models/ChatbotMessage/IChatbotBody.cs | 17 ++ .../Models/ChatbotMessage/IChatbotSection.cs | 16 ++ .../Models/ChatbotMessage/IChatbotValidate.cs | 16 ++ .../Models/ChatbotMessageInformation.cs | 33 +++ Source/ZoomNet/Resources/Chatbot.cs | 151 +++++++++++ Source/ZoomNet/Resources/IChatbot.cs | 86 +++++++ Source/ZoomNet/ZoomClient.cs | 6 + 29 files changed, 1376 insertions(+), 1 deletion(-) create mode 100644 Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs create mode 100644 Source/ZoomNet.UnitTests/Models/ChatbotValidateTests.cs create mode 100644 Source/ZoomNet/Json/NullableHexColorConverter.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotAction.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotActionStyle.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotActions.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachment.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachmentInformation.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotContent.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownItem.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownList.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownStaticSource.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotFormField.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotHeader.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageStyle.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageText.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/IChatbotBody.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/IChatbotSection.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessage/IChatbotValidate.cs create mode 100644 Source/ZoomNet/Models/ChatbotMessageInformation.cs create mode 100644 Source/ZoomNet/Resources/Chatbot.cs create mode 100644 Source/ZoomNet/Resources/IChatbot.cs diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs new file mode 100644 index 00000000..a2894f3d --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; +using ZoomNet.Models.ChatbotMessage; + +namespace ZoomNet.IntegrationTests.Tests; + +public class Chatbot : IIntegrationTest +{ + /// + public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, + CancellationToken cancellationToken) + { + var accountId = myUser?.AccountId ?? "{accountId}"; + var robotJId = "{robotId}@xmpp.zoom.us"; + var toJId = "{userId}@xmpp.zoom.us"; // User + //var toJId = "{channelId}@conference.xmpp.zoom.us"; // Channel + var response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, "Test message", false, cancellationToken); + await log.WriteLineAsync(response.MessageId); + await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, "*Updated test message*", true, cancellationToken); + await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, cancellationToken); + await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, + new ChatbotContent() + { + Head = new ChatbotHeader("A message header"), + Body = new List() + { + new ChatbotSection() + { + SidebarColor = Color.Red, + Sections = new List() + { + new ChatbotMessageLine("Section 1 Message") + }, + Footer = "I am a footer", + FooterIcon = "https://d24cgw3uvb9a9h.cloudfront.net/static/93516/image/new/ZoomLogo.png", + TimeStampFromDateTime = DateTime.Now.AddSeconds(120) + }, + new ChatbotSection() + { + SidebarColor = Color.Green, + Sections = new List() + { + new ChatbotMessageLine("Section 2 Message") + } + }, + new ChatbotAttachment() + { + ResourceUrl = "https://zoom.us", + ImageUrl = "https://d24cgw3uvb9a9h.cloudfront.net/static/93516/image/new/ZoomLogo.png", + Information = new ChatbotAttachmentInformation() + { + Title = new ChatbotMessageText("Text"), + Description = new ChatbotMessageText("Description") + } + }, + new ChatbotMessageLine("Non-section Message"), + new ChatbotDropdownList() + { + Text = "Channels: ", + StaticSource = ChatbotDropdownStaticSource.Channels, + }, + new ChatbotDropdownList() + { + Text = "Members: ", + StaticSource = ChatbotDropdownStaticSource.Members, + }, + new ChatbotFormFields() + { + Items = new List() + { + new ChatbotFormField() + { + Key = "field1", + Value = " ", + Editable = false, + Short = true + }, + new ChatbotFormField() + { + Key = "field2", + Value = "Test", + Editable = false, + Short = true + } + } + }, + new ChatbotActions() + { + Items = new List() + { + new ChatbotAction() + { + Text = "Button 1", + Value = "button1", + Style = ChatbotActionStyle.Primary + }, + new ChatbotAction() + { + Text = "Button 2", + Value = "button2", + Style = ChatbotActionStyle.Update + }, + new ChatbotAction() + { + Text = "Button 3", + Value = "button3", + Style = ChatbotActionStyle.Delete + }, + new ChatbotAction() + { + Text = "Button 4", + Value = "button4", + Style = ChatbotActionStyle.Disabled + } + } + } + } + }, true, cancellationToken); + await log.WriteLineAsync(response.MessageId); + await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, + new ChatbotContent() + { + Head = new ChatbotHeader("A message header") + { + SubHeader = new ChatbotMessageText("Added sub-header.") + }, + Body = new List() + { + new ChatbotSection() + { + SidebarColor = Color.Red, + Sections = new List() + { + new ChatbotMessageLine("Section 1 Message") + }, + Footer = "I am a footer", + FooterIcon = "https://d24cgw3uvb9a9h.cloudfront.net/static/93516/image/new/ZoomLogo.png", + TimeStampFromDateTime = DateTime.Now.AddSeconds(120) + }, + new ChatbotSection() + { + SidebarColor = Color.Green, + Sections = new List() + { + new ChatbotMessageLine("Section 2 Message") + } + }, + new ChatbotAttachment() + { + ResourceUrl = "https://zoom.us", + ImageUrl = "https://d24cgw3uvb9a9h.cloudfront.net/static/93516/image/new/ZoomLogo.png", + Information = new ChatbotAttachmentInformation() + { + Title = new ChatbotMessageText("Text"), + Description = new ChatbotMessageText("Description") + } + }, + new ChatbotMessageLine("Non-section Message"), + new ChatbotDropdownList() + { + Text = "Channels: ", + StaticSource = ChatbotDropdownStaticSource.Channels, + }, + new ChatbotDropdownList() + { + Text = "Members: ", + StaticSource = ChatbotDropdownStaticSource.Members, + }, + new ChatbotFormFields() + { + Items = new List() + { + new ChatbotFormField() + { + Key = "field1", + Value = " ", + Editable = false, + Short = true + }, + new ChatbotFormField() + { + Key = "field2", + Value = "Test", + Editable = false, + Short = true + } + } + }, + new ChatbotActions() + { + Items = new List() + { + new ChatbotAction() + { + Text = "Button 1", + Value = "button1", + Style = ChatbotActionStyle.Primary + }, + new ChatbotAction() + { + Text = "Button 2", + Value = "button2", + Style = ChatbotActionStyle.Update + }, + new ChatbotAction() + { + Text = "Button 3", + Value = "button3", + Style = ChatbotActionStyle.Delete + }, + new ChatbotAction() + { + Text = "Button 4", + Value = "button4", + Style = ChatbotActionStyle.Disabled + } + } + } + } + }, true, cancellationToken); + await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, + cancellationToken); + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 6b8563c7..72d283ca 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -149,7 +149,8 @@ private async Task RunApiTestsAsync(IConnectionInfo connectionInfo, IWebPro typeof(Roles), typeof(Users), typeof(Webinars), - typeof(Reports) + typeof(Reports), + typeof(Chatbot) }; // Get my user and permisisons diff --git a/Source/ZoomNet.UnitTests/Models/ChatbotValidateTests.cs b/Source/ZoomNet.UnitTests/Models/ChatbotValidateTests.cs new file mode 100644 index 00000000..9485e5c8 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/ChatbotValidateTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using Xunit; +using ZoomNet.Models.ChatbotMessage; + +namespace ZoomNet.UnitTests.Resources +{ + public class ChatbotValidateTests + { + [Fact] + public void ChatbotMessageLine_Validate_ThrowsForMarkdownAndLinks() + { + var token = new ChatbotMessageLine("abc") + { + Link = "http://link" + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Link property cannot be used with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotSectionNested_LineValidate_ThrowsForMarkdownAndLinks() + { + var token = new ChatbotSection() + { + Sections = new List() + { + new ChatbotMessageLine("abc") + { + Link = "http://link" + } + } + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Link property cannot be used with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotFormField_Validate_ThrowsForMarkdownAndEditable() + { + var token = new ChatbotFormField() + { + Editable = true + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Editable property cannot be used on Form Field with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotSectionNested_FormFieldValidate_ThrowsForMarkdownAndEditable() + { + var token = new ChatbotSection() + { + Sections = new List() + { + new ChatbotFormFields() + { + Items = new List() + { + new ChatbotFormField() + { + Editable = true + } + } + } + } + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Editable property cannot be used on Form Field with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotContentNested_FormFieldValidate_ThrowsForMarkdownAndEditable() + { + var token = new ChatbotContent() + { + Body = new List() + { + new ChatbotFormFields() + { + Items = new List() + { + new ChatbotFormField() + { + Editable = true + } + } + } + } + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Editable property cannot be used on Form Field with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotContentNested_LineValidate_ThrowsForMarkdownAndLink() + { + var token = new ChatbotContent() + { + Body = new List() + { + new ChatbotMessageLine("abc") + { + Link = "http://link" + } + } + }; + + token.Validate(false); + var ex = Assert.Throws(() => token.Validate(true)); + + Assert.Equal("Link property cannot be used with EnableMarkdownSupport", ex.Message); + } + + [Fact] + public void ChatbotContent_BodyNull_DoesNotThrow() + { + var token = new ChatbotContent() + { + Body = null + }; + + token.Validate(false); + token.Validate(true); + } + } +} diff --git a/Source/ZoomNet/IZoomClient.cs b/Source/ZoomNet/IZoomClient.cs index 773b5d73..db30cc17 100644 --- a/Source/ZoomNet/IZoomClient.cs +++ b/Source/ZoomNet/IZoomClient.cs @@ -112,5 +112,10 @@ public interface IZoomClient /// Gets the resource which allows you to manage call logs. /// ICallLogs CallLogs { get; } + + /// + /// Gets the resource which allows you to manage chatbot messages. + /// + IChatbot Chatbot { get; } } } diff --git a/Source/ZoomNet/Json/NullableHexColorConverter.cs b/Source/ZoomNet/Json/NullableHexColorConverter.cs new file mode 100644 index 00000000..8c2f3c02 --- /dev/null +++ b/Source/ZoomNet/Json/NullableHexColorConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Drawing; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ZoomNet.Json; + +/// +/// Converts a <> to or from JSON in Hex format. +/// +public class NullableHexColorConverter : JsonConverter +{ + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(Color?); + } + + /// + public override Color? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = reader.GetString(); + if (string.IsNullOrEmpty(str)) + return null; + str = str.Replace("#", string.Empty); + if (str.Length == 6) + str = $"FF{str}"; + return int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argB) + ? Color.FromArgb(argB) + : null; + } + + /// + public override void Write(Utf8JsonWriter writer, Color? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue($"#{value.Value.R:X2}{value.Value.G:X2}{value.Value.B:X2}"); + } +} diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index 65873989..c834a4d7 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -33,6 +33,23 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogType))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAction))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActions))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachment))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachmentInformation))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotContent))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownItem))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownList))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormField))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormFields))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotHeader))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageLine))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageStyle))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageText))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannel))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole))] diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotAction.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAction.cs new file mode 100644 index 00000000..fb5a78f3 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAction.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; +using ZoomNet.Json; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// An action which can be clicked. +/// +public class ChatbotAction +{ + /// + /// Gets or sets the text of this action. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the value of this action. + /// + [JsonPropertyName("value")] + public string Value { get; set; } + + /// + /// Gets or sets the style of this action. + /// + [JsonPropertyName("style")] + [JsonConverter(typeof(StringEnumConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ChatbotActionStyle Style { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotActionStyle.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotActionStyle.cs new file mode 100644 index 00000000..3e3fb7f1 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotActionStyle.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// Enumeration to indicate the style for the action. +/// +public enum ChatbotActionStyle +{ + /// + /// Invalid. Do not use. + /// + Invalid, + + /// + /// Members. + /// + [EnumMember(Value = "Primary")] + Primary, + + /// + /// Channels. + /// + [EnumMember(Value = "Update")] + Update, + + /// + /// Members. + /// + [EnumMember(Value = "Delete")] + Delete, + + /// + /// Channels. + /// + [EnumMember(Value = "Disabled")] + Disabled +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotActions.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotActions.cs new file mode 100644 index 00000000..dd15b8f7 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotActions.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message line containing one or more actions. +/// +public class ChatbotActions : IChatbotBody, IChatbotSection +{ + /// + /// Gets or sets the actions. + /// + [JsonPropertyName("items")] + public ICollection Items { get; set; } + + /// + /// Gets or sets the number of buttons visible. + /// If the number of items are over this limit, it will group the buttons into a list. + /// + [JsonPropertyName("limit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Limit { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachment.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachment.cs new file mode 100644 index 00000000..b3ce20e7 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachment.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message line containing an attachment. +/// +public class ChatbotAttachment : IChatbotBody, IChatbotSection +{ + /// + /// Gets or sets the resource URL. Used to download the file. + /// + [JsonPropertyName("resource_url")] + public string ResourceUrl { get; set; } + + /// + /// Gets or sets the image URL. + /// + [JsonPropertyName("img_url")] + public string ImageUrl { get; set; } + + /// + /// Gets or sets the information of this attachment. + /// + [JsonPropertyName("information")] + public ChatbotAttachmentInformation Information { get; set; } + + /// + /// Gets or sets the extension of the attachment. + /// + [JsonPropertyName("ext")] + public string Extension { get; set; } + + /// + /// Gets or sets the size in bytes. + /// + [JsonPropertyName("size")] + public int? Size { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachmentInformation.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachmentInformation.cs new file mode 100644 index 00000000..93f1ffd8 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotAttachmentInformation.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// The attachment information. +/// +public class ChatbotAttachmentInformation +{ + /// + /// Gets or sets the title line of the attachment. + /// + [JsonPropertyName("title")] + public ChatbotMessageText Title { get; set; } + + /// + /// Gets or sets the description line of the attachment. + /// + [JsonPropertyName("description")] + public ChatbotMessageText Description { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotContent.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotContent.cs new file mode 100644 index 00000000..b6219589 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotContent.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// The message content for a Chatbot message. +/// +public class ChatbotContent +{ + /// + /// Gets or sets the header and sub-header of the message. + /// + [JsonPropertyName("head")] + public ChatbotHeader Head { get; set; } + + /// + /// Gets or sets the body of the message. + /// + [JsonPropertyName("body")] + public ICollection Body { get; set; } + + /// + /// Verify that the content is valid. + /// + /// True if the content has markdown syntax enabled. + /// Some or all of the content is invalid. + internal void Validate(bool enableMarkdownSupport) + { + if (Body == null) + { + return; + } + + foreach (var message in Body.OfType()) + { + message.Validate(enableMarkdownSupport); + } + } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownItem.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownItem.cs new file mode 100644 index 00000000..b0d0e261 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownItem.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A dropdown list item. +/// +public class ChatbotDropdownItem +{ + /// + /// Gets or sets the text of this dropdown list item. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the value of this dropdown list item. + /// + [JsonPropertyName("value")] + public string Value { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownList.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownList.cs new file mode 100644 index 00000000..5fe6f789 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownList.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using ZoomNet.Json; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message line containing a dropdown list. +/// +public class ChatbotDropdownList : IChatbotBody, IChatbotSection +{ + /// + /// Gets or sets the text of this message line. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the default selected item. + /// + [JsonPropertyName("selected_item")] + public ChatbotDropdownItem SelectedItem { get; set; } + + /// + /// Gets or sets the data for this dropdown list. + /// + /// Should be null when is set. + [JsonPropertyName("select_items")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ICollection SelectItems { get; set; } + + /// + /// Gets or sets the static source of the data for this dropdown list. + /// + /// Should be when is set. + [JsonPropertyName("static_source")] + [JsonConverter(typeof(StringEnumConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ChatbotDropdownStaticSource StaticSource { get; set; } = default; +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownStaticSource.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownStaticSource.cs new file mode 100644 index 00000000..14d71ee2 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotDropdownStaticSource.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// Enumeration to indicate the type of static source to populate a dropdown list. +/// +public enum ChatbotDropdownStaticSource +{ + /// + /// Unspecified. Do not use. + /// + Unspecified, + + /// + /// Members. + /// + [EnumMember(Value = "members")] + Members, + + /// + /// Channels. + /// + [EnumMember(Value = "channels")] + Channels +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormField.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormField.cs new file mode 100644 index 00000000..d9155e6b --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormField.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A single form field element. +/// +public class ChatbotFormField : IChatbotValidate +{ + /// + /// Gets or sets a key. + /// + [JsonPropertyName("key")] + public string Key { get; set; } + + /// + /// Gets or sets a default value. A single space is used to indicate a blank value. + /// + [JsonPropertyName("value")] + public string Value { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be compressed to two columns. + /// + [JsonPropertyName("short")] + public bool Short { get; set; } + + /// + /// Gets or sets a value indicating whether the field is editable. + /// + [JsonPropertyName("editable")] + public bool Editable { get; set; } + + /// + /// Verify that the content is valid. + /// + /// True if the content has markdown syntax enabled. + /// Some or all of the content is invalid. + public void Validate(bool enableMarkdownSupport) + { + if (enableMarkdownSupport && Editable) + { + throw new InvalidOperationException("Editable property cannot be used on Form Field with EnableMarkdownSupport"); + } + } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs new file mode 100644 index 00000000..209358d5 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message line containing a set of form fields. +/// +public class ChatbotFormFields : IChatbotBody, IChatbotSection, IChatbotValidate +{ + /// + /// Gets or sets the fields of the message. + /// + [JsonPropertyName("items")] + public ICollection Items { get; set; } + + /// + public void Validate(bool enableMarkdownSupport) + { + foreach (var item in Items) + { + item.Validate(enableMarkdownSupport); + } + } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotHeader.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotHeader.cs new file mode 100644 index 00000000..bfd8d9f9 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotHeader.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message header which is above the main body of the message. +/// +public class ChatbotHeader +{ + /// + /// Initializes a new instance of the class. + /// + public ChatbotHeader() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text of the message header. + public ChatbotHeader(string text) + { + Text = text; + } + + /// + /// Gets or sets the text of the message. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the style of the message text. + /// + [JsonPropertyName("style")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChatbotMessageStyle Style { get; set; } + + /// + /// Gets or sets the sub-header of the message header. + /// + [JsonPropertyName("sub_head")] + public ChatbotMessageText SubHeader { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs new file mode 100644 index 00000000..50c7150d --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs @@ -0,0 +1,65 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A line of the message body. +/// +public class ChatbotMessageLine : IChatbotBody, IChatbotSection, IChatbotValidate +{ + /// + /// Initializes a new instance of the class. + /// + public ChatbotMessageLine() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text of the message line. + public ChatbotMessageLine(string text) + { + Text = text; + } + + /// + /// Gets or sets the text of the message. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the style of the message text. + /// + [JsonPropertyName("style")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChatbotMessageStyle Style { get; set; } + + /// + /// Gets or sets a value indicating whether the message is editable. + /// + [JsonPropertyName("editable")] + public bool Editable { get; set; } + + /// + /// Gets or sets the link for the message. + /// Converts the entire message text into a link. + /// Should only be used if not using markdown. + /// For markdown, use the undocumented link text feature: <https://example.com|Link Text>. + /// + /// The link. + [JsonPropertyName("link")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Link { get; set; } + + /// + public void Validate(bool enableMarkdownSupport) + { + if (enableMarkdownSupport && Link != null) + { + throw new InvalidOperationException("Link property cannot be used with EnableMarkdownSupport"); + } + } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageStyle.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageStyle.cs new file mode 100644 index 00000000..323ae972 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageStyle.cs @@ -0,0 +1,31 @@ +using System.Drawing; +using System.Text.Json.Serialization; +using ZoomNet.Json; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A style applied to a message. +/// +public class ChatbotMessageStyle +{ + /// + /// Gets or sets the color of this message. + /// + [JsonPropertyName("color")] + [JsonConverter(typeof(NullableHexColorConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Color? Color { get; set; } + + /// + /// Gets or sets a value indicating whether the bold style is set for this message. + /// + [JsonPropertyName("bold")] + public bool Bold { get; set; } + + /// + /// Gets or sets a value indicating whether the italic style is set for this message. + /// + [JsonPropertyName("italic")] + public bool Italic { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageText.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageText.cs new file mode 100644 index 00000000..b4961780 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageText.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// The attachment information field. +/// +public class ChatbotMessageText +{ + /// + /// Initializes a new instance of the class. + /// + public ChatbotMessageText() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text of the message line. + public ChatbotMessageText(string text) + { + Text = text; + } + + /// + /// Gets or sets the text of the message. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// Gets or sets the style of the message text. + /// + [JsonPropertyName("style")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChatbotMessageStyle Style { get; set; } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs new file mode 100644 index 00000000..c80badc2 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text.Json.Serialization; +using ZoomNet.Json; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A message section that groups together multiple messages with a common sidebar. +/// +public class ChatbotSection : IChatbotBody, IChatbotValidate +{ + /// + /// Gets or sets the color of the sidebar for this section. + /// + [JsonPropertyName("sidebar_color")] + [JsonConverter(typeof(NullableHexColorConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Color? SidebarColor { get; set; } + + /// + /// Gets or sets the associated messages for this section. + /// + [JsonPropertyName("sections")] + public ICollection Sections { get; set; } + + /// + /// Gets or sets the footer for this section. + /// + [JsonPropertyName("footer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Footer { get; set; } + + /// + /// Gets or sets the URL of the footer icon for this section. + /// + [JsonPropertyName("footer_icon")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string FooterIcon { get; set; } + + /// + /// Gets or sets a timestamp to display in the footer. Unix timestamp format or use . + /// + /// + /// There may be a timezone conversion issue on Zoom's side. + /// The value here is one hour off the time displayed on the Zoom message. + /// + [JsonPropertyName("ts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? TimeStamp { get; set; } + + /// + /// Sets the timestamp from a value. + /// + /// + /// There may be a timezone conversion issue on Zoom's side. + /// The value here is one hour off the time displayed on the Zoom message. + /// + [JsonIgnore] + public DateTime TimeStampFromDateTime + { + set => TimeStamp = value.ToUnixTime(Internal.UnixTimePrecision.Milliseconds); + } + + /// + public void Validate(bool enableMarkdownSupport) + { + foreach (var section in Sections.OfType()) + { + section.Validate(enableMarkdownSupport); + } + } +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/IChatbotBody.cs b/Source/ZoomNet/Models/ChatbotMessage/IChatbotBody.cs new file mode 100644 index 00000000..4edb2df7 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/IChatbotBody.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A part of the message body. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(ChatbotActions), "actions")] +[JsonDerivedType(typeof(ChatbotAttachment), "attachments")] +[JsonDerivedType(typeof(ChatbotDropdownList), "select")] +[JsonDerivedType(typeof(ChatbotFormFields), "fields")] +[JsonDerivedType(typeof(ChatbotMessageLine), "message")] +[JsonDerivedType(typeof(ChatbotSection), "section")] +public interface IChatbotBody +{ +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/IChatbotSection.cs b/Source/ZoomNet/Models/ChatbotMessage/IChatbotSection.cs new file mode 100644 index 00000000..b04831a8 --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/IChatbotSection.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A part of the section body. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(ChatbotActions), "actions")] +[JsonDerivedType(typeof(ChatbotAttachment), "attachments")] +[JsonDerivedType(typeof(ChatbotDropdownList), "select")] +[JsonDerivedType(typeof(ChatbotFormFields), "fields")] +[JsonDerivedType(typeof(ChatbotMessageLine), "message")] +public interface IChatbotSection +{ +} diff --git a/Source/ZoomNet/Models/ChatbotMessage/IChatbotValidate.cs b/Source/ZoomNet/Models/ChatbotMessage/IChatbotValidate.cs new file mode 100644 index 00000000..c327ffef --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessage/IChatbotValidate.cs @@ -0,0 +1,16 @@ +using System; + +namespace ZoomNet.Models.ChatbotMessage; + +/// +/// A Chatbot message validator. +/// +public interface IChatbotValidate +{ + /// + /// Verify that the content is valid. + /// + /// True if the content has markdown syntax enabled. + /// Some or all of the content is invalid. + internal void Validate(bool enableMarkdownSupport); +} diff --git a/Source/ZoomNet/Models/ChatbotMessageInformation.cs b/Source/ZoomNet/Models/ChatbotMessageInformation.cs new file mode 100644 index 00000000..45b4b60e --- /dev/null +++ b/Source/ZoomNet/Models/ChatbotMessageInformation.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models; + +/// +/// A response from the Chatbot endpoints. +/// +public class ChatbotMessageInformation +{ + /// + /// Gets or sets the message ID. + /// + [JsonPropertyName("message_id")] + public string MessageId { get; set; } + + /// + /// Gets or sets the To JID. May be a channel or a user. + /// + [JsonPropertyName("to_jid")] + public string ToJId { get; set; } + + /// + /// Gets or sets the Robot JID. + /// + [JsonPropertyName("robot_jid")] + public string RobotJId { get; set; } + + /// + /// Gets or sets the sent time of the message. + /// + [JsonPropertyName("sent_time")] + public string SentTime { get; set; } +} diff --git a/Source/ZoomNet/Resources/Chatbot.cs b/Source/ZoomNet/Resources/Chatbot.cs new file mode 100644 index 00000000..7ec8a5a5 --- /dev/null +++ b/Source/ZoomNet/Resources/Chatbot.cs @@ -0,0 +1,151 @@ +using Pathoschild.Http.Client; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using ZoomNet.Models; +using ZoomNet.Models.ChatbotMessage; + +namespace ZoomNet.Resources; + +/// +/// Allows you to manage Chatbot messages. +/// +/// +/// See Zoom documentation for more information. +/// +public class Chatbot : IChatbot +{ + private readonly IClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + internal Chatbot(IClient client) + { + _client = client; + } + + /// + /// Delete a Chatbot message. + /// + /// The message ID. + /// The account ID to which the message was sent. + /// The JID of the user on whose behalf the message is being sent. Optional. + /// The robot JID. + /// The cancellation token. + /// + /// The async task. + /// + public Task DeleteMessageAsync(string messageId, string accountId, string userJId, string robotJId, CancellationToken cancellationToken = default) + { + return _client + .DeleteAsync($"im/chat/messages/{messageId}") + .WithArgument("account_id", accountId) + .WithArgument("user_jid", userJId) + .WithArgument("robot_jid", robotJId) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Send Chatbot message. + /// + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The simple text message to send. + /// True if the message contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task SendMessageAsync(string accountId, string toJId, string robotJId, string message, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default) + { + return SendMessageAsync(accountId, toJId, robotJId, new ChatbotContent() { Head = new ChatbotHeader(message) }, enableMarkdownSupport, cancellationToken); + } + + /// + /// Send Chatbot message. + /// + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The content of the message. + /// True if the content contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task SendMessageAsync(string accountId, string toJId, string robotJId, ChatbotContent content, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default) + { + content.Validate(enableMarkdownSupport); + + var data = new JsonObject + { + { "robot_jid", robotJId }, + { "to_jid", toJId }, + { "account_id", accountId }, + { "content", content }, + { "is_markdown_support", enableMarkdownSupport } + }; + + return _client + .PostAsync("im/chat/messages") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Edit a Chatbot message. + /// + /// The message ID of the message to edit. + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The simple text message to send. + /// True if the message contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task EditMessageAsync(string messageId, string accountId, string toJId, string robotJId, string message, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default) + { + return EditMessageAsync(messageId, accountId, toJId, robotJId, new ChatbotContent() { Head = new ChatbotHeader(message) }, enableMarkdownSupport, cancellationToken); + } + + /// + /// Edit a Chatbot message. + /// + /// The message ID of the message to edit. + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The content of the message. + /// True if the content contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task EditMessageAsync(string messageId, string accountId, string toJId, string robotJId, ChatbotContent content, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default) + { + content.Validate(enableMarkdownSupport); + + var data = new JsonObject + { + { "robot_jid", robotJId }, + { "to_jid", toJId }, + { "account_id", accountId }, + { "content", content }, + { "is_markdown_support", enableMarkdownSupport } + }; + + return _client + .PutAsync($"im/chat/messages/{messageId}") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } +} diff --git a/Source/ZoomNet/Resources/IChatbot.cs b/Source/ZoomNet/Resources/IChatbot.cs new file mode 100644 index 00000000..e314696f --- /dev/null +++ b/Source/ZoomNet/Resources/IChatbot.cs @@ -0,0 +1,86 @@ +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; +using ZoomNet.Models.ChatbotMessage; + +namespace ZoomNet.Resources; + +/// +/// Allows you to manage Chatbot messages. +/// +/// +/// See Zoom documentation for more information. +/// +public interface IChatbot +{ + /// + /// Delete a Chatbot message. + /// + /// The message ID. + /// The account ID to which the message was sent. + /// The JID of the user on whose behalf the message is being sent. Optional. + /// The robot JID. + /// The cancellation token. + /// + /// The async task. + /// + public Task DeleteMessageAsync(string messageId, string accountId, string userJId, string robotJId, CancellationToken cancellationToken = default); + + /// + /// Send a Chatbot message. + /// + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The simple text message to send. + /// True if the message contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task SendMessageAsync(string accountId, string toJId, string robotJId, string message, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default); + + /// + /// Send a Chatbot message. + /// + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The content of the message. + /// True if the content contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task SendMessageAsync(string accountId, string toJId, string robotJId, ChatbotContent content, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default); + + /// + /// Edit a Chatbot message. + /// + /// The message ID of the message to edit. + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The simple text message to send. + /// True if the message contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task EditMessageAsync(string messageId, string accountId, string toJId, string robotJId, string message, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default); + + /// + /// Edit a Chatbot message. + /// + /// The message ID of the message to edit. + /// The account ID to which the message was sent. + /// The JID of group channel or user to whom the message should be sent. + /// The robot JID. + /// The content of the message. + /// True if the content contains markdown syntax. + /// The cancellation token. + /// + /// The async task. + /// + public Task EditMessageAsync(string messageId, string accountId, string toJId, string robotJId, ChatbotContent content, bool enableMarkdownSupport = false, CancellationToken cancellationToken = default); +} diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index b982c1b8..483a01fa 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -161,6 +161,11 @@ public static string Version /// public ICallLogs CallLogs { get; private set; } + /// + /// Gets the resource which allows you to manage chatbot messages. + /// + public IChatbot Chatbot { get; private set; } + #endregion #region CTOR @@ -265,6 +270,7 @@ private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool d Dashboards = new Dashboards(_fluentClient); Reports = new Reports(_fluentClient); CallLogs = new CallLogs(_fluentClient); + Chatbot = new Chatbot(_fluentClient); } /// From 6605a9d591a0bc0f70fbcb30da1d428f11ee5181 Mon Sep 17 00:00:00 2001 From: jericho Date: Tue, 8 Aug 2023 11:04:51 -0400 Subject: [PATCH 02/16] Move unit tests to match the location of the class being tested --- .../ParticipantDeviceConverter.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) rename Source/ZoomNet.UnitTests/{Utilities => Json}/ParticipantDeviceConverter.cs (87%) diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs similarity index 87% rename from Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs rename to Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs index 3b1f459a..9f331437 100644 --- a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet.UnitTests/Json/ParticipantDeviceConverter.cs @@ -7,7 +7,7 @@ using ZoomNet.Json; using ZoomNet.Models; -namespace ZoomNet.UnitTests.Utilities +namespace ZoomNet.UnitTests.Json { public class ParticipantDeviceConverterTests { @@ -89,10 +89,8 @@ public void Read_single(string value, ParticipantDevice expectedValue) // Assert result.ShouldNotBeNull(); result.ShouldBeOfType(); - - var resultAsArray = (ParticipantDevice[])result; - resultAsArray.Length.ShouldBe(1); - resultAsArray[0].ShouldBe(expectedValue); + result.Length.ShouldBe(1); + result[0].ShouldBe(expectedValue); } [Fact] @@ -114,11 +112,9 @@ public void Read_multiple() // Assert result.ShouldNotBeNull(); result.ShouldBeOfType(); - - var resultAsArray = (ParticipantDevice[])result; - resultAsArray.Length.ShouldBe(2); - resultAsArray[0].ShouldBe(ParticipantDevice.Unknown); - resultAsArray[1].ShouldBe(ParticipantDevice.Phone); + result.Length.ShouldBe(2); + result[0].ShouldBe(ParticipantDevice.Unknown); + result[1].ShouldBe(ParticipantDevice.Phone); } } } From 9389ea4aed23165b822cbcdf6084d236d2dc5ecc Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 10 Aug 2023 18:50:15 -0400 Subject: [PATCH 03/16] Sync JsonSerializerContext with model classes --- .../Json/ZoomNetJsonSerializerContext.cs | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index c834a4d7..66bd1915 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -33,23 +33,23 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType))] [JsonSerializable(typeof(ZoomNet.Models.CallLogType))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAction), TypeInfoPropertyName = "ChatbotMessageChatbotAction")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActions), TypeInfoPropertyName = "ChatbotMessageChatbotActions")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle), TypeInfoPropertyName = "ChatbotMessageChatbotActionStyle")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachment), TypeInfoPropertyName = "ChatbotMessageChatbotAttachment")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachmentInformation), TypeInfoPropertyName = "ChatbotMessageChatbotAttachmentInformation")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotContent), TypeInfoPropertyName = "ChatbotMessageChatbotContent")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownItem), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownItem")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownList), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownList")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownStaticSource")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormField), TypeInfoPropertyName = "ChatbotMessageChatbotFormField")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormFields), TypeInfoPropertyName = "ChatbotMessageChatbotFormFields")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotHeader), TypeInfoPropertyName = "ChatbotMessageChatbotHeader")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageLine), TypeInfoPropertyName = "ChatbotMessageChatbotMessageLine")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageStyle), TypeInfoPropertyName = "ChatbotMessageChatbotMessageStyle")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageText), TypeInfoPropertyName = "ChatbotMessageChatbotMessageText")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection), TypeInfoPropertyName = "ChatbotMessageChatbotSection")] [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAction))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActions))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachment))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachmentInformation))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotContent))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownItem))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownList))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormField))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormFields))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotHeader))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageLine))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageStyle))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageText))] - [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannel))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole))] @@ -297,6 +297,23 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType[]))] [JsonSerializable(typeof(ZoomNet.Models.CallLogType[]))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAction[]), TypeInfoPropertyName = "ChatbotMessageChatbotActionArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActions[]), TypeInfoPropertyName = "ChatbotMessageChatbotActionsArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle[]), TypeInfoPropertyName = "ChatbotMessageChatbotActionStyleArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachment[]), TypeInfoPropertyName = "ChatbotMessageChatbotAttachmentArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotAttachmentInformation[]), TypeInfoPropertyName = "ChatbotMessageChatbotAttachmentInformationArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotContent[]), TypeInfoPropertyName = "ChatbotMessageChatbotContentArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownItem[]), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownItemArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownList[]), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownListArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource[]), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownStaticSourceArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormField[]), TypeInfoPropertyName = "ChatbotMessageChatbotFormFieldArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotFormFields[]), TypeInfoPropertyName = "ChatbotMessageChatbotFormFieldsArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotHeader[]), TypeInfoPropertyName = "ChatbotMessageChatbotHeaderArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageLine[]), TypeInfoPropertyName = "ChatbotMessageChatbotMessageLineArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageStyle[]), TypeInfoPropertyName = "ChatbotMessageChatbotMessageStyleArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotMessageText[]), TypeInfoPropertyName = "ChatbotMessageChatbotMessageTextArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection[]), TypeInfoPropertyName = "ChatbotMessageChatbotSectionArray")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannel[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole[]))] @@ -532,6 +549,8 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoExtensionType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogTransferInfoNumberType?))] [JsonSerializable(typeof(ZoomNet.Models.CallLogType?))] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle?), TypeInfoPropertyName = "ChatbotMessageChatbotActionStyleNullable")] + [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource?), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownStaticSourceNullable")] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole?))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelType?))] [JsonSerializable(typeof(ZoomNet.Models.ChatMentionType?))] From 96a9bccbcf8ffd2b3fb1f6e670723c8e2aa518e3 Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 17 Aug 2023 09:42:31 -0400 Subject: [PATCH 04/16] Upgrade nuget packages --- Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj | 2 +- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index 7ea09734..d851df5a 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -14,7 +14,7 @@ - + diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 633ea3a5..7403e63c 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From b7b0f4a3c6cd45008062ee6b83bea48e81c5177b Mon Sep 17 00:00:00 2001 From: jericho Date: Sat, 5 Aug 2023 14:10:30 -0400 Subject: [PATCH 05/16] Remove unused 'using' statements --- Source/ZoomNet/Resources/Chatbot.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Chatbot.cs b/Source/ZoomNet/Resources/Chatbot.cs index 7ec8a5a5..c1719afe 100644 --- a/Source/ZoomNet/Resources/Chatbot.cs +++ b/Source/ZoomNet/Resources/Chatbot.cs @@ -2,7 +2,6 @@ using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using ZoomNet.Models; using ZoomNet.Models.ChatbotMessage; From 54ee66e3afe17bd6107a9814306d8cd3d571c9aa Mon Sep 17 00:00:00 2001 From: jericho Date: Sun, 6 Aug 2023 12:51:00 -0400 Subject: [PATCH 06/16] (GH-313) Readme to explain how to configure integration tests --- Source/ZoomNet.IntegrationTests/README.md | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Source/ZoomNet.IntegrationTests/README.md diff --git a/Source/ZoomNet.IntegrationTests/README.md b/Source/ZoomNet.IntegrationTests/README.md new file mode 100644 index 00000000..75d4d6e9 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/README.md @@ -0,0 +1,82 @@ +# ZoomNet integration tests + +Integration tests allow you to run live tests against the Zoom system to test the ZoomNet library. There are three main scenarios (we call the "suites"): +- General API tests +- Chatbot tests +- WebSocket client tests + + +## Configuration + +Before you can run the integration tests, there are a few settings you must configure in addition to a few environment variables you must set. + +### Step 1: configure your proxy tool + +```csharp +// Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. +var useProxy = true; +``` + +Line 38 (shown above) in `TestsRunner.cs` allows you to indicate whether you want to send all HTTP requests through a proxy running on your machine such as [Telerik's Fiddler](https://www.telerik.com/fiddler) for example. +I mention Fiddler simply because it's my preferred proxy tool, but feel free to use an alternative tool if you prefer. +By the way, there are two versions of Fiddler available for download on Telerik's web site: "Fiddler Everywhere" and "Fiddler Classic". +Both are very capable proxys but I personally prefer the classic version, therefore this is the one I recommend. +A proxy tool is very helpful because it allows you to see the content of each request and the content of their corresponding response from the Zoom API. This is helpful to investigate situations where you are not getting the result you were expecting. Providing the content of the request and/or the response is + +```csharp +// By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 +var proxyPort = 8888; +``` + +Line 41 (shown above) in `TestsRunner.cs` allows you to specify the port number used by your proxy tools. For instance, Fiddler classic uses port 8888 by default while Fiddler Everywhere uses 8886 by default. +Most proxys allow you to customize the port number if, for some reason, you are not satisfied with their default. Feel free to customize this port number if desired but make sure to update the `proxyPort` value accordingly. + +> :warning: You will get a `No connection could be made because the target machine actively refused it` exception when attempting to run the integration tests if you configure `useProxy` to `true` and your proxy is not running on you machine or the `proxyPort` value does not correspond to the port used by your proxy. + +### Step 2: configure which test "suite" you want to run + +```csharp +// What tests do you want to run +var testType = TestType.Api; +``` + +Line 44 (shown above) in `TestsRunner.cs` allows you to specify which of the test suites you want to run. As of this writing, there are three suites to choose from: the first only allows you to invoke a multitude of endpoints in the Zoom RST API, the second one allows you to test API calls that are intended to be invoked by a Chatbot app and the third one allows you to receive and parse webhook sent to you by Zoom via websockets. + + +### Step 3: configure which authentication flow you want to use + +```csharp +// Which connection type do you want to use? +var connectionType = ConnectionType.OAuthServerToServer; +``` + +Line 47 (shown above) in `TestsRunner.cs` allows you to specify which authentication flow you want to use. + +> :warning: ZoomNet allows you to select JWT as your authentication flow but keep in mind that Zoom has announced they are retiring this authentication mechanism and the projected end-of-life for JWT apps is September 1, 2023. + +### Step 4: environment variables + +The ZoomNet integration tests rely on various environment variables to store values that are specific to your environment, such as your client id and client secret for example. The exact list and name of these environment variables vary depending on the authentication flow you selected in "Step 3". The chart below lists all the environment variables for each connection type: + +| Connection Type | Environment variables | +|----------|-------------------| +| JWT | ZOOM_JWT_APIKEY
ZOOM_JWT_APISECRET | +| OAuthAuthorizationCode | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_AUTHORIZATIONCODE
**Note** the authorization code is intended to be used only once therefore the environment variable is cleared after the first use and a refresh token is used subsequently | +| OAuthRefreshToken | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_REFRESHTOKEN
**Note** The refresh token is initially created when an authorization code is used | +| OAuthClientCredentials | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN *(optional)*
**Note** If the access token is omitted, a new one will be requested and the environment variable will be updated accordingly | +| OAuthServerToServer | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_ACCOUNTID
ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN *(optional)*
**Note** If the access token is omitted, a new one will be requested and the environment variable will be updated accordingly | + +In addition to the environment variables listed in the table above, there are a few environment variable that are specific to each test suite that you selected in "Step 1": + +| Connection Type | Environment variables | +|----------|-------------------| +| Api | *no additional environment variable necessary* | +| WebSockets | ZOOM_WEBSOCKET_SUBSCRIPTIONID | +| Chatbot | ZOOM_OAUTH_ACCOUNTID
ZOOM_CHATBOT_ROBOTJID (this is your Chatbot app's JID)
ZOOM_CHATBOT_TOJID (this is the JID of the user who will receive the messages sent during the integration tests) | + +Here's a convenient sample PowerShell script that demonstrates how to set some of the necessary environment variables: + +```powershell +[Environment]::SetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", ".", "User") +[Environment]::SetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", "", "User") +``` From 9cf6777c900b35c53affa37cc95af0eea99bd1bd Mon Sep 17 00:00:00 2001 From: jericho Date: Sun, 6 Aug 2023 15:39:19 -0400 Subject: [PATCH 07/16] (GH-313) Test suites --- .../ZoomNet.IntegrationTests/ResultCodes.cs | 9 + Source/ZoomNet.IntegrationTests/TestSuite.cs | 117 ++++++++ .../TestSuites/ApiTestSuite.cs | 30 ++ .../TestSuites/ChatbotTestSuite.cs | 20 ++ .../TestSuites/WebSocketsTestSuite.cs | 50 +++ .../ZoomNet.IntegrationTests/Tests/Chatbot.cs | 19 +- .../ZoomNet.IntegrationTests/TestsRunner.cs | 284 ++++++------------ 7 files changed, 328 insertions(+), 201 deletions(-) create mode 100644 Source/ZoomNet.IntegrationTests/ResultCodes.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs diff --git a/Source/ZoomNet.IntegrationTests/ResultCodes.cs b/Source/ZoomNet.IntegrationTests/ResultCodes.cs new file mode 100644 index 00000000..77604b13 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/ResultCodes.cs @@ -0,0 +1,9 @@ +namespace ZoomNet.IntegrationTests +{ + internal enum ResultCodes + { + Success = 0, + Exception = 1, + Cancelled = 1223 + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuite.cs new file mode 100644 index 00000000..2d951c70 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuite.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace ZoomNet.IntegrationTests +{ + internal abstract class TestSuite + { + private const int MAX_ZOOM_API_CONCURRENCY = 5; + private const int TEST_NAME_MAX_LENGTH = 25; + private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; + + public ILoggerFactory LoggerFactory { get; init; } + + public IConnectionInfo ConnectionInfo { get; init; } + + public IWebProxy Proxy { get; init; } + + public Type[] Tests { get; init; } + + public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests) + { + ConnectionInfo = connectionInfo; + Proxy = proxy; + LoggerFactory = loggerFactory; + Tests = tests; + } + + public virtual async Task RunTestsAsync() + { + // 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()); + + // Get my user and permisisons + var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); + var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); + Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + + // Execute the async tests in parallel (with max degree of parallelism) + var results = await Tests.ForEachAsync( + async testType => + { + var log = new StringWriter(); + + try + { + var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); + await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); + } + catch (OperationCanceledException) + { + await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + } + catch (Exception e) + { + var exceptionMessage = e.GetBaseException().Message; + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + } + finally + { + lock (Console.Out) + { + Console.Out.WriteLine(log.ToString()); + } + } + }, MAX_ZOOM_API_CONCURRENCY) + .ConfigureAwait(false); + + // Display summary + var summary = new StringWriter(); + await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + + var nameMaxLength = Math.Min(results.Max(r => r.TestName.Length), TEST_NAME_MAX_LENGTH); + foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray()) + { + await summary.WriteLineAsync($"{TestName.ToExactLength(nameMaxLength)} : {Message}").ConfigureAwait(false); + } + + 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)) + { + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) return ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = ResultCodes.Cancelled; + else resultCode = results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + } + + return resultCode; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs new file mode 100644 index 00000000..54f7bef2 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using ZoomNet.IntegrationTests.Tests; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class ApiTestSuite : TestSuite + { + private static readonly Type[] _tests = new Type[] + { + typeof(Accounts), + typeof(CallLogs), + typeof(Chat), + typeof(CloudRecordings), + typeof(Contacts), + typeof(Dashboards), + typeof(Meetings), + typeof(Roles), + typeof(Users), + typeof(Webinars), + typeof(Reports), + }; + + public ApiTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, _tests) + { + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs new file mode 100644 index 00000000..93210eb2 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using ZoomNet.IntegrationTests.Tests; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class ChatbotTestSuite : TestSuite + { + private static readonly Type[] _tests = new Type[] + { + typeof(Chatbot), + }; + + public ChatbotTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, _tests) + { + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs new file mode 100644 index 00000000..4b9c9560 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models.Webhooks; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class WebSocketsTestSuite : TestSuite + { + private readonly string _subscriptionId; + + public WebSocketsTestSuite(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, Array.Empty()) + { + _subscriptionId = subscriptionId; + } + + public override async Task RunTestsAsync() + { + var logger = base.LoggerFactory.CreateLogger(); + var eventProcessor = new Func(async (webhookEvent, cancellationToken) => + { + if (!cancellationToken.IsCancellationRequested) + { + logger.LogInformation("Processing {eventType} event...", webhookEvent.EventType); + await Task.Delay(1, cancellationToken).ConfigureAwait(false); // This async call gets rid of "CS1998 This async method lacks 'await' operators and will run synchronously". + } + }); + + // 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); + exitEvent.WaitOne(); + + return ResultCodes.Success; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs index a2894f3d..f851048e 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs @@ -12,20 +12,22 @@ namespace ZoomNet.IntegrationTests.Tests; public class Chatbot : IIntegrationTest { /// - public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, - CancellationToken cancellationToken) + public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, CancellationToken cancellationToken) { - var accountId = myUser?.AccountId ?? "{accountId}"; - var robotJId = "{robotId}@xmpp.zoom.us"; - var toJId = "{userId}@xmpp.zoom.us"; // User - //var toJId = "{channelId}@conference.xmpp.zoom.us"; // Channel + var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); + var robotJId = Environment.GetEnvironmentVariable("ZOOM_CHATBOT_ROBOTJID", EnvironmentVariableTarget.User); + var toJId = Environment.GetEnvironmentVariable("ZOOM_CHATBOT_TOJID", EnvironmentVariableTarget.User); + var response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, "Test message", false, cancellationToken); await log.WriteLineAsync(response.MessageId); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, "*Updated test message*", true, cancellationToken); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, cancellationToken); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, new ChatbotContent() { @@ -126,6 +128,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie }, true, cancellationToken); await log.WriteLineAsync(response.MessageId); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, new ChatbotContent() { @@ -228,7 +231,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie } }, true, cancellationToken); await Task.Delay(1000, cancellationToken); - response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, - cancellationToken); + + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, cancellationToken); } } diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 72d283ca..0c945d20 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -1,38 +1,27 @@ using Microsoft.Extensions.Logging; using System; -using System.IO; -using System.Linq; using System.Net; -using System.Threading; using System.Threading.Tasks; -using ZoomNet.IntegrationTests.Tests; -using ZoomNet.Models.Webhooks; +using ZoomNet.IntegrationTests.TestSuites; namespace ZoomNet.IntegrationTests { internal class TestsRunner { - private const int MAX_ZOOM_API_CONCURRENCY = 5; - private const int TEST_NAME_MAX_LENGTH = 25; - private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; - - private enum ResultCodes - { - Success = 0, - Exception = 1, - Cancelled = 1223 - } - private enum TestType { - Api = 0, - WebSockets = 1, + Api, + WebSockets, + Chatbot, } private enum ConnectionType { - Jwt = 1, - OAuth = 2, + Jwt, // Zoom disabled the ability to create new JWT apps on June 1, 2023. The projected end-of-life for JWT apps is September 1, 2023. + OAuthAuthorizationCode, // Gets authorization code and sets refresh token. + OAuthRefreshToken, // Gets and sets refresh token and access token. + OAuthClientCredentials, // Gets and sets access token. For cleanliness, it should use a different access token environment variable so they don't cross contaminate. + OAuthServerToServer, // Gets the account id and access token and sets access token. Same as above. } private readonly ILoggerFactory _loggerFactory; @@ -42,216 +31,125 @@ public TestsRunner(ILoggerFactory loggerFactory) _loggerFactory = loggerFactory; } - public Task RunAsync() + public async Task RunAsync() { // ----------------------------------------------------------------------------- - // Do you want to proxy requests through Fiddler? Can be useful for debugging. - var useFiddler = true; - var fiddlerPort = 8888; // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + // Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. + var useProxy = true; - // What tests do you want to run and which connection type do you want to use? + // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + var proxyPort = 8888; + + // What tests do you want to run var testType = TestType.Api; - var connectionType = ConnectionType.OAuth; + + // Which connection type do you want to use? + 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 (very useful for debugging) - var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null; + // Configure the proxy if desired + var proxy = useProxy ? new WebProxy($"http://localhost:{proxyPort}") : null; - // Run tests either with a JWT or OAuth connection - return connectionType switch - { - ConnectionType.Jwt => RunTestsWithJwtConnectionAsync(testType, proxy), - ConnectionType.OAuth => RunTestsWithOAuthConnectionAsync(testType, proxy), - _ => throw new Exception("Unknwon connection type"), - }; - } - - private Task RunTestsWithJwtConnectionAsync(TestType testType, IWebProxy proxy) - { - if (testType != TestType.Api) throw new Exception("Only API tests are supported with JWT"); + // Get the connection info and test suite + var connectionInfo = GetConnectionInfo(connectionType); + var testSuite = GetTestSuite(connectionInfo, testType, proxy, _loggerFactory); - var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User); - var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User); - var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret); + // Run the tests + var resultCode = await testSuite.RunTestsAsync().ConfigureAwait(false); - return RunApiTestsAsync(connectionInfo, proxy); + // Return result + return (int)resultCode; } - private Task RunTestsWithOAuthConnectionAsync(TestType testType, IWebProxy proxy) + private static IConnectionInfo GetConnectionInfo(ConnectionType connectionType) { - var clientId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", EnvironmentVariableTarget.User); - var clientSecret = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", EnvironmentVariableTarget.User); - var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); - var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User); - var subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User); - - IConnectionInfo connectionInfo; - - // Server-to-Server OAuth - if (!string.IsNullOrEmpty(accountId)) + // Jwt + if (connectionType == ConnectionType.Jwt) { - connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId); - } - - // Standard OAuth - else - { - connectionInfo = OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken, - (newRefreshToken, newAccessToken) => - { - Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - }); - - //var authorizationCode = "<-- the code generated by Zoom when the app is authorized by the user -->"; - //connectionInfo = OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode, - // (newRefreshToken, newAccessToken) => - // { - // Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - // }); + var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User); + var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User); + return new JwtConnectionInfo(apiKey, apiSecret); } - // Execute either the API or Websocket tests - return testType switch - { - TestType.Api => RunApiTestsAsync(connectionInfo, proxy), - TestType.WebSockets => RunWebSocketTestsAsync(connectionInfo, subscriptionId, proxy), - _ => throw new Exception("Unknwon test type"), - }; - } - - private async Task RunApiTestsAsync(IConnectionInfo connectionInfo, IWebProxy proxy) - { - // Configure cancellation - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - }; + // OAuth + var clientId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", EnvironmentVariableTarget.User); + var clientSecret = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", EnvironmentVariableTarget.User); - // Configure ZoomNet client - var client = new ZoomClient(connectionInfo, proxy, null, _loggerFactory.CreateLogger()); + if (string.IsNullOrEmpty(clientId)) throw new Exception("You must set the ZOOM_OAUTH_CLIENTID environment variable before you can run integration tests."); + if (string.IsNullOrEmpty(clientSecret)) throw new Exception("You must set the ZOOM_OAUTH_CLIENTSECRET environment variable before you can run integration tests."); - // These are the integration tests that we will execute - var integrationTests = new Type[] + switch (connectionType) { - typeof(Accounts), - typeof(CallLogs), - typeof(Chat), - typeof(CloudRecordings), - typeof(Contacts), - typeof(Dashboards), - typeof(Meetings), - typeof(Roles), - typeof(Users), - typeof(Webinars), - typeof(Reports), - typeof(Chatbot) - }; - - // Get my user and permisisons - var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); - var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); - Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + case ConnectionType.OAuthAuthorizationCode: + { + var authorizationCode = Environment.GetEnvironmentVariable("ZOOM_OAUTH_AUTHORIZATIONCODE", EnvironmentVariableTarget.User); - // Execute the async tests in parallel (with max degree of parallelism) - var results = await integrationTests.ForEachAsync( - async testType => - { - var log = new StringWriter(); + if (string.IsNullOrEmpty(authorizationCode)) throw new Exception("Either the autorization code environment variable has not been set or it's no longer available because you already used it once."); - try + return OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode, + (newRefreshToken, newAccessToken) => + { + // Clear the authorization code because it's intended to be used only once + Environment.SetEnvironmentVariable("ZOOM_OAUTH_AUTHORIZATIONCODE", string.Empty, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + }); + } + case ConnectionType.OAuthRefreshToken: { - var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); + var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User); + if (string.IsNullOrEmpty(refreshToken)) throw new Exception("You must set the ZOOM_OAUTH_REFRESHTOKEN environment variable before you can run integration tests."); + + return OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + }); } - catch (OperationCanceledException) + case ConnectionType.OAuthClientCredentials: { - await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + var accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN", EnvironmentVariableTarget.User); + + return OAuthConnectionInfo.WithClientCredentials(clientId, clientSecret, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); } - catch (Exception e) + case ConnectionType.OAuthServerToServer: { - var exceptionMessage = e.GetBaseException().Message; - await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); + var accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN", EnvironmentVariableTarget.User); + + return OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); } - finally + default: { - lock (Console.Out) - { - Console.Out.WriteLine(log.ToString()); - } + throw new Exception("Unknwon connection type"); } - }, MAX_ZOOM_API_CONCURRENCY) - .ConfigureAwait(false); - - // Display summary - var summary = new StringWriter(); - await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); - await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); - - var nameMaxLength = Math.Min(results.Max(r => r.TestName.Length), TEST_NAME_MAX_LENGTH); - foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray()) - { - await summary.WriteLineAsync($"{TestName.ToExactLength(nameMaxLength)} : {Message}").ConfigureAwait(false); - } - - 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 = (int)ResultCodes.Success; - if (results.Any(result => result.ResultCode != ResultCodes.Success)) - { - if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; - else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; - else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; - } - - return resultCode; + }; } - private async Task RunWebSocketTestsAsync(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy) + private static TestSuite GetTestSuite(IConnectionInfo connectionInfo, TestType testType, IWebProxy proxy, ILoggerFactory loggerFactory) { - var logger = _loggerFactory.CreateLogger(); - var eventProcessor = new Func(async (webhookEvent, cancellationToken) => + switch (testType) { - if (!cancellationToken.IsCancellationRequested) - { - logger.LogInformation("Processing {eventType} event...", webhookEvent.EventType); - await Task.Delay(1, cancellationToken).ConfigureAwait(false); // This async call gets rid of "CS1998 This async method lacks 'await' operators and will run synchronously". - } - }); - - // 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(); + case TestType.Api: return new ApiTestSuite(connectionInfo, proxy, loggerFactory); + case TestType.Chatbot: return new ChatbotTestSuite(connectionInfo, proxy, loggerFactory); + case TestType.WebSockets: + { + var subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User); + return new WebSocketsTestSuite(connectionInfo, subscriptionId, proxy, loggerFactory); + } + default: throw new Exception("Unknwon test type"); }; - - // Start the websocket client - using var client = new ZoomWebSocketClient(connectionInfo, subscriptionId, eventProcessor, proxy, logger); - await client.StartAsync(cts.Token).ConfigureAwait(false); - exitEvent.WaitOne(); - - return (int)ResultCodes.Success; } } } From c4c70daa64c83bbf3de909fd74404b91a9d6e4df Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 7 Aug 2023 10:46:55 -0400 Subject: [PATCH 08/16] (GH-313) Add boolean to indicate if we want the current user to be fetched prior to running integration tests. This is necessary because in some scenarios (such as when running Chatbot tests) we do not have access to Zoom's REST API --- Source/ZoomNet.IntegrationTests/TestSuite.cs | 20 ++++++++++++++----- .../TestSuites/ApiTestSuite.cs | 2 +- .../TestSuites/ChatbotTestSuite.cs | 2 +- .../TestSuites/WebSocketsTestSuite.cs | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/TestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuite.cs index 2d951c70..eaaafd3b 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuite.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using ZoomNet.Models; namespace ZoomNet.IntegrationTests { @@ -22,12 +23,15 @@ internal abstract class TestSuite public Type[] Tests { get; init; } - public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests) + public bool FetchCurrentUserInfo { get; init; } + + public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests, bool fetchCurrentUserInfo) { ConnectionInfo = connectionInfo; Proxy = proxy; LoggerFactory = loggerFactory; Tests = tests; + FetchCurrentUserInfo = fetchCurrentUserInfo; } public virtual async Task RunTestsAsync() @@ -44,9 +48,15 @@ public virtual async Task RunTestsAsync() var client = new ZoomClient(ConnectionInfo, Proxy, null, LoggerFactory.CreateLogger()); // Get my user and permisisons - var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); - var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); - Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + User currentUser = null; + string[] currentUserPermissions = Array.Empty(); + + if (FetchCurrentUserInfo) + { + currentUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); + currentUserPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); + Array.Sort(currentUserPermissions); // Sort permissions alphabetically for convenience + } // Execute the async tests in parallel (with max degree of parallelism) var results = await Tests.ForEachAsync( @@ -57,7 +67,7 @@ public virtual async Task RunTestsAsync() try { var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); + await integrationTest.RunAsync(currentUser, currentUserPermissions, client, log, cts.Token).ConfigureAwait(false); return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); } catch (OperationCanceledException) diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs index 54f7bef2..26829f35 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs @@ -23,7 +23,7 @@ internal class ApiTestSuite : TestSuite }; public ApiTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, _tests) + base(connectionInfo, proxy, loggerFactory, _tests, true) { } } diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs index 93210eb2..1b95fa18 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs @@ -13,7 +13,7 @@ internal class ChatbotTestSuite : TestSuite }; public ChatbotTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, _tests) + base(connectionInfo, proxy, loggerFactory, _tests, false) { } } diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs index 4b9c9560..9146773f 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs @@ -12,7 +12,7 @@ internal class WebSocketsTestSuite : TestSuite private readonly string _subscriptionId; public WebSocketsTestSuite(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, Array.Empty()) + base(connectionInfo, proxy, loggerFactory, Array.Empty(), false) { _subscriptionId = subscriptionId; } From d9ade4b4150b9657e880b80f7e5c305869675c9f Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 17 Aug 2023 09:44:41 -0400 Subject: [PATCH 09/16] Fix typo in comment --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 0c945d20..e4f2d2c2 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -37,7 +37,7 @@ public async Task RunAsync() // Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. var useProxy = true; - // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + // By default Fiddler Classic uses port 8888 and Fiddler Everywhere uses port 8866 var proxyPort = 8888; // What tests do you want to run From a90a4e65828c4627e2acae592a102dd0b8def7ca Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 28 Aug 2023 11:17:03 -0400 Subject: [PATCH 10/16] Switch to NSubstitute --- Source/ZoomNet.UnitTests/MockSystemClock.cs | 17 ++++++++--------- .../ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Source/ZoomNet.UnitTests/MockSystemClock.cs b/Source/ZoomNet.UnitTests/MockSystemClock.cs index 72574193..f5580646 100644 --- a/Source/ZoomNet.UnitTests/MockSystemClock.cs +++ b/Source/ZoomNet.UnitTests/MockSystemClock.cs @@ -1,20 +1,19 @@ -using Moq; using System; using ZoomNet.Utilities; namespace ZoomNet.UnitTests { - internal class MockSystemClock : Mock + internal class MockSystemClock : ISystemClock { - public MockSystemClock(DateTime currentDateTime) : - this(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day, currentDateTime.Hour, currentDateTime.Minute, currentDateTime.Second, currentDateTime.Millisecond) - { } + private readonly DateTime _now; - public MockSystemClock(int year, int month, int day, int hour, int minute, int second, int millisecond) : - base(MockBehavior.Strict) + public DateTime Now => _now; + + public DateTime UtcNow => _now; + + public MockSystemClock(int year, int month, int day, int hour, int minute, int second, int millisecond) { - SetupGet(m => m.Now).Returns(new DateTime(year, month, day, hour, minute, second, millisecond, DateTimeKind.Utc)); - SetupGet(m => m.UtcNow).Returns(new DateTime(year, month, day, hour, minute, second, millisecond, DateTimeKind.Utc)); + _now = new DateTime(year, month, day, hour, minute, second, millisecond, DateTimeKind.Utc); } } } diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 7403e63c..4a5317e2 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive
- + From ff82cfee126897caa40d3a78b0532043fdc4d8c2 Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 28 Aug 2023 20:13:23 -0400 Subject: [PATCH 11/16] Add NSubstitute analyzer --- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 4a5317e2..2b542899 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -14,6 +14,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 2e06e9b226efe0595da0b004396ca79f8eec9865 Mon Sep 17 00:00:00 2001 From: jericho Date: Tue, 29 Aug 2023 14:22:52 -0400 Subject: [PATCH 12/16] Bring the DiagnosticHandler in line with the one in StrongGrid --- Source/ZoomNet/Extensions/Internal.cs | 5 ++- Source/ZoomNet/Utilities/DiagnosticHandler.cs | 39 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 43c4a8d2..908c4ecb 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -21,6 +21,7 @@ using ZoomNet.Json; using ZoomNet.Models; using ZoomNet.Utilities; +using static ZoomNet.Utilities.DiagnosticHandler; namespace ZoomNet { @@ -649,10 +650,10 @@ internal static IEnumerable> ParseQuerystring(this return querystringParameters; } - internal static (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) GetDiagnosticInfo(this IResponse response) + internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response) { var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME); - DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo); + DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo); return diagnosticInfo; } diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index c585bcde..6ec357af 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -18,6 +18,25 @@ namespace ZoomNet.Utilities /// internal class DiagnosticHandler : IHttpFilter { + internal class DiagnosticInfo + { + public WeakReference RequestReference { get; set; } + + public string Diagnostic { get; set; } + + public long RequestTimestamp { get; set; } + + public long ResponseTimestamp { get; set; } + + public DiagnosticInfo(WeakReference requestReference, string diagnostic, long requestTimestamp, long responseTimestamp) + { + RequestReference = requestReference; + Diagnostic = diagnostic; + RequestTimestamp = requestTimestamp; + ResponseTimestamp = responseTimestamp; + } + } + #region FIELDS internal const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-Diagnostic-Id"; @@ -29,7 +48,7 @@ internal class DiagnosticHandler : IHttpFilter #region PROPERTIES - internal static ConcurrentDictionary RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimeStamp)> DiagnosticsInfo { get; } = new ConcurrentDictionary, string, long, long)>(); + internal static ConcurrentDictionary DiagnosticsInfo { get; } = new(); #endregion @@ -59,12 +78,12 @@ public void OnRequest(IRequest request) var diagnostic = new StringBuilder(); diagnostic.AppendLine("REQUEST:"); - diagnostic.AppendLine($" {httpRequest.Method.Method} {httpRequest.RequestUri}"); + 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 WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); + DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); } /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. @@ -76,7 +95,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException) var httpResponse = response.Message; var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME); - if (DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimestamp) diagnosticInfo)) + if (DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo)) { var updatedDiagnostic = new StringBuilder(diagnosticInfo.Diagnostic); try @@ -118,8 +137,8 @@ public void OnResponse(IResponse response, bool httpErrorAsException) DiagnosticsInfo.TryUpdate( diagnosticId, - (diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp), - (diagnosticInfo.RequestReference, diagnosticInfo.Diagnostic, diagnosticInfo.RequestTimestamp, diagnosticInfo.ResponseTimestamp)); + new DiagnosticInfo(diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp), + diagnosticInfo); } } @@ -130,7 +149,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException) #region PRIVATE METHODS - private void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders) + private static void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders) { if (httpHeaders != null) { @@ -148,7 +167,7 @@ private void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders) } } - private void LogContent(StringBuilder diagnostic, HttpContent httpContent) + private static void LogContent(StringBuilder diagnostic, HttpContent httpContent) { if (httpContent == null) { @@ -172,14 +191,14 @@ private void LogContent(StringBuilder diagnostic, HttpContent httpContent) } } - private void Cleanup() + private static void Cleanup() { try { // Remove diagnostic information for requests that have been garbage collected foreach (string key in DiagnosticHandler.DiagnosticsInfo.Keys.ToArray()) { - if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo)) + if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out DiagnosticInfo diagnosticInfo)) { if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request)) { From 8cf264287db11106efdf6d3be5a2172e9d3e392f Mon Sep 17 00:00:00 2001 From: Eclyocy Date: Fri, 22 Sep 2023 03:27:44 +0300 Subject: [PATCH 13/16] Feature/319 phone call recordings and transcripts (#320) * feat: Added Zoom Phone call recordings endpoints: * get recording by call ID; * get recording transcript by recording ID. * chore: Added test for retrieve phone call recording transcript. * fix: Changed JsonObject to a custom class. --- .../Models/PhoneCallRecordings.cs | 275 ++++++++++++++++++ .../Resources/PhoneCallRecordings.cs | 75 +++++ .../ZoomNet.UnitTests/WebhookParserTests.cs | 4 +- Source/ZoomNet/IZoomClient.cs | 5 + .../Json/ZoomNetJsonSerializerContext.cs | 28 ++ Source/ZoomNet/Models/PhoneCallRecording.cs | 129 ++++++++ .../PhoneCallRecordingCalleeNumberType.cs | 23 ++ .../PhoneCallRecordingCallerNumberType.cs | 18 ++ .../PhoneCallRecordingDisclaimerStatus.cs | 23 ++ .../ZoomNet/Models/PhoneCallRecordingOwner.cs | 37 +++ .../PhoneCallRecordingOwnerExtensionStatus.cs | 22 ++ .../Models/PhoneCallRecordingOwnerType.cs | 22 ++ .../Models/PhoneCallRecordingTranscript.cs | 69 +++++ ...CallRecordingTranscriptTimelineFraction.cs | 39 +++ ...honeCallRecordingTranscriptTimelineUser.cs | 44 +++ .../ZoomNet/Models/PhoneCallRecordingType.cs | 22 ++ Source/ZoomNet/Models/PhoneCallUser.cs | 20 ++ Source/ZoomNet/Resources/IPhone.cs | 50 ++++ Source/ZoomNet/Resources/Phone.cs | 54 ++++ Source/ZoomNet/ZoomClient.cs | 4 + 20 files changed, 961 insertions(+), 2 deletions(-) create mode 100644 Source/ZoomNet.UnitTests/Models/PhoneCallRecordings.cs create mode 100644 Source/ZoomNet.UnitTests/Resources/PhoneCallRecordings.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecording.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingCalleeNumberType.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingCallerNumberType.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingDisclaimerStatus.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingOwner.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingOwnerExtensionStatus.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingOwnerType.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingTranscript.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineFraction.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineUser.cs create mode 100644 Source/ZoomNet/Models/PhoneCallRecordingType.cs create mode 100644 Source/ZoomNet/Models/PhoneCallUser.cs create mode 100644 Source/ZoomNet/Resources/IPhone.cs create mode 100644 Source/ZoomNet/Resources/Phone.cs diff --git a/Source/ZoomNet.UnitTests/Models/PhoneCallRecordings.cs b/Source/ZoomNet.UnitTests/Models/PhoneCallRecordings.cs new file mode 100644 index 00000000..f7b79345 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/PhoneCallRecordings.cs @@ -0,0 +1,275 @@ +using Shouldly; +using System; +using System.Text.Json; +using Xunit; +using ZoomNet.Json; +using ZoomNet.Models; + +namespace ZoomNet.UnitTests.Models +{ + public class PhoneCallRecordingsTests + { + #region constants + + internal const string PHONE_CALL_RECORDING = @"{ + ""id"": ""1234abcd5678efgh9012ijkl3456mnop"", + ""caller_number"": ""+12345678901"", + ""caller_number_type"": 2, + ""caller_name"": ""12345678901"", + ""callee_number"": ""123"", + ""callee_number_type"": 1, + ""callee_name"": ""Callee Name"", + ""direction"": ""inbound"", + ""duration"": 25, + ""download_url"": ""https://zoom.us/v2/phone/recording/download/Id_abc123DEF456ghi789J"", + ""file_url"": ""https://file.zoom.us/file?business=phone&filename=call_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3&jwt=eyJhbGciOiJIUzI1NiJ9.eyJoZGlnIjpmYWxzZSwiaXNzIjoiY3Jvc3NmaWxlIiwiYXVkIjoiZmlsZSIsIm9yaSI6InBieHdlYiIsImRpZyI6ImZmNDg5ZmE3Y2NhMjdlZmVmYzY3MmE4ZjBhODFmYjYwODBkNjI0NGZiNjQ5ZmQ5MThkY2NhMmI4YmQyYzYxYjMiLCJpYXQiOjE2OTQ4MDY1MDAsImlpYyI6ImF3MSIsImV4cCI6MTY5NDgwODMwMH0.UZ_6_j4f1ibvuiMjSAqba9pk51roqE9hAu54S8FDEMw&mode=play&path=zoomfs%3A%2F%2Fzoom-pbx%2Frecording%2F2023%2F8%2F15%2FbVaPMLDOTEq4rOIJYmStRA%2FWZYkJSSXTxywYfFaUHbMFw%2F1234abcd-5678-efgh-9012-ijkl3456mnop%2Fcall_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3"", + ""date_time"": ""2023-09-16T12:34:56Z"", + ""recording_type"": ""Automatic"", + ""call_log_id"": ""1234abcd-5678-efgh-9012-ijkl3456mnop"", + ""call_id"": ""1234567890123456789"", + ""owner"": { + ""type"": ""user"", + ""id"": ""bGcIjUHbbOOk8Yoi0_6dfT"", + ""name"": ""Owner Name"", + ""extension_number"": 800 + }, + ""site"": { + ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"" + }, + ""end_time"": ""2023-09-16T12:35:21Z"", + ""disclaimer_status"": 0 + }"; + + internal const string PHONE_CALL_RECORDING_EXTENDED = @"{ + ""id"": ""1234abcd5678efgh9012ijkl3456mnop"", + ""caller_number"": ""+12345678901"", + ""caller_number_type"": 2, + ""caller_name"": ""12345678901"", + ""callee_number"": ""123"", + ""callee_number_type"": 1, + ""callee_name"": ""Callee Name"", + ""outgoing_by"": { + ""name"": ""Call Initiator"", + ""extension_number"": ""100"" + }, + ""accepted_by"": { + ""name"": ""Call Receiver"", + ""extension_number"": ""200"" + }, + ""direction"": ""inbound"", + ""duration"": 25, + ""download_url"": ""https://zoom.us/v2/phone/recording/download/Id_abc123DEF456ghi789J"", + ""file_url"": ""https://file.zoom.us/file?business=phone&filename=call_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3&jwt=eyJhbGciOiJIUzI1NiJ9.eyJoZGlnIjpmYWxzZSwiaXNzIjoiY3Jvc3NmaWxlIiwiYXVkIjoiZmlsZSIsIm9yaSI6InBieHdlYiIsImRpZyI6ImZmNDg5ZmE3Y2NhMjdlZmVmYzY3MmE4ZjBhODFmYjYwODBkNjI0NGZiNjQ5ZmQ5MThkY2NhMmI4YmQyYzYxYjMiLCJpYXQiOjE2OTQ4MDY1MDAsImlpYyI6ImF3MSIsImV4cCI6MTY5NDgwODMwMH0.UZ_6_j4f1ibvuiMjSAqba9pk51roqE9hAu54S8FDEMw&mode=play&path=zoomfs%3A%2F%2Fzoom-pbx%2Frecording%2F2023%2F8%2F15%2FbVaPMLDOTEq4rOIJYmStRA%2FWZYkJSSXTxywYfFaUHbMFw%2F1234abcd-5678-efgh-9012-ijkl3456mnop%2Fcall_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3"", + ""date_time"": ""2023-09-16T12:34:56Z"", + ""recording_type"": ""Automatic"", + ""call_log_id"": ""1234abcd-5678-efgh-9012-ijkl3456mnop"", + ""call_id"": ""1234567890123456789"", + ""owner"": { + ""type"": ""call queue"", + ""id"": ""bGcIjUHbbOOk8Yoi0_6dfT"", + ""name"": ""Owner Name"", + ""extension_number"": 800, + ""extension_status"": ""inactive"" + }, + ""site"": { + ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"", + ""name"": ""Site Name"" + }, + ""end_time"": ""2023-09-16T12:35:21Z"", + ""disclaimer_status"": 1 + }"; + + internal const string PHONE_CALL_RECORDING_TRANSCRIPT = @"{ + ""type"": ""zoom_transcript"", + ""ver"": 1, + ""recording_id"": ""1234abcd5678efgh9012ijkl3456mnop"", + ""meeting_id"": ""abcdefghijklmnop1234567890123456"", + ""account_id"": ""yIuKOPVYTg7FU0cIpgErD3"", + ""host_id"": ""yg6gFTJIu88fdrtUOIGft5"", + ""recording_start"": ""2023-09-16T12:34:56Z"", + ""recording_end"": ""2023-09-16T12:35:21Z"", + ""timeline"": [ + { + ""text"": ""Lorem Ipsum."", + ""end_ts"": ""00:00:02.584"", + ""ts"": ""00:00:00.789"", + ""users"": [ + { + ""username"": ""+12345678901"", + ""multiple_people"": false, + ""user_id"": ""+12345678901"", + ""zoom_userid"": ""Unknown Speaker"", + ""client_type"": 0 + } + ] + }, + { + ""text"": ""Dolor sit amet."", + ""end_ts"": ""00:00:04.923"", + ""ts"": ""00:00:03.172"", + ""users"": [ + { + ""username"": ""Callee Name"", + ""multiple_people"": true, + ""user_id"": ""123"", + ""zoom_userid"": ""hYU_fr-6tdVBN0IPvvTxeR"", + ""client_type"": 1 + }, + { + ""username"": ""+12345678901"", + ""multiple_people"": false, + ""user_id"": ""+12345678901"", + ""zoom_userid"": ""Unknown Speaker"", + ""client_type"": 0 + } + ] + }, + { + ""text"": ""Consectetur adipiscing elit."", + ""end_ts"": ""00:00:05.000"", + ""ts"": ""00:00:08.435"", + ""users"": [] + } + ] + }"; + + #endregion + + #region tests + + [Fact] + public void Parse_Json_PhoneCallRecording() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize( + PHONE_CALL_RECORDING, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Id.ShouldBe("1234abcd5678efgh9012ijkl3456mnop"); + result.CallerNumber.ShouldBe("+12345678901"); + result.CallerNumberType.ShouldBe(PhoneCallRecordingCallerNumberType.External); + result.CallerName.ShouldBe("12345678901"); + result.CalleeNumber.ShouldBe("123"); + result.CalleeNumberType.ShouldBe(PhoneCallRecordingCalleeNumberType.Internal); + result.CalleeName.ShouldBe("Callee Name"); + result.CallInitiator.ShouldBeNull(); + result.CallReceiver.ShouldBeNull(); + result.Direction.ShouldBe(CallLogDirection.Inbound); + result.Duration.ShouldBe(25); + result.DownloadUrl.ShouldBe("https://zoom.us/v2/phone/recording/download/Id_abc123DEF456ghi789J"); + result.FileUrl.ShouldBe("https://file.zoom.us/file?business=phone&filename=call_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3&jwt=eyJhbGciOiJIUzI1NiJ9.eyJoZGlnIjpmYWxzZSwiaXNzIjoiY3Jvc3NmaWxlIiwiYXVkIjoiZmlsZSIsIm9yaSI6InBieHdlYiIsImRpZyI6ImZmNDg5ZmE3Y2NhMjdlZmVmYzY3MmE4ZjBhODFmYjYwODBkNjI0NGZiNjQ5ZmQ5MThkY2NhMmI4YmQyYzYxYjMiLCJpYXQiOjE2OTQ4MDY1MDAsImlpYyI6ImF3MSIsImV4cCI6MTY5NDgwODMwMH0.UZ_6_j4f1ibvuiMjSAqba9pk51roqE9hAu54S8FDEMw&mode=play&path=zoomfs%3A%2F%2Fzoom-pbx%2Frecording%2F2023%2F8%2F15%2FbVaPMLDOTEq4rOIJYmStRA%2FWZYkJSSXTxywYfFaUHbMFw%2F1234abcd-5678-efgh-9012-ijkl3456mnop%2Fcall_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3"); + result.StartDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 34, 56, DateTimeKind.Utc)); + result.Type.ShouldBe(PhoneCallRecordingType.Automatic); + result.CallLogId.ShouldBe("1234abcd-5678-efgh-9012-ijkl3456mnop"); + result.CallId.ShouldBe("1234567890123456789"); + result.Owner.Type.ShouldBe(PhoneCallRecordingOwnerType.User); + result.Owner.Id.ShouldBe("bGcIjUHbbOOk8Yoi0_6dfT"); + result.Owner.Name.ShouldBe("Owner Name"); + result.Owner.ExtensionNumber.ShouldBe(800); + result.Owner.ExtensionStatus.ShouldBeNull(); + result.Site.Id.ShouldBe("8f71O6rWT8KFUGQmJIFAdQ"); + result.Site.Name.ShouldBeNull(); + result.EndDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 35, 21, DateTimeKind.Utc)); + result.DisclaimerStatus.ShouldBe(PhoneCallRecordingDisclaimerStatus.Implicit); + } + + [Fact] + public void Parse_Json_PhoneCallRecording_Extended() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize( + PHONE_CALL_RECORDING_EXTENDED, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Id.ShouldBe("1234abcd5678efgh9012ijkl3456mnop"); + result.CallerNumber.ShouldBe("+12345678901"); + result.CallerNumberType.ShouldBe(PhoneCallRecordingCallerNumberType.External); + result.CallerName.ShouldBe("12345678901"); + result.CalleeNumber.ShouldBe("123"); + result.CalleeNumberType.ShouldBe(PhoneCallRecordingCalleeNumberType.Internal); + result.CalleeName.ShouldBe("Callee Name"); + result.CallInitiator.Name.ShouldBe("Call Initiator"); + result.CallInitiator.ExtensionNumber.ShouldBe("100"); + result.CallReceiver.Name.ShouldBe("Call Receiver"); + result.CallReceiver.ExtensionNumber.ShouldBe("200"); + result.Direction.ShouldBe(CallLogDirection.Inbound); + result.Duration.ShouldBe(25); + result.DownloadUrl.ShouldBe("https://zoom.us/v2/phone/recording/download/Id_abc123DEF456ghi789J"); + result.FileUrl.ShouldBe("https://file.zoom.us/file?business=phone&filename=call_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3&jwt=eyJhbGciOiJIUzI1NiJ9.eyJoZGlnIjpmYWxzZSwiaXNzIjoiY3Jvc3NmaWxlIiwiYXVkIjoiZmlsZSIsIm9yaSI6InBieHdlYiIsImRpZyI6ImZmNDg5ZmE3Y2NhMjdlZmVmYzY3MmE4ZjBhODFmYjYwODBkNjI0NGZiNjQ5ZmQ5MThkY2NhMmI4YmQyYzYxYjMiLCJpYXQiOjE2OTQ4MDY1MDAsImlpYyI6ImF3MSIsImV4cCI6MTY5NDgwODMwMH0.UZ_6_j4f1ibvuiMjSAqba9pk51roqE9hAu54S8FDEMw&mode=play&path=zoomfs%3A%2F%2Fzoom-pbx%2Frecording%2F2023%2F8%2F15%2FbVaPMLDOTEq4rOIJYmStRA%2FWZYkJSSXTxywYfFaUHbMFw%2F1234abcd-5678-efgh-9012-ijkl3456mnop%2Fcall_recording_1234abcd-5678-efgh-9012-ijkl3456mnop_20230916123456.mp3"); + result.StartDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 34, 56, DateTimeKind.Utc)); + result.Type.ShouldBe(PhoneCallRecordingType.Automatic); + result.CallLogId.ShouldBe("1234abcd-5678-efgh-9012-ijkl3456mnop"); + result.CallId.ShouldBe("1234567890123456789"); + result.Owner.Type.ShouldBe(PhoneCallRecordingOwnerType.CallQueue); + result.Owner.Id.ShouldBe("bGcIjUHbbOOk8Yoi0_6dfT"); + result.Owner.Name.ShouldBe("Owner Name"); + result.Owner.ExtensionNumber.ShouldBe(800); + result.Owner.ExtensionStatus.ShouldBe(PhoneCallRecordingOwnerExtensionStatus.Inactive); + result.Site.Id.ShouldBe("8f71O6rWT8KFUGQmJIFAdQ"); + result.Site.Name.ShouldBe("Site Name"); + result.EndDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 35, 21, DateTimeKind.Utc)); + result.DisclaimerStatus.ShouldBe(PhoneCallRecordingDisclaimerStatus.Agree); + } + + [Fact] + public void Parse_Json_PhoneCallRecordingTranscript() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize( + PHONE_CALL_RECORDING_TRANSCRIPT, JsonFormatter.SerializerOptions); + + // Assert + result.ShouldNotBeNull(); + result.Type.ShouldBe("zoom_transcript"); + result.Version.ShouldBe(1); + result.RecordingId.ShouldBe("1234abcd5678efgh9012ijkl3456mnop"); + result.MeetingId.ShouldBe("abcdefghijklmnop1234567890123456"); + result.AccountId.ShouldBe("yIuKOPVYTg7FU0cIpgErD3"); + result.HostId.ShouldBe("yg6gFTJIu88fdrtUOIGft5"); + result.StartDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 34, 56, DateTimeKind.Utc)); + result.EndDateTime.ShouldBe(new DateTime(2023, 9, 16, 12, 35, 21, DateTimeKind.Utc)); + result.TimelineFractions.ShouldNotBeNull(); + result.TimelineFractions.Length.ShouldBe(3); + result.TimelineFractions[0].Text.ShouldBe("Lorem Ipsum."); + result.TimelineFractions[0].StartTimeSpan.ShouldBe(TimeSpan.Parse("00:00:00.789")); + result.TimelineFractions[0].EndTimeSpan.ShouldBe(TimeSpan.Parse("00:00:02.584")); + result.TimelineFractions[0].Users.ShouldNotBeNull(); + result.TimelineFractions[0].Users.Length.ShouldBe(1); + result.TimelineFractions[0].Users[0].Username.ShouldBe("+12345678901"); + result.TimelineFractions[0].Users[0].IsMultiplePeople.ShouldBeFalse(); + result.TimelineFractions[0].Users[0].UserId.ShouldBe("+12345678901"); + result.TimelineFractions[0].Users[0].ZoomUserId.ShouldBe("Unknown Speaker"); + result.TimelineFractions[0].Users[0].ClientType.ShouldBe(0); + result.TimelineFractions[1].Text.ShouldBe("Dolor sit amet."); + result.TimelineFractions[1].StartTimeSpan.ShouldBe(TimeSpan.Parse("00:00:03.172")); + result.TimelineFractions[1].EndTimeSpan.ShouldBe(TimeSpan.Parse("00:00:04.923")); + result.TimelineFractions[1].Users.ShouldNotBeNull(); + result.TimelineFractions[1].Users.Length.ShouldBe(2); + result.TimelineFractions[1].Users[0].Username.ShouldBe("Callee Name"); + result.TimelineFractions[1].Users[0].IsMultiplePeople.ShouldBeTrue(); + result.TimelineFractions[1].Users[0].UserId.ShouldBe("123"); + result.TimelineFractions[1].Users[0].ZoomUserId.ShouldBe("hYU_fr-6tdVBN0IPvvTxeR"); + result.TimelineFractions[1].Users[0].ClientType.ShouldBe(1); + result.TimelineFractions[1].Users[1].Username.ShouldBe("+12345678901"); + result.TimelineFractions[1].Users[1].IsMultiplePeople.ShouldBeFalse(); + result.TimelineFractions[1].Users[1].UserId.ShouldBe("+12345678901"); + result.TimelineFractions[1].Users[1].ZoomUserId.ShouldBe("Unknown Speaker"); + result.TimelineFractions[1].Users[1].ClientType.ShouldBe(0); + result.TimelineFractions[2].Text.ShouldBe("Consectetur adipiscing elit."); + result.TimelineFractions[2].StartTimeSpan.ShouldBe(TimeSpan.Parse("00:00:08.435")); + result.TimelineFractions[2].EndTimeSpan.ShouldBe(TimeSpan.Parse("00:00:05.000")); + result.TimelineFractions[2].Users.ShouldNotBeNull(); + result.TimelineFractions[2].Users.Length.ShouldBe(0); + } + + #endregion + } +} diff --git a/Source/ZoomNet.UnitTests/Resources/PhoneCallRecordings.cs b/Source/ZoomNet.UnitTests/Resources/PhoneCallRecordings.cs new file mode 100644 index 00000000..50a051d0 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Resources/PhoneCallRecordings.cs @@ -0,0 +1,75 @@ +using RichardSzalay.MockHttp; +using Shouldly; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using ZoomNet.Resources; + +namespace ZoomNet.UnitTests.Resources +{ + public class PhoneCallRecordingsTests + { + [Fact] + public async Task GetRecordingAsync() + { + // Arrange + string callId = "1234567890123456789"; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect( + HttpMethod.Get, + Utils.GetZoomApiUri("phone/call_logs", callId, "recordings")) + .Respond( + "application/json", + Models.PhoneCallRecordingsTests.PHONE_CALL_RECORDING); + + var client = Utils.GetFluentClient(mockHttp); + var phone = new Phone(client); + + // Act + var result = await phone + .GetRecordingAsync(callId, CancellationToken.None) + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.Id.ShouldNotBeNull(); + result.CallId.ShouldBe(callId); + } + + [Fact] + public async Task GetRecordingTranscriptAsync() + { + // Arrange + string recordingId = "1234abcd5678efgh9012ijkl3456mnop"; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect( + HttpMethod.Get, + Utils.GetZoomApiUri("phone/recording_transcript/download/", recordingId)) + .Respond( + "application/json", + Models.PhoneCallRecordingsTests.PHONE_CALL_RECORDING_TRANSCRIPT); + + var client = Utils.GetFluentClient(mockHttp); + var phone = new Phone(client); + + // Act + var result = await phone + .GetRecordingTranscriptAsync(recordingId, CancellationToken.None) + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.RecordingId.ShouldBe(recordingId); + result.TimelineFractions.ShouldNotBeNull(); + } + } +} diff --git a/Source/ZoomNet.UnitTests/WebhookParserTests.cs b/Source/ZoomNet.UnitTests/WebhookParserTests.cs index f79af5c7..59936bcb 100644 --- a/Source/ZoomNet.UnitTests/WebhookParserTests.cs +++ b/Source/ZoomNet.UnitTests/WebhookParserTests.cs @@ -279,7 +279,7 @@ public void MeetingCreated() parsedEvent.Meeting.Id.ShouldBe(98884753832); parsedEvent.Meeting.HostId.ShouldBe("8lzIwvZTSOqjndWPbPqzuA"); parsedEvent.Meeting.Topic.ShouldBe("ZoomNet Unit Testing: instant meeting"); - parsedEvent.Meeting.Type.ShouldBe(Models.MeetingType.Instant); + parsedEvent.Meeting.Type.ShouldBe(MeetingType.Instant); parsedEvent.Meeting.JoinUrl.ShouldBe("https://zoom.us/j/98884753832?pwd=c21EQzg0SXY2dlNTOFF2TENpSm1aQT09"); parsedEvent.Meeting.Password.ShouldBe("PaSsWoRd"); parsedEvent.Meeting.Settings.ShouldNotBeNull(); @@ -301,7 +301,7 @@ public void MeetingDeleted() parsedEvent.Meeting.Id.ShouldBe(98884753832); parsedEvent.Meeting.HostId.ShouldBe("8lzIwvZTSOqjndWPbPqzuA"); parsedEvent.Meeting.Topic.ShouldBe("ZoomNet Unit Testing: instant meeting"); - parsedEvent.Meeting.Type.ShouldBe(Models.MeetingType.Instant); + parsedEvent.Meeting.Type.ShouldBe(MeetingType.Instant); parsedEvent.Meeting.JoinUrl.ShouldBeNull(); parsedEvent.Meeting.Password.ShouldBeNull(); parsedEvent.Meeting.Settings.ShouldBeNull(); diff --git a/Source/ZoomNet/IZoomClient.cs b/Source/ZoomNet/IZoomClient.cs index db30cc17..d0735a97 100644 --- a/Source/ZoomNet/IZoomClient.cs +++ b/Source/ZoomNet/IZoomClient.cs @@ -117,5 +117,10 @@ public interface IZoomClient /// Gets the resource which allows you to manage chatbot messages. ///
IChatbot Chatbot { get; } + + /// + /// Gets the resource which allows you to access Zoom Phone API endpoints. + /// + IPhone Phone { get; } } } diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index 66bd1915..0b2ab406 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -126,6 +126,17 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PastInstance))] [JsonSerializable(typeof(ZoomNet.Models.PastMeeting))] [JsonSerializable(typeof(ZoomNet.Models.PayMode))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecording))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCalleeNumberType))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCallerNumberType))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingDisclaimerStatus))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwner))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerExtensionStatus))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerType))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscript))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscriptTimelineFraction))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscriptTimelineUser))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingType))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType))] @@ -390,6 +401,17 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PastInstance[]))] [JsonSerializable(typeof(ZoomNet.Models.PastMeeting[]))] [JsonSerializable(typeof(ZoomNet.Models.PayMode[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecording[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCalleeNumberType[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCallerNumberType[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingDisclaimerStatus[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwner[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerExtensionStatus[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerType[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscript[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscriptTimelineFraction[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingTranscriptTimelineUser[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingType[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType[]))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType[]))] @@ -579,6 +601,12 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ParticipantStatus?))] [JsonSerializable(typeof(ZoomNet.Models.ParticipantsToPlaceInWaitingRoom?))] [JsonSerializable(typeof(ZoomNet.Models.PayMode?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCalleeNumberType?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingCallerNumberType?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingDisclaimerStatus?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerExtensionStatus?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingOwnerType?))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneCallRecordingType?))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType?))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType?))] [JsonSerializable(typeof(ZoomNet.Models.PollQuestionType?))] diff --git a/Source/ZoomNet/Models/PhoneCallRecording.cs b/Source/ZoomNet/Models/PhoneCallRecording.cs new file mode 100644 index 00000000..8f47f19b --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecording.cs @@ -0,0 +1,129 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call recording. + /// + public class PhoneCallRecording + { + /// Gets or sets the call ID. + /// The phone call's unique ID. + [JsonPropertyName("call_id")] + public string CallId { get; set; } + + /// Gets or sets the call log ID. + /// The phone call log's unique ID. + [JsonPropertyName("call_log_id")] + public string CallLogId { get; set; } + + /// Gets or sets the callee name. + /// The callee's contact name. + [JsonPropertyName("callee_name")] + public string CalleeName { get; set; } + + /// Gets or sets the callee number. + /// The callee's phone number. + [JsonPropertyName("callee_number")] + public string CalleeNumber { get; set; } + + /// Gets or sets the callee number type. + /// The callee's number type. + [JsonPropertyName("callee_number_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingCalleeNumberType? CalleeNumberType { get; set; } + + /// Gets or sets the caller name. + /// The caller's contact name. + [JsonPropertyName("caller_name")] + public string CallerName { get; set; } + + /// Gets or sets the caller number. + /// The caller;s phone number. + [JsonPropertyName("caller_number")] + public string CallerNumber { get; set; } + + /// Gets or sets the caller number type. + /// The caller's number type. + [JsonPropertyName("caller_number_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingCallerNumberType? CallerNumberType { get; set; } + + /// Gets or sets the call-initiating user. + /// The call-initiating user. + /// The recording must belong to the initiator and call queue for it to be available. + [JsonPropertyName("outgoing_by")] + public PhoneCallUser CallInitiator { get; set; } + + /// Gets or sets the call-receiving user. + /// The call-initiating user. + /// The recording must belong to the receiver and call queue for it to be available. + [JsonPropertyName("accepted_by")] + public PhoneCallUser CallReceiver { get; set; } + + /// Gets or sets the call recording start datetime. + /// The date and time at which the recording was received. + [JsonPropertyName("date_time")] + public DateTime StartDateTime { get; set; } + + /// Gets or sets the call direction. + /// The call's direction (inbound / outbound). + [JsonPropertyName("direction")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public CallLogDirection Direction { get; set; } + + /// Gets or sets the download URL for the call recording. + /// The URL from which to download the recording + /// (with no OAuth access token). + [JsonPropertyName("download_url")] + public string DownloadUrl { get; set; } + + /// Gets or sets the call duration. + /// The call recording's duration, in seconds. + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// Gets or sets the call recording end datetime. + /// The recording's end time. + [JsonPropertyName("end_time")] + public DateTime EndDateTime { get; set; } + + /// Gets or sets the call recording ID. + /// The recording's ID. + [JsonPropertyName("id")] + public string Id { get; set; } + + /// Gets or sets the call recording owner. + /// The owner of the recording. + [JsonPropertyName("owner")] + public PhoneCallRecordingOwner Owner { get; set; } + + /// Gets or sets the file URL for the call recording. + /// The URL from which to download the recording + /// (with OAuth access token included as query parameter). + /// Not documented by Zoom. + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + /// Gets or sets the call recording type. + /// Type of the call recording. + /// Not documented by Zoom. + [JsonPropertyName("recording_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingType? Type { get; set; } + + /// Gets or sets the call recording site information. + /// Site information. + /// Not documented by Zoom. + [JsonPropertyName("site")] + public CallLogSite Site { get; set; } + + /// Gets or sets the disclaimer status. + /// The status of disclaimer for recording. + /// Not documented by Zoom. + [JsonPropertyName("disclaimer_status")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingDisclaimerStatus DisclaimerStatus { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingCalleeNumberType.cs b/Source/ZoomNet/Models/PhoneCallRecordingCalleeNumberType.cs new file mode 100644 index 00000000..b10df441 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingCalleeNumberType.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the phone callee's number type. + /// + public enum PhoneCallRecordingCalleeNumberType + { + /// + /// Internal number. + /// + Internal = 1, + + /// + /// External number. + /// + External = 2, + + /// + /// Customized emergency number. + /// + CustomizedEmergency = 3 + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingCallerNumberType.cs b/Source/ZoomNet/Models/PhoneCallRecordingCallerNumberType.cs new file mode 100644 index 00000000..bf96620e --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingCallerNumberType.cs @@ -0,0 +1,18 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the phone caller's number type. + /// + public enum PhoneCallRecordingCallerNumberType + { + /// + /// Internal number. + /// + Internal = 1, + + /// + /// External number. + /// + External = 2 + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingDisclaimerStatus.cs b/Source/ZoomNet/Models/PhoneCallRecordingDisclaimerStatus.cs new file mode 100644 index 00000000..078074ad --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingDisclaimerStatus.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the status of disclaimer for phone call recording. + /// + public enum PhoneCallRecordingDisclaimerStatus + { + /// + /// Passive/implicit. + /// + Implicit = 0, + + /// + /// Agree (active/explicit and press 1). + /// + Agree = 1, + + /// + /// Passive agree (active/explicit and no press). + /// + PassiveAgree = 2 + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingOwner.cs b/Source/ZoomNet/Models/PhoneCallRecordingOwner.cs new file mode 100644 index 00000000..0af239a1 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingOwner.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call owner. + /// + public class PhoneCallRecordingOwner + { + /// Gets or sets the extension number. + /// The extension number associated with the call number. + [JsonPropertyName("extension_number")] + public int ExtensionNumber { get; set; } + + /// Gets or sets the owner ID. + /// The owner's ID. + [JsonPropertyName("id")] + public string Id { get; set; } + + /// Gets or sets the owner name. + /// Name of the owner. + [JsonPropertyName("name")] + public string Name { get; set; } + + /// Gets or sets the owner type. + /// The owner type: user or call queue. + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingOwnerType? Type { get; set; } + + /// Gets or sets the extension status. + /// The extension status: inactive or deleted. + [JsonPropertyName("extension_status")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public PhoneCallRecordingOwnerExtensionStatus? ExtensionStatus { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingOwnerExtensionStatus.cs b/Source/ZoomNet/Models/PhoneCallRecordingOwnerExtensionStatus.cs new file mode 100644 index 00000000..aef492ac --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingOwnerExtensionStatus.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the phone call owner extension status. + /// + public enum PhoneCallRecordingOwnerExtensionStatus + { + /// + /// Inactive. + /// + [EnumMember(Value = "inactive")] + Inactive, + + /// + /// Deleted. + /// + [EnumMember(Value = "deleted")] + Deleted + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingOwnerType.cs b/Source/ZoomNet/Models/PhoneCallRecordingOwnerType.cs new file mode 100644 index 00000000..8c5f2194 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingOwnerType.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the phone call owner types. + /// + public enum PhoneCallRecordingOwnerType + { + /// + /// user. + /// + [EnumMember(Value = "user")] + User, + + /// + /// callQueue. + /// + [EnumMember(Value = "call queue")] + CallQueue + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingTranscript.cs b/Source/ZoomNet/Models/PhoneCallRecordingTranscript.cs new file mode 100644 index 00000000..bad327e7 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingTranscript.cs @@ -0,0 +1,69 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call recording transcript. + /// + /// Not documented by Zoom. + public class PhoneCallRecordingTranscript + { + /// Gets or sets the recording type. + /// The type of the phone call recording. + /// + /// Not documented by Zoom.
+ /// Suspected to be an Enum, but available values unknown (apart from "zoom_transcript"). + ///
+ [JsonPropertyName("type")] + public string Type { get; set; } + + /// Gets or sets the recording version. + /// The version of the phone call recording. + /// Not documented by Zoom. + [JsonPropertyName("ver")] + public int Version { get; set; } + + /// Gets or sets the recording ID. + /// The ID of the phone call recording. + /// Not documented by Zoom. + [JsonPropertyName("recording_id")] + public string RecordingId { get; set; } + + /// Gets or sets the meeting ID. + /// The ID of the meeting. + /// Not documented by Zoom. + [JsonPropertyName("meeting_id")] + public string MeetingId { get; set; } + + /// Gets or sets the account ID. + /// The account ID. + /// Not documented by Zoom. + [JsonPropertyName("account_id")] + public string AccountId { get; set; } + + /// Gets or sets the host ID. + /// The host ID. + /// Not documented by Zoom. + [JsonPropertyName("host_id")] + public string HostId { get; set; } + + /// Gets or sets the call recording start datetime. + /// The phone call recording start datetime. + /// Not documented by Zoom. + [JsonPropertyName("recording_start")] + public DateTime StartDateTime { get; set; } + + /// Gets or sets the call recording end datetime. + /// The phone call recording end datetime. + /// Not documented by Zoom. + [JsonPropertyName("recording_end")] + public DateTime EndDateTime { get; set; } + + /// Gets or sets the call recording timeline. + /// The phone call recording timeline. + /// Not documented by Zoom. + [JsonPropertyName("timeline")] + public PhoneCallRecordingTranscriptTimelineFraction[] TimelineFractions { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineFraction.cs b/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineFraction.cs new file mode 100644 index 00000000..8f063550 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineFraction.cs @@ -0,0 +1,39 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call recording transcript timeline fraction. + /// + /// Not documented by Zoom. + public class PhoneCallRecordingTranscriptTimelineFraction + { + /// Gets or sets the transcribed text. + /// The phone call recording transcript fraction text. + /// Not documented by Zoom. + [JsonPropertyName("text")] + public string Text { get; set; } + + /// Gets or sets the transcribed text start timespan. + /// The phone call recording transcript fraction start timespan. + /// Not documented by Zoom. + [JsonPropertyName("ts")] + public TimeSpan StartTimeSpan { get; set; } + + /// Gets or sets the transcribed text end timespan. + /// The phone call recording transcript fraction end timespan. + /// Not documented by Zoom. + [JsonPropertyName("end_ts")] + public TimeSpan EndTimeSpan { get; set; } + + /// Gets or sets the transcribed text users. + /// The phone call recording transcript fraction users. + /// + /// Not documented by Zoom.
+ /// Might be empty. + ///
+ [JsonPropertyName("users")] + public PhoneCallRecordingTranscriptTimelineUser[] Users { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineUser.cs b/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineUser.cs new file mode 100644 index 00000000..f940800a --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingTranscriptTimelineUser.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call recording transcript timeline speaking user. + /// + /// Not documented by Zoom. + public class PhoneCallRecordingTranscriptTimelineUser + { + /// Gets or sets the username. + /// The user name. + /// Not documented by Zoom. + [JsonPropertyName("username")] + public string Username { get; set; } + + /// Gets or sets a value indicating whether multiple people are speaking. + /// Whether multiple people are speaking. + /// Not documented by Zoom. + [JsonPropertyName("multiple_people")] + public bool IsMultiplePeople { get; set; } + + /// Gets or sets the user ID. + /// The user ID. + /// Not documented by Zoom. + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + /// Gets or sets the Zoom user ID. + /// The Zoom user ID. + /// Not documented by Zoom. + [JsonPropertyName("zoom_userid")] + public string ZoomUserId { get; set; } + + /// Gets or sets the client type. + /// The client type. + /// + /// Not documented by Zoom.
+ /// Most likely is actually an enumerator. + ///
+ [JsonPropertyName("client_type")] + public int ClientType { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PhoneCallRecordingType.cs b/Source/ZoomNet/Models/PhoneCallRecordingType.cs new file mode 100644 index 00000000..e1419ebd --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallRecordingType.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate how is a phone call recorded. + /// + public enum PhoneCallRecordingType + { + /// + /// Record on demand. + /// + [EnumMember(Value = "OnDemand")] + OnDemand, + + /// + /// Record automatically. + /// + [EnumMember(Value = "Automatic")] + Automatic + } +} diff --git a/Source/ZoomNet/Models/PhoneCallUser.cs b/Source/ZoomNet/Models/PhoneCallUser.cs new file mode 100644 index 00000000..416f3532 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneCallUser.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Phone call user information. + /// + public class PhoneCallUser + { + /// Gets or sets the user name. + /// The user name. + [JsonPropertyName("name")] + public string Name { get; set; } + + /// Gets or sets the user extension number. + /// The user extension number. + [JsonPropertyName("extension_number")] + public string ExtensionNumber { get; set; } + } +} diff --git a/Source/ZoomNet/Resources/IPhone.cs b/Source/ZoomNet/Resources/IPhone.cs new file mode 100644 index 00000000..db218fec --- /dev/null +++ b/Source/ZoomNet/Resources/IPhone.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to access Zoom Phone API endpoints. + /// + /// + /// See + /// Zoom API documentation for more information. + /// + public interface IPhone + { + #region Recordings endpoints + + /// + /// Get recording of a specific phone call by its call ID or call log ID. + /// + /// The call ID or call log ID. + /// The cancellation token. + /// + /// The requested , if any. + /// + /// + /// See + /// Zoom endpoint documentation for more information. + /// + Task GetRecordingAsync( + string callId, CancellationToken cancellationToken = default); + + /// + /// Get transcript of a specific phone call recording by its recording ID. + /// + /// The call recording ID. + /// The cancellation token. + /// + /// The requested , if any. + /// + /// + /// See + /// Zoom endpoint documentation for more information. + /// + Task GetRecordingTranscriptAsync( + string recordingId, CancellationToken cancellationToken = default); + + #endregion + } +} diff --git a/Source/ZoomNet/Resources/Phone.cs b/Source/ZoomNet/Resources/Phone.cs new file mode 100644 index 00000000..e81e7580 --- /dev/null +++ b/Source/ZoomNet/Resources/Phone.cs @@ -0,0 +1,54 @@ +using Pathoschild.Http.Client; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + public class Phone : IPhone + { + #region private fields + + private readonly IClient _client; + + #endregion + + #region constructor + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + internal Phone(IClient client) + { + _client = client; + } + + #endregion + + #region Recordings endpoints + + /// + public Task GetRecordingAsync( + string callId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"phone/call_logs/{callId}/recordings") + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + public Task GetRecordingTranscriptAsync( + string recordingId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"phone/recording_transcript/download/{recordingId}") + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + #endregion + } +} diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index 483a01fa..104d0a9c 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -166,6 +166,9 @@ public static string Version /// public IChatbot Chatbot { get; private set; } + /// + public IPhone Phone { get; private set; } + #endregion #region CTOR @@ -271,6 +274,7 @@ private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool d Reports = new Reports(_fluentClient); CallLogs = new CallLogs(_fluentClient); Chatbot = new Chatbot(_fluentClient); + Phone = new Phone(_fluentClient); } /// From 8160f268e511b3a6ac0b4aa15b72944e76d3a122 Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 11 Sep 2023 11:52:06 -0400 Subject: [PATCH 14/16] Refresh build script --- appveyor.yml | 2 +- build.cake | 37 +++---------------------------------- global.json | 2 +- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a0f1b09b..93c0eef9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,7 +17,7 @@ before_build: # to run your custom scripts instead of automatic MSBuild build_script: - - ps: .\build.ps1 --target=AppVeyor + - ps: .\build.ps1 build.cake --target=AppVeyor # scripts to run after build after_build: diff --git a/build.cake b/build.cake index f9af46a9..6d874b79 100644 --- a/build.cake +++ b/build.cake @@ -1,10 +1,10 @@ // Install tools. #tool dotnet:?package=GitVersion.Tool&version=5.12.0 #tool dotnet:?package=coveralls.net&version=4.0.1 -#tool nuget:?package=GitReleaseManager&version=0.13.0 -#tool nuget:?package=ReportGenerator&version=5.1.23 +#tool nuget:?package=GitReleaseManager&version=0.15.0 +#tool nuget:?package=ReportGenerator&version=5.1.25 #tool nuget:?package=xunit.runner.console&version=2.5.0 -#tool nuget:?package=CodecovUploader&version=0.5.0 +#tool nuget:?package=CodecovUploader&version=0.6.2 // Install addins. #addin nuget:?package=Cake.Coveralls&version=1.1.0 @@ -51,9 +51,6 @@ var testCoverageExcludeFiles = new[] var nuGetApiUrl = Argument("NUGET_API_URL", EnvironmentVariable("NUGET_API_URL")); var nuGetApiKey = Argument("NUGET_API_KEY", EnvironmentVariable("NUGET_API_KEY")); -var feedzioApiUrl = Argument("FEEDZIO_API_URL", EnvironmentVariable("FEEDZIO_API_URL")); -var feedzioApiKey = Argument("FEEDZIO_API_KEY", EnvironmentVariable("FEEDZIO_API_KEY")); - var gitHubToken = Argument("GITHUB_TOKEN", EnvironmentVariable("GITHUB_TOKEN")); var gitHubUserName = Argument("GITHUB_USERNAME", EnvironmentVariable("GITHUB_USERNAME")); var gitHubPassword = Argument("GITHUB_PASSWORD", EnvironmentVariable("GITHUB_PASSWORD")); @@ -135,11 +132,6 @@ Setup(context => isTagged ); - Information("Feedz.io Info:\r\n\tApi Url: {0}\r\n\tApi Key: {1}", - feedzioApiUrl, - string.IsNullOrEmpty(feedzioApiKey) ? "[NULL]" : new string('*', feedzioApiKey.Length) - ); - Information("Nuget Info:\r\n\tApi Url: {0}\r\n\tApi Key: {1}", nuGetApiUrl, string.IsNullOrEmpty(nuGetApiKey) ? "[NULL]" : new string('*', nuGetApiKey.Length) @@ -420,28 +412,6 @@ Task("Publish-NuGet") } }); -Task("Publish-Feedzio") - .IsDependentOn("Create-NuGet-Package") - .WithCriteria(() => !isLocalBuild) - .WithCriteria(() => !isPullRequest) - .WithCriteria(() => isMainRepo) - .Does(() => -{ - if(string.IsNullOrEmpty(feedzioApiKey)) throw new InvalidOperationException("Could not resolve Feedz.io API key."); - if(string.IsNullOrEmpty(feedzioApiUrl)) throw new InvalidOperationException("Could not resolve Feedz.io API url."); - - var settings = new DotNetNuGetPushSettings - { - Source = feedzioApiUrl, - ApiKey = feedzioApiKey - }; - - foreach(var package in GetFiles(outputDir + "*.nupkg")) - { - DotNetNuGetPush(package, settings); - } -}); - Task("Create-Release-Notes") .Does(() => { @@ -544,7 +514,6 @@ Task("AppVeyor") .IsDependentOn("Upload-Coverage-Result-Codecov") .IsDependentOn("Create-NuGet-Package") .IsDependentOn("Upload-AppVeyor-Artifacts") - .IsDependentOn("Publish-Feedzio") .IsDependentOn("Publish-NuGet") .IsDependentOn("Publish-GitHub-Release") .Finally(() => diff --git a/global.json b/global.json index 7a74b8dd..3b883f76 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.306", + "version": "7.0.400", "rollForward": "patch", "allowPrerelease": false } From a6e7ea664a7862276d6f50ba8b887c503296efda Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 21 Sep 2023 20:43:35 -0400 Subject: [PATCH 15/16] Upgrade nuget packages --- .../ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj | 2 +- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index d851df5a..1c8393ff 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -14,7 +14,7 @@ - + diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 2b542899..7c1ea460 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 34f37066a16cd342a2bbd25674a58a6cc40260bd Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 21 Sep 2023 20:45:56 -0400 Subject: [PATCH 16/16] Forward cancellation token --- Source/ZoomNet.IntegrationTests/Tests/Meetings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index 4c6e0443..12f97718 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -140,7 +140,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie new BatchRegistrant { Email = "firstBatchRegistrant@example.com", FirstName = "Mariful", LastName = "Maruf" }, new BatchRegistrant { Email = "secondBatchRegistrant@example.com", FirstName = "Abdullah", LastName = "Galib" } }; - var registrantsInfo = await client.Meetings.PerformBatchRegistrationAsync(scheduledMeeting.Id, registrants, true).ConfigureAwait(false); + var registrantsInfo = await client.Meetings.PerformBatchRegistrationAsync(scheduledMeeting.Id, registrants, true, false, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Registrants {registrantsInfo} added to meeting {scheduledMeeting.Id}").ConfigureAwait(false); var registrationAnswers1 = new[] @@ -263,7 +263,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync($"Recurring meeting with no fixed time {newRecurringNoFixTimeMeeting.Id} created").ConfigureAwait(false); await client.Meetings.DeleteAsync(newRecurringNoFixTimeMeeting.Id, null, false, false, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"Recurring meeting with no fixed time {newRecurringNoFixTimeMeeting.Id} deleted").ConfigureAwait(false); + await log.WriteLineAsync($"Recurring meeting with no fixed time {newRecurringNoFixTimeMeeting.Id} deleted").ConfigureAwait(false); } } }