Skip to content

Commit

Permalink
Merge branch 'release/0.57.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Jan 23, 2023
2 parents d1c895e + 438e4d8 commit 9e83ecb
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 45 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,56 @@ namespace WebApplication1.Controllers
}
}
```

### Webhooks over websockets

As of this writing (October 2022), webhooks over websocket is in public beta testing and you can signup if you want to participate in the beta (see [here](https://marketplace.zoom.us/docs/api-reference/websockets/)).

ZoomNet offers a convenient client to receive and process webhooks events received over a websocket connection. This websocket client will automatically manage the connection, ensuring it is re-established if it's closed for some reason. Additionaly, it will manage the OAuth token and will automatically refresh it when it expires.

Here's how to use it in a C# console application:

```csharp
using System.Net;
using ZoomNet;
using ZoomNet.Models.Webhooks;

var clientId = "... your client id ...";
var clientSecret = "... your client secret ...";
var accountId = "... your account id ...";
var subscriptionId = "... your subscription id ..."; // See instructions below how to get this value
// This is the async delegate that gets invoked when a webhook event is received
var eventProcessor = new Func<Event, CancellationToken, Task>(async (webhookEvent, cancellationToken) =>
{
if (!cancellationToken.IsCancellationRequested)
{
// Add your custom logic to process this event
}
});

// Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the websocket client)
var cts = new CancellationTokenSource();
var exitEvent = new ManualResetEvent(false);
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
exitEvent.Set();
};

// 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();
}
```

#### How to get your websocket subscription id

When you configure your webhook over websocket in the Zoom Marketplace, Zoom will generate a URL like you can see in this screenshot:

![Screenshot](https://user-images.githubusercontent.com/112710/196733937-7813abdd-9cb5-4a35-ad69-d5f6ac9676e4.png)

Your subscription Id is the last part of the URL. In the example above, the generated URL is similar to `wss://api.zoom.us/v2/webhooks/events?subscription_id=1234567890` and therefore the subscription id is `1234567890`.
25 changes: 14 additions & 11 deletions Source/ZoomNet.IntegrationTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ public class Program
{
public static async Task<int> Main(string[] args)
{
/*
* Handy code to generate the 'JsonSerializable' attributes for ZoomNetJsonSerializerContext
*
//var serializerContext = GenerateAttributesForSerializerContext();

var services = new ServiceCollection();
ConfigureServices(services);
await using var serviceProvider = services.BuildServiceProvider();
var app = serviceProvider.GetService<TestsRunner>();
return await app.RunAsync().ConfigureAwait(false);
}

private static string GenerateAttributesForSerializerContext()
{
// Handy code to generate the 'JsonSerializable' attributes for ZoomNetJsonSerializerContext
var baseNamespace = "ZoomNet.Models";
var allTypes = System.Reflection.Assembly
.GetAssembly(typeof(ZoomClient))
Expand Down Expand Up @@ -51,13 +60,7 @@ public static async Task<int> Main(string[] args)
var nullableAttributes = string.Join("\r\n", typesSortedAlphabetically.Where(t => !string.IsNullOrEmpty(t.JsonSerializeAttributeNullable)).Select(t => t.JsonSerializeAttributeNullable));

var result = string.Join("\r\n\r\n", new[] { simpleAttributes, arrayAttributes, nullableAttributes });
*/

var services = new ServiceCollection();
ConfigureServices(services);
await using var serviceProvider = services.BuildServiceProvider();
var app = serviceProvider.GetService<TestsRunner>();
return await app.RunAsync().ConfigureAwait(false);
return result;
}

private static void ConfigureServices(ServiceCollection services)
Expand Down Expand Up @@ -87,7 +90,7 @@ private static LoggingConfiguration GetNLogConfiguration()
// Send logs to console
var consoleTarget = new ColoredConsoleTarget();
nLogConfig.AddTarget("ColoredConsole", consoleTarget);
nLogConfig.AddRule(NLog.LogLevel.Warn, NLog.LogLevel.Fatal, "ColoredConsole", "*");
nLogConfig.AddRule(new LoggingRule("*", NLog.LogLevel.Warn, NLog.LogLevel.Fatal, consoleTarget) { RuleName = "ColoredConsoleRule" });

return nLogConfig;
}
Expand Down
114 changes: 88 additions & 26 deletions Source/ZoomNet.IntegrationTests/TestsRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using ZoomNet.IntegrationTests.Tests;
using ZoomNet.Models.Webhooks;

namespace ZoomNet.IntegrationTests
{
Expand All @@ -22,10 +23,11 @@ private enum ResultCodes
Cancelled = 1223
}

private enum ConnectionMethods
private enum TestType
{
Jwt = 0,
OAuth = 1
WebSockets = 0,
ApiWithJwt = 1,
ApiWithOAuth = 2
}

private readonly ILoggerFactory _loggerFactory;
Expand All @@ -42,24 +44,33 @@ public async Task<int> RunAsync()
var useFiddler = true;
var fiddlerPort = 8888; // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866

// Do you want to use JWT or OAuth?
var connectionMethod = ConnectionMethods.OAuth;
// -----------------------------------;------------------------------------------
// What test do you want to run?
var testType = TestType.ApiWithOAuth;
// -----------------------------------------------------------------------------

// Configure ZoomNet client
IConnectionInfo connectionInfo;
if (connectionMethod == ConnectionMethods.Jwt)
// Ensure the Console is tall enough and centered on the screen
if (OperatingSystem.IsWindows()) Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight);
ConsoleUtils.CenterConsole();

// Configure the proxy if desired (very useful for debugging)
var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null;

if (testType == TestType.ApiWithJwt)
{
var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User);
var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User);
connectionInfo = new JwtConnectionInfo(apiKey, apiSecret);
var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret);
var resultCode = await RunApiTestsAsync(connectionInfo, proxy).ConfigureAwait(false);
return resultCode;

}
else
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))
Expand Down Expand Up @@ -88,22 +99,37 @@ public async Task<int> RunAsync()
// },
// null);
}
}

var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null;
var client = new ZoomClient(connectionInfo, proxy, null, _loggerFactory.CreateLogger<ZoomClient>());
var resultCode = await RunApiTestsAsync(connectionInfo, proxy).ConfigureAwait(false);
return resultCode;
}
else if (testType == TestType.WebSockets)
{
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;
}
else
{
throw new Exception("Unknwon test type");
}
}

// Configure Console
var source = new CancellationTokenSource();
private async Task<int> RunApiTestsAsync(IConnectionInfo connectionInfo, IWebProxy proxy)
{
// Configure cancellation
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
source.Cancel();
cts.Cancel();
};

// Ensure the Console is tall enough and centered on the screen
if (OperatingSystem.IsWindows()) Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight);
ConsoleUtils.CenterConsole();
// Configure ZoomNet client
var client = new ZoomClient(connectionInfo, proxy, null, _loggerFactory.CreateLogger<ZoomClient>());

// These are the integration tests that we will execute
var integrationTests = new Type[]
Expand All @@ -121,8 +147,8 @@ public async Task<int> 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);
var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false);
var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false);
Array.Sort(myPermissions); // Sort permissions alphabetically for convenience

// Execute the async tests in parallel (with max degree of parallelism)
Expand All @@ -134,7 +160,7 @@ public async Task<int> RunAsync()
try
{
var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType);
await integrationTest.RunAsync(myUser, myPermissions, client, log, source.Token).ConfigureAwait(false);
await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false);
return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE);
}
catch (OperationCanceledException)
Expand Down Expand Up @@ -164,10 +190,10 @@ public async Task<int> RunAsync()
await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false);
await summary.WriteLineAsync("**************************************************").ConfigureAwait(false);

var nameMaxLength = Math.Min(results.Max(r => r.TestName.Length), TEST_NAME_MAX_LENGTH);
foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray())
{
var name = TestName.Length <= TEST_NAME_MAX_LENGTH ? TestName : TestName.Substring(0, TEST_NAME_MAX_LENGTH - 3) + "...";
await summary.WriteLineAsync($"{name.PadRight(TEST_NAME_MAX_LENGTH, ' ')} : {Message}").ConfigureAwait(false);
await summary.WriteLineAsync($"{TestName.ToExactLength(nameMaxLength)} : {Message}").ConfigureAwait(false);
}

await summary.WriteLineAsync("**************************************************").ConfigureAwait(false);
Expand All @@ -188,7 +214,43 @@ public async Task<int> RunAsync()
else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode;
}

return await Task.FromResult(resultCode);
return resultCode;
}

private async Task<int> RunWebSocketTestsAsync(string clientId, string clientSecret, string accountId, string subscriptionId, IWebProxy proxy)
{
// Change the minimum logging level so we can see the traces from ZoomWebSocketClient
var config = NLog.LogManager.Configuration;
config.FindRuleByName("ColoredConsoleRule").EnableLoggingForLevel(NLog.LogLevel.Trace);
NLog.LogManager.Configuration = config; // Apply new config

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);
}
});

// Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the websocket client)
var cts = new CancellationTokenSource();
var exitEvent = new ManualResetEvent(false);
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
exitEvent.Set();
};

// 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();
}

return (int)ResultCodes.Success;
}
}
}
10 changes: 5 additions & 5 deletions Source/ZoomNet.sln
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30011.22
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{466D9D6F-B72C-4239-8293-9F00CFCD7F53}"
ProjectSection(SolutionItems) = preProject
..\README.md = ..\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZoomNet", "ZoomNet\ZoomNet.csproj", "{DBEC5425-7E6D-49C7-BAA2-81971DA8ED8F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoomNet", "ZoomNet\ZoomNet.csproj", "{DBEC5425-7E6D-49C7-BAA2-81971DA8ED8F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZoomNet.UnitTests", "ZoomNet.UnitTests\ZoomNet.UnitTests.csproj", "{605D7C37-6C78-45DE-B19B-8EF172687DBE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoomNet.UnitTests", "ZoomNet.UnitTests\ZoomNet.UnitTests.csproj", "{605D7C37-6C78-45DE-B19B-8EF172687DBE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZoomNet.IntegrationTests", "ZoomNet.IntegrationTests\ZoomNet.IntegrationTests.csproj", "{270D6775-D07D-43A6-BDB5-845B6E2C6D0B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoomNet.IntegrationTests", "ZoomNet.IntegrationTests\ZoomNet.IntegrationTests.csproj", "{270D6775-D07D-43A6-BDB5-845B6E2C6D0B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
8 changes: 8 additions & 0 deletions Source/ZoomNet/Extensions/Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,14 @@ internal static string ToHexString(this byte[] bytes)
return result.ToString();
}

internal static string ToExactLength(this string source, int totalWidth, string postfix = "...", char paddingChar = ' ')
{
if (string.IsNullOrEmpty(source)) return new string(paddingChar, totalWidth);
if (source.Length <= totalWidth) return source.PadRight(totalWidth, paddingChar);
var result = $"{source.Substring(0, totalWidth - (postfix?.Length ?? 0))}{postfix ?? string.Empty}";
return result;
}

/// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
/// <typeparam name="T">The response model to deserialize into.</typeparam>
/// <param name="httpContent">The content.</param>
Expand Down
17 changes: 14 additions & 3 deletions Source/ZoomNet/Resources/CloudRecordings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -367,10 +368,20 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable<string> registran
/// </returns>
public async Task<Stream> DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default)
{
var tokenHandler = _client.Filters.OfType<ITokenHandler>().SingleOrDefault();
var requestUri = downloadUrl + (tokenHandler != null ? "?access_token=" + tokenHandler.Token : string.Empty);
using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl))
{
var tokenHandler = _client.Filters.OfType<ITokenHandler>().SingleOrDefault();
if (tokenHandler != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token);
}

var response = await _client.BaseClient.SendAsync(request).ConfigureAwait(false);

return await _client.BaseClient.GetStreamAsync(requestUri).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
}

private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable<string> registrantIds, string status, CancellationToken cancellationToken = default)
Expand Down
1 change: 1 addition & 0 deletions Source/ZoomNet/ZoomNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="System.Text.Json" Version="7.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Websocket.Client" Version="4.4.43" />
</ItemGroup>

<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">
Expand Down
Loading

0 comments on commit 9e83ecb

Please sign in to comment.