diff --git a/NetCord.slnx b/NetCord.slnx
index c3ba02e9..23417afd 100644
--- a/NetCord.slnx
+++ b/NetCord.slnx
@@ -84,6 +84,7 @@
+
diff --git a/NetCord/Gateway/Invite.cs b/NetCord/Gateway/Invite.cs
index 2a15b626..434e951c 100644
--- a/NetCord/Gateway/Invite.cs
+++ b/NetCord/Gateway/Invite.cs
@@ -50,6 +50,10 @@ public Invite(JsonModels.JsonInvite jsonModel, RestClient client)
public int Uses => _jsonModel.Uses;
+ public DateTimeOffset? ExpiresAt => _jsonModel.ExpiresAt;
+
+ public IReadOnlyList? RoleIds => _jsonModel.RoleIds;
+
ulong? IInvite.ChannelId => ChannelId;
int? IInvite.MaxAge => MaxAge;
diff --git a/NetCord/Gateway/JsonModels/JsonInvite.cs b/NetCord/Gateway/JsonModels/JsonInvite.cs
index 6618f96c..16078e9b 100644
--- a/NetCord/Gateway/JsonModels/JsonInvite.cs
+++ b/NetCord/Gateway/JsonModels/JsonInvite.cs
@@ -44,4 +44,10 @@ public class JsonInvite
[JsonPropertyName("uses")]
public int Uses { get; set; }
+
+ [JsonPropertyName("expires_at")]
+ public DateTimeOffset? ExpiresAt { get; set; }
+
+ [JsonPropertyName("role_ids")]
+ public ulong[]? RoleIds { get; set; }
}
diff --git a/NetCord/IInvite.cs b/NetCord/IInvite.cs
index f4503371..73126e05 100644
--- a/NetCord/IInvite.cs
+++ b/NetCord/IInvite.cs
@@ -15,4 +15,5 @@ public interface IInvite
public bool? Temporary { get; }
public int? Uses { get; }
public DateTimeOffset? CreatedAt { get; }
+ public DateTimeOffset? ExpiresAt { get; }
}
diff --git a/NetCord/InviteFlags.cs b/NetCord/InviteFlags.cs
new file mode 100644
index 00000000..7daefb83
--- /dev/null
+++ b/NetCord/InviteFlags.cs
@@ -0,0 +1,7 @@
+namespace NetCord;
+
+[Flags]
+public enum InviteFlags
+{
+ IsGuestInvite = 1 << 0,
+}
diff --git a/NetCord/Rest/InviteProperties.cs b/NetCord/Rest/InviteProperties.cs
index 96d5d5bb..801da1f7 100644
--- a/NetCord/Rest/InviteProperties.cs
+++ b/NetCord/Rest/InviteProperties.cs
@@ -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; }
@@ -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? RoleIds { get; set; }
+
+ HttpContent IHttpSerializable.Serialize() => Serialize();
+
+ internal HttpContent Serialize()
+ {
+ JsonContent 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" }
+ };
+ }
}
diff --git a/NetCord/Rest/InviteTargetUsersJobStatus.cs b/NetCord/Rest/InviteTargetUsersJobStatus.cs
new file mode 100644
index 00000000..a6486bf0
--- /dev/null
+++ b/NetCord/Rest/InviteTargetUsersJobStatus.cs
@@ -0,0 +1,28 @@
+using NetCord.Rest.JsonModels;
+
+namespace NetCord.Rest;
+
+public class InviteTargetUsersJobStatus(JsonInviteTargetUsersJobStatus jsonModel) : IJsonModel
+{
+ JsonInviteTargetUsersJobStatus IJsonModel.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,
+}
diff --git a/NetCord/Rest/InviteTargetUsersProperties.cs b/NetCord/Rest/InviteTargetUsersProperties.cs
new file mode 100644
index 00000000..ab632bcc
--- /dev/null
+++ b/NetCord/Rest/InviteTargetUsersProperties.cs
@@ -0,0 +1,140 @@
+using System.Buffers.Text;
+using System.Diagnostics.CodeAnalysis;
+
+namespace NetCord.Rest;
+
+[GenerateMethodsForProperties]
+public partial class InviteTargetUsersProperties : IHttpSerializable
+{
+ private readonly Stream _stream;
+ private byte _read;
+
+ private InviteTargetUsersProperties(Stream stream)
+ {
+ _stream = stream;
+ }
+
+ public static InviteTargetUsersProperties FromStream(Stream stream) => new(stream);
+
+ public static InviteTargetUsersProperties FromEnumerable(IEnumerable userIds) => new(new UserIdsStream(userIds));
+
+ HttpContent IHttpSerializable.Serialize() => Serialize();
+
+ internal HttpContent Serialize()
+ {
+ if (Interlocked.Exchange(ref _read, 1) is 1)
+ ThrowAlreadySent();
+
+ return new StreamContent(_stream);
+ }
+
+ [DoesNotReturn]
+ private static void ThrowAlreadySent()
+ {
+ throw new InvalidOperationException("The invite target users have already been sent.");
+ }
+
+ private sealed class UserIdsStream(IEnumerable userIds) : Stream
+ {
+ private readonly IEnumerator _enumerator = userIds.GetEnumerator();
+ private readonly byte[] _buffer = new byte[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(buffer, offset, count));
+
+ public override int Read(Span 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 ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask();
+ }
+
+ public override ValueTask ReadAsync(Memory 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();
+ }
+ }
+}
+
diff --git a/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs b/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs
new file mode 100644
index 00000000..0846b453
--- /dev/null
+++ b/NetCord/Rest/JsonModels/JsonInviteTargetUsersJobStatus.cs
@@ -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; }
+}
diff --git a/NetCord/Rest/JsonModels/JsonRestInvite.cs b/NetCord/Rest/JsonModels/JsonRestInvite.cs
index 7133faaa..fd007431 100644
--- a/NetCord/Rest/JsonModels/JsonRestInvite.cs
+++ b/NetCord/Rest/JsonModels/JsonRestInvite.cs
@@ -39,12 +39,15 @@ public class JsonRestInvite
[JsonPropertyName("expires_at")]
public DateTimeOffset? ExpiresAt { get; set; }
- [JsonPropertyName("stage_instance")]
- public JsonStageInstance? StageInstance { get; set; }
-
[JsonPropertyName("guild_scheduled_event")]
public JsonGuildScheduledEvent? GuildScheduledEvent { get; set; }
+ [JsonPropertyName("flags")]
+ public InviteFlags? Flags { get; set; }
+
+ [JsonPropertyName("roles")]
+ public JsonRole[]? Roles { get; set; }
+
[JsonPropertyName("uses")]
public int? Uses { get; set; }
diff --git a/NetCord/Rest/RestClient.Channel.cs b/NetCord/Rest/RestClient.Channel.cs
index 6b018d5f..ed9f1acc 100644
--- a/NetCord/Rest/RestClient.Channel.cs
+++ b/NetCord/Rest/RestClient.Channel.cs
@@ -436,7 +436,10 @@ public async Task> GetGuildChannelInvitesAsync(ulong cha
[GenerateAlias([typeof(IGuildChannel)], nameof(IGuildChannel.Id))]
public async Task CreateGuildChannelInviteAsync(ulong channelId, InviteProperties? inviteProperties = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
{
- using (HttpContent content = new JsonContent(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);
}
diff --git a/NetCord/Rest/RestClient.Invite.cs b/NetCord/Rest/RestClient.Invite.cs
index 933c7cf3..c9c2c421 100644
--- a/NetCord/Rest/RestClient.Invite.cs
+++ b/NetCord/Rest/RestClient.Invite.cs
@@ -1,3 +1,6 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
using NetCord.Gateway;
namespace NetCord.Rest;
@@ -5,7 +8,7 @@ namespace NetCord.Rest;
public partial class RestClient
{
[GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))]
- public async Task GetGuildInviteAsync(string inviteCode, bool withCounts = false, bool withExpiration = false, ulong? guildScheduledEventId = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
+ public async Task GetInviteAsync(string inviteCode, bool withCounts = false, bool withExpiration = false, ulong? guildScheduledEventId = null, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
{
if (guildScheduledEventId.HasValue)
return new(await (await SendRequestAsync(HttpMethod.Get, $"/invites/{inviteCode}", $"?with_counts={withCounts}&with_expiration={withExpiration}&guild_scheduled_event_id={guildScheduledEventId}", null, properties, cancellationToken: cancellationToken).ConfigureAwait(false)).ToObjectAsync(Serialization.Default.JsonRestInvite).ConfigureAwait(false), this);
@@ -14,6 +17,97 @@ public async Task GetGuildInviteAsync(string inviteCode, bool withCo
}
[GenerateAlias([typeof(RestInvite)], nameof(RestInvite.Code), TypeNameOverride = nameof(Invite))]
- public async Task DeleteGuildInviteAsync(string inviteCode, RestRequestProperties? properties = null, CancellationToken cancellationToken = default)
+ public async Task DeleteInviteAsync(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 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.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.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 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));
}
diff --git a/NetCord/Rest/RestGuildInvite.cs b/NetCord/Rest/RestInvite.cs
similarity index 71%
rename from NetCord/Rest/RestGuildInvite.cs
rename to NetCord/Rest/RestInvite.cs
index e88ccb22..18be1a36 100644
--- a/NetCord/Rest/RestGuildInvite.cs
+++ b/NetCord/Rest/RestInvite.cs
@@ -29,10 +29,13 @@ public partial class RestInvite : IInvite, IJsonModel
public DateTimeOffset? ExpiresAt => _jsonModel.ExpiresAt;
- public StageInstance? StageInstance { get; }
-
public GuildScheduledEvent? GuildScheduledEvent { get; }
+ public InviteFlags? Flags => _jsonModel.Flags;
+
+ public IReadOnlyList? Roles { get; }
+
+ // Metadata
public int? Uses => _jsonModel.Uses;
public int? MaxUses => _jsonModel.MaxUses;
@@ -51,32 +54,29 @@ public RestInvite(JsonModels.JsonRestInvite jsonModel, RestClient client)
{
_jsonModel = jsonModel;
- var guild = jsonModel.Guild;
- if (guild is not null)
+ if (jsonModel.Guild is { } guild)
+ {
Guild = new(guild, client);
- var channel = jsonModel.Channel;
- if (channel is not null)
+ var guildId = Guild.Id;
+
+ if (jsonModel.Roles is { } roles)
+ Roles = roles.Select(role => new Role(role, guildId, client)).ToArray();
+ }
+
+ if (jsonModel.Channel is { } channel)
Channel = Channel.CreateFromJson(channel, client);
- var inviter = jsonModel.Inviter;
- if (inviter is not null)
+ if (jsonModel.Inviter is { } inviter)
Inviter = new(inviter, client);
- var targetUser = jsonModel.TargetUser;
- if (targetUser is not null)
+ if (jsonModel.TargetUser is { } targetUser)
TargetUser = new(targetUser, client);
- var targetApplication = jsonModel.TargetApplication;
- if (targetApplication is not null)
+ if (jsonModel.TargetApplication is { } targetApplication)
TargetApplication = new(targetApplication, client);
- var stageInstance = jsonModel.StageInstance;
- if (stageInstance is not null)
- StageInstance = new(stageInstance, client);
-
- var guildScheduledEvent = jsonModel.GuildScheduledEvent;
- if (guildScheduledEvent is not null)
+ if (jsonModel.GuildScheduledEvent is { } guildScheduledEvent)
GuildScheduledEvent = new(guildScheduledEvent, client);
_client = client;
diff --git a/NetCord/Serialization.cs b/NetCord/Serialization.cs
index 2b07ebb6..4bfdb709 100644
--- a/NetCord/Serialization.cs
+++ b/NetCord/Serialization.cs
@@ -284,4 +284,5 @@ namespace NetCord;
[JsonSerializable(typeof(JsonRadioGroupComponent))]
[JsonSerializable(typeof(JsonCheckboxGroupComponent))]
[JsonSerializable(typeof(JsonCheckboxComponent))]
+[JsonSerializable(typeof(JsonInviteTargetUsersJobStatus))]
internal partial class Serialization : JsonSerializerContext;
diff --git a/Tests/NetCord.Rest.Tests/AssemblyInfo.cs b/Tests/NetCord.Rest.Tests/AssemblyInfo.cs
new file mode 100644
index 00000000..300f5b1a
--- /dev/null
+++ b/Tests/NetCord.Rest.Tests/AssemblyInfo.cs
@@ -0,0 +1 @@
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
diff --git a/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs b/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs
new file mode 100644
index 00000000..8a12bfc5
--- /dev/null
+++ b/Tests/NetCord.Rest.Tests/GetInviteTargetUsersTest.cs
@@ -0,0 +1,95 @@
+using System.Net;
+using System.Text;
+
+namespace NetCord.Rest.Tests;
+
+[TestClass]
+public class GetInviteTargetUsersTest(TestContext context)
+{
+ private static object[] CreateData(IEnumerable userIds, string newLine, bool finalNewLine)
+ {
+ MemoryStream stream = new();
+ using StreamWriter writer = new(stream, Encoding.UTF8, leaveOpen: true);
+
+ writer.Write("user_id");
+
+ foreach (var userId in userIds)
+ {
+ writer.Write(newLine);
+ writer.Write(userId);
+ }
+
+ if (finalNewLine)
+ writer.Write(newLine);
+
+ writer.Flush();
+
+ stream.Position = 0;
+ return [userIds, stream];
+ }
+
+ public static IEnumerable