Skip to content

Commit

Permalink
Merge branch 'release/0.59.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Mar 29, 2023
2 parents 39d2ef6 + 7984c86 commit ba1af10
Show file tree
Hide file tree
Showing 44 changed files with 1,544 additions and 156 deletions.
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ var clientId = "... your client ID ...";
var clientSecret = "... your client secret ...";
var authorizationCode = "... the code that Zoom issued when you added the OAuth app to your account ...";
var redirectUri = "... the URI you have configured when setting up your OAuth app ..."; // Please note that Zoom sometimes accepts a null value and sometimes rejects it with a 'Redirect URI mismatch' error
var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, authorizationCode,
var connectionInfo = OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode,
(newRefreshToken, newAccessToken) =>
{
/*
Expand Down Expand Up @@ -112,7 +112,7 @@ Once the autorization code is converted into an access token and a refresh token
var clientId = "... your client ID ...";
var clientSecret = "... your client secret ...";
var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User);
var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, null,
var connectionInfo = OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken,
(newRefreshToken, newAccessToken) =>
{
/*
Expand All @@ -136,7 +136,7 @@ ZoomNet takes care of getting a new access token and it also refreshes a previou
var clientId = "... your client ID ...";
var clientSecret = "... your client secret ...";
var accountId = "... your account id ...";
var connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, accountId,
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId,
(_, newAccessToken) =>
{
/*
Expand All @@ -154,10 +154,43 @@ 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);
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId);
var zoomClient = new ZoomClient(connectionInfo);
```

#### Mutliple instances of your application in Server-to-Server OAuth scenarios

One important detail about Server-to-Server OAuth which is not widely known is that requesting a new token automatically invalidates a previously issued token EVEN THOUGH IT HASN'T REACHED ITS EXPIRATION DATE/TIME. This will affect you if you have multiple instances of your application running at the same time. To illustrate what this means, let's say that you have two instances of your application running at the same time. What is going to happen is that instance number 1 will request a new token which it will successfully use for some time until instance number 2 requests its own token. When this second token is issued, the token for instance 1 is invalidated which will cause instance 1 to request a new token. This new token will invalidate token number 2 which will cause instance 2 to request a new token, and so on. As you can see, instance 1 and 2 are fighting each other for a token.

There are a few ways you can overcome this problem:

Solution number 1:
You can create mutiple OAuth apps in Zoom's management dashboard, one for each instance of your app. This means that each instance will have their own clientId, clientSecret and accountId and therefore they can independently request tokens without interfering with each other.

This puts the onus is on you to create and manage these Zoom apps. Additionally, you are responsible for ensuring that the `OAuthConnectionInfo` in your C# code is initialized with the appropriate values for each instance.

This is a simple and effective solution when you have a relatively small number of instances but, in my opinion, it becomes overwhelming when your number of instances becomes too large.


Solution number 2:
Create a single Zoom OAuth app. Contact Zoom support and request additional "token indices" (also known as "group numbers") for this OAuth app. Subsequently, new tokens can be "scoped" to a given index which means that a token issued for a specific index does not invalidate token for any other index. Hopefully, Zoom will grant you enough token indices and you will be able to dedicate one index for each instance of your application and you can subsequently modify your C# code to "scope"" you OAuth connection to a desired index, like so:

```csharp
// you initialize the connection info for your first instance like this:
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId, 0);

// for your second instance, like this:
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId, 1);

// instance number 3:
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId, 2);

... and so on ...
```

Just like solution number 1, this solution works well for scenarios where you have a relatively small number of instances and where Zoom has granted you enough indices.


### Webhook Parser

Here's a basic example of a .net 6.0 API controller which parses the webhook from Zoom:
Expand Down Expand Up @@ -363,7 +396,8 @@ Console.CancelKeyPress += (s, e) =>
};

// Start the websocket client
using (var client = new ZoomWebSocketClient(clientId, clientSecret, accountId, subscriptionId, eventProcessor, proxy, logger))
var connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId);
using (var client = new ZoomWebSocketClient(connectionInfo, subscriptionId, eventProcessor, proxy, logger))
{
await client.StartAsync(cts.Token).ConfigureAwait(false);
exitEvent.WaitOne();
Expand Down
27 changes: 27 additions & 0 deletions Source/ZoomNet.IntegrationTests/Tests/CallLogs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ZoomNet.Models;

namespace ZoomNet.IntegrationTests.Tests
{
public class CallLogs : IIntegrationTest
{
public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) return;

await log.WriteLineAsync("\n***** CALL LOGS *****\n").ConfigureAwait(false);

var now = DateTime.UtcNow;
var from = now.Subtract(TimeSpan.FromDays(30));
var to = now;

// GET USER'S CALL LOGS
var allCallLogs = await client.CallLogs.GetAsync(myUser.Email, from, to, CallLogType.All, null, 100, null, cancellationToken);
var missedCalllogs = await client.CallLogs.GetAsync(myUser.Email, from, to, CallLogType.Missed, null, 100, null, cancellationToken);
await log.WriteLineAsync($"All call Logs: {allCallLogs.Records.Length}. Missed call logs: {missedCalllogs.Records.Length}").ConfigureAwait(false);
}
}
}
11 changes: 11 additions & 0 deletions Source/ZoomNet.IntegrationTests/Tests/Meetings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie
var instantMeeting = (InstantMeeting)await client.Meetings.GetAsync(newInstantMeeting.Id, null, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"Instant meeting {instantMeeting.Id} retrieved").ConfigureAwait(false);

var localRecordingToken = await client.Meetings.GetTokenForLocalRecordingAsync(newInstantMeeting.Id, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"The token for local recording is: {localRecordingToken}").ConfigureAwait(false);

await client.Meetings.EndAsync(newInstantMeeting.Id, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"Instant meeting {newInstantMeeting.Id} ended").ConfigureAwait(false);

Expand Down Expand Up @@ -131,6 +134,14 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie

if (myUser.Type == UserType.Licensed)
{
var registrants = new List<BatchRegistrant>
{
new BatchRegistrant { Email = "[email protected]", FirstName = "Mariful", LastName = "Maruf" },
new BatchRegistrant { Email = "[email protected]", FirstName = "Abdullah", LastName = "Galib" }
};
var registrantsInfo = await client.Meetings.PerformBatchRegistrationAsync(scheduledMeeting.Id, registrants, true).ConfigureAwait(false);
await log.WriteLineAsync($"Registrants {registrantsInfo} added to meeting {scheduledMeeting.Id}").ConfigureAwait(false);

var registrationAnswers1 = new[]
{
new RegistrationAnswer { Title = "Are you happy?", Answer = "Yes" }
Expand Down
5 changes: 2 additions & 3 deletions Source/ZoomNet.IntegrationTests/Tests/Reports.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -19,12 +18,12 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie
var from = now.Subtract(TimeSpan.FromDays(30));
var to = now;

//GET ALL HOSTS
// GET ALL HOSTS
var activeHosts = await client.Reports.GetHostsAsync(from, to, ReportHostType.Active, 30, null, cancellationToken);
var inactiveHosts = await client.Reports.GetHostsAsync(from, to, ReportHostType.Inactive, 30, null, cancellationToken);
await log.WriteLineAsync($"Active Hosts: {activeHosts.Records.Length}. Inactive Hosts: {inactiveHosts.Records.Length}").ConfigureAwait(false);

//GET ALL MEETINGS
// GET ALL MEETINGS
var pastMeetings = await client.Reports.GetMeetingsAsync(myUser.Id, from, to, ReportMeetingType.Past, 30, null, cancellationToken);

int totalParticipants = 0;
Expand Down
8 changes: 8 additions & 0 deletions Source/ZoomNet.IntegrationTests/Tests/Webinars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie
await log.WriteLineAsync($" - there are {registrationQuestions.Questions.Count(q => q.IsRequired)} required custom questions.").ConfigureAwait(false);
await log.WriteLineAsync($" - there are {registrationQuestions.Questions.Count(q => !q.IsRequired)} optional custom questions.").ConfigureAwait(false);

var registrants = new List<BatchRegistrant>
{
new BatchRegistrant { Email = "[email protected]", FirstName = "Mariful", LastName = "Maruf" },
new BatchRegistrant { Email = "[email protected]", FirstName = "Abdullah", LastName = "Galib" }
};
var registrantsInfo = await client.Webinars.PerformBatchRegistrationAsync(scheduledWebinar.Id, registrants, true).ConfigureAwait(false);
await log.WriteLineAsync($"Registrants {registrantsInfo} added to meeting {scheduledWebinar.Id}").ConfigureAwait(false);

var registrationAnswers1 = new[]
{
new RegistrationAnswer { Title = "Are you happy?", Answer = "Yes" }
Expand Down
133 changes: 69 additions & 64 deletions Source/ZoomNet.IntegrationTests/TestsRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ private enum ResultCodes

private enum TestType
{
WebSockets = 0,
ApiWithJwt = 1,
ApiWithOAuth = 2
Api = 0,
WebSockets = 1,
}

private enum ConnectionType
{
Jwt = 1,
OAuth = 2,
}

private readonly ILoggerFactory _loggerFactory;
Expand All @@ -37,15 +42,16 @@ public TestsRunner(ILoggerFactory loggerFactory)
_loggerFactory = loggerFactory;
}

public async Task<int> RunAsync()
public Task<int> RunAsync()
{
// -----------------------------------------------------------------------------
// Do you want to proxy requests through Fiddler? Can be useful for debugging.
var useFiddler = true;
var fiddlerPort = 8888; // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866

// What test do you want to run?
var testType = TestType.ApiWithOAuth;
// What tests do you want to run and which connection type do you want to use?
var testType = TestType.Api;
var connectionType = ConnectionType.OAuth;
// -----------------------------------------------------------------------------

// Ensure the Console is tall enough and centered on the screen
Expand All @@ -55,67 +61,66 @@ public async Task<int> RunAsync()
// Configure the proxy if desired (very useful for debugging)
var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null;

if (testType == TestType.ApiWithJwt)
// Run tests either with a JWT or OAuth connection
return connectionType switch
{
var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User);
var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User);
var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret);
var resultCode = await RunApiTestsAsync(connectionInfo, proxy).ConfigureAwait(false);
return resultCode;
ConnectionType.Jwt => RunTestsWithJwtConnectionAsync(testType, proxy),
ConnectionType.OAuth => RunTestsWithOAuthConnectionAsync(testType, proxy),
_ => throw new Exception("Unknwon connection type"),
};
}

}
else if (testType == TestType.ApiWithOAuth)
{
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);
IConnectionInfo connectionInfo;

// 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}");
});
}
private Task<int> RunTestsWithJwtConnectionAsync(TestType testType, IWebProxy proxy)
{
if (testType != TestType.Api) throw new Exception("Only API tests are supported with JWT");

// Standard OAuth
else
{
connectionInfo = new OAuthConnectionInfo(clientId, clientSecret, refreshToken, null,
(newRefreshToken, newAccessToken) =>
{
Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, 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);
// },
// null);
}
var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User);
var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User);
var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret);

var resultCode = await RunApiTestsAsync(connectionInfo, proxy).ConfigureAwait(false);
return resultCode;
}
else if (testType == TestType.WebSockets)
return RunApiTestsAsync(connectionInfo, proxy);
}

private Task<int> RunTestsWithOAuthConnectionAsync(TestType testType, IWebProxy proxy)
{
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 subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User);

IConnectionInfo connectionInfo;

// Server-to-Server OAuth
if (!string.IsNullOrEmpty(accountId))
{
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 subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User);
var resultCode = await RunWebSocketTestsAsync(clientId, clientSecret, accountId, subscriptionId, proxy).ConfigureAwait(false);
return resultCode;
connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId);
}

// Standard OAuth
else
{
throw new Exception("Unknwon test type");
connectionInfo = OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken,
(newRefreshToken, newAccessToken) =>
{
Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User);
});

//var authorizationCode = "<-- the code generated by Zoom when the app is authorized by the user -->";
//connectionInfo = OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode,
// (newRefreshToken, newAccessToken) =>
// {
// Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User);
// });
}

// Execute either the API or Websocket tests
return testType switch
{
TestType.Api => RunApiTestsAsync(connectionInfo, proxy),
TestType.WebSockets => RunWebSocketTestsAsync(connectionInfo, subscriptionId, proxy),
_ => throw new Exception("Unknwon test type"),
};
}

private async Task<int> RunApiTestsAsync(IConnectionInfo connectionInfo, IWebProxy proxy)
Expand All @@ -135,6 +140,7 @@ private async Task<int> RunApiTestsAsync(IConnectionInfo connectionInfo, IWebPro
var integrationTests = new Type[]
{
typeof(Accounts),
typeof(CallLogs),
typeof(Chat),
typeof(CloudRecordings),
typeof(Contacts),
Expand Down Expand Up @@ -217,14 +223,15 @@ private async Task<int> RunApiTestsAsync(IConnectionInfo connectionInfo, IWebPro
return resultCode;
}

private async Task<int> RunWebSocketTestsAsync(string clientId, string clientSecret, string accountId, string subscriptionId, IWebProxy proxy)
private async Task<int> RunWebSocketTestsAsync(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy)
{
var logger = _loggerFactory.CreateLogger<ZoomWebSocketClient>();
var eventProcessor = new Func<Event, CancellationToken, Task>(async (webhookEvent, cancellationToken) =>
{
if (!cancellationToken.IsCancellationRequested)
{
logger.LogInformation("Processing {eventType} event...", webhookEvent.EventType);
await Task.Delay(1, cancellationToken).ConfigureAwait(false); // This async call gets rid of "CS1998 This async method lacks 'await' operators and will run synchronously".
}
});

Expand All @@ -239,11 +246,9 @@ private async Task<int> RunWebSocketTestsAsync(string clientId, string clientSec
};

// Start the websocket client
using (var client = new ZoomWebSocketClient(clientId, clientSecret, accountId, subscriptionId, eventProcessor, proxy, logger))
{
await client.StartAsync(cts.Token).ConfigureAwait(false);
exitEvent.WaitOne();
}
using var client = new ZoomWebSocketClient(connectionInfo, subscriptionId, eventProcessor, proxy, logger);
await client.StartAsync(cts.Token).ConfigureAwait(false);
exitEvent.WaitOne();

return (int)ResultCodes.Success;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Logzio.DotNet.NLog" Version="1.0.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ 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,
var connectionInfo = OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode,
(newRefreshToken, newAccessToken) =>
{
// Intentionally left blank
Expand Down
Loading

0 comments on commit ba1af10

Please sign in to comment.