Browse Source

Squashed commit of the following:

commit 73740e4169
Author: quin lynch <lynchquin@gmail.com>
Date:   Wed Aug 4 23:17:04 2021 -0300

    add sharded events

commit dac6ba3603
Merge: 22e3a529 39b7e715
Author: quin lynch <lynchquin@gmail.com>
Date:   Wed Aug 4 22:16:12 2021 -0300

    Merge branch 'feature/stage-channels' of https://github.com/Discord-Net-Labs/Discord.Net-Labs into feature/stage-channels

commit 22e3a529e0
Author: quin lynch <lynchquin@gmail.com>
Date:   Wed Aug 4 22:15:03 2021 -0300

    Threads pre 2

commit 39b7e715f3
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Date:   Tue Aug 3 20:43:21 2021 -0300

    Update README.md

commit 56536f6448
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Date:   Tue Aug 3 20:28:27 2021 -0300

    Update README.md

commit d98e79452c
Author: quin lynch <lynchquin@gmail.com>
Date:   Sat Jul 31 21:22:56 2021 -0300

    Request to speak stuff

commit 950fe80cec
Author: quin lynch <lynchquin@gmail.com>
Date:   Sat Jul 31 20:59:11 2021 -0300

    Rest stage channels

commit 360b9436bb
Author: quin lynch <lynchquin@gmail.com>
Date:   Sat Jul 31 20:29:38 2021 -0300

    Stage channel socket side

commit 928edaa03b
Author: quin lynch <lynchquin@gmail.com>
Date:   Sat Jul 31 19:19:40 2021 -0300

    Add models

commit ec58a9b7e8
Author: quin lynch <lynchquin@gmail.com>
Date:   Sat Jul 31 18:59:09 2021 -0300

    initial stage channel events
pull/1923/head
quin lynch 3 years ago
parent
commit
0113e26cca
35 changed files with 1056 additions and 7 deletions
  1. +107
    -0
      src/Discord.Net.Core/Discord.Net.Core.xml
  2. +75
    -0
      src/Discord.Net.Core/Entities/Channels/IStageChannel.cs
  3. +24
    -0
      src/Discord.Net.Core/Entities/Channels/StageInstanceProperties.cs
  4. +14
    -0
      src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs
  5. +21
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  6. +6
    -0
      src/Discord.Net.Core/Entities/Users/IVoiceState.cs
  7. +30
    -0
      src/Discord.Net.Rest/API/Common/StageInstance.cs
  8. +3
    -0
      src/Discord.Net.Rest/API/Common/VoiceState.cs
  9. +21
    -0
      src/Discord.Net.Rest/API/Rest/CreateStageInstanceParams.cs
  10. +19
    -0
      src/Discord.Net.Rest/API/Rest/ModifyStageInstanceParams.cs
  11. +17
    -0
      src/Discord.Net.Rest/API/Rest/ModifyVoiceStateParams.cs
  12. +69
    -0
      src/Discord.Net.Rest/Discord.Net.Rest.xml
  13. +64
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  14. +16
    -0
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  15. +2
    -0
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  16. +109
    -0
      src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs
  17. +46
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  18. +3
    -0
      src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs
  19. +2
    -0
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  20. +2
    -0
      src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs
  21. +5
    -1
      src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs
  22. +59
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  23. +4
    -1
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
  24. +102
    -0
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml
  25. +2
    -2
      src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs
  26. +7
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  27. +64
    -0
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  28. +2
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  29. +116
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs
  30. +25
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  31. +3
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs
  32. +4
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  33. +4
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
  34. +7
    -3
      src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs
  35. +2
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs

+ 107
- 0
src/Discord.Net.Core/Discord.Net.Core.xml View File

@@ -1914,6 +1914,70 @@
A read-only collection of users that can access this channel.
</returns>
</member>
<member name="T:Discord.IStageChannel">
<summary>
Represents a generic Stage Channel.
</summary>
</member>
<member name="P:Discord.IStageChannel.Topic">
<summary>
Gets the topic of the Stage instance.
</summary>
<remarks>
If the stage isn't live then this property will be set to <see langword="null"/>.
</remarks>
</member>
<member name="P:Discord.IStageChannel.PrivacyLevel">
<summary>
The <see cref="T:Discord.StagePrivacyLevel"/> of the current stage.
</summary>
<remarks>
If the stage isn't live then this property will be set to <see langword="null"/>.
</remarks>
</member>
<member name="P:Discord.IStageChannel.DiscoverableDisabled">
<summary>
<see langword="true"/> if stage discovery is disabled, otherwise <see langword="false"/>.
</summary>
</member>
<member name="P:Discord.IStageChannel.Live">
<summary>
<see langword="true"/> when the stage is live, otherwise <see langword="false"/>.
</summary>
<remarks>
If the stage isn't live then this property will be set to <see langword="null"/>.
</remarks>
</member>
<member name="M:Discord.IStageChannel.StartStageAsync(System.String,Discord.StagePrivacyLevel,Discord.RequestOptions)">
<summary>
Starts the stage, creating a stage instance.
</summary>
<param name="topic">The topic for the stage/</param>
<param name="privacyLevel">The privacy level of the stage</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous start operation.
</returns>
</member>
<member name="M:Discord.IStageChannel.ModifyInstanceAsync(System.Action{Discord.StageInstanceProperties},Discord.RequestOptions)">
<summary>
Modifies the current stage instance.
</summary>
<param name="func">The properties to modify the stage instance with.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous modify operation.
</returns>
</member>
<member name="M:Discord.IStageChannel.StopStageAsync(Discord.RequestOptions)">
<summary>
Stops the stage, deleting the stage instance.
</summary>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous stop operation.
</returns>
</member>
<member name="T:Discord.ITextChannel">
<summary>
Represents a generic channel in a guild that can send and receive messages.
@@ -2202,6 +2266,21 @@
<param name="id"> Sets the ID of the channel to apply this position to. </param>
<param name="position"> Sets the new zero-based position of this channel. </param>
</member>
<member name="T:Discord.StageInstanceProperties">
<summary>
Represents properties to use when modifying a stage instance.
</summary>
</member>
<member name="P:Discord.StageInstanceProperties.Topic">
<summary>
Gets or sets the topic of the stage.
</summary>
</member>
<member name="P:Discord.StageInstanceProperties.PrivacyLevel">
<summary>
Gets or sets the privacy level of the stage.
</summary>
</member>
<member name="T:Discord.TextChannelProperties">
<summary>
Provides properties that are used to modify an <see cref="T:Discord.ITextChannel"/> with the specified changes.
@@ -3344,6 +3423,29 @@
with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
</returns>
</member>
<member name="M:Discord.IGuild.GetStageChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)">
<summary>
Gets a stage channel in this guild
</summary>
<param name="id">The snowflake identifier for the stage channel.</param>
<param name="mode">The <see cref="T:Discord.CacheMode"/> that determines whether the object should be fetched from cache.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous get operation. The task result contains the stage channel associated
with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
</returns>
</member>
<member name="M:Discord.IGuild.GetStageChannelsAsync(Discord.CacheMode,Discord.RequestOptions)">
<summary>
Gets a collection of all stage channels in this guild.
</summary>
<param name="mode">The <see cref="T:Discord.CacheMode"/> that determines whether the object should be fetched from cache.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous get operation. The task result contains a read-only collection of
stage channels found within this guild.
</returns>
</member>
<member name="M:Discord.IGuild.GetAFKChannelAsync(Discord.CacheMode,Discord.RequestOptions)">
<summary>
Gets the AFK voice channel in this guild.
@@ -9780,6 +9882,11 @@
<c>true</c> if the user is streaming; otherwise <c>false</c>.
</returns>
</member>
<member name="P:Discord.IVoiceState.RequestToSpeakTimestamp">
<summary>
Gets the time on which the user requested to speak.
</summary>
</member>
<member name="T:Discord.IWebhookUser">
<summary> Represents a Webhook Discord user. </summary>
</member>


+ 75
- 0
src/Discord.Net.Core/Entities/Channels/IStageChannel.cs View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a generic Stage Channel.
/// </summary>
public interface IStageChannel : IVoiceChannel
{
/// <summary>
/// Gets the topic of the Stage instance.
/// </summary>
/// <remarks>
/// If the stage isn't live then this property will be set to <see langword="null"/>.
/// </remarks>
string Topic { get; }

/// <summary>
/// The <see cref="StagePrivacyLevel"/> of the current stage.
/// </summary>
/// <remarks>
/// If the stage isn't live then this property will be set to <see langword="null"/>.
/// </remarks>
StagePrivacyLevel? PrivacyLevel { get; }

/// <summary>
/// <see langword="true"/> if stage discovery is disabled, otherwise <see langword="false"/>.
/// </summary>
bool? DiscoverableDisabled { get; }

/// <summary>
/// <see langword="true"/> when the stage is live, otherwise <see langword="false"/>.
/// </summary>
/// <remarks>
/// If the stage isn't live then this property will be set to <see langword="null"/>.
/// </remarks>
bool Live { get; }

/// <summary>
/// Starts the stage, creating a stage instance.
/// </summary>
/// <param name="topic">The topic for the stage/</param>
/// <param name="privacyLevel">The privacy level of the stage</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous start operation.
/// </returns>
Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null);

/// <summary>
/// Modifies the current stage instance.
/// </summary>
/// <param name="func">The properties to modify the stage instance with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modify operation.
/// </returns>
Task ModifyInstanceAsync(Action<StageInstanceProperties> func, RequestOptions options = null);

/// <summary>
/// Stops the stage, deleting the stage instance.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous stop operation.
/// </returns>
Task StopStageAsync(RequestOptions options = null);

Task RequestToSpeak(RequestOptions options = null);
}
}

+ 24
- 0
src/Discord.Net.Core/Entities/Channels/StageInstanceProperties.cs View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents properties to use when modifying a stage instance.
/// </summary>
public class StageInstanceProperties
{
/// <summary>
/// Gets or sets the topic of the stage.
/// </summary>
public Optional<string> Topic { get; set; }

/// <summary>
/// Gets or sets the privacy level of the stage.
/// </summary>
public Optional<StagePrivacyLevel> PrivacyLevel { get; set; }
}
}

+ 14
- 0
src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
public enum StagePrivacyLevel
{
Public = 1,
GuildOnly = 2,
}
}

+ 21
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -530,6 +530,27 @@ namespace Discord
/// </returns>
Task<IVoiceChannel> GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a stage channel in this guild
/// </summary>
/// <param name="id">The snowflake identifier for the stage channel.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the stage channel associated
/// with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
Task<IStageChannel> GetStageChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets a collection of all stage channels in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// stage channels found within this guild.
/// </returns>
Task<IReadOnlyCollection<IStageChannel>> GetStageChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary>
/// Gets the AFK voice channel in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>


+ 6
- 0
src/Discord.Net.Core/Entities/Users/IVoiceState.cs View File

@@ -1,3 +1,5 @@
using System;

namespace Discord
{
/// <summary>
@@ -62,5 +64,9 @@ namespace Discord
/// <c>true</c> if the user is streaming; otherwise <c>false</c>.
/// </returns>
bool IsStreaming { get; }
/// <summary>
/// Gets the time on which the user requested to speak.
/// </summary>
DateTimeOffset? RequestToSpeakTimestamp { get; }
}
}

+ 30
- 0
src/Discord.Net.Rest/API/Common/StageInstance.cs View File

@@ -0,0 +1,30 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class StageInstance
{
[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("guild_id")]
public ulong GuildId { get; set; }

[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("topic")]
public string Topic { get; set; }

[JsonProperty("privacy_level")]
public StagePrivacyLevel PrivacyLevel { get; set; }

[JsonProperty("discoverable_disabled")]
public bool DiscoverableDisabled { get; set; }
}
}

+ 3
- 0
src/Discord.Net.Rest/API/Common/VoiceState.cs View File

@@ -1,5 +1,6 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;

namespace Discord.API
{
@@ -28,5 +29,7 @@ namespace Discord.API
public bool Suppress { get; set; }
[JsonProperty("self_stream")]
public bool SelfStream { get; set; }
[JsonProperty("request_to_speak_timestamp")]
public Optional<DateTimeOffset?> RequestToSpeakTimestamp { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Rest/CreateStageInstanceParams.cs View File

@@ -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 CreateStageInstanceParams
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("topic")]
public string Topic { get; set; }

[JsonProperty("privacy_level")]
public Optional<StagePrivacyLevel> PrivacyLevel { get; set; }
}
}

+ 19
- 0
src/Discord.Net.Rest/API/Rest/ModifyStageInstanceParams.cs View File

@@ -0,0 +1,19 @@
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 ModifyStageInstanceParams
{

[JsonProperty("topic")]
public Optional<string> Topic { get; set; }

[JsonProperty("privacy_level")]
public Optional<StagePrivacyLevel> PrivacyLevel { get; set; }
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Rest/ModifyVoiceStateParams.cs View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
using System;

namespace Discord.API.Rest
{
internal class ModifyVoiceStateParams
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("suppress")]
public Optional<bool> Suppressed { get; set; }

[JsonProperty("request_to_speak_timestamp")]
public Optional<DateTimeOffset> RequestToSpeakTimestamp { get; set; }
}
}

+ 69
- 0
src/Discord.Net.Rest/Discord.Net.Rest.xml View File

@@ -2281,6 +2281,38 @@
Represents a REST-based news channel in a guild that has the same properties as a <see cref="T:Discord.Rest.RestTextChannel"/>.
</summary>
</member>
<member name="T:Discord.Rest.RestStageChannel">
<summary>
Represents a REST-based stage channel in a guild.
</summary>
</member>
<member name="P:Discord.Rest.RestStageChannel.Topic">
<inheritdoc/>
</member>
<member name="P:Discord.Rest.RestStageChannel.PrivacyLevel">
<inheritdoc/>
</member>
<member name="P:Discord.Rest.RestStageChannel.DiscoverableDisabled">
<inheritdoc/>
</member>
<member name="P:Discord.Rest.RestStageChannel.Live">
<inheritdoc/>
</member>
<member name="M:Discord.Rest.RestStageChannel.ModifyInstanceAsync(System.Action{Discord.StageInstanceProperties},Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.Rest.RestStageChannel.StartStageAsync(System.String,Discord.StagePrivacyLevel,Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.Rest.RestStageChannel.StopStageAsync(Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.Rest.RestStageChannel.UpdateAsync(Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.Rest.RestStageChannel.RequestToSpeak(Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="T:Discord.Rest.RestTextChannel">
<summary>
Represents a REST-based channel in a guild that can send and receive messages.
@@ -3130,6 +3162,28 @@
voice channels found within this guild.
</returns>
</member>
<member name="M:Discord.Rest.RestGuild.GetStageChannelAsync(System.UInt64,Discord.RequestOptions)">
<summary>
Gets a stage channel in this guild
</summary>
<param name="id">The snowflake identifier for the stage channel.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous get operation. The task result contains the stage channel associated
with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
</returns>
</member>
<member name="M:Discord.Rest.RestGuild.GetStageChannelsAsync(Discord.RequestOptions)">
<summary>
Gets a collection of all stage channels in this guild.
</summary>
<param name="mode">The <see cref="T:Discord.CacheMode"/> that determines whether the object should be fetched from cache.</param>
<param name="options">The options to be used when sending the request.</param>
<returns>
A task that represents the asynchronous get operation. The task result contains a read-only collection of
stage channels found within this guild.
</returns>
</member>
<member name="M:Discord.Rest.RestGuild.GetCategoryChannelsAsync(Discord.RequestOptions)">
<summary>
Gets a collection of all category channels in this guild.
@@ -3496,6 +3550,12 @@
<member name="M:Discord.Rest.RestGuild.Discord#IGuild#GetCategoriesAsync(Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.Rest.RestGuild.Discord#IGuild#GetStageChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.Rest.RestGuild.Discord#IGuild#GetStageChannelsAsync(Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.Rest.RestGuild.Discord#IGuild#GetVoiceChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
@@ -4365,6 +4425,9 @@
<member name="P:Discord.Rest.RestGroupUser.Discord#IVoiceState#IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.Rest.RestGroupUser.Discord#IVoiceState#RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="T:Discord.Rest.RestGuildUser">
<summary>
Represents a REST-based guild user.
@@ -4456,6 +4519,9 @@
<member name="P:Discord.Rest.RestGuildUser.Discord#IVoiceState#IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.Rest.RestGuildUser.Discord#IVoiceState#RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="T:Discord.Rest.RestSelfUser">
<summary>
Represents the logged-in REST-based user.
@@ -4679,6 +4745,9 @@
<member name="P:Discord.Rest.RestWebhookUser.Discord#IVoiceState#IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.Rest.RestWebhookUser.Discord#IVoiceState#RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="P:Discord.Rest.RestWebhook.Token">
<inheritdoc />
</member>


+ 64
- 0
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -577,6 +577,70 @@ namespace Discord.API
return await SendAsync<ChannelThreads>("GET", () => $"channels/{channelId}/users/@me/threads/archived/private{query}", bucket, options: options);
}

// stage
public async Task<StageInstance> CreateStageInstanceAsync(CreateStageInstanceParams args, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds();

return await SendJsonAsync<StageInstance>("POST", () => $"stage-instances", args, bucket, options: options).ConfigureAwait(false);
}

public async Task<StageInstance> ModifyStageInstanceAsync(ulong channelId, ModifyStageInstanceParams args, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(channelId: channelId);

return await SendJsonAsync<StageInstance>("PATCH", () => $"stage-instances/{channelId}", args, bucket, options: options).ConfigureAwait(false);
}

public async Task DeleteStageInstanceAsync(ulong channelId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

options = RequestOptions.CreateOrClone(options);

try
{
await SendAsync("DELETE", $"stage-instances/{channelId}", options: options).ConfigureAwait(false);
}
catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) { }
}

public async Task<StageInstance> GetStageInstanceAsync(ulong channelId, RequestOptions options = null)
{
Preconditions.NotEqual(channelId, 0, nameof(channelId));

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(channelId: channelId);

try
{
return await SendAsync<StageInstance>("POST", () => $"stage-instances/{channelId}", bucket, options: options).ConfigureAwait(false);
}
catch(HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound)
{
return null;
}
}

public async Task ModifyMyVoiceState(ulong guildId, ModifyVoiceStateParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds();

await SendJsonAsync("PATCH", () => $"guilds/{guildId}/voice-states/@me", args, bucket, options: options).ConfigureAwait(false);
}

// roles
public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
{


+ 16
- 0
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using StageInstance = Discord.API.StageInstance;

namespace Discord.Rest
{
@@ -92,6 +93,21 @@ namespace Discord.Rest
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}

public static async Task<StageInstance> ModifyAsync(IStageChannel channel, BaseDiscordClient client,
Action<StageInstanceProperties> func, RequestOptions options = null)
{
var args = new StageInstanceProperties();
func(args);

var apiArgs = new ModifyStageInstanceParams()
{
PrivacyLevel = args.PrivacyLevel,
Topic = args.Topic
};

return await client.ApiClient.ModifyStageInstanceAsync(channel.Id, apiArgs, options);
}

//Invites
public static async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(IGuildChannel channel, BaseDiscordClient client,
RequestOptions options)


+ 2
- 0
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -40,6 +40,8 @@ namespace Discord.Rest
return RestTextChannel.Create(discord, guild, model);
case ChannelType.Voice:
return RestVoiceChannel.Create(discord, guild, model);
case ChannelType.Stage:
return RestStageChannel.Create(discord, guild, model);
case ChannelType.Category:
return RestCategoryChannel.Create(discord, guild, model);
case ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread:


+ 109
- 0
src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using StageInstance = Discord.API.StageInstance;

namespace Discord.Rest
{
/// <summary>
/// Represents a REST-based stage channel in a guild.
/// </summary>
public class RestStageChannel : RestVoiceChannel, IStageChannel
{
/// <inheritdoc/>
public string Topic { get; private set; }

/// <inheritdoc/>
public StagePrivacyLevel? PrivacyLevel { get; private set; }

/// <inheritdoc/>
public bool? DiscoverableDisabled { get; private set; }

/// <inheritdoc/>
public bool Live { get; private set; }
internal RestStageChannel(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, guild, id)
{

}

internal static new RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestStageChannel(discord, guild, model.Id);
entity.Update(model);
return entity;
}

internal void Update(StageInstance model, bool isLive = false)
{
this.Live = isLive;
if(isLive)
{
this.Topic = model.Topic;
this.PrivacyLevel = model.PrivacyLevel;
this.DiscoverableDisabled = model.DiscoverableDisabled;
}
else
{
this.Topic = null;
this.PrivacyLevel = null;
this.DiscoverableDisabled = null;
}
}

/// <inheritdoc/>
public async Task ModifyInstanceAsync(Action<StageInstanceProperties> func, RequestOptions options = null)
{
var model = await ChannelHelper.ModifyAsync(this, Discord, func, options);

Update(model, true);
}

/// <inheritdoc/>
public async Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null)
{
var args = new API.Rest.CreateStageInstanceParams()
{
ChannelId = this.Id,
PrivacyLevel = privacyLevel,
Topic = topic
};

var model = await Discord.ApiClient.CreateStageInstanceAsync(args, options);

Update(model, true);
}

/// <inheritdoc/>
public async Task StopStageAsync(RequestOptions options = null)
{
await Discord.ApiClient.DeleteStageInstanceAsync(this.Id, options);

Update(null, false);
}

/// <inheritdoc/>
public override async Task UpdateAsync(RequestOptions options = null)
{
await base.UpdateAsync(options);

var model = await Discord.ApiClient.GetStageInstanceAsync(this.Id, options);

Update(model, model != null);
}

/// <inheritdoc/>
public Task RequestToSpeak(RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams()
{
ChannelId = this.Id,
RequestToSpeakTimestamp = DateTimeOffset.UtcNow
};
return Discord.ApiClient.ModifyMyVoiceState(this.Guild.Id, args, options);
}
}
}

+ 46
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -451,6 +451,35 @@ namespace Discord.Rest
var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false);
return channels.OfType<RestVoiceChannel>().ToImmutableArray();
}
/// <summary>
/// Gets a stage channel in this guild
/// </summary>
/// <param name="id">The snowflake identifier for the stage channel.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the stage channel associated
/// with the specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
public async Task<RestStageChannel> GetStageChannelAsync(ulong id, RequestOptions options = null)
{
var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false);
return channel as RestStageChannel;
}

/// <summary>
/// Gets a collection of all stage channels in this guild.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// stage channels found within this guild.
/// </returns>
public async Task<IReadOnlyCollection<RestStageChannel>> GetStageChannelsAsync(RequestOptions options = null)
{
var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false);
return channels.OfType<RestStageChannel>().ToImmutableArray();
}

/// <summary>
/// Gets a collection of all category channels in this guild.
@@ -952,6 +981,22 @@ namespace Discord.Rest
return null;
}
/// <inheritdoc />
async Task<IStageChannel> IGuild.GetStageChannelAsync(ulong id, CacheMode mode, RequestOptions options )
{
if (mode == CacheMode.AllowDownload)
return await GetStageChannelAsync(id, options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<IReadOnlyCollection<IStageChannel>> IGuild.GetStageChannelsAsync(CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
return await GetStageChannelsAsync(options).ConfigureAwait(false);
else
return null;
}
/// <inheritdoc />
async Task<IVoiceChannel> IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options)
{
if (mode == CacheMode.AllowDownload)
@@ -1109,5 +1154,6 @@ namespace Discord.Rest
/// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
}
}

+ 3
- 0
src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;
using Model = Discord.API.User;

@@ -37,5 +38,7 @@ namespace Discord.Rest
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
}
}

+ 2
- 0
src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs View File

@@ -169,5 +169,7 @@ namespace Discord.Rest
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
}
}

+ 2
- 0
src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs View File

@@ -107,5 +107,7 @@ namespace Discord.Rest
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
}
}

+ 5
- 1
src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs View File

@@ -1,10 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;

namespace Discord.API.Gateway
{
internal class GuildMemberUpdateEvent : GuildMember
{
[JsonProperty("joined_at")]
public new DateTimeOffset? JoinedAt { get; set; }

[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}


+ 59
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -592,5 +592,64 @@ namespace Discord.WebSocket
}
internal readonly AsyncEvent<Func<SocketThreadUser, Task>> _threadMemberLeft = new AsyncEvent<Func<SocketThreadUser, Task>>();

/// <summary>
/// Fired when a stage is started.
/// </summary>
public event Func<SocketStageChannel, Task> StageStarted
{
add { _stageStarted.Add(value); }
remove { _stageStarted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, Task>> _stageStarted = new AsyncEvent<Func<SocketStageChannel, Task>>();

/// <summary>
/// Fired when a stage ends.
/// </summary>
public event Func<SocketStageChannel, Task> StageEnded
{
add { _stageEnded.Add(value); }
remove { _stageEnded.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, Task>> _stageEnded = new AsyncEvent<Func<SocketStageChannel, Task>>();

/// <summary>
/// Fired when a stage is updated.
/// </summary>
public event Func<SocketStageChannel, SocketStageChannel, Task> StageUpdated
{
add { _stageUpdated.Add(value); }
remove { _stageUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketStageChannel, Task>> _stageUpdated = new AsyncEvent<Func<SocketStageChannel, SocketStageChannel, Task>>();

/// <summary>
/// Fired when a user requests to speak within a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> RequestToSpeak
{
add { _requestToSpeak.Add(value); }
remove { _requestToSpeak.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _requestToSpeak = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();

/// <summary>
/// Fired when a speaker is added in a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> SpeakerAdded
{
add { _speakerAdded.Add(value); }
remove { _speakerAdded.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _speakerAdded = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();

/// <summary>
/// Fired when a speaker is removed from a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> SpeakerRemoved
{
add { _speakerRemoved.Add(value); }
remove { _speakerRemoved.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _speakerRemoved = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();
}
}

+ 4
- 1
src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
@@ -17,6 +17,9 @@
<PropertyGroup>
<DocumentationFile>..\Discord.Net.WebSocket\Discord.Net.WebSocket.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net461|AnyCPU'">
<DefineConstants>DEBUG;TRACE;DEBUG_LIMITS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />


+ 102
- 0
src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml View File

@@ -781,6 +781,36 @@
Fired when a user leaves a thread
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.StageStarted">
<summary>
Fired when a stage is started.
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.StageEnded">
<summary>
Fired when a stage ends.
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.StageUpdated">
<summary>
Fired when a stage is updated.
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.RequestToSpeak">
<summary>
Fired when a user requests to speak within a stage channel.
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.SpeakerAdded">
<summary>
Fired when a speaker is added in a stage channel.
</summary>
</member>
<member name="E:Discord.WebSocket.BaseSocketClient.SpeakerRemoved">
<summary>
Fired when a speaker is removed from a stage channel.
</summary>
</member>
<member name="P:Discord.WebSocket.DiscordShardedClient.Latency">
<inheritdoc />
</member>
@@ -2142,6 +2172,40 @@
</note>
</remarks>
</member>
<member name="T:Discord.WebSocket.SocketStageChannel">
<summary>
Represents a stage channel recieved over the gateway.
</summary>
</member>
<member name="P:Discord.WebSocket.SocketStageChannel.Topic">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketStageChannel.PrivacyLevel">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketStageChannel.DiscoverableDisabled">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketStageChannel.Live">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketStageChannel.Speakers">
<summary>
Gets a collection of users who are speakers within the stage.
</summary>
</member>
<member name="M:Discord.WebSocket.SocketStageChannel.StartStageAsync(System.String,Discord.StagePrivacyLevel,Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.WebSocket.SocketStageChannel.ModifyInstanceAsync(System.Action{Discord.StageInstanceProperties},Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.WebSocket.SocketStageChannel.StopStageAsync(Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="M:Discord.WebSocket.SocketStageChannel.RequestToSpeak(Discord.RequestOptions)">
<inheritdoc/>
</member>
<member name="T:Discord.WebSocket.SocketTextChannel">
<summary>
Represents a WebSocket-based channel in a guild that can send and receive messages.
@@ -2901,6 +2965,14 @@
A read-only collection of voice channels found within this guild.
</returns>
</member>
<member name="P:Discord.WebSocket.SocketGuild.StageChannels">
<summary>
Gets a collection of all stage channels in this guild.
</summary>
<returns>
A read-only collection of stage channels found within this guild.
</returns>
</member>
<member name="P:Discord.WebSocket.SocketGuild.CategoryChannels">
<summary>
Gets a collection of all category channels in this guild.
@@ -3076,6 +3148,15 @@
A voice channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
</returns>
</member>
<member name="M:Discord.WebSocket.SocketGuild.GetStageChannel(System.UInt64)">
<summary>
Gets a stage channel in this guild.
</summary>
<param name="id">The snowflake identifier for the stage channel.</param>
<returns>
A stage channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
</returns>
</member>
<member name="M:Discord.WebSocket.SocketGuild.GetCategoryChannel(System.UInt64)">
<summary>
Gets a category channel in this guild.
@@ -3417,6 +3498,12 @@
<member name="M:Discord.WebSocket.SocketGuild.Discord#IGuild#GetVoiceChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.WebSocket.SocketGuild.Discord#IGuild#GetStageChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.WebSocket.SocketGuild.Discord#IGuild#GetStageChannelsAsync(Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
<member name="M:Discord.WebSocket.SocketGuild.Discord#IGuild#GetAFKChannelAsync(Discord.CacheMode,Discord.RequestOptions)">
<inheritdoc />
</member>
@@ -4358,6 +4445,9 @@
<member name="P:Discord.WebSocket.SocketGroupUser.Discord#IVoiceState#IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.WebSocket.SocketGroupUser.Discord#IVoiceState#RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="T:Discord.WebSocket.SocketGuildUser">
<summary>
Represents a WebSocket-based guild user.
@@ -4407,6 +4497,9 @@
<member name="P:Discord.WebSocket.SocketGuildUser.IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.WebSocket.SocketGuildUser.RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="P:Discord.WebSocket.SocketGuildUser.IsPending">
<inheritdoc />
</member>
@@ -4654,6 +4747,9 @@
<member name="P:Discord.WebSocket.SocketThreadUser.IsStreaming">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketThreadUser.RequestToSpeakTimestamp">
<inheritdoc/>
</member>
<member name="M:Discord.WebSocket.SocketThreadUser.GetPermissions(Discord.IGuildChannel)">
<inheritdoc/>
</member>
@@ -4819,6 +4915,9 @@
<member name="P:Discord.WebSocket.SocketVoiceState.VoiceSessionId">
<inheritdoc />
</member>
<member name="P:Discord.WebSocket.SocketVoiceState.RequestToSpeakTimestamp">
<inheritdoc/>
</member>
<member name="P:Discord.WebSocket.SocketVoiceState.IsMuted">
<inheritdoc />
</member>
@@ -4968,6 +5067,9 @@
<member name="P:Discord.WebSocket.SocketWebhookUser.Discord#IVoiceState#IsStreaming">
<inheritdoc />
</member>
<member name="P:Discord.WebSocket.SocketWebhookUser.Discord#IVoiceState#RequestToSpeakTimestamp">
<inheritdoc />
</member>
<member name="T:Discord.WebSocket.SocketVoiceServer">
<summary>
Represents a WebSocket-based voice server.


+ 2
- 2
src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;

namespace Discord.WebSocket
@@ -35,4 +35,4 @@ namespace Discord.WebSocket
}
private readonly AsyncEvent<Func<int, int, DiscordSocketClient, Task>> _shardLatencyUpdatedEvent = new AsyncEvent<Func<int, int, DiscordSocketClient, Task>>();
}
}
}

+ 7
- 0
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -386,6 +386,13 @@ namespace Discord.WebSocket

client.ThreadMemberJoined += (user) => _threadMemberJoined.InvokeAsync(user);
client.ThreadMemberLeft += (user) => _threadMemberLeft.InvokeAsync(user);
client.StageEnded += (stage) => _stageEnded.InvokeAsync(stage);
client.StageStarted += (stage) => _stageStarted.InvokeAsync(stage);
client.StageUpdated += (stage1, stage2) => _stageUpdated.InvokeAsync(stage1, stage2);

client.RequestToSpeak += (stage, user) => _requestToSpeak.InvokeAsync(stage, user);
client.SpeakerAdded += (stage, user) => _speakerAdded.InvokeAsync(stage, user);
client.SpeakerRemoved += (stage, user) => _speakerRemoved.InvokeAsync(stage, user);
}

//IDiscordClient


+ 64
- 0
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -1770,6 +1770,29 @@ namespace Discord.WebSocket
}
}

if (user is SocketGuildUser guildUser && data.ChannelId.HasValue)
{
SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value);

if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null)
{
if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue)
{
await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser);
return;
}
if(before.IsSuppressed && !after.IsSuppressed)
{
await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser);
return;
}
if(!before.IsSuppressed && after.IsSuppressed)
{
await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser);
}
}
}

await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false);
}
break;
@@ -2181,6 +2204,47 @@ namespace Discord.WebSocket

break;

case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE":
{
await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false);

var data = (payload as JToken).ToObject<StageInstance>(_serializer);

var guild = State.GetGuild(data.GuildId);

if(guild == null)
{
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
}

var stageChannel = guild.GetStageChannel(data.ChannelId);

if(stageChannel == null)
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}

SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null;

stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE" ? true : type == "STAGE_INSTANCE_DELETE" ? false : false);

switch (type)
{
case "STAGE_INSTANCE_CREATE":
await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel);
return;
case "STAGE_INSTANCE_DELETE":
await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel);
return;
case "STAGE_INSTANCE_UPDATE":
await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel);
return;
}
}
break;

//Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


+ 2
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -58,6 +58,8 @@ namespace Discord.WebSocket
return SocketCategoryChannel.Create(guild, state, model);
case ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread:
return SocketThreadChannel.Create(guild, state, model);
case ChannelType.Stage:
return SocketStageChannel.Create(guild, state, model);
default:
return new SocketGuildChannel(guild.Discord, model.Id, guild);
}


+ 116
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs View File

@@ -0,0 +1,116 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using StageInstance = Discord.API.StageInstance;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a stage channel recieved over the gateway.
/// </summary>
public class SocketStageChannel : SocketVoiceChannel, IStageChannel
{
/// <inheritdoc/>
public string Topic { get; private set; }

/// <inheritdoc/>
public StagePrivacyLevel? PrivacyLevel { get; private set; }

/// <inheritdoc/>
public bool? DiscoverableDisabled { get; private set; }

/// <inheritdoc/>
public bool Live { get; private set; } = false;

/// <summary>
/// Gets a collection of users who are speakers within the stage.
/// </summary>
public IReadOnlyCollection<SocketGuildUser> Speakers
=> this.Users.Where(x => !x.IsSuppressed).ToImmutableArray();

internal new SocketStageChannel Clone() => MemberwiseClone() as SocketStageChannel;


internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
: base(discord, id, guild)
{

}

internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model)
{
var entity = new SocketStageChannel(guild.Discord, model.Id, guild);
entity.Update(state, model);
return entity;
}

internal override void Update(ClientState state, Model model)
{
base.Update(state, model);
}

internal void Update(StageInstance model, bool isLive = false)
{
this.Live = isLive;
if (isLive)
{
this.Topic = model.Topic;
this.PrivacyLevel = model.PrivacyLevel;
this.DiscoverableDisabled = model.DiscoverableDisabled;
}
else
{
this.Topic = null;
this.PrivacyLevel = null;
this.DiscoverableDisabled = null;
}
}

/// <inheritdoc/>
public async Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null)
{
var args = new API.Rest.CreateStageInstanceParams()
{
ChannelId = this.Id,
Topic = topic,
PrivacyLevel = privacyLevel,
};

var model = await Discord.ApiClient.CreateStageInstanceAsync(args, options).ConfigureAwait(false);

this.Update(model, true);
}

/// <inheritdoc/>
public async Task ModifyInstanceAsync(Action<StageInstanceProperties> func, RequestOptions options = null)
{
var model = await ChannelHelper.ModifyAsync(this, Discord, func, options);

this.Update(model, true);
}

/// <inheritdoc/>
public async Task StopStageAsync(RequestOptions options = null)
{
await Discord.ApiClient.DeleteStageInstanceAsync(this.Id, options);

Update(null, false);
}

/// <inheritdoc/>
public Task RequestToSpeak(RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams()
{
ChannelId = this.Id,
RequestToSpeakTimestamp = DateTimeOffset.UtcNow
};
return Discord.ApiClient.ModifyMyVoiceState(this.Guild.Id, args, options);
}
}
}

+ 25
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -271,6 +271,14 @@ namespace Discord.WebSocket
public IReadOnlyCollection<SocketVoiceChannel> VoiceChannels
=> Channels.OfType<SocketVoiceChannel>().ToImmutableArray();
/// <summary>
/// Gets a collection of all stage channels in this guild.
/// </summary>
/// <returns>
/// A read-only collection of stage channels found within this guild.
/// </returns>
public IReadOnlyCollection<SocketStageChannel> StageChannels
=> Channels.OfType<SocketStageChannel>().ToImmutableArray();
/// <summary>
/// Gets a collection of all category channels in this guild.
/// </summary>
/// <returns>
@@ -652,6 +660,15 @@ namespace Discord.WebSocket
public SocketVoiceChannel GetVoiceChannel(ulong id)
=> GetChannel(id) as SocketVoiceChannel;
/// <summary>
/// Gets a stage channel in this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the stage channel.</param>
/// <returns>
/// A stage channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketStageChannel GetStageChannel(ulong id)
=> GetChannel(id) as SocketStageChannel;
/// <summary>
/// Gets a category channel in this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the category channel.</param>
@@ -1354,6 +1371,12 @@ namespace Discord.WebSocket
Task<IVoiceChannel> IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IVoiceChannel>(GetVoiceChannel(id));
/// <inheritdoc />
Task<IStageChannel> IGuild.GetStageChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
=> Task.FromResult<IStageChannel>(GetStageChannel(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IStageChannel>> IGuild.GetStageChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
=> Task.FromResult<IReadOnlyCollection<IStageChannel>>(StageChannels);
/// <inheritdoc />
Task<IVoiceChannel> IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IVoiceChannel>(AFKChannel);
/// <inheritdoc />
@@ -1462,5 +1485,7 @@ namespace Discord.WebSocket
_audioLock?.Dispose();
_audioClient?.Dispose();
}

}
}

+ 3
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;
using Model = Discord.API.User;

@@ -66,5 +67,7 @@ namespace Discord.WebSocket
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
}
}

+ 4
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -57,7 +57,11 @@ namespace Discord.WebSocket
/// <inheritdoc />
public bool IsStreaming => VoiceState?.IsStreaming ?? false;
/// <inheritdoc />
public DateTimeOffset? RequestToSpeakTimestamp => VoiceState?.RequestToSpeakTimestamp ?? null;
/// <inheritdoc />
public bool? IsPending { get; private set; }

/// <inheritdoc />
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks);
/// <summary>


+ 4
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs View File

@@ -110,6 +110,10 @@ namespace Discord.WebSocket
public bool IsStreaming
=> GuildUser.IsStreaming;

/// <inheritdoc/>
public DateTimeOffset? RequestToSpeakTimestamp
=> GuildUser.RequestToSpeakTimestamp;

private SocketGuildUser GuildUser { get; set; }

internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member)


+ 7
- 3
src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs View File

@@ -13,7 +13,7 @@ namespace Discord.WebSocket
/// <summary>
/// Initializes a default <see cref="SocketVoiceState"/> with everything set to <c>null</c> or <c>false</c>.
/// </summary>
public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, false, false, false, false, false, false);
public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, null, false, false, false, false, false, false);

[Flags]
private enum Flags : byte
@@ -35,6 +35,8 @@ namespace Discord.WebSocket
public SocketVoiceChannel VoiceChannel { get; }
/// <inheritdoc />
public string VoiceSessionId { get; }
/// <inheritdoc/>
public DateTimeOffset? RequestToSpeakTimestamp { get; private set; }

/// <inheritdoc />
public bool IsMuted => (_voiceStates & Flags.Muted) != 0;
@@ -48,11 +50,13 @@ namespace Discord.WebSocket
public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0;
/// <inheritdoc />
public bool IsStreaming => (_voiceStates & Flags.SelfStream) != 0;

internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream)
internal SocketVoiceState(SocketVoiceChannel voiceChannel, DateTimeOffset? requestToSpeak, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream)
{
VoiceChannel = voiceChannel;
VoiceSessionId = sessionId;
RequestToSpeakTimestamp = requestToSpeak;

Flags voiceStates = Flags.Normal;
if (isSelfMuted)
@@ -71,7 +75,7 @@ namespace Discord.WebSocket
}
internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model)
{
return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream);
return new SocketVoiceState(voiceChannel, model.RequestToSpeakTimestamp.IsSpecified ? model.RequestToSpeakTimestamp.Value : null, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream);
}

/// <summary>


+ 2
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -138,5 +138,7 @@ namespace Discord.WebSocket
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
}
}

Loading…
Cancel
Save