diff --git a/src/Discord.Net.Core/Discord.Net.Core.xml b/src/Discord.Net.Core/Discord.Net.Core.xml
index 9e288d1a1..3c0e6afa2 100644
--- a/src/Discord.Net.Core/Discord.Net.Core.xml
+++ b/src/Discord.Net.Core/Discord.Net.Core.xml
@@ -1982,6 +1982,41 @@
of webhooks that is available in this channel.
+
+
+ Represents a thread channel inside of a guild.
+
+
+
+
+ if the current thread is archived, otherwise .
+
+
+
+
+ Duration to automatically archive the thread after recent activity.
+
+
+
+
+ Timestamp when the thread's archive status was last changed, used for calculating recent activity.
+
+
+
+
+ if the current thread is locked, otherwise
+
+
+
+
+ An approximate count of users in a thread, stops counting at 50.
+
+
+
+
+ An approximate count of messages in a thread, stops counting at 50.
+
+
Represents a generic voice channel in a guild.
@@ -2081,6 +2116,71 @@
Thrown if the value does not fall within [0, 21600].
+
+
+ Represents the thread auto archive duration.
+
+
+
+
+ One hour (60 minutes).
+
+
+
+
+ One day (1440 minutes).
+
+
+
+
+ Three days (4320 minutes).
+
+ This option is explicity avaliable to nitro users.
+
+
+
+
+
+ One week (10080 minutes).
+
+ This option is explicity avaliable to nitro users.
+
+
+
+
+
+ Gets or sets whether or not the thread is archived.
+
+
+
+
+ Gets or sets whether or not the thread is locked.
+
+
+
+
+ Gets or sets the name of the thread.
+
+
+
+
+ Gets or sets the auto archive duration.
+
+
+
+
+ Gets or sets the slow-mode ratelimit in seconds for this channel.
+
+
+ Setting this value to anything above zero will require each user to wait X seconds before
+ sending another message; setting this value to 0 will disable slow-mode for this channel.
+
+ Users with or
+ will be exempt from slow-mode.
+
+
+ Thrown if the value does not fall within [0, 21600].
+
Provides properties that are used to modify an with the specified changes.
@@ -7029,6 +7129,21 @@
Flag given to messages that came from the urgent message system.
+
+
+ Flag given to messages has an associated thread, with the same id as the message
+
+
+
+
+ Flag given to messages that is only visible to the user who invoked the Interaction.
+
+
+
+
+ Flag given to messages that is an Interaction Response and the bot is "thinking"
+
+
Properties that are used to modify an with the specified changes.
diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
new file mode 100644
index 000000000..1459c954c
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents a thread channel inside of a guild.
+ ///
+ public interface IThreadChannel : ITextChannel, IGuildChannel
+ {
+ ///
+ /// if the current thread is archived, otherwise .
+ ///
+ bool Archived { get; }
+
+ ///
+ /// Duration to automatically archive the thread after recent activity.
+ ///
+ ThreadArchiveDuration AutoArchiveDuration { get; }
+
+ ///
+ /// Timestamp when the thread's archive status was last changed, used for calculating recent activity.
+ ///
+ DateTimeOffset ArchiveTimestamp { get; }
+
+ ///
+ /// if the current thread is locked, otherwise
+ ///
+ bool Locked { get; }
+
+ ///
+ /// An approximate count of users in a thread, stops counting at 50.
+ ///
+ int MemberCount { get; }
+
+ ///
+ /// An approximate count of messages in a thread, stops counting at 50.
+ ///
+ int MessageCount { get; }
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs
new file mode 100644
index 000000000..01d1574bf
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents the thread auto archive duration.
+ ///
+ public enum ThreadArchiveDuration
+ {
+ ///
+ /// One hour (60 minutes).
+ ///
+ OneHour = 60,
+
+ ///
+ /// One day (1440 minutes).
+ ///
+ OneDay = 1440,
+
+ ///
+ /// Three days (4320 minutes).
+ ///
+ /// This option is explicity avaliable to nitro users.
+ ///
+ ///
+ ThreeDays = 4320,
+
+ ///
+ /// One week (10080 minutes).
+ ///
+ /// This option is explicity avaliable to nitro users.
+ ///
+ ///
+ OneWeek = 10080,
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs
new file mode 100644
index 000000000..10a0d7654
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.Entities
+{
+ public class ThreadChannelProperties
+ {
+ ///
+ /// Gets or sets whether or not the thread is archived.
+ ///
+ public Optional Archived { get; set; }
+
+ ///
+ /// Gets or sets whether or not the thread is locked.
+ ///
+ public Optional Locked { get; set; }
+
+ ///
+ /// Gets or sets the name of the thread.
+ ///
+ public Optional Name { get; set; }
+
+ ///
+ /// Gets or sets the auto archive duration.
+ ///
+ public Optional AutoArchiveDuration { get; set; }
+
+ ///
+ /// Gets or sets the slow-mode ratelimit in seconds for this channel.
+ ///
+ ///
+ /// Setting this value to anything above zero will require each user to wait X seconds before
+ /// sending another message; setting this value to 0 will disable slow-mode for this channel.
+ ///
+ /// Users with or
+ /// will be exempt from slow-mode.
+ ///
+ ///
+ /// Thrown if the value does not fall within [0, 21600].
+ public Optional SlowModeInterval { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs
index 52d0f0e9e..6f9450372 100644
--- a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs
+++ b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs
@@ -32,5 +32,17 @@ namespace Discord
/// Flag given to messages that came from the urgent message system.
///
Urgent = 1 << 4,
+ ///
+ /// Flag given to messages has an associated thread, with the same id as the message
+ ///
+ HasThread = 1 << 5,
+ ///
+ /// Flag given to messages that is only visible to the user who invoked the Interaction.
+ ///
+ Ephemeral = 1 << 6,
+ ///
+ /// Flag given to messages that is an Interaction Response and the bot is "thinking"
+ ///
+ Loading = 1 << 7
}
}
diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs
index 57a5ce9ab..2dd004021 100644
--- a/src/Discord.Net.Rest/API/Common/Channel.cs
+++ b/src/Discord.Net.Rest/API/Common/Channel.cs
@@ -49,5 +49,21 @@ namespace Discord.API
//GroupChannel
[JsonProperty("icon")]
public Optional Icon { get; set; }
+
+ //ThreadChannel
+ [JsonProperty("member")]
+ public Optional ThreadMember { get; set; }
+
+ [JsonProperty("thread_metadata")]
+ public Optional ThreadMetadata { get; set; }
+
+ [JsonProperty("owner_id")]
+ public Optional OwnerId { get; set; }
+
+ [JsonProperty("message_count")]
+ public Optional MessageCount { get; set; }
+
+ [JsonProperty("member_count")]
+ public Optional MemberCount { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs
new file mode 100644
index 000000000..eccac8e54
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.API.Rest
+{
+ internal class ChannelThreads
+ {
+ [JsonProperty("threads")]
+ public Channel[] Threads { get; set; }
+
+ [JsonProperty("members")]
+ public ThreadMember[] Members { get; set; }
+
+ [JsonProperty("has_more")]
+ public bool HasMore { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs
index bd25c7e1a..ba09f9b8a 100644
--- a/src/Discord.Net.Rest/API/Common/Guild.cs
+++ b/src/Discord.Net.Rest/API/Common/Guild.cs
@@ -76,5 +76,7 @@ namespace Discord.API
public Optional ApproximateMemberCount { get; set; }
[JsonProperty("approximate_presence_count")]
public Optional ApproximatePresenceCount { get; set; }
+ [JsonProperty("threads")]
+ public Optional Threads { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs
new file mode 100644
index 000000000..b4c542454
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs
@@ -0,0 +1,24 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.API
+{
+ internal class ThreadMember
+ {
+ [JsonProperty("id")]
+ public Optional Id { get; set; }
+
+ [JsonProperty("user_id")]
+ public Optional UserId { get; set; }
+
+ [JsonProperty("join_timestamp")]
+ public DateTimeOffset JoinTimestamp { get; set; }
+
+ [JsonProperty("flags")]
+ public int Flags { get; set; } // No enum type (yet?)
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs
new file mode 100644
index 000000000..f6da249f7
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs
@@ -0,0 +1,24 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.API
+{
+ internal class ThreadMetadata
+ {
+ [JsonProperty("archived")]
+ public bool Archived { get; set; }
+
+ [JsonProperty("auto_archive_duration")]
+ public int AutoArchiveDuration { get; set; }
+
+ [JsonProperty("archive_timestamp")]
+ public DateTimeOffset ArchiveTimestamp { get; set; }
+
+ [JsonProperty("locked")]
+ public Optional Locked { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs
index b8c7a124f..b2800a066 100644
--- a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs
+++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs
@@ -20,5 +20,8 @@ namespace Discord.API.Rest
[JsonProperty("components")]
public Optional Components { get; set; }
+
+ [JsonProperty("flags")]
+ public Optional Flags { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs
new file mode 100644
index 000000000..167d94099
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.API.Rest
+{
+ internal class StartThreadParams
+ {
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("duration")]
+ public ThreadArchiveDuration Duration { get; set; }
+
+ [JsonProperty("type")]
+ public Optional Type { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs
index 36259b01a..a56b3d7f6 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -413,6 +413,160 @@ namespace Discord.API
break;
}
}
+
+ // Threads
+ public async Task StartThreadAsync(ulong channelId, ulong messageId, StartThreadParams args, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+ Preconditions.NotEqual(messageId, 0, nameof(messageId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(0, channelId);
+
+ return await SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/threads", args, bucket, options: options).ConfigureAwait(false);
+ }
+
+ public async Task StartThreadAsync(ulong channelId, StartThreadParams args, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(0, channelId);
+
+ return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options).ConfigureAwait(false);
+ }
+
+ public async Task JoinThreadAsync(ulong channelId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ await SendAsync("PUT", $"channels/{channelId}/thread-members/@me", options: options).ConfigureAwait(false);
+ }
+
+ public async Task AddThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+ Preconditions.NotEqual(userId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ await SendAsync("PUT", $"channels/{channelId}/thread-members/{userId}", options: options).ConfigureAwait(false);
+ }
+
+ public async Task LeaveThreadAsync(ulong channelId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ await SendAsync("DELETE", $"channels/{channelId}/thread-members/@me", options: options).ConfigureAwait(false);
+ }
+
+ public async Task RemoveThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+ Preconditions.NotEqual(userId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ await SendAsync("DELETE", $"channels/{channelId}/thread-members/{userId}", options: options).ConfigureAwait(false);
+ }
+
+ public async Task ListThreadMembersAsync(ulong channelId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(channelId: channelId);
+
+ return await SendAsync("GET", () => $"/channels/{channelId}", bucket, options: options);
+ }
+
+ public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(channelId: channelId);
+
+ return await SendAsync("GET", $"channels/{channelId}/threads/active");
+ }
+
+ public async Task GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(channelId: channelId);
+
+ string query = "";
+
+ if (limit.HasValue)
+ {
+ query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}";
+ }
+ else if (before.HasValue)
+ {
+ query = $"?before={before.Value.ToString("O")}";
+ }
+
+ return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/public{query}", bucket, options: options);
+ }
+
+ public async Task GetPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null,
+ RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(channelId: channelId);
+
+ string query = "";
+
+ if (limit.HasValue)
+ {
+ query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}";
+ }
+ else if (before.HasValue)
+ {
+ query = $"?before={before.Value.ToString("O")}";
+ }
+
+ return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/private{query}", bucket, options: options);
+ }
+
+ public async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null,
+ RequestOptions options = null)
+ {
+ Preconditions.NotEqual(channelId, 0, nameof(channelId));
+
+ options = RequestOptions.CreateOrClone(options);
+
+ var bucket = new BucketIds(channelId: channelId);
+
+ string query = "";
+
+ if (limit.HasValue)
+ {
+ query = $"?before={SnowflakeUtils.ToSnowflake(before.GetValueOrDefault(DateTimeOffset.UtcNow))}&limit={limit.Value}";
+ }
+ else if (before.HasValue)
+ {
+ query = $"?before={before.Value.ToString("O")}";
+ }
+
+ return await SendAsync("GET", () => $"channels/{channelId}/users/@me/threads/archived/private{query}", bucket, options: options);
+ }
+
+ // roles
public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
index ec32623c7..f030f91ca 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
@@ -333,6 +333,7 @@ namespace Discord.Rest
Embeds = args.Embed.IsSpecified ? new API.Embed[] { args.Embed.Value.ToModel() } : Optional.Create(),
AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Unspecified,
Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified,
+ Flags = args.Flags
};
return await client.ApiClient.ModifyInteractionResponse(apiArgs, message.Token, options).ConfigureAwait(false);
diff --git a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
index 910f6d909..da02b737a 100644
--- a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
+++ b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using Newtonsoft.Json;
using System;
@@ -8,18 +8,26 @@ namespace Discord.API.Gateway
{
[JsonProperty("unavailable")]
public bool? Unavailable { get; set; }
+
[JsonProperty("member_count")]
public int MemberCount { get; set; }
+
[JsonProperty("large")]
public bool Large { get; set; }
[JsonProperty("presences")]
public Presence[] Presences { get; set; }
+
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
+
[JsonProperty("channels")]
public Channel[] Channels { get; set; }
+
[JsonProperty("joined_at")]
public DateTimeOffset JoinedAt { get; set; }
+
+ [JsonProperty("threads")]
+ public Channel[] Threads { get; set; }
}
}
diff --git a/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs
new file mode 100644
index 000000000..ab7989268
--- /dev/null
+++ b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs
@@ -0,0 +1,24 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.API.Gateway
+{
+ internal class ThreadListSyncEvent
+ {
+ [JsonProperty("guild_id")]
+ public ulong GuildId { get; set; }
+
+ [JsonProperty("channel_ids")]
+ public Optional ChannelIds { get; set; }
+
+ [JsonProperty("threads")]
+ public Channel[] Threads { get; set; }
+
+ [JsonProperty("members")]
+ public ThreadMember[] Members { get; set; }
+ }
+}
diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
index 3b6b3ec5a..f29f13cf6 100644
--- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
+++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
@@ -540,5 +540,37 @@ namespace Discord.WebSocket
remove { _applicationCommandDeleted.Remove(value); }
}
internal readonly AsyncEvent> _applicationCommandDeleted = new AsyncEvent>();
+
+ ///
+ /// Fired when a thread is created within a guild.
+ ///
+ public event Func ThreadCreated
+ {
+ add { _threadCreated.Add(value); }
+ remove { _threadCreated.Remove(value); }
+ }
+ internal readonly AsyncEvent> _threadCreated = new AsyncEvent>();
+
+ ///
+ /// Fired when a thread is updated within a guild.
+ ///
+ public event Func ThreadUpdated
+ {
+ add { _threadUpdated.Add(value); }
+ remove { _threadUpdated.Remove(value); }
+ }
+
+ internal readonly AsyncEvent> _threadUpdated = new AsyncEvent>();
+
+ ///
+ /// Fired when a thread is deleted.
+ ///
+ public event Func, Task> ThreadDeleted
+ {
+ add { _threadDeleted.Add(value); }
+ remove { _threadDeleted.Remove(value); }
+ }
+ internal readonly AsyncEvent, Task>> _threadDeleted = new AsyncEvent, Task>>();
+
}
}
diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
index 56932f7d8..3f1f63db3 100644
--- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
+++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
@@ -8,7 +8,7 @@
net461;netstandard2.0;netstandard2.1
netstandard2.0;netstandard2.1
true
- 2.4.8
+ 2.4.9
https://github.com/Discord-Net-Labs/Discord.Net-Labs
https://github.com/Discord-Net-Labs/Discord.Net-Labs
Temporary.png
diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml
index 89d29ba09..e5b4c84a0 100644
--- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml
+++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml
@@ -2355,6 +2355,209 @@
+
+
+ Represents a thread channel inside of a guild.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Gets a message from this message channel.
+
+
+ This method follows the same behavior as described in .
+ Please visit its documentation for more details on this method.
+
+ The snowflake identifier of the message.
+ The options to be used when sending the request.
+
+ A task that represents an asynchronous get operation for retrieving the message. The task result contains
+ the retrieved message; null if no message is found with the specified identifier.
+
+
+
+
+ Gets the last N messages from this message channel.
+
+
+ This method follows the same behavior as described in .
+ Please visit its documentation for more details on this method.
+
+ The numbers of message to be gotten from.
+ The options to be used when sending the request.
+
+ Paged collection of messages.
+
+
+
+
+ Gets a collection of messages in this channel.
+
+
+ This method follows the same behavior as described in .
+ Please visit its documentation for more details on this method.
+
+ The ID of the starting message to get the messages from.
+ The direction of the messages to be gotten from.
+ The numbers of message to be gotten from.
+ The options to be used when sending the request.
+
+ Paged collection of messages.
+
+
+
+
+ Gets a collection of messages in this channel.
+
+
+ This method follows the same behavior as described in .
+ Please visit its documentation for more details on this method.
+
+ The starting message to get the messages from.
+ The direction of the messages to be gotten from.
+ The numbers of message to be gotten from.
+ The options to be used when sending the request.
+
+ Paged collection of messages.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Message content is too long, length must be less or equal to .
+
+
+
+
+
+
+ Message content is too long, length must be less or equal to .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Represents a WebSocket-based voice channel in a guild.
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index f9eaeb005..dc9dedd34 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -1862,6 +1862,8 @@ namespace Discord.WebSocket
}
}
break;
+
+ // Interactions
case "INTERACTION_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false);
@@ -1969,6 +1971,110 @@ namespace Discord.WebSocket
await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false);
}
break;
+
+ // Threads
+ case "THREAD_CREATE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false);
+
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var guild = State.GetGuild(data.GuildId.Value);
+
+ if(guild == null)
+ {
+ await UnknownGuildAsync(type, data.GuildId.Value);
+ return;
+ }
+
+ var threadChannel = (SocketThreadChannel)guild.AddChannel(this.State, data);
+
+ await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false);
+ }
+
+ break;
+ case "THREAD_UPDATE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false);
+
+ var data = (payload as JToken).ToObject(_serializer);
+ var guild = State.GetGuild(data.GuildId.Value);
+ if (guild == null)
+ {
+ await UnknownGuildAsync(type, data.GuildId.Value);
+ return;
+ }
+
+ var channel = (SocketThreadChannel)guild.GetChannel(data.Id);
+
+ var before = channel.Clone();
+ channel.Update(State, data);
+
+ if (!(guild?.IsSynced ?? true))
+ {
+ await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
+ return;
+ }
+
+ await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, channel).ConfigureAwait(false);
+
+ }
+ break;
+ case "THREAD_DELETE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false);
+
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var guild = State.GetGuild(data.GuildId.Value);
+
+ if(guild == null)
+ {
+ await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false);
+ return;
+ }
+
+ var thread = (SocketThreadChannel)guild.GetChannel(data.Id);
+
+ var cacheable = new Cacheable(thread, data.Id, thread != null, null);
+
+ await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false);
+ }
+ break;
+ case "THREAD_LIST_SYNC":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false);
+
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var guild = State.GetGuild(data.GuildId);
+
+ if(guild == null)
+ {
+ await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
+ return;
+ }
+
+ foreach(var thread in data.Threads)
+ {
+ var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id);
+
+ if(entity == null)
+ {
+ guild.AddChannel(this.State, thread);
+ }
+ else
+ {
+ entity.Update(this.State, thread);
+ }
+ }
+ }
+ break;
+ case "THREAD_MEMBER_UPDATE":
+ break;
+ case "THREAD_MEMBERS_UPDATE": // based on intents
+ break;
+
//Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);
diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
index 3cc8496d9..9f63ad7b4 100644
--- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
+++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
@@ -56,6 +56,8 @@ namespace Discord.WebSocket
return SocketVoiceChannel.Create(guild, state, model);
case ChannelType.Category:
return SocketCategoryChannel.Create(guild, state, model);
+ case ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread:
+ return SocketThreadChannel.Create(guild, state, model);
default:
return new SocketGuildChannel(guild.Discord, model.Id, guild);
}
diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
index de896be81..192d74328 100644
--- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
+++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
@@ -50,6 +50,12 @@ namespace Discord.WebSocket
Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)),
ChannelPermission.ViewChannel)).ToImmutableArray();
+ ///
+ /// Gets a collection of threads within this text channel.
+ ///
+ public IReadOnlyCollection Threads
+ => Guild.ThreadChannels.Where(x => x.ParentChannel.Id == this.Id).ToImmutableArray();
+
internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
: base(discord, id, guild)
{
diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
new file mode 100644
index 000000000..d9583d1ea
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
@@ -0,0 +1,348 @@
+using Discord.Rest;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Model = Discord.API.Channel;
+
+namespace Discord.WebSocket
+{
+ ///
+ /// Represents a thread channel inside of a guild.
+ ///
+ public class SocketThreadChannel : SocketGuildChannel, IThreadChannel, ISocketMessageChannel
+ {
+ ///
+ /// if this thread is private, otherwise
+ ///
+ public bool IsPrivateThread { get; private set; }
+
+ ///
+ /// Gets the parent channel this thread resides in.
+ ///
+ public SocketTextChannel ParentChannel { get; private set; }
+
+ ///
+ public int MessageCount { get; private set; }
+
+ ///
+ public int MemberCount { get; private set; }
+
+ ///
+ public bool Archived { get; private set; }
+
+ ///
+ public DateTimeOffset ArchiveTimestamp { get; private set; }
+
+ ///
+ public ThreadArchiveDuration AutoArchiveDuration { get; private set; }
+
+ ///
+ public bool Locked { get; private set; }
+
+ ///
+ public bool IsNsfw { get; private set; }
+
+ ///
+ public string Topic { get; private set; }
+
+ ///
+ public int SlowModeInterval { get; private set; }
+
+ ///
+ public string Mention { get; private set; }
+
+ ///
+ public ulong? CategoryId { get; private set; }
+
+ ///
+ public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create();
+
+ public new IReadOnlyCollection Users = ImmutableArray.Create();
+
+ private readonly MessageCache _messages;
+
+ internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent)
+ : base(discord, id, guild)
+ {
+ this.ParentChannel = parent;
+ }
+
+ internal static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model)
+ {
+ var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value);
+ var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent);
+ entity.Update(state, model);
+ return entity;
+ }
+
+ internal override void Update(ClientState state, Model model)
+ {
+ base.Update(state, model);
+
+ this.MessageCount = model.MessageCount.GetValueOrDefault(-1);
+ this.MemberCount = model.MemberCount.GetValueOrDefault(-1);
+
+ this.IsPrivateThread = model.Type == ChannelType.PrivateThread;
+
+ if (model.ThreadMetadata.IsSpecified)
+ {
+ this.Archived = model.ThreadMetadata.Value.Archived;
+ this.ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp;
+ this.AutoArchiveDuration = (ThreadArchiveDuration)model.ThreadMetadata.Value.AutoArchiveDuration;
+ this.Locked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false);
+ }
+ }
+
+ ///
+ public virtual Task SyncPermissionsAsync(RequestOptions options = null)
+ => ChannelHelper.SyncPermissionsAsync(this, Discord, options);
+
+ ///
+ public Task ModifyAsync(Action func, RequestOptions options = null)
+ => ChannelHelper.ModifyAsync(this, Discord, func, options);
+
+ //Messages
+ ///
+ public SocketMessage GetCachedMessage(ulong id)
+ => _messages?.Get(id);
+
+ ///
+ /// Gets a message from this message channel.
+ ///
+ ///
+ /// This method follows the same behavior as described in .
+ /// Please visit its documentation for more details on this method.
+ ///
+ /// The snowflake identifier of the message.
+ /// The options to be used when sending the request.
+ ///
+ /// A task that represents an asynchronous get operation for retrieving the message. The task result contains
+ /// the retrieved message; null if no message is found with the specified identifier.
+ ///
+ public async Task GetMessageAsync(ulong id, RequestOptions options = null)
+ {
+ IMessage msg = _messages?.Get(id);
+ if (msg == null)
+ msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false);
+ return msg;
+ }
+
+ ///
+ /// Gets the last N messages from this message channel.
+ ///
+ ///
+ /// This method follows the same behavior as described in .
+ /// Please visit its documentation for more details on this method.
+ ///
+ /// The numbers of message to be gotten from.
+ /// The options to be used when sending the request.
+ ///
+ /// Paged collection of messages.
+ ///
+ public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options);
+
+ ///
+ /// Gets a collection of messages in this channel.
+ ///
+ ///
+ /// This method follows the same behavior as described in .
+ /// Please visit its documentation for more details on this method.
+ ///
+ /// The ID of the starting message to get the messages from.
+ /// The direction of the messages to be gotten from.
+ /// The numbers of message to be gotten from.
+ /// The options to be used when sending the request.
+ ///
+ /// Paged collection of messages.
+ ///
+ public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options);
+
+ ///
+ /// Gets a collection of messages in this channel.
+ ///
+ ///
+ /// This method follows the same behavior as described in .
+ /// Please visit its documentation for more details on this method.
+ ///
+ /// The starting message to get the messages from.
+ /// The direction of the messages to be gotten from.
+ /// The numbers of message to be gotten from.
+ /// The options to be used when sending the request.
+ ///
+ /// Paged collection of messages.
+ ///
+ public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options);
+
+ ///
+ public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch)
+ => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit);
+
+ ///
+ public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
+ => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit);
+
+ ///
+ public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
+ => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit);
+
+ ///
+ public Task> GetPinnedMessagesAsync(RequestOptions options = null)
+ => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options);
+
+ ///
+ /// Message content is too long, length must be less or equal to .
+ public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null)
+ => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, options);
+
+ ///
+ public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null)
+ => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, options, isSpoiler);
+
+ ///
+ /// Message content is too long, length must be less or equal to .
+ public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null)
+ => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, options, isSpoiler);
+
+ ///
+ public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null)
+ => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
+ ///
+ public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null)
+ => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);
+
+ ///
+ public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null)
+ => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false);
+
+ ///
+ public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
+ => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options);
+
+ ///
+ public Task DeleteMessageAsync(IMessage message, RequestOptions options = null)
+ => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options);
+
+ ///
+ public Task TriggerTypingAsync(RequestOptions options = null)
+ => ChannelHelper.TriggerTypingAsync(this, Discord, options);
+
+ ///
+ public IDisposable EnterTypingState(RequestOptions options = null)
+ => ChannelHelper.EnterTypingState(this, Discord, options);
+
+ internal void AddMessage(SocketMessage msg)
+ => _messages?.Add(msg);
+
+ internal SocketMessage RemoveMessage(ulong id)
+ => _messages?.Remove(id);
+
+ //Users
+ ///
+ public new SocketThreadUser GetUser(ulong id)
+ {
+ var user = Users.FirstOrDefault(x => x.Id == id);
+ return user;
+ }
+
+ public Task GetUsersAsync()
+ {
+
+ }
+
+ private string DebuggerDisplay => $"{Name} ({Id}, Thread)";
+ internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel;
+
+ //ITextChannel
+ ///
+ Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support webhooks");
+ ///
+ Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support webhooks");
+ ///
+ Task> ITextChannel.GetWebhooksAsync(RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support webhooks");
+
+ //IGuildChannel
+ ///
+ Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
+ => Task.FromResult(GetUser(id));
+ ///
+ IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
+ => ImmutableArray.Create>(Users).ToAsyncEnumerable();
+
+ //IMessageChannel
+ ///
+ async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
+ {
+ if (mode == CacheMode.AllowDownload)
+ return await GetMessageAsync(id, options).ConfigureAwait(false);
+ else
+ return GetCachedMessage(id);
+ }
+ ///
+ IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options);
+ ///
+ IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options);
+ ///
+ IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options)
+ => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options);
+ ///
+ async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
+ => await GetPinnedMessagesAsync(options).ConfigureAwait(false);
+
+ ///
+ async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component)
+ => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component).ConfigureAwait(false);
+ ///
+ async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component)
+ => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component).ConfigureAwait(false);
+ ///
+ async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component)
+ => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component).ConfigureAwait(false);
+
+ // INestedChannel
+ ///
+ Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options)
+ => Task.FromResult(this.ParentChannel.Category);
+ Task INestedChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support invites");
+ Task INestedChannel.CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support invites");
+ Task INestedChannel.CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support invites");
+ Task> INestedChannel.GetInvitesAsync(RequestOptions options)
+ => throw new NotSupportedException("Thread channels don't support invites");
+
+ public Task JoinAsync()
+ {
+
+ }
+
+ public Task LeaveAsync()
+ {
+
+ }
+
+ public Task AddThreadMember(IGuildUser user)
+ {
+
+ }
+
+ public Task RemoveThreadMember(IGuildUser user)
+ {
+
+ }
+
+
+ }
+}
diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
index e8748dda9..547dc6d7f 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -277,6 +277,14 @@ namespace Discord.WebSocket
public IReadOnlyCollection CategoryChannels
=> Channels.OfType().ToImmutableArray();
///
+ /// Gets a collection of all thread channels in this guild.
+ ///
+ ///
+ /// A read-only collection of thread channels found within this guild.
+ ///
+ public IReadOnlyCollection ThreadChannels
+ => Channels.OfType().ToImmutableArray();
+ ///
/// Gets the current logged-in user.
///
public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null;
diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
new file mode 100644
index 000000000..66a5579c4
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Model = Discord.API.ThreadMember;
+using MemberModel = Discord.API.GuildMember;
+using Discord.API;
+using System.Collections.Immutable;
+
+namespace Discord.WebSocket
+{
+ public class SocketThreadUser : IGuildUser
+ {
+ ///
+ /// Gets the this user is in.
+ ///
+ public SocketThreadChannel Thread { get; private set; }
+
+ ///
+ /// Gets the timestamp for when this user joined this thread.
+ ///
+ public DateTimeOffset ThreadJoinedAt { get; private set; }
+
+ ///
+ /// Gets the guild this user is in.
+ ///
+ public SocketGuild Guild { get; private set; }
+
+ ///
+ public DateTimeOffset? JoinedAt
+ => GuildUser.JoinedAt;
+
+ ///
+ public string Nickname
+ => GuildUser.Nickname;
+
+ ///
+ public DateTimeOffset? PremiumSince
+ => GuildUser.PremiumSince;
+
+ ///
+ public bool? IsPending
+ => GuildUser.IsPending;
+
+ ///
+ public string AvatarId
+ => GuildUser.AvatarId;
+
+ ///
+ public string Discriminator
+ => GuildUser.Discriminator;
+
+ ///
+ public ushort DiscriminatorValue
+ => GuildUser.DiscriminatorValue;
+
+ ///
+ public bool IsBot
+ => GuildUser.IsBot;
+
+ ///
+ public bool IsWebhook
+ => GuildUser.IsWebhook;
+
+ ///
+ public string Username
+ => GuildUser.Username;
+
+ ///
+ public UserProperties? PublicFlags
+ => GuildUser.PublicFlags;
+
+ ///
+ public DateTimeOffset CreatedAt
+ => GuildUser.CreatedAt;
+
+ ///
+ public ulong Id
+ => GuildUser.Id;
+
+ ///
+ public string Mention
+ => GuildUser.Mention;
+
+ ///
+ public UserStatus Status
+ => GuildUser.Status;
+
+ ///
+ public IImmutableSet ActiveClients
+ => GuildUser.ActiveClients;
+
+ ///
+ public IImmutableList Activities
+ => GuildUser.Activities;
+
+ ///
+ public bool IsDeafened
+ => GuildUser.IsDeafened;
+
+ ///
+ public bool IsMuted
+ => GuildUser.IsMuted;
+
+ ///
+ public bool IsSelfDeafened
+ => GuildUser.IsSelfDeafened;
+
+ ///
+ public bool IsSelfMuted
+ => GuildUser.IsSelfMuted;
+
+ ///
+ public bool IsSuppressed
+ => GuildUser.IsSuppressed;
+
+ ///
+ public IVoiceChannel VoiceChannel
+ => GuildUser.VoiceChannel;
+
+ ///
+ public string VoiceSessionId
+ => GuildUser.VoiceSessionId;
+
+ ///
+ public bool IsStreaming
+ => GuildUser.IsStreaming;
+
+ private SocketGuildUser GuildUser { get; set; }
+
+ internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member)
+ {
+ this.Thread = thread;
+ this.Guild = guild;
+ this.GuildUser = member;
+ }
+
+ internal SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member)
+ {
+ var entity = new SocketThreadUser(guild, thread, member);
+ entity.Update(model);
+ return entity;
+ }
+
+ internal void Update(Model model)
+ {
+ this.ThreadJoinedAt = model.JoinTimestamp;
+ }
+
+
+ ///
+ public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel);
+
+ ///
+ public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options);
+
+ ///
+ public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options);
+
+ ///
+ public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options);
+
+ ///
+ public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options);
+
+ ///
+ public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options);
+
+ ///
+ public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options);
+
+ ///
+ public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options);
+
+ ///
+ public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options);
+
+ ///
+ public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options);
+
+ ///
+ public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options);
+
+ ///
+ public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => GuildUser.GetAvatarUrl(format, size);
+
+ ///
+ public string GetDefaultAvatarUrl() => GuildUser.GetDefaultAvatarUrl();
+
+ ///
+ public Task CreateDMChannelAsync(RequestOptions options = null) => GuildUser.CreateDMChannelAsync(options);
+
+ ///
+ GuildPermissions IGuildUser.GuildPermissions => this.GuildUser.GuildPermissions;
+
+ ///
+ IGuild IGuildUser.Guild => this.Guild;
+
+ ///
+ ulong IGuildUser.GuildId => this.Guild.Id;
+
+ ///
+ IReadOnlyCollection IGuildUser.RoleIds => this.GuildUser.Roles.Select(x => x.Id).ToImmutableArray();
+
+ ///
+ /// Gets the guild user of this thread user.
+ ///
+ ///
+ public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser;
+ }
+}