diff --git a/README.md b/README.md index 4f4f16f2..e054b8f7 100644 --- a/README.md +++ b/README.md @@ -56,16 +56,19 @@ var apiSecret = "... your API secret ..."; var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret); ``` +> **Warning:** Zoom has announced that this authentication method would be obsolete in June 2023. The recommendation is to swith to Server-to-Server OAuth. + + #### Connection using OAuth Using OAuth is much more complicated than using JWT but at the same time, it is more flexible because you can define which permissions your app requires. When a user installs your app, they are presented with the list of permissions your app requires and they are given the opportunity to accept. The Zoom documentation has a document about [how to create an OAuth app](https://marketplace.zoom.us/docs/guides/build/oauth-app) and another document about the [OAuth autorization flow](https://marketplace.zoom.us/docs/guides/auth/oauth) but I personnality was very confused by the later document so here is a brief step-by-step summary: - you create an OAuth app, define which permissions your app requires and publish the app in the Zoom marketplace. - user installs your app. During installation, user is presentd with a screen listing the permissons your app requires. User must click `accept`. -- Zoom generates a "authorization code". This code can be used only once to generate the first access token and refresh token. I CAN'T STRESS THIS ENOUGH: the authorization code can be used only one time. This was the confusing part to me: somehow I didn't understand that this code could be used only one time and I was attempting to use it repeatedly. Zoom would accept the code one time and would reject it subsequently, which lead to many hours of frustration while trying to figure out why the code was sometimes rejected. +- Zoom generates a "authorization code". This code can be used only once to generate the first access token and refresh token. I CAN'T STRESS THIS ENOUGH: the authorization code can be used only one time. This was the confusing part to me: somehow I didn't understand that this code could be used only one time and I was attempting to use it repeatedly. Zoom would accept the code the first time and would reject it subsequently, which lead to many hours of frustration while trying to figure out why the code was sometimes rejected. - The access token is valid for 60 minutes and must therefore be "refreshed" periodically. -ZoomNet takes care of generating the access token and refresh token but it's your responsability to store these generated values. +ZoomNet takes care of generating the access token and the refresh token but it's your responsability to store these generated values. ```csharp var clientId = "... your client ID ..."; @@ -73,16 +76,67 @@ var clientSecret = "... your client secret ..."; var refreshToken = "... the refresh token previously issued by Zoom ..."; var accessToken = "... the access token previously issued by Zoom ..."; var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, accessToken, - (newRefreshToken, newAccessToken) => - { - /* + (newRefreshToken, newAccessToken) => + { + /* Save the new refresh token and the access token to a safe place so you can provide it the next time - you need to instantiate an OAuthConnectionInfo + you need to instantiate an OAuthConnectionInfo. + */ + }); +``` + +For demonstration purposes, here's how you could use your operating system's environment variables to store the tokens + +```csharp +var clientId = "... your client ID ..."; +var clientSecret = "... your client secret ..."; +var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User); +var accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", EnvironmentVariableTarget.User); +var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); +``` + +#### Connection using Server-to-Server OAuth + +This authentication method is the replacement for JWT authentication which Zoom announced will be made obsolete in June 2023. + +From Zoom's documentation: +> A Server-to-Server OAuth app enables you to securely integrate with Zoom APIs and get your account owner access token without user interaction. This is different from the OAuth app type, which requires user authentication. See Using OAuth 2.0 for details. + +ZoomNet takes care of getting a new access token and it also refreshes a previously issued token when it expires (Server-to-Server access token are valid for one hour). +Therefore + +```csharp +var clientId = "... your client ID ..."; +var clientSecret = "... your client secret ..."; +var accountId = "... your account id ..."; +var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, accountId, + (_, newAccessToken) => + { + /* + Server-to-Server OAuth does not use a refresh token. That's why I used '_' as the first parameter + in this delegate declaration. Furthermore, ZoomNet will take care of getting a new access token + and to refresh it whenever it expires therefore there is no need for you to preserve the token + like you must do for the 'standard' OAuth authentication. + + In fact, this delegate is completely optional when using Server-to-Server OAuth. Feel free to pass + a null value in lieu of a delegate. */ }); ``` +The delegate being optional in the server-to-server scenario you can therefore simplify the connection info declaration like so: + +```csharp +var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, accountId, null); +``` + + ### Client You declare your client variable like so: diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs index 499006e3..3a5eb970 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chat.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chat.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Threading; @@ -46,14 +47,46 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync($"Account channel \"{channel.Id}\" has {paginatedMembers.TotalRecords} members").ConfigureAwait(false); // SEND A MESSAGE TO THE CHANNEL - var messageId = await client.Chat.SendMessageToChannelAsync(channel.Id, "This is a test from integration test", null, cancellationToken).ConfigureAwait(false); + var messageId = await client.Chat.SendMessageToChannelAsync(channel.Id, "This is a test from integration test", null, null, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Message \"{messageId}\" sent").ConfigureAwait(false); await Task.Delay(1000, cancellationToken).ConfigureAwait(false); // Allow the Zoom system to process this message and avoid subsequent "message doesn't exist" error messages // UPDATE THE MESSAGE - await client.Chat.UpdateMessageToChannelAsync(messageId, channel.Id, "This is an updated message from integration testing", null, cancellationToken).ConfigureAwait(false); + await client.Chat.UpdateMessageToChannelAsync(messageId, channel.Id, "This is an updated message from integration testing.\nThis message contains simple text.", null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Message \"{messageId}\" updated").ConfigureAwait(false); + // REPLY TO THE MESSAGE + messageId = await client.Chat.SendMessageToChannelAsync(channel.Id, "This is a reply to the message.", messageId, null, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Reply \"{messageId}\" sent").ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); // Allow the Zoom system to process this message and avoid subsequent "message doesn't exist" error messages + + // Check that this computer has a folder containing sample images which we can use to send files to the channel + var samplePicturesFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Samples"); + if (Directory.Exists(samplePicturesFolder)) + { + var rnd = new Random(); + var samplePictures = Directory.EnumerateFiles(samplePicturesFolder, "*.jpg"); + if (samplePictures.Any()) + { + // SEND A FILE TO THE CHANNEL + var samplePicture = samplePictures.ElementAt(rnd.Next(0, samplePictures.Count())); + using var fileToSendStream = File.OpenRead(samplePicture); + var sentFileId = await client.Chat.SendFileAsync(null, "me", null, channel.Id, Path.GetFileName(samplePicture), fileToSendStream, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"File {sentFileId} sent").ConfigureAwait(false); + + // UPLOAD A FILE + samplePicture = samplePictures.ElementAt(rnd.Next(0, samplePictures.Count())); + using var fileToUploadStream = File.OpenRead(samplePicture); + var uploadedFileId = await client.Chat.UploadFileAsync("me", Path.GetFileName(samplePicture), fileToUploadStream, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"File {uploadedFileId} uploaded").ConfigureAwait(false); + + // SEND A MESSAGE WITH ATTACHMENT + messageId = await client.Chat.SendMessageToChannelAsync(channel.Id, "This message has an attachment", null, new[] { uploadedFileId }, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Message \"{messageId}\" sent with attachment").ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); // Allow the Zoom system to process this message and avoid subsequent "message doesn't exist" error messages + } + } + // RETRIEVE LIST OF MESSAGES var paginatedMessages = await client.Chat.GetMessagesToChannelAsync(channel.Id, 100, null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"There are {paginatedMessages.TotalRecords} messages in channel \"{channel.Id}\"").ConfigureAwait(false); diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index ce274174..1fb92a36 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -29,6 +29,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie // CLEANUP PREVIOUS INTEGRATION TESTS THAT MIGHT HAVE BEEN INTERRUPTED BEFORE THEY HAD TIME TO CLEANUP AFTER THEMSELVES var cleanUpTasks = paginatedScheduledMeetings.Records .Union(paginatedLiveMeetings.Records) + .Union(paginatedUpcomingMeetings.Records) .Where(m => m.Topic.StartsWith("ZoomNet Integration Testing:")) .Select(async oldMeeting => { diff --git a/Source/ZoomNet.IntegrationTests/Tests/Webinars.cs b/Source/ZoomNet.IntegrationTests/Tests/Webinars.cs index 6a680eaf..1d48c530 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Webinars.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Webinars.cs @@ -22,7 +22,6 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie // CLEANUP PREVIOUS INTEGRATION TESTS THAT MIGHT HAVE BEEN INTERRUPTED BEFORE THEY HAD TIME TO CLEANUP AFTER THEMSELVES var cleanUpTasks = paginatedWebinars.Records - .Union(paginatedWebinars.Records) .Where(m => m.Topic.StartsWith("ZoomNet Integration Testing:")) .Select(async oldWebinar => { diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index f2bf79c6..b14f9a0f 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -58,23 +58,38 @@ public async Task RunAsync() { 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 accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", EnvironmentVariableTarget.User); - connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, accessToken, - (newRefreshToken, newAccessToken) => - { - Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); - }); - - //var authorizationCode = "<-- the code generated by Zoom when the app is authorized by the user -->"; - //connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode, - // (newRefreshToken, newAccessToken) => - // { - // Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - // Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); - // }); + + // Server-to-Server OAuth + if (!string.IsNullOrEmpty(accountId)) + { + connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, accountId, + (_, newAccessToken) => + { + Console.Out.WriteLine($"A new access token was issued: {newAccessToken}"); + }); + } + + // Standard OAuth + else + { + connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); + + //var authorizationCode = "<-- the code generated by Zoom when the app is authorized by the user -->"; + //connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode, + // (newRefreshToken, newAccessToken) => + // { + // Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + // Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + // }); + } } var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null; diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index e4a7be46..422e778b 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/Utilities/OAuthTokenHandlerTests.cs b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs index 44769b59..40be9691 100644 --- a/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs +++ b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs @@ -25,7 +25,12 @@ public void Attempt_to_refresh_token_multiple_times_despite_exception() var clientId = "abc123"; var clientSecret = "xyz789"; var authorizationCode = "INVALID_AUTH_CODE"; - var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode, null); + var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode, + (newRefreshToken, newAccessToken) => + { + // Intentionally left blank + }, + null); var apiResponse = "{ \"reason\":\"Invalid authorization code " + authorizationCode + "\",\"error\":\"invalid_request\"}"; var mockHttp = new MockHttpMessageHandler(); diff --git a/Source/ZoomNet.UnitTests/WebhookParserTests.cs b/Source/ZoomNet.UnitTests/WebhookParserTests.cs index 7002ca14..83bc2023 100644 --- a/Source/ZoomNet.UnitTests/WebhookParserTests.cs +++ b/Source/ZoomNet.UnitTests/WebhookParserTests.cs @@ -30,8 +30,8 @@ public class WebhookParserTests ""settings"": { ""use_pmi"": false, ""alternative_hosts"": """" - } } + } }, ""event_ts"": 1617628462392 }"; @@ -128,6 +128,28 @@ public class WebhookParserTests } }"; + private const string MEETING_SERVICE_ISSUE_WEBHOOK = @" + { + ""event"": ""meeting.alert"", + ""event_ts"": 1626230691572, + ""payload"": { + ""account_id"": ""AAAAAABBBB"", + ""object"": { + ""id"": 1234567890, + ""uuid"": ""4444AAAiAAAAAiAiAiiAii=="", + ""host_id"": ""x1yCzABCDEfg23HiJKl4mN"", + ""topic"": ""My Meeting"", + ""type"": 2, + ""join_time"": ""2021-07-13T21:44:51Z"", + ""timezone"": ""America/Los_Angeles"", + ""duration"": 60, + ""issues"": [ + ""Unstable audio quality"" + ] + } + } + }"; + #endregion [Fact] @@ -239,5 +261,25 @@ public void MeetingSharingStarted() parsedMeeting.Duration.ShouldBe(60); parsedMeeting.Timezone.ShouldBe("America/Los_Angeles"); } + + [Fact] + public void MeetingServiceIssue() + { + var parsedEvent = (MeetingServiceIssueEvent)new WebhookParser().ParseEventWebhook(MEETING_SERVICE_ISSUE_WEBHOOK); + + parsedEvent.EventType.ShouldBe(EventType.MeetingServiceIssue); + parsedEvent.AccountId.ShouldBe("AAAAAABBBB"); + parsedEvent.Timestamp.ShouldBe(new DateTime(2021, 7, 14, 2, 44, 51, 572, DateTimeKind.Utc)); + parsedEvent.Issues.ShouldBe(new[] { "Unstable audio quality" }); + + parsedEvent.Meeting.GetType().ShouldBe(typeof(ScheduledMeeting)); + var parsedMeeting = (ScheduledMeeting)parsedEvent.Meeting; + parsedMeeting.Id.ShouldBe(1234567890L); + parsedMeeting.Topic.ShouldBe("My Meeting"); + parsedMeeting.Uuid.ShouldBe("4444AAAiAAAAAiAiAiiAii=="); + parsedMeeting.HostId.ShouldBe("x1yCzABCDEfg23HiJKl4mN"); + parsedMeeting.Duration.ShouldBe(60); + parsedMeeting.Timezone.ShouldBe("America/Los_Angeles"); + } } } diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index a1cb3567..24a0d403 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -12,12 +12,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 0c96affe..e4e1cbf2 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -1,5 +1,6 @@ using Pathoschild.Http.Client; using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -404,7 +405,7 @@ internal static async Task AsString(this IRequest request, Encoding enco /// Returns the human readable representation of the TimeSpan. internal static string ToDurationString(this TimeSpan timeSpan) { - void AppendFormatIfNecessary(StringBuilder stringBuilder, string timePart, int value) + static void AppendFormatIfNecessary(StringBuilder stringBuilder, string timePart, int value) { if (value <= 0) return; stringBuilder.AppendFormat($" {value} {timePart}{(value > 1 ? "s" : string.Empty)}"); @@ -467,7 +468,7 @@ internal static string EnsureEndsWith(this string value, string suffix) internal static T GetPropertyValue(this JsonElement element, string name, T defaultValue) { - return GetPropertyValue(element, name, default, false); + return GetPropertyValue(element, name, defaultValue, false); } internal static T GetPropertyValue(this JsonElement element, string name) @@ -983,40 +984,69 @@ private static T GetPropertyValue(this JsonElement element, string name, T de if (typeOfT.IsEnum) { - switch (property.Value.ValueKind) + return property.Value.ValueKind switch { - case JsonValueKind.String: return (T)Enum.Parse(typeof(T), property.Value.GetString()); - case JsonValueKind.Number: return (T)Enum.ToObject(typeof(T), property.Value.GetInt16()); - default: throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)); - } + JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()), + JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()), + _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)), + }; } if (typeOfT.IsGenericType && typeOfT.GetGenericTypeDefinition() == typeof(Nullable<>)) { - typeOfT = Nullable.GetUnderlyingType(typeOfT); - } - - switch (typeOfT) - { - case Type boolType when boolType == typeof(bool): return (T)(object)property.Value.GetBoolean(); - case Type strType when strType == typeof(string): return (T)(object)property.Value.GetString(); - case Type bytesType when bytesType == typeof(byte[]): return (T)(object)property.Value.GetBytesFromBase64(); - case Type sbyteType when sbyteType == typeof(sbyte): return (T)(object)property.Value.GetSByte(); - case Type byteType when byteType == typeof(byte): return (T)(object)property.Value.GetByte(); - case Type shortType when shortType == typeof(short): return (T)(object)property.Value.GetInt16(); - case Type ushortType when ushortType == typeof(ushort): return (T)(object)property.Value.GetUInt16(); - case Type intType when intType == typeof(int): return (T)(object)property.Value.GetInt32(); - case Type uintType when uintType == typeof(uint): return (T)(object)property.Value.GetUInt32(); - case Type longType when longType == typeof(long): return (T)(object)property.Value.GetInt64(); - case Type ulongType when ulongType == typeof(ulong): return (T)(object)property.Value.GetUInt64(); - case Type doubleType when doubleType == typeof(double): return (T)(object)property.Value.GetDouble(); - case Type floatType when floatType == typeof(float): return (T)(object)property.Value.GetSingle(); - case Type decimalType when decimalType == typeof(decimal): return (T)(object)property.Value.GetDecimal(); - case Type datetimeType when datetimeType == typeof(DateTime): return (T)(object)property.Value.GetDateTime(); - case Type offsetType when offsetType == typeof(DateTimeOffset): return (T)(object)property.Value.GetDateTimeOffset(); - case Type guidType when guidType == typeof(Guid): return (T)(object)property.Value.GetGuid(); - default: throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)); + var underlyingType = Nullable.GetUnderlyingType(typeOfT); + var getElementValue = typeof(Internal) + .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(underlyingType); + + return (T)getElementValue.Invoke(null, new object[] { property.Value }); + } + + if (typeOfT.IsArray) + { + var elementType = typeOfT.GetElementType(); + var getElementValue = typeof(Internal) + .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(elementType); + + var arrayList = new ArrayList(property.Value.GetArrayLength()); + foreach (var arrayElement in property.Value.EnumerateArray()) + { + var elementValue = getElementValue.Invoke(null, new object[] { arrayElement }); + arrayList.Add(elementValue); + } + + return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T)); } + + return property.Value.GetElementValue(); + } + + private static T GetElementValue(this JsonElement element) + { + var typeOfT = typeof(T); + + return typeOfT switch + { + Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(), + Type strType when strType == typeof(string) => (T)(object)element.GetString(), + Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(), + Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(), + Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(), + Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(), + Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(), + Type intType when intType == typeof(int) => (T)(object)element.GetInt32(), + Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(), + Type longType when longType == typeof(long) => (T)(object)element.GetInt64(), + Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(), + Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(), + Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(), + Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(), + Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(), + Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(), + Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(), + _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)), + }; } } } diff --git a/Source/ZoomNet/Extensions/Public.cs b/Source/ZoomNet/Extensions/Public.cs index 23fdafa8..2dca00bb 100644 --- a/Source/ZoomNet/Extensions/Public.cs +++ b/Source/ZoomNet/Extensions/Public.cs @@ -100,14 +100,16 @@ public static Task LeaveChannelAsync(this IChat chatResource, string channelId, /// The chat resource. /// The email address of the contact to whom you would like to send the message. /// The message. + /// The reply message's ID. + /// A list of the file IDs to send. This field only accepts a maximum of six file IDs. /// Mentions. /// The cancellation token. /// /// The message Id. /// - public static Task SendMessageToContactAsync(this IChat chatResource, string recipientEmail, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) + public static Task SendMessageToContactAsync(this IChat chatResource, string recipientEmail, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default) { - return chatResource.SendMessageToContactAsync("me", recipientEmail, message, mentions, cancellationToken); + return chatResource.SendMessageToContactAsync("me", recipientEmail, message, replyMessageId, fileIds, mentions, cancellationToken); } /// @@ -116,14 +118,16 @@ public static Task SendMessageToContactAsync(this IChat chatResource, st /// The chat resource. /// The channel Id. /// The message. + /// The reply message's ID. + /// A list of the file IDs to send. This field only accepts a maximum of six file IDs. /// Mentions. /// The cancellation token. /// /// The message Id. /// - public static Task SendMessageToChannelAsync(this IChat chatResource, string channelId, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) + public static Task SendMessageToChannelAsync(this IChat chatResource, string channelId, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default) { - return chatResource.SendMessageToChannelAsync("me", channelId, message, mentions, cancellationToken); + return chatResource.SendMessageToChannelAsync("me", channelId, message, replyMessageId, fileIds, mentions, cancellationToken); } /// @@ -252,7 +256,7 @@ public static async Task ParseEventWebhookAsync(IWebhookParser parser, St /// public static Task DownloadFileAsync(this ICloudRecordings cloudRecordingsResource, RecordingFile recordingFile, CancellationToken cancellationToken = default) { - return cloudRecordingsResource.DownloadFileAsync(recordingFile.DownloadUrl); + return cloudRecordingsResource.DownloadFileAsync(recordingFile.DownloadUrl, cancellationToken); } /// @@ -267,7 +271,7 @@ public static Task DownloadFileAsync(this ICloudRecordings cloudRecordin /// public static Task InviteParticipantAsync(this IMeetings meetingsResource, long meetingId, string emailAddress, CancellationToken cancellationToken = default) { - return meetingsResource.InviteParticipantsAsync(meetingId, new[] { emailAddress }); + return meetingsResource.InviteParticipantsAsync(meetingId, new[] { emailAddress }, cancellationToken); } } } diff --git a/Source/ZoomNet/IZoomClient.cs b/Source/ZoomNet/IZoomClient.cs index 213f76da..4f54bc6e 100644 --- a/Source/ZoomNet/IZoomClient.cs +++ b/Source/ZoomNet/IZoomClient.cs @@ -1,3 +1,4 @@ +using System; using ZoomNet.Resources; namespace ZoomNet @@ -46,6 +47,7 @@ public interface IZoomClient /// /// The data compliance resource. /// + [Obsolete("The Data Compliance API is deprecated")] IDataCompliance DataCompliance { get; } /// diff --git a/Source/ZoomNet/Json/DayOfWeekConverter.cs b/Source/ZoomNet/Json/DayOfWeekConverter.cs index 509535a8..fec89404 100644 --- a/Source/ZoomNet/Json/DayOfWeekConverter.cs +++ b/Source/ZoomNet/Json/DayOfWeekConverter.cs @@ -19,8 +19,7 @@ internal class DayOfWeekConverter : JsonConverter if (reader.TokenType == JsonTokenType.Null) return null; - var rawValue = reader.GetString(); - var value = Convert.ToInt32(rawValue) - 1; + var value = reader.GetInt32() - 1; return (DayOfWeek)value; } @@ -37,8 +36,8 @@ public override void Write(Utf8JsonWriter writer, DayOfWeek? value, JsonSerializ } else { - var singleDay = (Convert.ToInt32(value.Value) + 1).ToString(); - writer.WriteStringValue(singleDay); + var singleDay = Convert.ToInt32(value.Value) + 1; + writer.WriteNumberValue(singleDay); } } } diff --git a/Source/ZoomNet/Json/WebhookEventConverter.cs b/Source/ZoomNet/Json/WebhookEventConverter.cs index 2dac4fbd..98088a36 100644 --- a/Source/ZoomNet/Json/WebhookEventConverter.cs +++ b/Source/ZoomNet/Json/WebhookEventConverter.cs @@ -33,7 +33,7 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe break; case EventType.MeetingServiceIssue: var meetingServiceIssueEvent = payloadJsonProperty.ToObject(options); - meetingServiceIssueEvent.Issues = payloadJsonProperty.GetPropertyValue("object/issues", string.Empty); + meetingServiceIssueEvent.Issues = payloadJsonProperty.GetPropertyValue("object/issues", Array.Empty()); webHookEvent = meetingServiceIssueEvent; break; case EventType.MeetingCreated: @@ -91,7 +91,7 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe break; case EventType.MeetingRegistrationCancelled: var meetingRegistrationCancelledEvent = payloadJsonProperty.ToObject(options); - meetingRegistrationCancelledEvent.Registrant = payloadJsonProperty.GetProperty("objectregistrant", true).Value.ToObject(); + meetingRegistrationCancelledEvent.Registrant = payloadJsonProperty.GetProperty("object/registrant", true).Value.ToObject(); webHookEvent = meetingRegistrationCancelledEvent; break; case EventType.MeetingRegistrationDenied: diff --git a/Source/ZoomNet/Models/MeetingSettings.cs b/Source/ZoomNet/Models/MeetingSettings.cs index 9b8d5b39..7f099482 100644 --- a/Source/ZoomNet/Models/MeetingSettings.cs +++ b/Source/ZoomNet/Models/MeetingSettings.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Json.Serialization; namespace ZoomNet.Models @@ -8,100 +9,110 @@ namespace ZoomNet.Models public class MeetingSettings { /// - /// Gets or sets the value indicating whether to start video when host joins the meeting. + /// Gets or sets the value indicating alternative hosts email addresses or IDs. Multiple value separated by semicolon. /// - [JsonPropertyName("host_video")] - public bool? StartVideoWhenHostJoins { get; set; } + [JsonPropertyName("alternative_hosts")] + public string AlternativeHosts { get; set; } /// - /// Gets or sets the value indicating whether to start video when participants join the meeting. + /// Gets or sets the approval type. /// - [JsonPropertyName("participant_video")] - public bool? StartVideoWhenParticipantsJoin { get; set; } + [JsonPropertyName("approval_type")] + public ApprovalType? ApprovalType { get; set; } /// - /// Gets or sets the value indicating whether the meeting should be hosted in China. + /// Gets or sets the value indicating how participants can join the audio portion of the meeting. /// - [JsonPropertyName("cn_meeting")] - public bool? HostInChina { get; set; } + [JsonPropertyName("audio")] + public AudioType? Audio { get; set; } /// - /// Gets or sets the value indicating whether the meeting should be hosted in India. + /// Gets or sets the value indicating if audio is recorded and if so, when the audio is saved. /// - [JsonPropertyName("in_meeting")] - public bool? HostInIndia { get; set; } + [JsonPropertyName("auto_recording")] + public RecordingType? AutoRecording { get; set; } /// - /// Gets or sets the value indicating whether participants can join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings. + /// Gets or sets the value indicating whether registration is closed after event date. /// - [JsonPropertyName("join_before_host")] - public bool? JoinBeforeHost { get; set; } + [JsonPropertyName("close_registration")] + public bool? CloseRegistration { get; set; } /// - /// Gets or sets the value indicating whether participants are muted upon entry. + /// Gets or sets the value indicating whether the meeting should be hosted in China. /// - [JsonPropertyName("mute_upon_entry")] - public bool? MuteUponEntry { get; set; } + [JsonPropertyName("cn_meeting")] + [Obsolete("Deprecated")] + public bool? HostInChina { get; set; } /// - /// Gets or sets the value indicating whether a watermark should be displayed when viewing shared screen. + /// Gets or sets the contact email for registration. /// - [JsonPropertyName("watermark")] - public bool? Watermark { get; set; } + [JsonPropertyName("contact_email")] + public string ContactEmail { get; set; } /// - /// Gets or sets the value indicating whether to use Personal Meeting ID. Only used for scheduled meetings and recurring meetings with no fixed time. + /// Gets or sets the contact name for registration. /// - [JsonPropertyName("use_pmi")] - public bool? UsePmi { get; set; } + [JsonPropertyName("contact_name")] + public string ContactName { get; set; } /// - /// Gets or sets the approval type. + /// Gets or sets the value indicating that only signed-in users can join this meeting. /// - [JsonPropertyName("approval_type")] - public ApprovalType? ApprovalType { get; set; } + [JsonPropertyName("enforce_login")] + [Obsolete("This field is deprecated and will not be supported in the future.")] + public bool? EnforceLogin { get; set; } /// - /// Gets or sets the registration type. Used for recurring meeting with fixed time only. + /// Gets or sets the value indicating only signed-in users with specified domains can join this meeting. /// - [JsonPropertyName("registration_type")] - public RegistrationType? RegistrationType { get; set; } + [JsonPropertyName("enforce_login_domains")] + [Obsolete("This field is deprecated and will not be supported in the future.")] + public string EnforceLoginDomains { get; set; } /// - /// Gets or sets the value indicating how participants can join the audio portion of the meeting. + /// Gets or sets the list of global dial-in countries. /// - [JsonPropertyName("audio")] - public AudioType? Audio { get; set; } + [JsonPropertyName("global_dial_in_countries")] + public string[] GlobalDialInCountries { get; set; } /// - /// Gets or sets the value indicating if audio is recorded and if so, when the audio is saved. + /// Gets or sets the value indicating whether the 'Allow host to save video order' feature is enabled. /// - [JsonPropertyName("auto_recording")] - public RecordingType? AutoRecording { get; set; } + [JsonPropertyName("host_save_video_order")] + public bool? HostCanSaveVideoOrder { get; set; } /// - /// Gets or sets the value indicating that only signed-in users can join this meeting. + /// Gets or sets the value indicating whether to start video when host joins the meeting. /// - [JsonPropertyName("enforce_login")] - public bool? EnforceLogin { get; set; } + [JsonPropertyName("host_video")] + public bool? StartVideoWhenHostJoins { get; set; } /// - /// Gets or sets the value indicating only signed-in users with specified domains can join this meeting. + /// Gets or sets the value indicating whether the meeting should be hosted in India. /// - [JsonPropertyName("enforce_login_domains")] - public string EnforceLoginDomains { get; set; } + [JsonPropertyName("in_meeting")] + [Obsolete("Deprecated")] + public bool? HostInIndia { get; set; } /// - /// Gets or sets the value indicating alternative hosts emails or IDs. Multiple value separated by comma. + /// Gets or sets the value indicating whether participants can join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings. /// - [JsonPropertyName("alternative_hosts")] - public string AlternativeHosts { get; set; } + [JsonPropertyName("join_before_host")] + public bool? JoinBeforeHost { get; set; } /// - /// Gets or sets the value indicating whether registration is closed after event date. + /// Gets or sets the value indicating whether participants are muted upon entry. /// - [JsonPropertyName("close_registration")] - public bool? CloseRegistration { get; set; } + [JsonPropertyName("mute_upon_entry")] + public bool? MuteUponEntry { get; set; } + + /// + /// Gets or sets the value indicating whether to start video when participants join the meeting. + /// + [JsonPropertyName("participant_video")] + public bool? StartVideoWhenParticipantsJoin { get; set; } /// /// Gets or sets the value indicating whether a confirmation email is sent when a participant registers. @@ -110,33 +121,33 @@ public class MeetingSettings public bool? SendRegistrationConfirmationEmail { get; set; } /// - /// Gets or sets the value indicating whether to use a waiting room. + /// Gets or sets the registration type. Used for recurring meeting with fixed time only. /// - [JsonPropertyName("waiting_room")] - public bool? WaitingRoom { get; set; } + [JsonPropertyName("registration_type")] + public RegistrationType? RegistrationType { get; set; } /// - /// Gets or sets the list of global dial-in countries. + /// Gets or sets the value indicating whether to ask the permission to unmute partecipants. /// - [JsonPropertyName("global_dial_in_countries")] - public string[] GlobalDialInCountries { get; set; } + [JsonPropertyName("request_permission_to_unmute_participants")] + public bool? RequestPermissionToUnmutePartecipants { get; set; } /// - /// Gets or sets the contact name for registration. + /// Gets or sets the value indicating whether to use Personal Meeting ID. Only used for scheduled meetings and recurring meetings with no fixed time. /// - [JsonPropertyName("contact_name")] - public string ContactName { get; set; } + [JsonPropertyName("use_pmi")] + public bool? UsePmi { get; set; } /// - /// Gets or sets the contact email for registration. + /// Gets or sets the value indicating whether to use a waiting room. /// - [JsonPropertyName("contact_email")] - public string ContactEmail { get; set; } + [JsonPropertyName("waiting_room")] + public bool? WaitingRoom { get; set; } /// - /// Gets or sets the value indicating whether to ask the permission to unmute partecipants. + /// Gets or sets the value indicating whether a watermark should be displayed when viewing shared screen. /// - [JsonPropertyName("request_permission_to_unmute_participants")] - public bool? RequestPermissionToUnmutePartecipants { get; set; } + [JsonPropertyName("watermark")] + public bool? Watermark { get; set; } } } diff --git a/Source/ZoomNet/Models/MeetingSummary.cs b/Source/ZoomNet/Models/MeetingSummary.cs new file mode 100644 index 00000000..086571a1 --- /dev/null +++ b/Source/ZoomNet/Models/MeetingSummary.cs @@ -0,0 +1,89 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Summary information about a meeting. + /// + public class MeetingSummary + { + /// Gets or sets the meeting description. + /// + /// The length of agenda gets truncated to 250 characters + /// when you list all meetings for a user. To view the complete + /// agenda of a meeting, retrieve details for a single meeting, + /// use the Get a meeting API. + /// + [JsonPropertyName("agenda")] + public string Agenda { get; set; } + + /// + /// Gets or sets the date and time when the meeting was created. + /// + [JsonPropertyName("created_at")] + public DateTime CreatedOn { get; set; } + + /// + /// Gets or sets the duration in minutes. + /// + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// + /// Gets or sets the ID of the user who is set as the host of the meeting. + /// + [JsonPropertyName("host_id")] + public string HostId { get; set; } + + /// + /// Gets or sets the meeting id, also known as the meeting number. + /// + [JsonPropertyName("id")] + public long Id { get; set; } + + /// + /// Gets or sets the URL for the host to start the meeting. + /// + [JsonPropertyName("start_url")] + public string StartUrl { get; set; } + + /// + /// Gets or sets the personal meeting id. + /// + [JsonPropertyName("pmi")] + public string PersonalMeetingId { get; set; } + + /// + /// Gets or sets the start time. + /// + [JsonPropertyName("start_time")] + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the timezone. + /// For example, "America/Los_Angeles". + /// Please reference our timezone list for supported timezones and their formats. + /// + [JsonPropertyName("timezone")] + public string Timezone { get; set; } + + /// + /// Gets or sets the topic of the meeting. + /// + [JsonPropertyName("topic")] + public string Topic { get; set; } + + /// + /// Gets or sets the meeting type. + /// + [JsonPropertyName("type")] + public MeetingType Type { get; set; } + + /// + /// Gets or sets the unique id. + /// + [JsonPropertyName("uuid")] + public string Uuid { get; set; } + } +} diff --git a/Source/ZoomNet/Models/OAuthGrantType.cs b/Source/ZoomNet/Models/OAuthGrantType.cs index 1671d2e0..372ae312 100644 --- a/Source/ZoomNet/Models/OAuthGrantType.cs +++ b/Source/ZoomNet/Models/OAuthGrantType.cs @@ -7,6 +7,12 @@ namespace ZoomNet.Models /// public enum OAuthGrantType { + /// + /// Account Credentials. This is the grant type for "Server-to-Server" OAuth. + /// + [EnumMember(Value = "account_credentials")] + AccountCredentials, + /// /// Authorization code. This is the most commonly used grant type for Zoom APIs. /// diff --git a/Source/ZoomNet/Models/Panelist.cs b/Source/ZoomNet/Models/Panelist.cs index 566cb396..6fab2d37 100644 --- a/Source/ZoomNet/Models/Panelist.cs +++ b/Source/ZoomNet/Models/Panelist.cs @@ -33,5 +33,11 @@ public class Panelist /// [JsonPropertyName("join_url")] public string JoinUrl { get; set; } + + /// + /// Gets or sets the panelist's virtual background id. + /// + [JsonPropertyName("virtual_background_id")] + public string VirtualBackgroundId { get; set; } } } diff --git a/Source/ZoomNet/Models/ScreenshareContentType.cs b/Source/ZoomNet/Models/ScreenshareContentType.cs index ee2d821d..58bafd93 100644 --- a/Source/ZoomNet/Models/ScreenshareContentType.cs +++ b/Source/ZoomNet/Models/ScreenshareContentType.cs @@ -23,6 +23,24 @@ public enum ScreenshareContentType /// Desktop. /// [EnumMember(Value = "desktop")] - Desktop + Desktop, + + /// + /// Airplay. + /// + [EnumMember(Value = "airplay")] + Airplay, + + /// + /// Camera. + /// + [EnumMember(Value = "camera")] + Camera, + + /// + /// An unrecognized application, such as a third party app. + /// + [EnumMember(Value = "unknown")] + Unknown } } diff --git a/Source/ZoomNet/Models/Webhooks/AppDeauthorizedEvent.cs b/Source/ZoomNet/Models/Webhooks/AppDeauthorizedEvent.cs index f44944f7..3dec048b 100644 --- a/Source/ZoomNet/Models/Webhooks/AppDeauthorizedEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/AppDeauthorizedEvent.cs @@ -26,14 +26,6 @@ public class AppDeauthorizedEvent : Event [JsonPropertyName("client_id")] public string ClientId { get; set; } - /// - /// Gets or sets a value indicating whether the user has authorized you to store their data even after they uninstall your app. - /// If the value is 'false', you must delete the user's data and call the [Data Compliance API](https://marketplace.zoom.us/docs/api-reference/zoom-api/data-compliance/compliance) within ten days of receiving the deauthorization webhook. - /// If the value is 'true', no further action is required on your end. - /// - [JsonPropertyName("user_data_retention")] - public bool CanPreserveUserData { get; set; } - /// /// Gets or sets the time at which the user uninstalled your app. /// diff --git a/Source/ZoomNet/Models/Webhooks/EventType.cs b/Source/ZoomNet/Models/Webhooks/EventType.cs index 4f3f019d..70bba9b6 100644 --- a/Source/ZoomNet/Models/Webhooks/EventType.cs +++ b/Source/ZoomNet/Models/Webhooks/EventType.cs @@ -21,7 +21,7 @@ public enum EventType /// /// A service issue has been encountered during a meeting. /// - [EnumMember(Value = "meeting.alerts")] + [EnumMember(Value = "meeting.alert")] MeetingServiceIssue, /// @@ -37,154 +37,160 @@ public enum EventType MeetingDeleted, /// - /// A meeting has been updated. + /// A meeting has ended. /// - [EnumMember(Value = "meeting.updated")] - MeetingUpdated, + [EnumMember(Value = "meeting.ended")] + MeetingEnded, /// - /// A meeting has been permanently deleted. + /// A meeting live stream has started. /// - [EnumMember(Value = "meeting.permanently_deleted")] - MeetingPermanentlyDeleted, + [EnumMember(Value = "meeting.live_streaming_started")] + MeetingLiveStreamStarted, /// - /// A meeting has started. + /// A meeting live stream has stopped. /// - [EnumMember(Value = "meeting.started")] - MeetingStarted, + [EnumMember(Value = "meeting.live_streaming_stopped")] + MeetingLiveStreamStopped, /// - /// A meeting has ended. + /// A meeting host has admitted a participant from a waiting room to the meeting. /// - [EnumMember(Value = "meeting.ended")] - MeetingEnded, + [EnumMember(Value = "meeting.participant_admitted")] + MeetingParticipantAdmitted, /// - /// A meeting has been recovered. + /// An attendee completed an end-of-meeting experience feedback survey for a meeting. /// - [EnumMember(Value = "meeting.recovered")] - MeetingRecovered, + [EnumMember(Value = "meeting.participant_feedback")] + MeetingParticipantFeedback, /// - /// A participant has registered for a meeting. + /// An attendee has joined the meeting before the host. /// - [EnumMember(Value = "meeting.registration_created")] - MeetingRegistrationCreated, + [EnumMember(Value = "meeting.participant_jbh_joined")] + MeetingParticipantJoinedBeforeHost, /// - /// A meeting registration has been approved. + /// An attendee is wating for the host to join the meeting. /// - [EnumMember(Value = "meeting.registration_approved")] - MeetingRegistrationApproved, + [EnumMember(Value = "meeting.participant_jbh_waiting")] + MeetingParticipantWaitingForHost, /// - /// A meeting registration has been cancelled. + /// An attendee has joined a meting waiting room. /// - [EnumMember(Value = "meeting.registration_cancelled")] - MeetingRegistrationCancelled, + [EnumMember(Value = "meeting.participant_joined_waiting_room")] + MeetingParticipantJoinedWaitingRoom, /// - /// A meeting registration has been denied. + /// A meeting host has admitted a participant from a waiting room to the meeting. /// - [EnumMember(Value = "meeting.registration_denied")] - MeetingRegistrationDenied, + [EnumMember(Value = "meeting.participant_joined")] + MeetingParticipantJoined, /// - /// An attendee or the host has started sharing their screen during a meeting. + /// An attendee has left a meting waiting room. /// - [EnumMember(Value = "meeting.sharing_started")] - MeetingSharingStarted, + [EnumMember(Value = "meeting.participant_left_waiting_room")] + MeetingParticipantLeftWaitingRoom, /// - /// An attendee or the host has stoped sharing their screen during a meeting. + /// A meeting participant has left the meeting. /// - [EnumMember(Value = "meeting.sharing_ended")] - MeetingSharingEnded, + [EnumMember(Value = "meeting.participant_left")] + MeetingParticipantLeft, /// - /// An attendee is wating for the host to join the meeting. + /// A meeting participant who has already joined a meeting is sent back to the waiting room during the meeting. /// - [EnumMember(Value = "meeting.participant_jbh_waiting")] - MeetingParticipantWaitingForHost, + [EnumMember(Value = "meeting.participant_put_in_waiting_room")] + MeetingParticipantSentToWaitingRoom, /// - /// An attendee has joined the meeting before the host. + /// A host or meeting attendee changed their role during the meeting. /// - [EnumMember(Value = "meeting.participant_jbh_joined")] - MeetingParticipantJoinedBeforeHost, + [EnumMember(Value = "meeting.participant_role_changed")] + MeetingParticipantRolechanged, /// - /// An attendee has joined a meting waiting room. + /// A meeting has been permanently deleted. /// - [EnumMember(Value = "meeting.participant_joined_waiting_room")] - MeetingParticipantJoinedWaitingRoom, + [EnumMember(Value = "meeting.permanently_deleted")] + MeetingPermanentlyDeleted, /// - /// An attendee has left a meting waiting room. + /// A meeting has been recovered. /// - [EnumMember(Value = "meeting.participant_left_waiting_room")] - MeetingParticipantLeftWaitingRoom, + [EnumMember(Value = "meeting.recovered")] + MeetingRecovered, /// - /// A meeting host has admitted a participant from a waiting room to the meeting. + /// A meeting registration has been approved. /// - [EnumMember(Value = "meeting.participant_admitted")] - MeetingParticipantAdmitted, + [EnumMember(Value = "meeting.registration_approved")] + MeetingRegistrationApproved, /// - /// A meeting host has admitted a participant from a waiting room to the meeting. + /// A meeting registration has been cancelled. /// - [EnumMember(Value = "meeting.participant_joined")] - MeetingParticipantJoined, + [EnumMember(Value = "meeting.registration_cancelled")] + MeetingRegistrationCancelled, /// - /// A meeting participant who has already joined a meeting is sent back to the waiting room during the meeting. + /// A participant has registered for a meeting. /// - [EnumMember(Value = "meeting.participant_put_in_waiting_room")] - MeetingParticipantSentToWaitingRoom, + [EnumMember(Value = "meeting.registration_created")] + MeetingRegistrationCreated, /// - /// A meeting participant has left the meeting. + /// A meeting registration has been denied. /// - [EnumMember(Value = "meeting.participant_left")] - MeetingParticipantLeft, + [EnumMember(Value = "meeting.registration_denied")] + MeetingRegistrationDenied, /// - /// A meeting live stream has started. + /// An attendee or the host has stoped sharing their screen during a meeting. /// - [EnumMember(Value = "meeting.live_streaming_started")] - MeetingLiveStreamStarted, + [EnumMember(Value = "meeting.sharing_ended")] + MeetingSharingEnded, /// - /// A meeting live stream has stoipped. + /// An attendee or the host has started sharing their screen during a meeting. /// - [EnumMember(Value = "meeting.live_streaming_stopped")] - MeetingLiveStreamStopped, + [EnumMember(Value = "meeting.sharing_started")] + MeetingSharingStarted, /// - /// A webinar has been created. + /// A meeting has started. /// - [EnumMember(Value = "webinar.created")] - WebinarCreated, + [EnumMember(Value = "meeting.started")] + MeetingStarted, /// - /// A webinar has been deleted. + /// A meeting has been updated. /// - [EnumMember(Value = "webinar.deleted")] - WebinarDeleted, + [EnumMember(Value = "meeting.updated")] + MeetingUpdated, /// - /// A webinar has been updated. + /// A service issue has been encountered during a webinar. /// - [EnumMember(Value = "webinar.updated")] - WebinarUpdated, + [EnumMember(Value = "webinar.alerts")] + WebinarServiceIssue, /// - /// A webinar has started. + /// A webinar has been created. /// - [EnumMember(Value = "webinar.started")] - WebinarStarted, + [EnumMember(Value = "webinar.created")] + WebinarCreated, + + /// + /// A webinar has been deleted. + /// + [EnumMember(Value = "webinar.deleted")] + WebinarDeleted, /// /// A webinar has ended. @@ -193,16 +199,16 @@ public enum EventType WebinarEnded, /// - /// A service issue has been encountered during a webinar. + /// A webinar host or participant joined a webinar. /// - [EnumMember(Value = "webinar.alerts")] - WebinarServiceIssue, + [EnumMember(Value = "webinar.participant_joined")] + WebinarParticipantJoined, /// - /// A participant has registered for a webinar. + /// A webinar host or participant left a webinar. /// - [EnumMember(Value = "webinar.registration_created")] - WebinarRegistrationCreated, + [EnumMember(Value = "webinar.participant_left")] + WebinarParticipantLeft, /// /// A webinar registration has been approved. @@ -217,16 +223,16 @@ public enum EventType WebinarRegistrationCancelled, /// - /// A webinar registration has been denied. + /// A participant has registered for a webinar. /// - [EnumMember(Value = "webinar.registration_denied")] - WebinarRegistrationDenied, + [EnumMember(Value = "webinar.registration_created")] + WebinarRegistrationCreated, /// - /// An app user or account user has started sharing their screen during a webinar. + /// A webinar registration has been denied. /// - [EnumMember(Value = "webinar.sharing_started")] - WebinarSharingStarted, + [EnumMember(Value = "webinar.registration_denied")] + WebinarRegistrationDenied, /// /// An app user or account user has stopped sharing their screen during a webinar. @@ -235,15 +241,21 @@ public enum EventType WebinarSharingEnded, /// - /// A webinar host or participant joined a webinar. + /// An app user or account user has started sharing their screen during a webinar. /// - [EnumMember(Value = "webinar.participant_joined")] - WebinarParticipantJoined, + [EnumMember(Value = "webinar.sharing_started")] + WebinarSharingStarted, /// - /// A webinar host or participant left a webinar. + /// A webinar has started. /// - [EnumMember(Value = "webinar.participant_left")] - WebinarParticipantLeft, + [EnumMember(Value = "webinar.started")] + WebinarStarted, + + /// + /// A webinar has been updated. + /// + [EnumMember(Value = "webinar.updated")] + WebinarUpdated, } } diff --git a/Source/ZoomNet/Models/Webhooks/MeetingServiceIssueEvent.cs b/Source/ZoomNet/Models/Webhooks/MeetingServiceIssueEvent.cs index b74d77a6..bbfb3a01 100644 --- a/Source/ZoomNet/Models/Webhooks/MeetingServiceIssueEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/MeetingServiceIssueEvent.cs @@ -14,6 +14,6 @@ public class MeetingServiceIssueEvent : MeetingEvent /// /// Gets or sets the issues that occured during the meeting. /// - public string Issues { get; set; } + public string[] Issues { get; set; } } } diff --git a/Source/ZoomNet/Models/WebinarSummary.cs b/Source/ZoomNet/Models/WebinarSummary.cs new file mode 100644 index 00000000..fa7766b5 --- /dev/null +++ b/Source/ZoomNet/Models/WebinarSummary.cs @@ -0,0 +1,98 @@ +using System; +using System.Text.Json.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Summary information about a webinar. + /// + public class WebinarSummary + { + /// + /// Gets or sets the webinar agenda. + /// + /// The agenda. + [JsonPropertyName("agenda")] + public string Agenda { get; set; } + + /// + /// Gets or sets the date and time when the meeting was created. + /// + /// The meeting created time. + [JsonPropertyName("created_at")] + public DateTime CreatedOn { get; set; } + + /// + /// Gets or sets the duration in minutes. + /// + /// The duration in minutes. + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// + /// Gets or sets the ID of the user who is set as the host of the webinar. + /// + /// + /// The user id. + /// + [JsonPropertyName("host_id")] + public string HostId { get; set; } + + /// + /// Gets or sets the webinar id, also known as the webinar number. + /// + /// + /// The id. + /// + [JsonPropertyName("id")] + public long Id { get; set; } + + /// + /// Gets or sets the URL to join the webinar. + /// + /// The join URL. + [JsonPropertyName("join_url")] + public string JoinUrl { get; set; } + + /// + /// Gets or sets the webinar start time. + /// + /// The webinar start time. + [JsonPropertyName("start_time")] + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the timezone. + /// For example, "America/Los_Angeles". + /// Please reference our timezone list for supported timezones and their formats. + /// + /// The webinar timezone. For example, "America/Los_Angeles". Please reference our timezone list for supported timezones and their formats. + [JsonPropertyName("timezone")] + public string Timezone { get; set; } + + /// + /// Gets or sets the topic of the meeting. + /// + /// + /// The topic. + /// + [JsonPropertyName("topic")] + public string Topic { get; set; } + + /// + /// Gets or sets the webinar type. + /// + /// The webinar type. + [JsonPropertyName("type")] + public WebinarType Type { get; set; } + + /// + /// Gets or sets the unique id. + /// + /// + /// The unique id. + /// + [JsonPropertyName("uuid")] + public string Uuid { get; set; } + } +} diff --git a/Source/ZoomNet/OAuthConnectionInfo.cs b/Source/ZoomNet/OAuthConnectionInfo.cs index 3f7a25f4..da05face 100644 --- a/Source/ZoomNet/OAuthConnectionInfo.cs +++ b/Source/ZoomNet/OAuthConnectionInfo.cs @@ -16,6 +16,12 @@ namespace ZoomNet /// public class OAuthConnectionInfo : IConnectionInfo { + /// + /// Gets the account id. + /// + /// This is relevant only when using Server-to-Server authentication. + public string AccountId { get; } + /// /// Gets the client id. /// @@ -80,10 +86,10 @@ public class OAuthConnectionInfo : IConnectionInfo /// /// Your Client Id. /// Your Client Secret. - public OAuthConnectionInfo(string clientId, string clientSecret) + public OAuthConnectionInfo(string clientId!!, string clientSecret!!) { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - ClientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); + ClientId = clientId; + ClientSecret = clientSecret; GrantType = OAuthGrantType.ClientCredentials; } @@ -100,18 +106,20 @@ public OAuthConnectionInfo(string clientId, string clientSecret) /// always necessary. It seems that some developers get a "REDIRECT URI MISMATCH" exception when /// they omit this value but other developers don't. Therefore, the redirectUri parameter is /// marked as optional in ZoomNet which allows you to specify it or omit it depending on your - /// situation. See this Github issue for more details. + /// situation. See this Github issue + /// and this support thread + /// for more details. /// /// Your Client Id. /// Your Client Secret. /// The authorization code. /// The delegate invoked when the token is refreshed. /// The Redirect Uri. - public OAuthConnectionInfo(string clientId, string clientSecret, string authorizationCode, OnTokenRefreshedDelegate onTokenRefreshed, string redirectUri = null) + public OAuthConnectionInfo(string clientId!!, string clientSecret!!, string authorizationCode!!, OnTokenRefreshedDelegate onTokenRefreshed!!, string redirectUri = null) { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - ClientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); - AuthorizationCode = authorizationCode ?? throw new ArgumentNullException(nameof(authorizationCode)); + ClientId = clientId; + ClientSecret = clientSecret; + AuthorizationCode = authorizationCode; RedirectUri = redirectUri; TokenExpiration = DateTime.MinValue; GrantType = OAuthGrantType.AuthorizationCode; @@ -129,15 +137,35 @@ public OAuthConnectionInfo(string clientId, string clientSecret, string authoriz /// The refresh token. /// The access token. Access tokens expire after 1 hour. ZoomNet will automatically refresh this token when it expires. /// The delegate invoked when the token is refreshed. - public OAuthConnectionInfo(string clientId, string clientSecret, string refreshToken, string accessToken, OnTokenRefreshedDelegate onTokenRefreshed) + public OAuthConnectionInfo(string clientId!!, string clientSecret!!, string refreshToken!!, string accessToken!!, OnTokenRefreshedDelegate onTokenRefreshed!!) { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - ClientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); - RefreshToken = refreshToken ?? throw new ArgumentNullException(nameof(refreshToken)); - AccessToken = accessToken ?? throw new ArgumentNullException(nameof(accessToken)); + ClientId = clientId; + ClientSecret = clientSecret; + RefreshToken = refreshToken; + AccessToken = accessToken; TokenExpiration = string.IsNullOrEmpty(accessToken) ? DateTime.MinValue : DateTime.MaxValue; // Set expiration to DateTime.MaxValue when an access token is provided because we don't know when it will expire GrantType = OAuthGrantType.RefreshToken; OnTokenRefreshed = onTokenRefreshed; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Use this constructor when you want to use Server-to-Server OAuth authentication. + /// + /// Your Client Id. + /// Your Client Secret. + /// Your Account Id. + /// The delegate invoked when the token is refreshed. In the Server-to-Server scenario, this delegate is optional. + public OAuthConnectionInfo(string clientId!!, string clientSecret!!, string accountId!!, OnTokenRefreshedDelegate onTokenRefreshed) + { + ClientId = clientId; + ClientSecret = clientSecret; + AccountId = accountId; + TokenExpiration = DateTime.MinValue; + GrantType = OAuthGrantType.AccountCredentials; + OnTokenRefreshed = onTokenRefreshed; + } } } diff --git a/Source/ZoomNet/Resources/Chat.cs b/Source/ZoomNet/Resources/Chat.cs index d132e34a..7ef2126b 100644 --- a/Source/ZoomNet/Resources/Chat.cs +++ b/Source/ZoomNet/Resources/Chat.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Net.Http; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -303,36 +305,16 @@ public Task JoinChannelAsync(string channelId, CancellationToken cancell .AsObject("id"); } - /// - /// Send a message to a user on on the sender's contact list. - /// - /// The unique identifier of the sender. - /// The email address of the contact to whom you would like to send the message. - /// The message. - /// Mentions. - /// The cancellation token. - /// - /// The message Id. - /// - public Task SendMessageToContactAsync(string userId, string recipientEmail, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) + /// + public Task SendMessageToContactAsync(string userId, string recipientEmail, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default) { - return SendMessageAsync(userId, recipientEmail, null, message, mentions, cancellationToken); + return SendMessageAsync(userId, recipientEmail, null, message, replyMessageId, fileIds, mentions, cancellationToken); } - /// - /// Send a message to a channel of which the sender is a member. - /// - /// The unique identifier of the sender. - /// The channel Id. - /// The message. - /// Mentions. - /// The cancellation token. - /// - /// The message Id. - /// - public Task SendMessageToChannelAsync(string userId, string channelId, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default) + /// + public Task SendMessageToChannelAsync(string userId, string channelId, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default) { - return SendMessageAsync(userId, null, channelId, message, mentions, cancellationToken); + return SendMessageAsync(userId, null, channelId, message, replyMessageId, fileIds, mentions, cancellationToken); } /// @@ -431,14 +413,53 @@ public Task DeleteMessageToChannelAsync(string messageId, string userId, string return DeleteMessageAsync(messageId, userId, null, channelId, cancellationToken); } - private Task SendMessageAsync(string userId, string recipientEmail, string channelId, string message, IEnumerable mentions, CancellationToken cancellationToken) + /// + public Task SendFileAsync(string messageId, string userId, string recipientId, string channelId, string fileName, Stream fileData, CancellationToken cancellationToken = default) { - Debug.Assert(recipientEmail != null || channelId != null, "You must provide either recipientEmail or channelId"); - Debug.Assert(recipientEmail == null || channelId == null, "You can't provide both recipientEmail and channelId"); + return _client + .PostAsync($"https://file.zoom.us/v2/chat/users/{userId}/messages/files") + .WithBody(bodyBuilder => + { + // The file name as well as the name of the other 'parts' in the request must be quoted otherwise the Zoom API would return the following error message: Invalid 'Content-Disposition' in multipart form + var content = new MultipartFormDataContent + { + { new StreamContent(fileData), "files", $"\"{fileName}\"" } + }; + if (!string.IsNullOrEmpty(messageId)) content.Add(new StringContent(messageId), "\"reply_main_message_id\""); + if (!string.IsNullOrEmpty(channelId)) content.Add(new StringContent(channelId), "\"to_channel\""); + if (!string.IsNullOrEmpty(recipientId)) content.Add(new StringContent(recipientId), "\"to_contact\""); + + return content; + }) + .WithCancellationToken(cancellationToken) + .AsObject("id"); + } + /// + public Task UploadFileAsync(string userId, string fileName, Stream fileData, CancellationToken cancellationToken = default) + { + return _client + .PostAsync($"https://file.zoom.us/v2/chat/users/{userId}/files") + .WithBody(bodyBuilder => + { + // The file name must be quoted otherwise the Zoom API would return the following error message: Invalid 'Content-Disposition' in multipart form + var content = new MultipartFormDataContent + { + { new StreamContent(fileData), "file", $"\"{fileName}\"" } + }; + return content; + }) + .WithCancellationToken(cancellationToken) + .AsObject("id"); + } + + private Task SendMessageAsync(string userId, string recipientEmail, string channelId, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default) + { var data = new JsonObject { { "message", message }, + { "file_ids", fileIds?.ToArray() }, + { "reply_main_message_id", replyMessageId }, { "to_contact", recipientEmail }, { "to_channel", channelId }, { "at_items", mentions } diff --git a/Source/ZoomNet/Resources/DataCompliance.cs b/Source/ZoomNet/Resources/DataCompliance.cs index c041b8d8..f9a2e091 100644 --- a/Source/ZoomNet/Resources/DataCompliance.cs +++ b/Source/ZoomNet/Resources/DataCompliance.cs @@ -18,6 +18,7 @@ namespace ZoomNet.Resources /// This resource can only be used when you connect to Zoom using OAuth. It cannot be used with a Jwt connection. /// See Zoom documentation for more information. /// + [Obsolete("The Data Complaince API is deprecated")] public class DataCompliance : IDataCompliance { private readonly Pathoschild.Http.Client.IClient _client; @@ -42,6 +43,7 @@ internal DataCompliance(Pathoschild.Http.Client.IClient client) /// /// The async task. /// + [Obsolete("The Data Complaince API is deprecated")] public Task NotifyAsync(string userId, long accountId, AppDeauthorizedEvent deauthorizationEventReceived, CancellationToken cancellationToken = default) { // Prepare the request (but do not dispatch it yet) diff --git a/Source/ZoomNet/Resources/IChat.cs b/Source/ZoomNet/Resources/IChat.cs index 6f9177e0..f5b9c87f 100644 --- a/Source/ZoomNet/Resources/IChat.cs +++ b/Source/ZoomNet/Resources/IChat.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models; @@ -172,12 +173,14 @@ public interface IChat /// The unique identifier of the sender. /// The email address of the contact to whom you would like to send the message. /// The message. + /// The reply message's ID. + /// A list of the file IDs to send. This field only accepts a maximum of six file IDs. /// Mentions. /// The cancellation token. /// /// The message Id. /// - Task SendMessageToContactAsync(string userId, string recipientEmail, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default); + Task SendMessageToContactAsync(string userId, string recipientEmail, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default); /// /// Send a message to a channel of which the sender is a member. @@ -185,12 +188,14 @@ public interface IChat /// The unique identifier of the sender. /// The channel Id. /// The message. + /// The reply message's ID. + /// A list of the file IDs to send. This field only accepts a maximum of six file IDs. /// Mentions. /// The cancellation token. /// /// The message Id. /// - Task SendMessageToChannelAsync(string userId, string channelId, string message, IEnumerable mentions = null, CancellationToken cancellationToken = default); + Task SendMessageToChannelAsync(string userId, string channelId, string message, string replyMessageId = null, IEnumerable fileIds = null, IEnumerable mentions = null, CancellationToken cancellationToken = default); /// /// Retrieve the chat messages sent/received to/from a contact. @@ -269,5 +274,42 @@ public interface IChat /// The async task. /// Task DeleteMessageToChannelAsync(string messageId, string userId, string channelId, CancellationToken cancellationToken = default); + + /// + /// Send a file on Zoom to either an individual user in your contact list or a channel of which you are a member. + /// + /// The reply message's ID. + /// The unique identifier of the sender. + /// The unique identifier of the contact to whom you would like to send the file. + /// The unique identifier of the channel to which to send the file. + /// The file name. + /// The binary data. + /// The cancellation token. + /// + /// The message ID. + /// + /// + /// Zoom Cloud Storage will store the files sent through this API. + /// If you do not use Zoom Cloud Storage, Zoom Cloud will temporarily store these files for 7 day + /// You can only send a maximum of 16 megabytes for images and 20 megabytes for all other file types. + /// + Task SendFileAsync(string messageId, string userId, string recipientId, string channelId, string fileName, Stream fileData, CancellationToken cancellationToken = default); + + /// + /// Upload a file to chat. + /// + /// The user Id. + /// The file name. + /// The binary data. + /// The cancellation token. + /// + /// The file ID. + /// + /// + /// Zoom Cloud Storage will store the files sent through this API. + /// If you do not use Zoom Cloud Storage, Zoom Cloud will temporarily store these files for 7 day + /// You can only send a maximum of 16 megabytes for images and 20 megabytes for all other file types. + /// + Task UploadFileAsync(string userId, string fileName, Stream fileData, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/IContacts.cs b/Source/ZoomNet/Resources/IContacts.cs index 46512e50..91053097 100644 --- a/Source/ZoomNet/Resources/IContacts.cs +++ b/Source/ZoomNet/Resources/IContacts.cs @@ -7,7 +7,6 @@ namespace ZoomNet.Resources /// /// Allows you to manage contacts. /// - /// /// /// See Zoom documentation for more information. /// @@ -28,7 +27,7 @@ public interface IContacts /// /// Search contacts. /// - /// The search keyword: either first name, last na,me or email of the contact. + /// The search keyword: either first name, last name or email of the contact. /// Indicate whether you want the status of a contact to be included in the response. /// The number of records returned within a single API call. /// The paging token. diff --git a/Source/ZoomNet/Resources/IDataCompliance.cs b/Source/ZoomNet/Resources/IDataCompliance.cs index ae2a3195..b0519cdd 100644 --- a/Source/ZoomNet/Resources/IDataCompliance.cs +++ b/Source/ZoomNet/Resources/IDataCompliance.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models.Webhooks; @@ -12,6 +13,7 @@ namespace ZoomNet.Resources /// This resource can only be used when you connect to Zoom using OAuth. It cannot be used with a Jwt connection. /// See Zoom documentation for more information. /// + [Obsolete("The Data Complaince API is deprecated")] public interface IDataCompliance { /// @@ -25,6 +27,7 @@ public interface IDataCompliance /// /// The async task. /// + [Obsolete("The Data Complaince API is deprecated")] Task NotifyAsync(string userId, long accountId, AppDeauthorizedEvent deauthorizationEventReceived, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index 59c86483..71a0d0d1 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -15,7 +15,7 @@ namespace ZoomNet.Resources public interface IMeetings { /// - /// Retrieve all meetings of the specified type for a user. + /// Retrieve summary information about all meetings of the specified type for a user. /// /// The user Id or email address. /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. @@ -23,16 +23,16 @@ public interface IMeetings /// The current page number of returned records. /// The cancellation token. /// - /// An array of meetings. + /// An array of meeting summaries. /// /// - /// This call omits 'occurrences'. Therefore the 'Occurrences' property will be empty. + /// To obtain the full details about a given meeting you must invoke . /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] - Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); + Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); /// - /// Retrieve all meetings of the specified type for a user. + /// Retrieve summary information about all meetings of the specified type for a user. /// /// The user Id or email address. /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. @@ -40,12 +40,12 @@ public interface IMeetings /// The paging token. /// The cancellation token. /// - /// An array of meetings. + /// An array of meeting summaries. /// /// - /// This call omits 'occurrences'. Therefore the 'Occurrences' property will be empty. + /// To obtain the full details about a given meeting you must invoke . /// - Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); + Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); /// /// Creates an instant meeting for a user. diff --git a/Source/ZoomNet/Resources/IWebinars.cs b/Source/ZoomNet/Resources/IWebinars.cs index c39356a8..1797b70c 100644 --- a/Source/ZoomNet/Resources/IWebinars.cs +++ b/Source/ZoomNet/Resources/IWebinars.cs @@ -15,29 +15,35 @@ namespace ZoomNet.Resources public interface IWebinars { /// - /// Retrieve all webinars for a user. + /// Retrieve summary information about all webinars for a user. /// /// The user Id or email address. /// The number of records returned within a single API call. /// The current page number of returned records. /// The cancellation token. /// - /// An array of webinars. + /// An array of webinar summaries. /// + /// + /// To obtain the full details about a given webinar you must invoke . + /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] - Task> GetAllAsync(string userId, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); + Task> GetAllAsync(string userId, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); /// - /// Retrieve all webinars for a user. + /// Retrieve summary information about all webinars for a user. /// /// The user Id or email address. /// The number of records returned within a single API call. /// The paging token. /// The cancellation token. /// - /// An array of webinars. + /// An array of webinar summaries. /// - Task> GetAllAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); + /// + /// To obtain the full details about a given webinar you must invoke . + /// + Task> GetAllAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default); /// /// Creates a scheduled webinar for a user. @@ -182,11 +188,12 @@ public interface IWebinars /// The webinar ID. /// Panelist's email address. /// Panelist's full name. + /// The virtual background ID to bind. /// The cancellation token. /// /// The async task. /// - Task AddPanelistAsync(long webinarId, string email, string fullName, CancellationToken cancellationToken = default); + Task AddPanelistAsync(long webinarId, string email, string fullName, string virtualBackgroundId = null, CancellationToken cancellationToken = default); /// /// Add multiple panelists to a webinar. @@ -197,7 +204,7 @@ public interface IWebinars /// /// The async task. /// - Task AddPanelistsAsync(long webinarId, IEnumerable<(string Email, string FullName)> panelists, CancellationToken cancellationToken = default); + Task AddPanelistsAsync(long webinarId, IEnumerable<(string Email, string FullName, string VirtualBackgroundId)> panelists, CancellationToken cancellationToken = default); /// /// Remove a single panelist from a webinar. diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index b861ed68..f254e36f 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -29,19 +29,9 @@ internal Meetings(Pathoschild.Http.Client.IClient client) _client = client; } - /// - /// Retrieve all meetings of the specified type for a user. - /// - /// The user Id or email address. - /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. - /// The number of records returned within a single API call. - /// The current page number of returned records. - /// The cancellation token. - /// - /// An array of . - /// + /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] - public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) + public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -54,21 +44,11 @@ public Task> GetAllAsync(string userId, MeetingListTy .WithArgument("page_size", recordsPerPage) .WithArgument("page_number", page) .WithCancellationToken(cancellationToken) - .AsPaginatedResponse("meetings"); + .AsPaginatedResponse("meetings"); } - /// - /// Retrieve all meetings of the specified type for a user. - /// - /// The user Id or email address. - /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of . - /// - public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) + /// + public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -81,7 +61,7 @@ public Task> GetAllAsync(string userId, Meet .WithArgument("page_size", recordsPerPage) .WithArgument("next_page_token", pagingToken) .WithCancellationToken(cancellationToken) - .AsPaginatedResponseWithToken("meetings"); + .AsPaginatedResponseWithToken("meetings"); } /// diff --git a/Source/ZoomNet/Resources/Webinars.cs b/Source/ZoomNet/Resources/Webinars.cs index b80f1cdc..20498a8a 100644 --- a/Source/ZoomNet/Resources/Webinars.cs +++ b/Source/ZoomNet/Resources/Webinars.cs @@ -29,18 +29,9 @@ internal Webinars(Pathoschild.Http.Client.IClient client) _client = client; } - /// - /// Retrieve all webinars for a user. - /// - /// The user Id or email address. - /// The number of records returned within a single API call. - /// The current page number of returned records. - /// The cancellation token. - /// - /// An array of . - /// + /// [Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")] - public Task> GetAllAsync(string userId, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) + public Task> GetAllAsync(string userId, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -52,20 +43,11 @@ public Task> GetAllAsync(string userId, int recordsPe .WithArgument("page_size", recordsPerPage) .WithArgument("page_number", page) .WithCancellationToken(cancellationToken) - .AsPaginatedResponse("webinars"); + .AsPaginatedResponse("webinars"); } - /// - /// Retrieve all webinars for a user. - /// - /// The user Id or email address. - /// The number of records returned within a single API call. - /// The paging token. - /// The cancellation token. - /// - /// An array of . - /// - public Task> GetAllAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) + /// + public Task> GetAllAsync(string userId, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -77,7 +59,7 @@ public Task> GetAllAsync(string userId, int .WithArgument("page_size", recordsPerPage) .WithArgument("next_page_token", pagingToken) .WithCancellationToken(cancellationToken) - .AsPaginatedResponseWithToken("webinars"); + .AsPaginatedResponseWithToken("webinars"); } /// @@ -357,13 +339,14 @@ public Task GetPanelistsAsync(long webinarId, CancellationToken canc /// The webinar ID. /// Panelist's email address. /// Panelist's full name. + /// The virtual background ID to bind. /// The cancellation token. /// /// The async task. /// - public Task AddPanelistAsync(long webinarId, string email, string fullName, CancellationToken cancellationToken = default) + public Task AddPanelistAsync(long webinarId, string email, string fullName, string virtualBackgroundId = null, CancellationToken cancellationToken = default) { - return AddPanelistsAsync(webinarId, new (string Email, string FullName)[] { (email, fullName) }, cancellationToken); + return AddPanelistsAsync(webinarId, new (string Email, string FullName, string VirtualBackgroundId)[] { (email, fullName, virtualBackgroundId) }, cancellationToken); } /// @@ -375,11 +358,18 @@ public Task AddPanelistAsync(long webinarId, string email, string fullName, Canc /// /// The async task. /// - public Task AddPanelistsAsync(long webinarId, IEnumerable<(string Email, string FullName)> panelists, CancellationToken cancellationToken = default) + public Task AddPanelistsAsync(long webinarId, IEnumerable<(string Email, string FullName, string VirtualBackgroundId)> panelists, CancellationToken cancellationToken = default) { var data = new JsonObject { - { "panelists", panelists.Select(p => new JsonObject { { "email", p.Email }, { "name", p.FullName } }) } + { + "panelists", panelists.Select(p => new JsonObject + { + { "email", p.Email }, + { "name", p.FullName }, + { "virtual_background_id", p.VirtualBackgroundId }, + }) + } }; return _client diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index c9769d45..c47fda7b 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Runtime.Serialization; using System.Text; using System.Text.Json; using System.Threading; @@ -15,7 +14,7 @@ namespace ZoomNet.Utilities { /// - /// Handler to ensure requests to the Zoom API include a valid JWT token. + /// Handler to ensure requests to the Zoom API include a valid OAuth token. /// /// internal class OAuthTokenHandler : IHttpFilter, ITokenHandler @@ -40,14 +39,13 @@ public IConnectionInfo ConnectionInfo private readonly HttpClient _httpClient; private readonly TimeSpan _clockSkew; - public OAuthTokenHandler(OAuthConnectionInfo connectionInfo, HttpClient httpClient, TimeSpan? clockSkew = null) + public OAuthTokenHandler(OAuthConnectionInfo connectionInfo!!, HttpClient httpClient!!, TimeSpan? clockSkew = null) { - if (connectionInfo == null) throw new ArgumentNullException(nameof(connectionInfo)); if (string.IsNullOrEmpty(connectionInfo.ClientId)) throw new ArgumentNullException(nameof(connectionInfo.ClientId)); if (string.IsNullOrEmpty(connectionInfo.ClientSecret)) throw new ArgumentNullException(nameof(connectionInfo.ClientSecret)); _connectionInfo = connectionInfo; - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _httpClient = httpClient; _clockSkew = clockSkew.GetValueOrDefault(TimeSpan.FromMinutes(5)); } @@ -75,10 +73,13 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - var grantType = _connectionInfo.GrantType.GetAttributeOfType().Value; + var grantType = _connectionInfo.GrantType.ToEnumString(); var requestUrl = $"https://api.zoom.us/oauth/token?grant_type={grantType}"; switch (_connectionInfo.GrantType) { + case OAuthGrantType.AccountCredentials: + requestUrl += $"&account_id={_connectionInfo.AccountId}"; + break; case OAuthGrantType.AuthorizationCode: requestUrl += $"&code={_connectionInfo.AuthorizationCode}"; if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) requestUrl += $"&redirect_uri={_connectionInfo.RedirectUri}"; @@ -106,7 +107,6 @@ public string RefreshTokenIfNecessary(bool forceRefresh) _connectionInfo.RefreshToken = jsonResponse.GetPropertyValue("refresh_token", string.Empty); _connectionInfo.AccessToken = jsonResponse.GetPropertyValue("access_token", string.Empty); - _connectionInfo.GrantType = OAuthGrantType.RefreshToken; _connectionInfo.TokenExpiration = requestTime.AddSeconds(jsonResponse.GetPropertyValue("expires_in", 60 * 60)); _connectionInfo.TokenScope = new ReadOnlyDictionary( jsonResponse.GetPropertyValue("scope", string.Empty) @@ -117,7 +117,12 @@ public string RefreshTokenIfNecessary(bool forceRefresh) .ToDictionary( x => x.Key, x => x.SelectMany(c => c.Value).ToArray())); - _connectionInfo.OnTokenRefreshed(_connectionInfo.RefreshToken, _connectionInfo.AccessToken); + + // Please note that Server-to-Server OAuth does not use the refresh token. + // Therefore change the grant type to 'RefreshToken' only when the response includes a refresh token. + if (!string.IsNullOrEmpty(_connectionInfo.RefreshToken)) _connectionInfo.GrantType = OAuthGrantType.RefreshToken; + + _connectionInfo.OnTokenRefreshed?.Invoke(_connectionInfo.RefreshToken, _connectionInfo.AccessToken); } finally { diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index 5d26eee6..73cb9d23 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -95,6 +95,7 @@ public static string Version /// /// The data compliance resource. /// + [Obsolete("The Data Compliance API is deprecated")] public IDataCompliance DataCompliance { get; private set; } /// diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 4e3c24e4..4f989b34 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -36,13 +36,13 @@ - + - + @@ -58,6 +58,7 @@ true + 612,618 diff --git a/global.json b/global.json index 20eb15d6..3d5658fe 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.202", + "version": "6.0.301", "rollForward": "latestFeature" } } \ No newline at end of file