Skip to content

Commit

Permalink
Merge branch 'release/0.48.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Aug 15, 2022
2 parents c744111 + 063e4ef commit 4be066f
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 53 deletions.
40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The last version of ZoomNet that supported `.NET 4.6.1`, `.NET 4.7.2` and `.NET
## Usage

### Connection Information
Before you start using the ZoomNet client, you must decide how you are going to connect to the Zoom API. ZoomNet supports two distinct ways of connecting to Zoom: JWT and OAuth.
Before you start using the ZoomNet client, you must decide how you are going to connect to the Zoom API. ZoomNet supports three ways of connecting to Zoom: JWT, OAuth and Server-to-Server OAuth.

#### Connection using JWT
This is the simplest way to connect to the Zoom API. Zoom expects you to use a key and a secret to generate a JSON object with a signed payload and to provide this JSON object with every API request. The good news is that ZoomNet takes care of the intricacies of generating this JSON object: you simply provide the key and the secret and ZoomNet takes care of the rest. Super easy!
Expand All @@ -54,6 +54,7 @@ When you have the API key and secret, you can instantiate a 'connection info' ob
var apiKey = "... your API key ...";
var apiSecret = "... your API secret ...";
var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret);
var zoomClient = new ZoomClient(connectionInfo);
```

> **Warning:** <a href="https://marketplace.zoom.us/docs/guides/build/jwt-app/jwt-faq/">Zoom has announced</a> that this authentication method would be obsolete in June 2023. The recommendation is to swith to Server-to-Server OAuth.
Expand All @@ -68,26 +69,37 @@ The Zoom documentation has a document about [how to create an OAuth app](https:/
- 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 the refresh token but it's your responsability to store these generated values.

When you initially add an OAuth application to your Zoom account, you will be issued an "authorization code".
You can provide this autorization code to ZoomNet like so:
```csharp
var clientId = "... your client ID ...";
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,
var authorizationCode = "... the code that Zoom issued when you added the OAuth app to your account ...";
var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode,
(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.
This callback is invoked when the authorization code
is converted into an access token and also when the
access token is subsequently refreshed.
You should use this callback to save the two new tokens
to a safe place so you can provide them the next time 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:
*/
Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User);
Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User);
});
var zoomClient = new ZoomClient(connectionInfo);
```

For demonstration purposes, here's how you could use your operating system's environment variables to store the tokens
> **Warning:** This sample I just provided can be used only when Zoom issues a new the autorization code. ZoomNet will take care of converting this code into an access token at which point the autorization code is no longer valid.
Once the autorization code is converted into an access token, you can instantiate a 'connection info' object like so:
```csharp
var clientId = "... your client ID ...";
var clientSecret = "... your client secret ...";
Expand All @@ -99,6 +111,7 @@ var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToke
Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User);
Environment.SetEnvironmentVariable("ZOOM_OAUTH_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User);
});
var zoomClient = new ZoomClient(connectionInfo);
```

#### Connection using Server-to-Server OAuth
Expand All @@ -109,7 +122,6 @@ 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 ...";
Expand All @@ -128,18 +140,12 @@ var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, accountId,
a null value in lieu of a delegate.
*/
});
var zoomClient = new ZoomClient(connectionInfo);
```

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:
```csharp
var zoomClient = new ZoomClient(connectionInfo);
```
3 changes: 2 additions & 1 deletion Source/ZoomNet.IntegrationTests/Tests/Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ await client.Users.UpdateAsync(myUser.Id,
manager: "Bob",
phoneNumbers: new[]
{
new PhoneNumber { Country = Country.Canada, CountryCode = "+1", Number = "555-555-1234", Type = PhoneType.Office }
new PhoneNumber { Country = Country.Canada, CountryCode = "+1", Number = "555-555-1234", Type = PhoneType.Office },
new PhoneNumber { Country = Country.United_States_of_America, CountryCode = "+1", Number = "555-666-1234" }
},
cancellationToken: cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync("My user was updated").ConfigureAwait(false);
Expand Down
18 changes: 9 additions & 9 deletions Source/ZoomNet.IntegrationTests/TestsRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ public async Task<int> RunAsync()
// These are the integration tests that we will execute
var integrationTests = new Type[]
{
typeof(Accounts),
typeof(Chat),
typeof(CloudRecordings),
typeof(Contacts),
typeof(Dashboards),
typeof(Meetings),
typeof(Roles),
//typeof(Accounts),
//typeof(Chat),
//typeof(CloudRecordings),
//typeof(Contacts),
//typeof(Dashboards),
//typeof(Meetings),
//typeof(Roles),
typeof(Users),
typeof(Webinars),
typeof(Reports)
//typeof(Webinars),
//typeof(Reports)
};

// Get my user and permisisons
Expand Down
68 changes: 44 additions & 24 deletions Source/ZoomNet/Extensions/Internal.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Pathoschild.Http.Client;
using Pathoschild.Http.Client.Extensibility;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -343,15 +344,15 @@ internal static async Task<JsonDocument> AsRawJsonDocument(this IRequest request
return await response.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing).ConfigureAwait(false);
}

/// <summary>
/// Replace the current error handler which treats HTTP200 as success with a handler that treats HTTP200 as failure.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="customExceptionMessage">An optional custom error message.</param>
/// <returns>Returns the request builder for chaining.</returns>
internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, string customExceptionMessage = null)
{
var currentErrorHandler = request.Filters.OfType<ZoomErrorHandler>().SingleOrDefault();
var newErrorHandler = new ZoomErrorHandler(true, customExceptionMessage);

// Replace the current error handler which treats HTTP200 as success with a handler that treats HTTP200 as failure
request.Filters.Replace(currentErrorHandler, newErrorHandler);

return request;
return request.WithFilter(new ZoomErrorHandler(true, customExceptionMessage));
}

/// <summary>Set the body content of the HTTP request.</summary>
Expand All @@ -370,6 +371,42 @@ internal static IRequest WithJsonBody<T>(this IRequest request, T body)
return request.WithBody(bodyBuilder => bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json")));
}

/// <summary>Add a filter to a request.</summary>
/// <typeparam name="TFilter">The type of filter.</typeparam>
/// <param name="request">The request.</param>
/// <param name="filter">The filter.</param>
/// <param name="replaceExisting">
/// 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.
/// </param>
/// <returns>Returns the request builder for chaining.</returns>
internal static IRequest WithFilter<TFilter>(this IRequest request, TFilter filter, bool replaceExisting = true)
where TFilter : IHttpFilter
{
var matchingFilters = request.Filters.OfType<TFilter>().ToArray();

if (matchingFilters.Length == 0 || !replaceExisting)
{
request.Filters.Add(filter);
}
else
{
// Replace the first matching filter with the new filter
var collectionAsList = request.Filters as IList<IHttpFilter>;
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++)
{
request.Filters.Remove(matchingFilters[i]);
}
}

return request;
}

/// <summary>Asynchronously retrieve the response body as a <see cref="string"/>.</summary>
/// <param name="response">The response.</param>
/// <param name="encoding">The encoding. You can leave this parameter null and the encoding will be
Expand Down Expand Up @@ -688,23 +725,6 @@ internal static (WeakReference<HttpRequestMessage> RequestReference, string Diag
return (isError, errorMessage, errorCode);
}

internal static void Replace<T>(this ICollection<T> collection, T oldValue, T newValue)
{
// In case the collection is ordered, we'll be able to preserve the order
if (collection is IList<T> collectionAsList)
{
var oldIndex = collectionAsList.IndexOf(oldValue);
collectionAsList.RemoveAt(oldIndex);
collectionAsList.Insert(oldIndex, newValue);
}
else
{
// No luck, so just remove then add
collection.Remove(oldValue);
collection.Add(newValue);
}
}

/// <summary>Convert an enum to its string representation.</summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="enumValue">The value.</param>
Expand Down
2 changes: 1 addition & 1 deletion Source/ZoomNet/Models/PhoneNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class PhoneNumber

/// <summary>Gets or sets the type.</summary>
[JsonPropertyName("label")]
public PhoneType Type { get; set; }
public PhoneType? Type { get; set; }

/// <summary>Gets or sets the phone number.</summary>
[JsonPropertyName("number")]
Expand Down
2 changes: 1 addition & 1 deletion build.cake
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Install tools.
#tool dotnet:?package=GitVersion.Tool&version=5.10.3
#tool dotnet:?package=coveralls.net&version=4.0.0
#tool dotnet:?package=coveralls.net&version=4.0.1
#tool nuget:?package=GitReleaseManager&version=0.13.0
#tool nuget:?package=ReportGenerator&version=5.1.9
#tool nuget:?package=xunit.runner.console&version=2.4.2
Expand Down

0 comments on commit 4be066f

Please sign in to comment.