diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7bd5f9f5..dd76b6d1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "2.2.0", + "version": "2.3.0", "commands": [ "dotnet-cake" ] diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 5d024013..5d707c93 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -126,6 +126,7 @@ public async Task RunAsync() // Get my user and permisisons var myUser = await client.Users.GetCurrentAsync(source.Token).ConfigureAwait(false); var myPermissions = await client.Users.GetCurrentPermissionsAsync(source.Token).ConfigureAwait(false); + Array.Sort(myPermissions); // Sort permissions alphabetically for convenience // Execute the async tests in parallel (with max degree of parallelism) var results = await integrationTests.ForEachAsync( diff --git a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs index d1dfab76..0805ccc2 100644 --- a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs +++ b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs @@ -50,7 +50,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_DASHBOARDPARTICIPANT_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_DASHBOARDPARTICIPANT_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Models/Registrant.cs b/Source/ZoomNet.UnitTests/Models/Registrant.cs index 2620f552..c0881e36 100644 --- a/Source/ZoomNet.UnitTests/Models/Registrant.cs +++ b/Source/ZoomNet.UnitTests/Models/Registrant.cs @@ -47,7 +47,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_REGISTRANT_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_REGISTRANT_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs index 15381420..45124c2c 100644 --- a/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs @@ -471,7 +471,7 @@ public void Parse_json() // Arrange // Act - var result = JsonSerializer.Deserialize(SINGLE_CLOUD_RECORDING_JSON, ZoomNetJsonFormatter.SerializerOptions); + var result = JsonSerializer.Deserialize(SINGLE_CLOUD_RECORDING_JSON, JsonFormatter.SerializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs index 40be9691..88cab27d 100644 --- a/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs +++ b/Source/ZoomNet.UnitTests/Utilities/OAuthTokenHandlerTests.cs @@ -35,7 +35,7 @@ public void Attempt_to_refresh_token_multiple_times_despite_exception() var mockHttp = new MockHttpMessageHandler(); mockHttp - .When(HttpMethod.Post, $"https://api.zoom.us/oauth/token?grant_type=authorization_code&code={authorizationCode}") + .When(HttpMethod.Post, $"https://api.zoom.us/oauth/token") .Respond(HttpStatusCode.BadRequest, "application/json", apiResponse); var handler = new OAuthTokenHandler(connectionInfo, mockHttp.ToHttpClient(), null); diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs index 23f155d6..3b1f459a 100644 --- a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs @@ -84,7 +84,7 @@ public void Read_single(string value, ParticipantDevice expectedValue) // Act jsonReader.Read(); - var result = converter.Read(ref jsonReader, objectType, ZoomNetJsonFormatter.DeserializerOptions); + var result = converter.Read(ref jsonReader, objectType, JsonFormatter.DeserializerOptions); // Assert result.ShouldNotBeNull(); @@ -109,7 +109,7 @@ public void Read_multiple() // Act jsonReader.Read(); - var result = converter.Read(ref jsonReader, objectType, ZoomNetJsonFormatter.DeserializerOptions); + var result = converter.Read(ref jsonReader, objectType, JsonFormatter.DeserializerOptions); // Assert result.ShouldNotBeNull(); diff --git a/Source/ZoomNet.UnitTests/Utils.cs b/Source/ZoomNet.UnitTests/Utils.cs index ac314c82..8b038da0 100644 --- a/Source/ZoomNet.UnitTests/Utils.cs +++ b/Source/ZoomNet.UnitTests/Utils.cs @@ -22,7 +22,7 @@ public static Pathoschild.Http.Client.IClient GetFluentClient(MockHttpMessageHan // Remove all the built-in formatters and replace them with our custom JSON formatter client.Formatters.Clear(); - client.Formatters.Add(new ZoomNetJsonFormatter()); + client.Formatters.Add(new JsonFormatter()); // Order is important: DiagnosticHandler must be first. // Also, the list of filters must be kept in sync with the filters in ZoomClient in the ZoomNet project. diff --git a/Source/ZoomNet.UnitTests/WebhookParserTests.cs b/Source/ZoomNet.UnitTests/WebhookParserTests.cs index db131a0d..de9d116e 100644 --- a/Source/ZoomNet.UnitTests/WebhookParserTests.cs +++ b/Source/ZoomNet.UnitTests/WebhookParserTests.cs @@ -243,14 +243,15 @@ public void MeetingUpdated() parsedEvent.Operator.ShouldBe("someone@example.com"); parsedEvent.OperatorId.ShouldBe("8lzIwvZTSOqjndWPbPqzuA"); parsedEvent.ModifiedFields.ShouldNotBeNull(); - parsedEvent.ModifiedFields.Length.ShouldBe(3); - parsedEvent.ModifiedFields[0].FieldName.ShouldBe("id"); - parsedEvent.ModifiedFields[0].OldValue.ShouldBe(94890226305); - parsedEvent.ModifiedFields[0].NewValue.ShouldBe(94890226305); - parsedEvent.ModifiedFields[1].FieldName.ShouldBe("topic"); - parsedEvent.ModifiedFields[1].OldValue.ShouldBe("ZoomNet Unit Testing: scheduled meeting"); - parsedEvent.ModifiedFields[1].NewValue.ShouldBe("ZoomNet Unit Testing: UPDATED scheduled meeting"); - parsedEvent.ModifiedFields[2].FieldName.ShouldBe("settings"); + parsedEvent.ModifiedFields.Length.ShouldBe(2); + parsedEvent.ModifiedFields[0].FieldName.ShouldBe("topic"); + parsedEvent.ModifiedFields[0].OldValue.ShouldBe("ZoomNet Unit Testing: scheduled meeting"); + parsedEvent.ModifiedFields[0].NewValue.ShouldBe("ZoomNet Unit Testing: UPDATED scheduled meeting"); + parsedEvent.ModifiedFields[1].FieldName.ShouldBe("settings"); + parsedEvent.MeetingFields.ShouldNotBeNull(); + parsedEvent.MeetingFields.Length.ShouldBe(1); + parsedEvent.MeetingFields[0].FieldName.ShouldBe("id"); + parsedEvent.MeetingFields[0].Value.ShouldBe(94890226305); } [Fact] diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 483b3584..0dd8072d 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -1,11 +1,12 @@ +using HttpMultipartParser; using Pathoschild.Http.Client; -using Pathoschild.Http.Client.Extensibility; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -227,6 +228,19 @@ internal static Encoding GetEncoding(this HttpContent content, Encoding defaultE return encoding; } + /// + /// Returns the value of a parameter or the default value if it doesn't exist. + /// + /// The parser. + /// The name of the parameter. + /// The default value. + /// The value of the parameter. + internal static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue) + { + if (parser.HasParameter(name)) return parser.GetParameterValue(name); + else return defaultValue; + } + /// Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type. /// The response model to deserialize into. /// The response. @@ -352,13 +366,16 @@ internal static async Task AsRawJsonDocument(this IRequest request /// Returns the request builder for chaining. internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, string customExceptionMessage = null) { - return request.WithFilter(new ZoomErrorHandler(true, customExceptionMessage)); + return request + .WithoutFilter() + .WithFilter(new ZoomErrorHandler(true, customExceptionMessage)); } /// Set the body content of the HTTP request. /// The type of object to serialize into a JSON string. /// The request. /// The value to serialize into the HTTP body content. + /// Indicates if the charset should be omitted from the 'Content-Type' request header. /// Returns the request builder for chaining. /// /// This method is equivalent to IRequest.AsBody<T>(T body) because omitting the media type @@ -366,45 +383,19 @@ internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, stri /// formatter happens to be the JSON formatter. However, I don't feel good about relying on the /// default ordering of the items in the MediaTypeFormatterCollection. /// - internal static IRequest WithJsonBody(this IRequest request, T body) - { - return request.WithBody(bodyBuilder => bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"))); - } - - /// Add a filter to a request. - /// The type of filter. - /// The request. - /// The filter. - /// - /// When true, the first filter of matching type is replaced with the new filter (thereby preserving the position of the filter in the list of filters) and any other filter of matching type is removed. - /// When false, the filter is simply added to the list of filters. - /// - /// Returns the request builder for chaining. - internal static IRequest WithFilter(this IRequest request, TFilter filter, bool replaceExisting = true) - where TFilter : IHttpFilter + internal static IRequest WithJsonBody(this IRequest request, T body, bool omitCharSet = false) { - var matchingFilters = request.Filters.OfType().ToArray(); - - if (matchingFilters.Length == 0 || !replaceExisting) + return request.WithBody(bodyBuilder => { - request.Filters.Add(filter); - } - else - { - // Replace the first matching filter with the new filter - var collectionAsList = request.Filters as IList; - var indexOfMatchingFilter = collectionAsList.IndexOf(matchingFilters[0]); - collectionAsList.RemoveAt(indexOfMatchingFilter); - collectionAsList.Insert(indexOfMatchingFilter, filter); - - // Remove any other matching filter - for (int i = 1; i < matchingFilters.Length; i++) + var httpContent = bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json")); + + if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet)) { - request.Filters.Remove(matchingFilters[i]); + httpContent.Headers.ContentType.CharSet = string.Empty; } - } - return request; + return httpContent; + }); } /// Asynchronously retrieve the response body as a . @@ -660,9 +651,6 @@ internal static (WeakReference RequestReference, string Diag internal static async Task<(bool, string, int?)> GetErrorMessageAsync(this HttpResponseMessage message) { - // Assume there is no error - var isError = false; - // Default error code int? errorCode = null; @@ -696,25 +684,29 @@ internal static (WeakReference RequestReference, string Diag try { var rootJsonElement = JsonDocument.Parse(responseContent).RootElement; - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; - errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); - if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + + if (rootJsonElement.ValueKind == JsonValueKind.Object) { - var errorDetails = string.Join( - " ", - jsonErrorDetails - .EnumerateArray() - .Select(jsonErrorDetail => - { - var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; - return errorDetail; - }) - .Where(errorDetail => !string.IsNullOrEmpty(errorDetail))); - - if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; - } + errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; + errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); + if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) + { + var errorDetails = string.Join( + " ", + jsonErrorDetails + .EnumerateArray() + .Select(jsonErrorDetail => + { + var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty; + return errorDetail; + }) + .Where(message => !string.IsNullOrEmpty(message))); - isError = errorCode.HasValue; + if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}"; + } + + return (errorCode.HasValue, errorMessage, errorCode); + } } catch { @@ -722,7 +714,31 @@ internal static (WeakReference RequestReference, string Diag } } - return (isError, errorMessage, errorCode); + return (!message.IsSuccessStatusCode, errorMessage, errorCode); + } + + internal static async Task CompressAsync(this Stream source) + { + var compressedStream = new MemoryStream(); + using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true)) + { + await source.CopyToAsync(gzip).ConfigureAwait(false); + } + + compressedStream.Position = 0; + return compressedStream; + } + + internal static async Task DecompressAsync(this Stream source) + { + var decompressedStream = new MemoryStream(); + using (var gzip = new GZipStream(source, CompressionMode.Decompress, true)) + { + await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false); + } + + decompressedStream.Position = 0; + return decompressedStream; } /// Convert an enum to its string representation. @@ -821,7 +837,7 @@ internal static bool TryToEnum(this string str, out T enumValue) internal static T ToObject(this JsonElement element, JsonSerializerOptions options = null) { - return JsonSerializer.Deserialize(element, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return JsonSerializer.Deserialize(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions); } internal static void Add(this JsonObject jsonObject, string propertyName, T value) @@ -865,14 +881,13 @@ private static async Task AsObject(this HttpContent httpContent, string pr if (string.IsNullOrEmpty(propertyName)) { - return JsonSerializer.Deserialize(responseContent, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return JsonSerializer.Deserialize(responseContent, options ?? JsonFormatter.DeserializerOptions); } var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default); if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property)) { - var propertyContent = property.GetRawText(); - return JsonSerializer.Deserialize(propertyContent, options ?? ZoomNetJsonFormatter.DeserializerOptions); + return property.ToObject(options); } else if (throwIfPropertyIsMissing) { @@ -951,7 +966,7 @@ private static async Task> AsPaginatedResponse(this Http PageCount = pageCount, PageNumber = pageNumber, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; @@ -990,7 +1005,7 @@ private static async Task> AsPaginatedResponseWith { NextPageToken = nextPageToken, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; @@ -1033,7 +1048,7 @@ private static async Task> AsPaginated To = to, NextPageToken = nextPageToken, PageSize = pageSize, - Records = jsonProperty.HasValue ? JsonSerializer.Deserialize(jsonProperty.Value, options ?? ZoomNetJsonFormatter.DeserializerOptions) : Array.Empty() + Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject(options) : Array.Empty() }; if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value; diff --git a/Source/ZoomNet/Json/ZoomNetJsonFormatter.cs b/Source/ZoomNet/Json/JsonFormatter.cs similarity index 96% rename from Source/ZoomNet/Json/ZoomNetJsonFormatter.cs rename to Source/ZoomNet/Json/JsonFormatter.cs index 3e29dca5..721364e1 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonFormatter.cs +++ b/Source/ZoomNet/Json/JsonFormatter.cs @@ -12,7 +12,7 @@ namespace ZoomNet.Json { - internal class ZoomNetJsonFormatter : MediaTypeFormatterBase + internal class JsonFormatter : MediaTypeFormatterBase { internal static readonly JsonSerializerOptions SerializerOptions; internal static readonly JsonSerializerOptions DeserializerOptions; @@ -22,7 +22,7 @@ internal class ZoomNetJsonFormatter : MediaTypeFormatterBase private const int DefaultBufferSize = 1024; - static ZoomNetJsonFormatter() + static JsonFormatter() { SerializerOptions = new JsonSerializerOptions() { @@ -64,7 +64,7 @@ static ZoomNetJsonFormatter() DeserializationContext = new ZoomNetJsonSerializerContext(DeserializerOptions); } - public ZoomNetJsonFormatter() + public JsonFormatter() { this.AddMediaType("application/json"); } diff --git a/Source/ZoomNet/Json/WebhookEventConverter.cs b/Source/ZoomNet/Json/WebhookEventConverter.cs index 06c3a1c8..560438d6 100644 --- a/Source/ZoomNet/Json/WebhookEventConverter.cs +++ b/Source/ZoomNet/Json/WebhookEventConverter.cs @@ -58,7 +58,13 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); meetingUpdatedEvent.ModifiedFields = oldMeetingValues.Keys - .Select(key => (key, oldMeetingValues[key], newMeetingValues[key])) + .Where(key => !oldMeetingValues[key].Equals(newMeetingValues[key])) + .Select(key => (FieldName: key, OldValue: oldMeetingValues[key], NewValue: newMeetingValues[key])) + .ToArray(); + + meetingUpdatedEvent.MeetingFields = oldMeetingValues.Keys + .Where(key => oldMeetingValues[key].Equals(newMeetingValues[key])) + .Select(key => (FieldName: key, Value: oldMeetingValues[key])) .ToArray(); webHookEvent = meetingUpdatedEvent; @@ -195,7 +201,13 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); webinarUpdatedEvent.ModifiedFields = oldWebinarValues.Keys - .Select(key => (key, oldWebinarValues[key], newWebinarValues[key])) + .Where(key => !oldWebinarValues[key].Equals(newWebinarValues[key])) + .Select(key => (FieldName: key, OldValue: oldWebinarValues[key], NewValue: newWebinarValues[key])) + .ToArray(); + + webinarUpdatedEvent.WebinarFields = oldWebinarValues.Keys + .Where(key => oldWebinarValues[key].Equals(newWebinarValues[key])) + .Select(key => (FieldName: key, Value: oldWebinarValues[key])) .ToArray(); webHookEvent = webinarUpdatedEvent; diff --git a/Source/ZoomNet/Models/RecordingContentType.cs b/Source/ZoomNet/Models/RecordingContentType.cs index 2109bafa..ec460711 100644 --- a/Source/ZoomNet/Models/RecordingContentType.cs +++ b/Source/ZoomNet/Models/RecordingContentType.cs @@ -47,12 +47,36 @@ public enum RecordingContentType [EnumMember(Value = "chat_file")] ChatFile, + /// Active speaker. + [EnumMember(Value = "active_speaker")] + ActiveSpeaker, + + /// Poll. + [EnumMember(Value = "poll")] + Poll, + /// timeline. [EnumMember(Value = "timeline")] Timeline, - /// Active speaker. - [EnumMember(Value = "active_speaker")] - ActiveSpeaker + /// closed_caption. + [EnumMember(Value = "closed_caption")] + ClosedCaption, + + /// Audio interpretation. + [EnumMember(Value = "audio_interpretation")] + AudioInterpretation, + + /// Summary. + [EnumMember(Value = "summary")] + Summary, + + /// Summary next steps. + [EnumMember(Value = "summary_next_steps")] + SummaryNextSteps, + + /// Summary smart chapters. + [EnumMember(Value = "summary_smart_chapters")] + SummarySmartChapters, } } diff --git a/Source/ZoomNet/Models/RecordingFileType.cs b/Source/ZoomNet/Models/RecordingFileType.cs index df78d7da..447b4e7d 100644 --- a/Source/ZoomNet/Models/RecordingFileType.cs +++ b/Source/ZoomNet/Models/RecordingFileType.cs @@ -34,6 +34,14 @@ public enum RecordingFileType /// File contains closed captions of the recording in VTT file format. [EnumMember(Value = "cc")] - ClosedCaptioning + ClosedCaptioning, + + /// File containing polling data in csv format. + [EnumMember(Value = "csv")] + PollingData, + + /// Summary file of the recording in JSON file format + [EnumMember(Value = "summary")] + Summary, } } diff --git a/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs b/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs index c4d4cb8f..73eeade6 100644 --- a/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/MeetingUpdatedEvent.cs @@ -29,5 +29,11 @@ public class MeetingUpdatedEvent : Event /// Gets or sets the fields that have been modified. /// public (string FieldName, object OldValue, object NewValue)[] ModifiedFields { get; set; } + + /// + /// Gets or sets the fields about the meeting. + /// + /// Typically, this array will contain fields such as Id, Uuid, etc. + public (string FieldName, object Value)[] MeetingFields { get; set; } } } diff --git a/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs b/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs index bd0703e3..d7238386 100644 --- a/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs +++ b/Source/ZoomNet/Models/Webhooks/WebinarUpdatedEvent.cs @@ -23,5 +23,11 @@ public class WebinarUpdatedEvent : Event /// Gets or sets the fields that have been modified. /// public (string FieldName, object OldValue, object NewValue)[] ModifiedFields { get; set; } + + /// + /// Gets or sets the fields about the webinar. + /// + /// Typically, this array will contain fields such as Id, Uuid, etc. + public (string FieldName, object Value)[] WebinarFields { get; set; } } } diff --git a/Source/ZoomNet/OAuthConnectionInfo.cs b/Source/ZoomNet/OAuthConnectionInfo.cs index 9178c13c..063ae402 100644 --- a/Source/ZoomNet/OAuthConnectionInfo.cs +++ b/Source/ZoomNet/OAuthConnectionInfo.cs @@ -72,6 +72,11 @@ public class OAuthConnectionInfo : IConnectionInfo /// public string RedirectUri { get; } + /// + /// Gets the cryptographically random string used to correlate the authorization request to the token request. + /// + public string CodeVerifier { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -118,7 +123,8 @@ public OAuthConnectionInfo(string clientId, string clientSecret) /// 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) + /// The cryptographically random string used to correlate the authorization request to the token request. + public OAuthConnectionInfo(string clientId, string clientSecret, string authorizationCode, OnTokenRefreshedDelegate onTokenRefreshed, string redirectUri = null, string codeVerifier = null) { if (string.IsNullOrEmpty(clientId)) throw new ArgumentNullException(nameof(clientId)); if (string.IsNullOrEmpty(clientSecret)) throw new ArgumentNullException(nameof(clientSecret)); @@ -131,6 +137,7 @@ public OAuthConnectionInfo(string clientId, string clientSecret, string authoriz TokenExpiration = DateTime.MinValue; GrantType = OAuthGrantType.AuthorizationCode; OnTokenRefreshed = onTokenRefreshed; + CodeVerifier = codeVerifier; } /// diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 26a249f3..59e6db61 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -75,25 +75,30 @@ public string RefreshTokenIfNecessary(bool forceRefresh) { _lock.EnterWriteLock(); - var grantType = _connectionInfo.GrantType.ToEnumString(); - var requestUrl = $"https://api.zoom.us/oauth/token?grant_type={grantType}"; + var contentValues = new Dictionary() + { + { "grant_type", _connectionInfo.GrantType.ToEnumString() }, + }; + switch (_connectionInfo.GrantType) { case OAuthGrantType.AccountCredentials: - requestUrl += $"&account_id={_connectionInfo.AccountId}"; + contentValues.Add("account_id", _connectionInfo.AccountId); break; case OAuthGrantType.AuthorizationCode: - requestUrl += $"&code={_connectionInfo.AuthorizationCode}"; - if (!string.IsNullOrEmpty(_connectionInfo.RedirectUri)) requestUrl += $"&redirect_uri={_connectionInfo.RedirectUri}"; + 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: - requestUrl += $"&refresh_token={_connectionInfo.RefreshToken}"; + contentValues.Add("refresh_token", _connectionInfo.RefreshToken); break; } var requestTime = DateTime.UtcNow; - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + 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(); diff --git a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs index 6fac193b..b87b275e 100644 --- a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs +++ b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs @@ -62,7 +62,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException) throw new ZoomException(errorMessage, response.Message, diagnosticLog); } - else if (!isError && response.IsSuccessStatusCode) + else if (!isError) { return; } diff --git a/Source/ZoomNet/WebhookParser.cs b/Source/ZoomNet/WebhookParser.cs index 9a17514c..974e7e87 100644 --- a/Source/ZoomNet/WebhookParser.cs +++ b/Source/ZoomNet/WebhookParser.cs @@ -31,7 +31,7 @@ public class WebhookParser : IWebhookParser /// An . public Event ParseEventWebhook(string requestBody) { - var webHookEvent = JsonSerializer.Deserialize(requestBody, ZoomNetJsonFormatter.DeserializerOptions); + var webHookEvent = JsonSerializer.Deserialize(requestBody, JsonFormatter.DeserializerOptions); return webHookEvent; } } diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index 73cb9d23..bfebc42c 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -220,7 +220,7 @@ private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool d // Remove all the built-in formatters and replace them with our custom JSON formatter _fluentClient.Formatters.Clear(); - _fluentClient.Formatters.Add(new ZoomNetJsonFormatter()); + _fluentClient.Formatters.Add(new JsonFormatter()); // Order is important: the token handler (either JWT or OAuth) must be first, followed by DiagnosticHandler and then by ErrorHandler. if (connectionInfo is JwtConnectionInfo jwtConnectionInfo) diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 4f989b34..1a70284a 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -36,13 +36,13 @@ - - + + - + - +