Skip to content

Commit

Permalink
Merge branch 'release/0.80.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Aug 7, 2024
2 parents a1a8a67 + e8d0ae4 commit 9ed8171
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 115 deletions.
27 changes: 1 addition & 26 deletions GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1 @@
mode: ContinuousDelivery
branches:
master:
regex: (master|main)
mode: ContinuousDelivery
tag:
increment: Patch
prevent-increment-of-merged-branch-version: true
track-merge-target: false
feature:
regex: feature(s)?[/-]
mode: ContinuousDeployment
develop:
regex: dev(elop)?(ment)?$
mode: ContinuousDeployment
tag: beta
hotfix:
regex: hotfix(es)?[/-]
mode: ContinuousDeployment
tag: hotfix
release:
regex: release(s)?[/-]
mode: ContinuousDeployment
tag: rc
ignore:
sha: []
workflow: GitFlow/v1 # https://github.com/GitTools/GitVersion/blob/main/docs/input/docs/reference/configuration.md#snippet-/docs/workflows/GitFlow/v1.yml
18 changes: 15 additions & 3 deletions Source/ZoomNet.IntegrationTests/Tests/CloudRecordings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,23 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie
var paginatedRecordings = await client.CloudRecordings.GetRecordingsForUserAsync(myUser.Id, false, null, null, 100, null, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"User {myUser.Id} has {paginatedRecordings.TotalRecords} recordings stored in the cloud").ConfigureAwait(false);

// GROUP THE RECORDINGS BY MEETING
var recordingFilesGroupedByMeeting = paginatedRecordings.Records
.SelectMany(record => record.RecordingFiles)
.GroupBy(recordingFile => recordingFile.MeetingId);

// DOWNLOAD THE FILES
foreach (var recordingFile in paginatedRecordings.Records.SelectMany(record => record.RecordingFiles))
foreach (var group in recordingFilesGroupedByMeeting)
{
var stream = await client.CloudRecordings.DownloadFileAsync(recordingFile, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"Downloaded {recordingFile.DownloadUrl}").ConfigureAwait(false);
const int ttl = 60 * 5; // 5 minutes
var recordingInfo = await client.CloudRecordings.GetRecordingInformationAsync(group.Key, ttl, cancellationToken).ConfigureAwait(false);

foreach (var recordingFile in group)
{
var stream = await client.CloudRecordings.DownloadFileAsync(recordingFile.DownloadUrl, recordingInfo.DownloadAccessToken, cancellationToken).ConfigureAwait(false);
await log.WriteLineAsync($"Downloaded {recordingFile.DownloadUrl}").ConfigureAwait(false);
}

}
}
}
Expand Down
36 changes: 35 additions & 1 deletion Source/ZoomNet.UnitTests/Resources/CloudRecordingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,41 @@ public async Task DownloadFileAsync_with_expired_token()
var recordings = new CloudRecordings(client);

// Act
var result = await recordings.DownloadFileAsync(downloadUrl, CancellationToken.None).ConfigureAwait(true);
var result = await recordings.DownloadFileAsync(downloadUrl, null, CancellationToken.None).ConfigureAwait(true);

// Assert
mockHttp.VerifyNoOutstandingExpectation();
mockHttp.VerifyNoOutstandingRequest();
result.ShouldNotBeNull();
}

/// <summary>
/// While researching <a href="https://github.com/Jericho/ZoomNet/issues/348">Issue 348</a>, it was discovered
/// that the OAuth session token took precendence over the alternate token specified when invoking DownloadFileAsync.
/// This unit test was used to demonstrate the problem and ultimately to demonstrate that it was fixed.
/// </summary>
[Fact]
public async Task DownloadFileAsync_with_alternate_token()
{
// Arrange
var downloadUrl = "http://dummywebsite.com/dummyfile.txt";

var mockHttp = new MockHttpMessageHandler();
mockHttp
.Expect(HttpMethod.Get, downloadUrl)
.With(request => request.Headers.Authorization?.Parameter == "alternate_download_token")
.Respond(HttpStatusCode.OK, new StringContent("This is the content of the file"));

var connectionInfo = OAuthConnectionInfo.ForServerToServer(
"MyClientId",
"MyClientSecret",
"MyAccountId",
accessToken: "Expired_token");

var client = new ZoomClient(connectionInfo, mockHttp.ToHttpClient(), null, null);

// Act
var result = await client.CloudRecordings.DownloadFileAsync(downloadUrl, "alternate_download_token", CancellationToken.None).ConfigureAwait(true);

// Assert
mockHttp.VerifyNoOutstandingExpectation();
Expand Down
81 changes: 77 additions & 4 deletions Source/ZoomNet/Extensions/Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
using ZoomNet.Json;
using ZoomNet.Models;
using ZoomNet.Utilities;
using static ZoomNet.Utilities.DiagnosticHandler;

namespace ZoomNet
{
Expand All @@ -37,6 +36,7 @@ internal enum UnixTimePrecision
}

private static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly int DEFAULT_DEGREE_OF_PARALLELISM = Environment.ProcessorCount > 1 ? Environment.ProcessorCount / 2 : 1;

/// <summary>
/// Converts a 'unix time', which is expressed as the number of seconds (or milliseconds) since
Expand Down Expand Up @@ -518,6 +518,10 @@ internal static T GetPropertyValue<T>(this JsonElement element, string[] names)
return element.GetPropertyValue<T>(names, default, true);
}

internal static Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, Task<TResult>> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);

internal static Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, int, Task<TResult>> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);

internal static async Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, Task<TResult>> action, int maxDegreeOfParalellism)
{
var allTasks = new List<Task<TResult>>();
Expand All @@ -543,6 +547,35 @@ internal static async Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<
return results;
}

internal static async Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, int, Task<TResult>> action, int maxDegreeOfParalellism)
{
var allTasks = new List<Task<TResult>>();
using var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism);
foreach (var (item, index) in items.Select((value, i) => (value, i)))
{
await throttler.WaitAsync();
allTasks.Add(
Task.Run(async () =>
{
try
{
return await action(item, index).ConfigureAwait(false);
}
finally
{
throttler.Release();
}
}));
}

var results = await Task.WhenAll(allTasks).ConfigureAwait(false);
return results;
}

internal static Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Task> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);

internal static Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, int, Task> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);

internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Task> action, int maxDegreeOfParalellism)
{
var allTasks = new List<Task>();
Expand All @@ -567,6 +600,30 @@ internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Ta
await Task.WhenAll(allTasks).ConfigureAwait(false);
}

internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, int, Task> action, int maxDegreeOfParalellism)
{
var allTasks = new List<Task>();
using var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism);
foreach (var (item, index) in items.Select((value, i) => (value, i)))
{
await throttler.WaitAsync();
allTasks.Add(
Task.Run(async () =>
{
try
{
await action(item, index).ConfigureAwait(false);
}
finally
{
throttler.Release();
}
}));
}

await Task.WhenAll(allTasks).ConfigureAwait(false);
}

/// <summary>
/// Gets the attribute of the specified type.
/// </summary>
Expand Down Expand Up @@ -644,8 +701,8 @@ internal static IEnumerable<KeyValuePair<string, string>> ParseQuerystring(this

internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
{
var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME);
DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
return diagnosticInfo;
}

Expand Down Expand Up @@ -889,6 +946,11 @@ internal static string ToExactLength(this string source, int totalWidth, string
return result;
}

internal static bool IsNullableType(this Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}

/// <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 Expand Up @@ -1102,8 +1164,10 @@ private static T GetPropertyValue<T>(this JsonElement element, string[] names, T
};
}

if (typeOfT.IsGenericType && typeOfT.GetGenericTypeDefinition() == typeof(Nullable<>))
if (typeOfT.IsNullableType())
{
if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;

var underlyingType = Nullable.GetUnderlyingType(typeOfT);
var getElementValue = typeof(Internal)
.GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
Expand All @@ -1114,6 +1178,8 @@ private static T GetPropertyValue<T>(this JsonElement element, string[] names, T

if (typeOfT.IsArray)
{
if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;

var elementType = typeOfT.GetElementType();
var getElementValue = typeof(Internal)
.GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
Expand All @@ -1136,6 +1202,13 @@ private static T GetElementValue<T>(this JsonElement element)
{
var typeOfT = typeof(T);

if (element.ValueKind == JsonValueKind.Null)
{
return typeOfT.IsNullableType()
? (T)default
: throw new Exception($"JSON contains a null value but {typeOfT.FullName} is not nullable");
}

return typeOfT switch
{
Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
Expand Down
5 changes: 3 additions & 2 deletions Source/ZoomNet/Extensions/Public.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,14 @@ public static Event VerifyAndParseEventWebhook(this IWebhookParser parser, strin
/// </summary>
/// <param name="cloudRecordingsResource">The cloud recordings resource.</param>
/// <param name="recordingFile">The recording file to download.</param>
/// <param name="accessToken">The access token. If this parameter is omitted, the token for the current oAuth session will be used.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="Stream"/> containing the file.
/// </returns>
public static Task<Stream> DownloadFileAsync(this ICloudRecordings cloudRecordingsResource, RecordingFile recordingFile, CancellationToken cancellationToken = default)
public static Task<Stream> DownloadFileAsync(this ICloudRecordings cloudRecordingsResource, RecordingFile recordingFile, string accessToken = null, CancellationToken cancellationToken = default)
{
return cloudRecordingsResource.DownloadFileAsync(recordingFile.DownloadUrl, cancellationToken);
return cloudRecordingsResource.DownloadFileAsync(recordingFile.DownloadUrl, accessToken, cancellationToken);
}

/// <summary>
Expand Down
8 changes: 8 additions & 0 deletions Source/ZoomNet/Models/Recording.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,13 @@ public class Recording
/// <summary>Gets or sets the type of recorded meeting or webinar.</summary>
[JsonPropertyName("type")]
public RecordingType Type { get; set; }

/// <summary>Gets or sets the cloud recording's passcode to be used in the URL. Directly splice this recording's passcode in play_url or share_url with ?pwd= to access and play.</summary>
[JsonPropertyName("recording_play_passcode")]
public string PlayPasscode { get; set; }

/// <summary>Gets or sets the token to download the meeting's recording.</summary>
[JsonPropertyName("download_access_token")]
public string DownloadAccessToken { get; set; }
}
}
Loading

0 comments on commit 9ed8171

Please sign in to comment.