Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NetCord.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<File Path="Tests/Directory.Build.targets" />
<Project Path="Tests/ColorTest/ColorTest.csproj" />
<Project Path="Tests/MentionTest/MentionTest.csproj" />
<Project Path="Tests/NetCord.Rest.Tests/NetCord.Rest.Tests.csproj" />
<Project Path="Tests/NetCord.Test.Hosting.AspNetCore/NetCord.Test.Hosting.AspNetCore.csproj" />
<Project Path="Tests/NetCord.Test.Hosting/NetCord.Test.Hosting.csproj" />
<Project Path="Tests/NetCord.Test.Sharded.Hosting/NetCord.Test.Sharded.Hosting.csproj" />
Expand Down
24 changes: 23 additions & 1 deletion NetCord/Rest/InviteProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace NetCord.Rest;

[GenerateMethodsForProperties]
public partial class InviteProperties
public partial class InviteProperties : IHttpSerializable
{
[JsonPropertyName("max_age")]
public int? MaxAge { get; set; }
Expand All @@ -25,4 +25,26 @@ public partial class InviteProperties

[JsonPropertyName("target_application_id")]
public ulong? TargetApplicationId { get; set; }

[JsonIgnore]
public InviteTargetUsersProperties? TargetUsers { get; set; }

[JsonPropertyName("role_ids")]
public IEnumerable<ulong>? RoleIds { get; set; }

HttpContent IHttpSerializable.Serialize() => Serialize();

internal HttpContent Serialize()
{
JsonContent<InviteProperties> inviteContent = new(this, Serialization.Default.InviteProperties);

if (TargetUsers is not { } targetUsers)
return inviteContent;

return new MultipartFormDataContent()
{
{ inviteContent, "payload_json" },
{ targetUsers.Serialize(), "target_users_file", "target_users_file" }
};
}
}
28 changes: 28 additions & 0 deletions NetCord/Rest/InviteTargetUsersJobStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using NetCord.Rest.JsonModels;

namespace NetCord.Rest;

public class InviteTargetUsersJobStatus(JsonInviteTargetUsersJobStatus jsonModel) : IJsonModel<JsonInviteTargetUsersJobStatus>
{
JsonInviteTargetUsersJobStatus IJsonModel<JsonInviteTargetUsersJobStatus>.JsonModel => jsonModel;

public InviteTargetUsersJobStatusCode Status => jsonModel.Status;

public int TotalUsers => jsonModel.TotalUsers;

public int ProcessedUsers => jsonModel.ProcessedUsers;

public DateTimeOffset CreatedAt => jsonModel.CreatedAt;

public DateTimeOffset? CompletedAt => jsonModel.CompletedAt;

public string? ErrorMessage => jsonModel.ErrorMessage;
}

public enum InviteTargetUsersJobStatusCode
{
Unspecified = 0,
Processing = 1,
Completed = 2,
Failed = 3,
}
131 changes: 131 additions & 0 deletions NetCord/Rest/InviteTargetUsersProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Buffers;
using System.Buffers.Text;

namespace NetCord.Rest;

[GenerateMethodsForProperties]
public partial class InviteTargetUsersProperties : IHttpSerializable
{
private readonly Stream _stream;

private InviteTargetUsersProperties(Stream stream)
{
_stream = stream;
}

public static InviteTargetUsersProperties FromStream(Stream stream) => new(stream);

public static InviteTargetUsersProperties FromEnumerable(IEnumerable<ulong> userIds) => new(new UserIdsStream(userIds));

HttpContent IHttpSerializable.Serialize() => Serialize();

internal HttpContent Serialize() => new StreamContent(_stream);

private sealed class UserIdsStream(IEnumerable<ulong> userIds) : Stream
{
private readonly IEnumerator<ulong> _enumerator = userIds.GetEnumerator();
private byte[] _buffer = ArrayPool<byte>.Shared.Rent(22);
private int _startPosition;
private int _endPosition;

public override bool CanRead => true;

public override bool CanSeek => false;

public override bool CanWrite => false;

public override long Length => throw new NotSupportedException();

public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }

public override void Flush() => throw new NotSupportedException();

public override int Read(byte[] buffer, int offset, int count) => Read(new Span<byte>(buffer, offset, count));

public override int Read(Span<byte> buffer)
{
int totalWritten = 0;

if (_startPosition != _endPosition)
{
var bufferedLength = _endPosition - _startPosition;

var length = Math.Min(bufferedLength, buffer.Length);

_buffer.AsSpan(_startPosition, length).CopyTo(buffer);
_startPosition += length;

if (length != bufferedLength)
return length;

totalWritten += length;
}

var newLine = "\r\n"u8;

while (_enumerator.MoveNext())
{
var userId = _enumerator.Current;
if (!Utf8Formatter.TryFormat(userId, buffer[totalWritten..], out var written))
{
_ = Utf8Formatter.TryFormat(userId, _buffer, out var writtenToBuffer);

_buffer.AsSpan(0, _startPosition = written = buffer.Length - totalWritten).CopyTo(buffer[totalWritten..]);

totalWritten += written;

newLine.CopyTo(_buffer.AsSpan(writtenToBuffer));
_endPosition = writtenToBuffer + newLine.Length;
break;
}

totalWritten += written;

if (!newLine.TryCopyTo(buffer[totalWritten..]))
{
var remaining = buffer.Length - totalWritten;
if (remaining > 0)
{
newLine[..remaining].CopyTo(buffer[totalWritten..]);
totalWritten += remaining;
}

newLine[remaining..].CopyTo(_buffer);
_startPosition = 0;
_endPosition = newLine.Length - remaining;

break;
}

totalWritten += newLine.Length;
}

return totalWritten;
}

public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
}

public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
return new(Read(buffer.Span));
}

public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

protected override void Dispose(bool disposing)
{
if (disposing)
{
_enumerator.Dispose();
if (Interlocked.Exchange(ref _buffer, null!) is { } buffer)
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}

24 changes: 24 additions & 0 deletions NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace NetCord.Rest.JsonModels;

public class JsonInviteTargetUsersJobStatus
{
[JsonPropertyName("status")]
public InviteTargetUsersJobStatusCode Status { get; set; }

[JsonPropertyName("total_users")]
public int TotalUsers { get; set; }

[JsonPropertyName("processed_users")]
public int ProcessedUsers { get; set; }

[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }

[JsonPropertyName("completed_at")]
public DateTimeOffset? CompletedAt { get; set; }

[JsonPropertyName("error_message")]
public string? ErrorMessage { get; set; }
}
5 changes: 4 additions & 1 deletion NetCord/Rest/RestClient.Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,10 @@ public async Task<IEnumerable<RestInvite>> GetGuildChannelInvitesAsync(ulong cha
[GenerateAlias([typeof(IGuildChannel)], nameof(IGuildChannel.Id))]
public async Task<RestInvite> CreateGuildChannelInviteAsync(ulong channelId, InviteProperties? inviteProperties = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
{
using (HttpContent content = new JsonContent<InviteProperties?>(inviteProperties, Serialization.Default.InviteProperties))
if (inviteProperties is null)
return new(await (await SendRequestAsync(HttpMethod.Post, $"/channels/{channelId}/invites", null, new(channelId), properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this);

using (HttpContent content = inviteProperties.Serialize())
return new(await (await SendRequestAsync(HttpMethod.Post, content, $"/channels/{channelId}/invites", null, new(channelId), properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this);
}

Expand Down
94 changes: 94 additions & 0 deletions NetCord/Rest/RestClient.Invite.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;

using NetCord.Gateway;

namespace NetCord.Rest;
Expand All @@ -16,4 +19,95 @@ public async Task<RestInvite> GetGuildInviteAsync(string inviteCode, bool withCo
[GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))]
public async Task<RestInvite> DeleteGuildInviteAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
=> new(await (await SendRequestAsync(HttpMethod.Delete, $"/invites/{inviteCode}", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this);

[GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))]
public async IAsyncEnumerable<ulong> GetInviteTargetUsersAsync(string inviteCode, RestRequestProperties? properties = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var stream = await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}/target-users", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false);

var array = ArrayPool<byte>.Shared.Rent(4096);
var buffer = array.AsMemory();

try
{
int processingIndex;
int processingEndIndex;

// Skip header
while (true)
{
int bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);

if (bytesRead is 0)
yield break;

var span = buffer.Span[..bytesRead];

processingIndex = span.IndexOf((byte)'\n');

if (processingIndex >= 0)
{
processingEndIndex = bytesRead;
break;
}
}

processingIndex++;

while (true)
{
while (processingIndex < processingEndIndex)
{
var span = buffer.Span[processingIndex..processingEndIndex];

int end = span.IndexOf((byte)'\n');

if (end < 0)
break;

var line = span[..end];

if (line.EndsWith((byte)'\r'))
line = line[..^1];

yield return Snowflake.Parse(line);

processingIndex += end + 1;
}

buffer.Span[processingIndex..processingEndIndex].CopyTo(buffer.Span);

int bytesRead = await stream.ReadAsync(buffer[(processingEndIndex - processingIndex)..], cancellationToken).ConfigureAwait(false);

if (bytesRead is 0)
{
var span = buffer.Span[..(processingEndIndex - processingIndex)];
if (!span.IsEmpty)
yield return Snowflake.Parse(span);

break;
}

processingEndIndex = bytesRead + processingEndIndex - processingIndex;
processingIndex = 0;
}
}
finally
{
ArrayPool<byte>.Shared.Return(array);
await stream.DisposeAsync().ConfigureAwait(false);
}
}

public async Task UpdateInviteTargetUsersAsync(string inviteCode, InviteTargetUsersProperties inviteTargetUsersProperties, RestRequestProperties? requestProperties = null, CancellationToken cancellationToken = default)
{
using (HttpContent content = new MultipartFormDataContent()
{
{ inviteTargetUsersProperties.Serialize(), "target_users_file", "target_users_file" }
})
await SendRequestAsync(HttpMethod.Put, content, $"/invites/{inviteCode}/target-users", null, null, requestProperties, cancellationToken: cancellationToken).ConfigureAwait(false);
}

public async Task<InviteTargetUsersJobStatus> GetInviteTargetUsersJobStatusAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
=> new(await (await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}/target-users/job-status", null, null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonInviteTargetUsersJobStatus).ConfigureAwait(false));
}
1 change: 1 addition & 0 deletions NetCord/Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,5 @@ namespace NetCord;
[JsonSerializable(typeof(JsonRadioGroupComponent))]
[JsonSerializable(typeof(JsonCheckboxGroupComponent))]
[JsonSerializable(typeof(JsonCheckboxComponent))]
[JsonSerializable(typeof(JsonInviteTargetUsersJobStatus))]
internal partial class Serialization : JsonSerializerContext;
1 change: 1 addition & 0 deletions Tests/NetCord.Rest.Tests/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
Loading