Skip to content

Commit

Permalink
Merge branch 'release/0.82.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Oct 26, 2024
2 parents bf2b415 + 154e538 commit 6e5d9b1
Show file tree
Hide file tree
Showing 28 changed files with 342 additions and 1,912 deletions.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"cake.tool": {
"version": "4.0.0",
"version": "4.2.0",
"commands": [
"dotnet-cake"
]
Expand Down
6 changes: 6 additions & 0 deletions GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
workflow: GitFlow/v1 # https://github.com/GitTools/GitVersion/blob/main/docs/input/docs/reference/configuration.md#snippet-/docs/workflows/GitFlow/v1.yml

branches:
develop:
label: beta # default is 'alpha' for the 'develop' branch. I prefer 'beta'.
release:
label: rc # default is 'beta' for the 'release' branch. I prefer 'RC'.
21 changes: 21 additions & 0 deletions Source/ZoomNet.UnitTests/Extensions/InternalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,5 +374,26 @@ public void ToEnumString(MyEnum value, string expected)

}
}


public class GetErrorMessageAsync
{
[Fact]
public async Task CanHandleUnescapedDoubleQuotesInErrorMessage()
{
// Arrange
const string responseContent = @"{""code"":104, ""message"":""Invalid access token, does not contain scopes:[""zoom_events_basic:read"",""zoom_events_basic:read:admin""]""}";
var message = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseContent) };
var response = new MockFluentHttpResponse(message, null, CancellationToken.None);

// Act
var (isError, errorMessage, errorCode) = await response.Message.GetErrorMessageAsync();

// Assert
isError.ShouldBeTrue();
errorMessage.ShouldStartWith("Invalid access token, does not contain scopes");
errorCode.ShouldBe(104);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public void Write_multiple()
[InlineData("win 11", ParticipantDevice.Windows)]
[InlineData("Zoom Rooms", ParticipantDevice.ZoomRoom)]
[InlineData("win 10+ 17763", ParticipantDevice.Windows)]
[InlineData("Web Browser Chrome 129", ParticipantDevice.Web)]
public void Read_single(string value, ParticipantDevice expectedValue)
{
// Arrange
Expand Down
54 changes: 32 additions & 22 deletions Source/ZoomNet/Extensions/Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -945,29 +945,39 @@ internal static bool IsNullableType(this Type type)
private static async Task<JsonElement> ParseZoomResponseAsync(this HttpContent responseFromZoomApi, CancellationToken cancellationToken = default)
{
var responseContent = await responseFromZoomApi.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(responseContent)) return default; // the 'ValueKind' property of the default JsonElement is JsonValueKind.Undefined
if (string.IsNullOrEmpty(responseContent)) return default; // FYI: the 'ValueKind' property of the default JsonElement is JsonValueKind.Undefined

const string pattern = @"(.*?)(?<=""message"":"")(.*?)(?=""})(.*?$)";
var matches = Regex.Match(responseContent, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
var prefix = matches.Groups[1].Value;
var message = matches.Groups[2].Value;
var postfix = matches.Groups[3].Value;

if (string.IsNullOrEmpty(message)) return JsonDocument.Parse(responseContent).RootElement;

/*
Sometimes the error message is malformed due to the presence of double quotes that are not properly escaped.
See: https://devforum.zoom.us/t/list-events-endpoint-returns-invalid-json-in-the-payload/115792 for more info.
One instance where this problem was observed is when retrieving the list of events without having the necessary permissions to do so.
The result is the following response with unescaped double-quotes in the error message:
{
"code": 104,
"message": "Invalid access token, does not contain scopes:["zoom_events_basic:read","zoom_events_basic:read:admin"]"
}
*/
var escapedMessage = Regex.Replace(message, @"(?<!\\)""", "\\\"", RegexOptions.Compiled); // Replace un-escaped double-quotes with properly escaped double-quotes
var result = $"{prefix}{escapedMessage}{postfix}";
return JsonDocument.Parse(result).RootElement;
try
{
// Attempt to parse the response with the assumption that JSON is well-formed
// If the JSON is malformed, a JsonException will be thrown
return JsonDocument.Parse(responseContent).RootElement;
}
catch (JsonException)
{
/*
Sometimes the error message is malformed due to the presence of double quotes that are not properly escaped.
See: https://devforum.zoom.us/t/list-events-endpoint-returns-invalid-json-in-the-payload/115792 for more info.
One instance where this problem was observed is when retrieving the list of events without having the necessary permissions to do so.
The result is the following response with unescaped double-quotes in the error message:
{
"code": 104,
"message": "Invalid access token, does not contain scopes:["zoom_events_basic:read","zoom_events_basic:read:admin"]"
}
*/
const string pattern = @"(.*?)(?<=""message"":"")(.*?)(?=""})(.*?$)";
var matches = Regex.Match(responseContent, pattern, RegexOptions.Compiled | RegexOptions.Singleline);
if (matches.Groups.Count != 4) throw;

var prefix = matches.Groups[1].Value;
var message = matches.Groups[2].Value;
var postfix = matches.Groups[3].Value;
if (string.IsNullOrEmpty(message)) throw;

var escapedMessage = Regex.Replace(message, @"(?<!\\)""", "\\\"", RegexOptions.Compiled); // Replace un-escaped double-quotes with properly escaped double-quotes
var result = $"{prefix}{escapedMessage}{postfix}";
return JsonDocument.Parse(result).RootElement;
}
}

/// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
Expand Down
17 changes: 17 additions & 0 deletions Source/ZoomNet/Extensions/Public.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -351,5 +352,21 @@ public static Task<Webinar> GetAsync(this IWebinars webinarResource, long webina
{
return webinarResource.GetAsync(webinarId, occurrenceId, false, cancellationToken);
}

/// <summary>
/// Adds user to a group.
/// </summary>
/// <param name="groupsResource">The group resource.</param>
/// <param name="groupId">The ID of the group.</param>
/// <param name="emailAddress">An email address of user to add to the group.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the operation. The result will be a string representing the ID of the added user.</returns>
public static async Task<string> AddUserToGroupAsync(this IGroups groupsResource, string groupId, string emailAddress, CancellationToken cancellationToken = default)
{
var result = await groupsResource.AddUsersToGroupAsync(groupId, new[] { emailAddress }, cancellationToken).ConfigureAwait(false);

// We added a single member to a group therefore the array returned from the Zoom API contains a single element
return result.Single();
}
}
}
5 changes: 5 additions & 0 deletions Source/ZoomNet/IZoomClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,10 @@ public interface IZoomClient
/// Gets the resource which allows you to manage SMS messages and sessions.
/// </summary>
ISms Sms { get; }

/// <summary>
/// Gets the resource that allows you to manage groups.
/// </summary>
IGroups Groups { get; }
}
}
5 changes: 5 additions & 0 deletions Source/ZoomNet/Json/ParticipantDeviceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public override void Write(Utf8JsonWriter writer, ParticipantDevice[] value, Jso
private static ParticipantDevice Convert(string deviceAsString)
{
if (string.IsNullOrWhiteSpace(deviceAsString)) return ParticipantDevice.Unknown;

// See https://github.com/Jericho/ZoomNet/issues/369 for details about the underlying problem
// See https://github.com/Jericho/ZoomNet/issues/370 for details about this workaround
if (deviceAsString.StartsWith("Web Browser", StringComparison.OrdinalIgnoreCase)) return ParticipantDevice.Web;

return deviceAsString.Trim().ToEnum<ParticipantDevice>();
}
}
Expand Down
126 changes: 12 additions & 114 deletions Source/ZoomNet/Resources/Accounts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@

namespace ZoomNet.Resources
{
/// <summary>
/// Allows you to manage sub accounts under the master account.
/// </summary>
/// <seealso cref="ZoomNet.Resources.IAccounts" />
/// <remarks>
/// See <a href="https://marketplace.zoom.us/docs/api-reference/zoom-api/accounts/accounts">Zoom documentation</a> for more information.
/// </remarks>
/// <inheritdoc/>
public class Accounts : IAccounts
{
private readonly Pathoschild.Http.Client.IClient _client;
Expand All @@ -29,15 +23,7 @@ internal Accounts(Pathoschild.Http.Client.IClient client)
_client = client;
}

/// <summary>
/// Retrieve all the sub accounts under the master account.
/// </summary>
/// <param name="recordsPerPage">The number of records returned within a single API call.</param>
/// <param name="page">The current page number of returned records.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// An array of <see cref="Account" />.
/// </returns>
/// <inheritdoc/>
[Obsolete("Zoom is in the process of deprecating the \"page number\" and \"page count\" fields.")]
public Task<PaginatedResponse<Account>> GetAllAsync(int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default)
{
Expand All @@ -54,15 +40,7 @@ public Task<PaginatedResponse<Account>> GetAllAsync(int recordsPerPage = 30, int
.AsPaginatedResponse<Account>("accounts");
}

/// <summary>
/// Retrieve all the sub accounts under the master account.
/// </summary>
/// <param name="recordsPerPage">The number of records returned within a single API call.</param>
/// <param name="pagingToken">The paging token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// An array of <see cref="Account" />.
/// </returns>
/// <inheritdoc/>
public Task<PaginatedResponseWithToken<Account>> GetAllAsync(int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default)
{
if (recordsPerPage < 1 || recordsPerPage > 300)
Expand All @@ -78,22 +56,7 @@ public Task<PaginatedResponseWithToken<Account>> GetAllAsync(int recordsPerPage
.AsPaginatedResponseWithToken<Account>("accounts");
}

/// <summary>
/// Create a sub account under the master account.
/// </summary>
/// <param name="firstName">User's first name.</param>
/// <param name="lastName">User's last name.</param>
/// <param name="email">User's email address.</param>
/// <param name="password">User's password.</param>
/// <param name="useSharedVirtualRoomConnectors">Enable/disable the option for a sub account to use shared Virtual Room Connector(s).</param>
/// <param name="roomConnectorsIpAddresses">The IP addresses of the Room Connectors that you would like to share with the sub account.</param>
/// <param name="useSharedMeetingConnectors">Enable/disable the option for a sub account to use shared Meeting Connector(s).</param>
/// <param name="meetingConnectorsIpAddresses">The IP addresses of the Meeting Connectors that you would like to share with the sub account.</param>
/// <param name="payMode">Payee.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The <see cref="Account" />.
/// </returns>
/// <inheritdoc/>
public Task<Account> CreateAsync(string firstName, string lastName, string email, string password, bool useSharedVirtualRoomConnectors = false, IEnumerable<string> roomConnectorsIpAddresses = null, bool useSharedMeetingConnectors = false, IEnumerable<string> meetingConnectorsIpAddresses = null, PayMode payMode = PayMode.Master, CancellationToken cancellationToken = default)
{
var data = new JsonObject
Expand Down Expand Up @@ -122,14 +85,7 @@ public Task<Account> CreateAsync(string firstName, string lastName, string email
.AsObject<Account>();
}

/// <summary>
/// Retrieve the details of a sub account.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The <see cref="Account" />.
/// </returns>
/// <inheritdoc/>
public Task<Account> GetAsync(long accountId, CancellationToken cancellationToken = default)
{
// The information returned from this API call is vastly different than what is returned by GetAllAsync
Expand All @@ -139,17 +95,7 @@ public Task<Account> GetAsync(long accountId, CancellationToken cancellationToke
.AsObject<Account>();
}

/// <summary>
/// Disassociate a Sub Account from the Master Account.
/// </summary>
/// <param name="accountId">The account Id that must be disassociated from its master account.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The async task.
/// </returns>
/// <remarks>
/// This will leave the Sub Account intact but it will no longer be associated with the master account.
/// </remarks>
/// <inheritdoc/>
public Task DisassociateAsync(long accountId, CancellationToken cancellationToken = default)
{
return _client
Expand All @@ -158,19 +104,7 @@ public Task DisassociateAsync(long accountId, CancellationToken cancellationToke
.AsMessage();
}

/// <summary>
/// Update a Sub Account's options under the Master Account.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="useSharedVirtualRoomConnectors">Enable/disable the option for a sub account to use shared Virtual Room Connector(s).</param>
/// <param name="roomConnectorsIpAddresses">The IP addresses of the Room Connectors that you would like to share with the sub account.</param>
/// <param name="useSharedMeetingConnectors">Enable/disable the option for a sub account to use shared Meeting Connector(s).</param>
/// <param name="meetingConnectorsIpAddresses">The IP addresses of the Meeting Connectors that you would like to share with the sub account.</param>
/// <param name="payMode">Payee.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The async task.
/// </returns>
/// <inheritdoc/>
public Task UpdateOptionsAsync(long accountId, bool? useSharedVirtualRoomConnectors = null, IEnumerable<string> roomConnectorsIpAddresses = null, bool? useSharedMeetingConnectors = null, IEnumerable<string> meetingConnectorsIpAddresses = null, PayMode? payMode = null, CancellationToken cancellationToken = default)
{
var data = new JsonObject
Expand All @@ -189,14 +123,7 @@ public Task UpdateOptionsAsync(long accountId, bool? useSharedVirtualRoomConnect
.AsMessage();
}

/// <summary>
/// Retrieve an account's meeting authentication settings.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The <see cref="AuthenticationSettings">settings</see>.
/// </returns>
/// <inheritdoc/>
public async Task<AuthenticationSettings> GetMeetingAuthenticationSettingsAsync(long accountId, CancellationToken cancellationToken = default)
{
var response = await _client
Expand All @@ -215,14 +142,7 @@ public async Task<AuthenticationSettings> GetMeetingAuthenticationSettingsAsync(
return settings;
}

/// <summary>
/// Retrieve an account's recording authentication settings.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The <see cref="AuthenticationSettings">settings</see>.
/// </returns>
/// <inheritdoc/>
public async Task<AuthenticationSettings> GetRecordingAuthenticationSettingsAsync(long accountId, CancellationToken cancellationToken = default)
{
var response = await _client
Expand All @@ -241,14 +161,7 @@ public async Task<AuthenticationSettings> GetRecordingAuthenticationSettingsAsyn
return settings;
}

/// <summary>
/// Retrieve a sub account's managed domains.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// An array of managed domains and their status.
/// </returns>
/// <inheritdoc/>
public async Task<(string Domain, string Status)[]> GetManagedDomainsAsync(long accountId, CancellationToken cancellationToken = default)
{
var response = await _client
Expand All @@ -269,14 +182,7 @@ public async Task<AuthenticationSettings> GetRecordingAuthenticationSettingsAsyn
return managedDomains;
}

/// <summary>
/// Retrieve a sub account's trusted domains.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// An array of trusted domains.
/// </returns>
/// <inheritdoc/>
public Task<string[]> GetTrustedDomainsAsync(long accountId, CancellationToken cancellationToken = default)
{
return _client
Expand All @@ -285,15 +191,7 @@ public Task<string[]> GetTrustedDomainsAsync(long accountId, CancellationToken c
.AsObject<string[]>("trusted_domains");
}

/// <summary>
/// Change the owner of a Sub Account to another user on the same account.
/// </summary>
/// <param name="accountId">The account Id.</param>
/// <param name="newOwnerEmail">The new owner's email address.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The async task.
/// </returns>
/// <inheritdoc/>
public Task UpdateOwnerAsync(long accountId, string newOwnerEmail, CancellationToken cancellationToken = default)
{
var data = new JsonObject
Expand Down
Loading

0 comments on commit 6e5d9b1

Please sign in to comment.