diff --git a/Source/ZoomNet/JwtConnectionInfo.cs b/Source/ZoomNet/JwtConnectionInfo.cs index eb498085..1f9ac2b2 100644 --- a/Source/ZoomNet/JwtConnectionInfo.cs +++ b/Source/ZoomNet/JwtConnectionInfo.cs @@ -1,8 +1,11 @@ +using System; + namespace ZoomNet { /// /// Connect using JWT. /// + [Obsolete("As of September 8, 2023, the JWT app type has been deprecated. Use Server-to-Server OAuth or OAuth apps to replace the functionality of all JWT apps in your account. See the JWT deprecation FAQ and migration guide for details.")] public class JwtConnectionInfo : IConnectionInfo { /// diff --git a/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs new file mode 100644 index 00000000..02b9278d --- /dev/null +++ b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ZoomNet.Models +{ + /// + /// Information about the Enable continuous meeting chat feature. + /// + public class ContinuousMeetingChatSettings + { + /// + /// Gets or sets whether to enable the Enable continuous meeting chat setting. + /// + [JsonPropertyName("enable")] + public bool? Enable { get; set; } + + /// + /// Gets or sets whether to enable the Automatically add invited external users setting. + /// + [JsonPropertyName("auto_add_invited_external_users")] + public bool? AutoAddInvitedExternalUsers { get; set; } + + /// + /// Gets or sets the channel's ID. + /// + [JsonPropertyName("channel_id")] + public string ChannelId { get; set; } + } +} diff --git a/Source/ZoomNet/Models/MeetingSettings.cs b/Source/ZoomNet/Models/MeetingSettings.cs index 8a81cf63..557df330 100644 --- a/Source/ZoomNet/Models/MeetingSettings.cs +++ b/Source/ZoomNet/Models/MeetingSettings.cs @@ -159,6 +159,12 @@ public class MeetingSettings [JsonPropertyName("watermark")] public bool? Watermark { get; set; } + /// + /// Gets or sets information about the Enable continuous meeting chat feature. + /// + [JsonPropertyName("continuous_meeting_chat")] + public ContinuousMeetingChatSettings ContinuousMeetingChat { get; set; } + /// /// Gets or sets the value indicating whether to allow attendees to join the meeting from multiple devices. /// This setting only works for meetings that require registration. diff --git a/Source/ZoomNet/Utilities/JwtTokenHandler.cs b/Source/ZoomNet/Utilities/JwtTokenHandler.cs index bfff4329..041a13f0 100644 --- a/Source/ZoomNet/Utilities/JwtTokenHandler.cs +++ b/Source/ZoomNet/Utilities/JwtTokenHandler.cs @@ -74,13 +74,16 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - _tokenExpiration = DateTime.UtcNow.Add(_tokenLifeSpan); - var jwtPayload = new Dictionary() + if (forceRefresh || TokenIsExpired()) { - { "iss", _connectionInfo.ApiKey }, - { "exp", _tokenExpiration.ToUnixTime() } - }; - _jwtToken = JWT.Encode(jwtPayload, Encoding.ASCII.GetBytes(_connectionInfo.ApiSecret), JwsAlgorithm.HS256); + _tokenExpiration = DateTime.UtcNow.Add(_tokenLifeSpan); + var jwtPayload = new Dictionary() + { + { "iss", _connectionInfo.ApiKey }, + { "exp", _tokenExpiration.ToUnixTime() } + }; + _jwtToken = JWT.Encode(jwtPayload, Encoding.ASCII.GetBytes(_connectionInfo.ApiSecret), JwsAlgorithm.HS256); + } } finally { diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 5ebdd044..05f088d5 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -75,61 +75,64 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - var contentValues = new Dictionary() + if (forceRefresh || TokenIsExpired()) { - { "grant_type", _connectionInfo.GrantType.ToEnumString() }, - }; - - switch (_connectionInfo.GrantType) - { - case OAuthGrantType.AccountCredentials: - contentValues.Add("account_id", _connectionInfo.AccountId); - break; - case OAuthGrantType.AuthorizationCode: - contentValues.Add("code", _connectionInfo.AuthorizationCode); - if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) contentValues.Add("redirect_uri", _connectionInfo.RedirectUri); - if (!string.IsNullOrEmpty(_connectionInfo.CodeVerifier)) contentValues.Add("code_verifier", _connectionInfo.CodeVerifier); - break; - case OAuthGrantType.RefreshToken: - contentValues.Add("refresh_token", _connectionInfo.RefreshToken); - break; + var contentValues = new Dictionary() + { + { "grant_type", _connectionInfo.GrantType.ToEnumString() }, + }; + + switch (_connectionInfo.GrantType) + { + case OAuthGrantType.AccountCredentials: + contentValues.Add("account_id", _connectionInfo.AccountId); + break; + case OAuthGrantType.AuthorizationCode: + contentValues.Add("code", _connectionInfo.AuthorizationCode); + if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) contentValues.Add("redirect_uri", _connectionInfo.RedirectUri); + if (!string.IsNullOrEmpty(_connectionInfo.CodeVerifier)) contentValues.Add("code_verifier", _connectionInfo.CodeVerifier); + break; + case OAuthGrantType.RefreshToken: + contentValues.Add("refresh_token", _connectionInfo.RefreshToken); + break; + } + + var requestTime = DateTime.UtcNow; + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.zoom.us/oauth/token"); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_connectionInfo.ClientId}:{_connectionInfo.ClientSecret}"))); + request.Content = new FormUrlEncodedContent(contentValues); + var response = _httpClient.SendAsync(request).ConfigureAwait(false).GetAwaiter().GetResult(); + var responseContent = response.Content.ReadAsStringAsync(null).ConfigureAwait(false).GetAwaiter().GetResult(); + + if (string.IsNullOrEmpty(responseContent)) throw new Exception(response.ReasonPhrase); + + var jsonResponse = JsonDocument.Parse(responseContent).RootElement; + + if (!response.IsSuccessStatusCode) + { + var reason = jsonResponse.GetPropertyValue("reason", "The Zoom API did not provide a reason"); + throw new ZoomException(reason, response, "No diagnostic available", null); + } + + _connectionInfo.RefreshToken = jsonResponse.GetPropertyValue("refresh_token", string.Empty); + _connectionInfo.AccessToken = jsonResponse.GetPropertyValue("access_token", string.Empty); + _connectionInfo.TokenExpiration = requestTime.AddSeconds(jsonResponse.GetPropertyValue("expires_in", 60 * 60)); + _connectionInfo.TokenScope = new ReadOnlyDictionary( + jsonResponse.GetPropertyValue("scope", string.Empty) + .Split(' ') + .Select(x => x.Split(new[] { ':' }, 2)) + .Select(x => new KeyValuePair(x[0], x.Skip(1).ToArray())) + .GroupBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.SelectMany(c => c.Value).ToArray())); + + // 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); } - - var requestTime = DateTime.UtcNow; - var request = new HttpRequestMessage(HttpMethod.Post, "https://api.zoom.us/oauth/token"); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_connectionInfo.ClientId}:{_connectionInfo.ClientSecret}"))); - request.Content = new FormUrlEncodedContent(contentValues); - var response = _httpClient.SendAsync(request).ConfigureAwait(false).GetAwaiter().GetResult(); - var responseContent = response.Content.ReadAsStringAsync(null).ConfigureAwait(false).GetAwaiter().GetResult(); - - if (string.IsNullOrEmpty(responseContent)) throw new Exception(response.ReasonPhrase); - - var jsonResponse = JsonDocument.Parse(responseContent).RootElement; - - if (!response.IsSuccessStatusCode) - { - var reason = jsonResponse.GetPropertyValue("reason", "The Zoom API did not provide a reason"); - throw new ZoomException(reason, response, "No diagnostic available", null); - } - - _connectionInfo.RefreshToken = jsonResponse.GetPropertyValue("refresh_token", string.Empty); - _connectionInfo.AccessToken = jsonResponse.GetPropertyValue("access_token", string.Empty); - _connectionInfo.TokenExpiration = requestTime.AddSeconds(jsonResponse.GetPropertyValue("expires_in", 60 * 60)); - _connectionInfo.TokenScope = new ReadOnlyDictionary( - jsonResponse.GetPropertyValue("scope", string.Empty) - .Split(' ') - .Select(x => x.Split(new[] { ':' }, 2)) - .Select(x => new KeyValuePair(x[0], x.Skip(1).ToArray())) - .GroupBy(x => x.Key) - .ToDictionary( - x => x.Key, - x => x.SelectMany(c => c.Value).ToArray())); - - // 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/build.cake b/build.cake index 746a88a3..d0329230 100644 --- a/build.cake +++ b/build.cake @@ -2,8 +2,8 @@ #tool dotnet:?package=GitVersion.Tool&version=5.12.0 #tool dotnet:?package=coveralls.net&version=4.0.1 #tool nuget:https://f.feedz.io/jericho/jericho/nuget/?package=GitReleaseManager&version=0.17.0-collaborators0003 -#tool nuget:?package=ReportGenerator&version=5.2.0 -#tool nuget:?package=xunit.runner.console&version=2.6.6 +#tool nuget:?package=ReportGenerator&version=5.2.1 +#tool nuget:?package=xunit.runner.console&version=2.7.0 #tool nuget:?package=CodecovUploader&version=0.7.1 // Install addins. diff --git a/global.json b/global.json index f43378f6..667094ad 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.201", "rollForward": "patch", "allowPrerelease": false }