Merge Labs 3.X into dev (#1923)

* meta: bump version

* Null or empty fix (#176)

* Add components and stickers to ReplyAsync extension

* Fixed null or empty

* Changed Label to Description

* -||-

Co-authored-by: quin lynch <lynchquin@gmail.com>

* More regions (#177)

* Preconditions

* ChannelHelper

* RestDMChannel

* RestGroupChannel

* RestBan

* RestGroupUser

* EntityExtensions

* DiscordSocketClient

* DiscordSocketClient

* Discord.net.core.xml fix (#178)

* Changed Label to Description

* Added Discord- .MessageComponent .ISticker[]

,Discord.MessageComponent,Discord.ISticker[] to ReplyAsync

* Remove references to labs

* Update Discord.Net.sln

* Added SendMessagesInThreads and StartEmbeddedActivities. (#175)

* Added SendMessagesInThreads and StartEmbeddedActivities.

Adjusted owner perms.
Change UsePublicThreads -> CreatePublicThreads
Change UsePrivateThreads -> CreatePrivateThreads

* removed extra ///

* Added UsePublicThreads and UsePrivateThreads back with Obsolete Attribute

* removed 'false' from Obsolete Attribute

* Squashed commit of the following:

commit dca41a348e36a9b4e7006ef3a76377eb32aad276
Author: quin lynch <lynchquin@gmail.com>
Date:   Thu Sep 23 07:02:19 2021 -0300

    Autocomplete commands

* meta: xml. closes #171

* Revert user agent and $device to dnet

* meta: bump version

* meta: bump vers

* Fix sticker args

* Grammer fix (#179)

* Made IVoiceChannel mentionable

* Embeds array for send message async (#181)

* meta: bump version

* meta: bump vers

* Fix sticker args

* Grammer fix (#179)

* Added embeds for SendMessageAsync

* [JsonProperty("embed")] forgot to remove this

 public Optional<Embed> Embed { get; set; }

* It has been done as requested.

* Changed the old way of handeling single embeds

* Moved embeds param and added options param

* xmls

Co-authored-by: quin lynch <lynchquin@gmail.com>

* Fix thread permissions (#183)

* Update GuildPermissionsTests.cs

* Update GuildPermissions.cs

* Use compound assignment (#186)

* Used compound assignment

* -||-

* -||-

* Remove unnecessary suppression (#188)

* Inlined variable declarations (#185)

* Fixed some warnings (#184)

* Fixed some warnings

* Another fixed warning

* Changed the SSendFileAsync to SendFileAsync

* Removed para AlwaysAcknowledgeInteractions

* Moved it back to the previous version

* Added periods to the end like quin requested!! :((

Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>

* Object initialization can be simplified fixed (#189)

* Conditional-expression-simplification (#193)

* Capitlazation fixes (#192)

* Removed-this. (#191)

* Use 'switch' expression (#187)

* Use 'switch' expression

* Reverted it to the old switch case

* Fixed-compiler-error (#194)

* Submitting updates to include new permissions. (#195)

* Submitting updates to include new permissions.

* Make old permissions obsolete and update tests

Co-authored-by: quin lynch <lynchquin@gmail.com>

* Update azure-pipelines.yml

* Update azure-pipelines.yml

* Update azure-pipelines.yml

* Add support for long in autocomplete option

* Add support for sending files with multiple embeds (#196)

* Add support for sending files with multiple embeds

* Simplify prepending single embed to embed array

* Consistency for embeds endpoints (#197)

* Changed the way of handling prepending of embeds.

For consistency.

* reformatted the summary

* Revert pipeline

* Fix duplicate merge conflicts

* Changed minimum slash command name length to 1 per Discord API docs (#198)

* Channel endpoints requirements correction (#199)

* Added some requirements to channels for topic

* Changed check from NotNullOrEmpty to NotNullOrEmpty

* Added some requirements to channels for name

Preconditions.LessThan

* Formatting of file

* Added restriction for description not being null (#200)

* Update azure-pipelines.yml

* Update deploy.yml

* Remove version tag from proj

* Update deploy.yml

* Removed versions from project files

* Removed style of the nuget badge and added logo (#201)

The style was not properly added to it and the plastic version does not look good with the discord badge.
I thought it would look better with a logo

* Fix Type not being set in SocketApplicationCommand

* Remove useless GuildId property

* meta: update XML

* Add Autocomplete to SlashCommandOptionBuilder

* Added autocomplete in SlashCommandOptionBuilder. (#206)

Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>

* Fix duplicate autocomplete

* Fix #208

* Fix sub commands being interpreted as a parameter for autocomplete

* Fix exposed optional

* Support the discord:// protocol in buttons (#207)

* Update UrlValidation.cs

* Update ComponentBuilder.cs

* Add docs and better error messages.

* Fix wonky intentation

* Add competing activity status type (#205)

* Update GuildPermissionsTests.cs

* Update GuildPermissions.cs

* Add competing status type

* Add Icons to IRole (#204)

* Added icon field to IRole

* Added GetGuildRoleIconUrl()

* Added Clean Content Function (#174)

* Added Clean Content Function

* Fixed Spelling problems and bad var handling

* Add StripMarkDown Method

* Clean Content Expanded (#212)

* Implement CleanContent In IMessage & RestMessage

* Update Spelling and Documentation

* Add SanatizeMessage to MessageHelper and Refactor Rest and Socket Message

* Add event for autocomplete interaction (#214)

* Spelling corrections (#215)

* Remove null collections

* Followup with file async warnings (#216)

* Changed from NotNullOrWhitespace to NotNullOrEmpty

* Added NotNullOrEmpty on filename

* Added system to interpret from the path

* Added a check for if it contains a period

* It has been done, how ever it will break stuff

* Changed to use ??= how ever still added error check

* Added space under check

* Changed from with a period to valid file extension

* Added checks for SendFileAsync

* Removed filename != null &&

* Add channel types in application command options. (#217)

* add channel types in application command options

* Indent Docs

* Stage instance audit logs as well as thread audit log type

* Update azure-pipelines.yml

* Update azure-pipelines.yml

* Fix system messages not including mentioned users. Added ContextMenuCommand message type

* Remove file extension check (#218)

* Fix NRE in modify guild channel

* Fix 429's not being accounted for in ratelimit updates

* meta: add net5 framework

Co-Authored-By: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>

* Proper doc logos (#221)

* Update GuildPermissionsTests.cs

* Update GuildPermissions.cs

* Add competing activity status type

* logo changes

* logo text as path

* add missing logo

* Update package logo and favicon

* Update docfx references

* Remove XML files and use original pipeline format

* Remove console writeline

* Remove Console.WriteLine

* Remove useless log

* Rename Available sticker field to IsAvailable

* Rename Available to IsAvailable in stickers

* Add summary indent for role members

* Add summary indent to SocketInvite

* Rename DefaultPermission to IsDefaultPermission

* Rename Default to IsDefault and Required to IsRequired in IApplicationCommandOption

* Rename Default and Required to IsDefault and IsRequired in IApplicationCommandOption. Rename DefaultPermission to IsDefaultPermission in IApplicationCommand

* Remove extra white spaces

* Renamed Joined, Archived, and Locked to HasJoined, IsArchived, and IsLocked

* Rename Live and DiscoverableDisabled to IsDiscoverableDisabled and IsLive in IStageChannel

* Remove newline

* Add indent to summaries

* Remove unnecessary json serializer field

* Fix ToEntity for roletags incorrectly using IsPremiumSubscriber

* Update RestChannel for new channel types

* Fix different rest channels not deserializing properly

* fully qualify internal for UrlValidation and add indent to summary

* Add missing periods to InteractionResponseType

* Fix summary in IApplicationCommandOptionChoice

* Update IApplicationCommandOption summaries

* Update IApplicationCommandInteractionDataOption summaries

* Update IApplicationCommandInteractionData summaries

* Update IApplicationCommand summaries

* Update ApplicationCommandType summaries

* rename DefaultPermission to IsDefaultPermission in ApplicationCommandProperties

* update ApplicationCommandOptionChoiceProperties summaries

* Rename Default, Required, and Autocomplete to IsDefault, IsRequired, and IsAutocomplete in ApplicationCommandOptionProperties

* Update SlashCommandProperties summaries

* update SlashCommandBuilder boolean field names, summaries, and choice parameters

* Update SelectMenuOption summaries, Rename Default to IsDefault in SelectMenuOption

* update SelectMenuComponent summaries. Rename Disabled to IsDisabled in SelectMenuComponent

* update ComponentBuilder summaries and boolean fields.

* Update ButtonComponent summaries and boolean fields

* update ActionRowComponent summaries

* Update UserCommandBuilder

* Update MessageCommandBuilder summaries and boolean properties

* Update IGuild summary

* Update IGuild summaries

* Update StagePrivacyLevel summary

* update IThreadChannel summaries

* Update IStageChannel summaries

* Refactor summaries and boolean property names

* General cleanup (#223)

* General cleanup

* Add Async suffix to SendAutocompleteResult

* Fix more formatting

* Fix unused RequestOptions in GetActiveThreadsAsync

* Add message to ArgumentNullException

* Ephemeral attachments

* Add missing jsonproperty attribute

* Add IMessage.Interaction

* Update attachment checks for embed urls

* meta: bump version

* Remove old package configs and update image

* Update package logos

* Fix logo reference for azure

* Deprecate old package definitions in favor for target file

* Deprecate old package definitions in favor for target file

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update package ids

* Fix url validation

* meta: bump version

* Fix assignment of UserMentions (#233)

* Fix CleanContent (#231)

* Fix SocketSlashCommandData access modifier. (#237)

Fixes #229

* Update README with better header (#232)

* Update README with better header

Adds HTML elements that implement the main logo & improve the redirection tag positions.

* Resolving border issue in light-mode

* Update sponsor section

* Implement checks for interaction respond times and multiple interaction responses. closes #236, #235

* Add response check to socket auto complete

* meta: bump versions

* Fix #239

* meta: bump version

* meta: update logo

* meta: bump versions

* Revert received at time, confirmed by discord staff to be accurate

* Merge branch 'release/3.x' of https://github.com/Discord-Net-Labs/Discord.Net-Labs into merger-labs

Update requested changes of obsolete and references to labs.

Added `Interaction` to `IMessage`
Fixed grammar
Fixed bugs relating to interactions.

* Update docs

* Update CHANGELOG.md

* meta: docs building

* Update docs.yml

* Update docs.yml

* Fix docfx version

* Update docs.yml

* Update docs.bat

* Rename docs repo for clone

* update docfx version

* Update docs.bat

* Update docfx version

* Remove docs from pipeline

* FAQ revamped, metadata updated (#241)

* FAQ revamped, metadata updated

* Update FAQ.md

* Update README.md

* Docs index improvement

* Fix InvalidOperationException in modify channel

* feature: guild avatars, closes #238

* feature: modify role icons

* meta: changelog

* meta: bump version

* Update README.md

* Fix non value type options not being included in autocomplete

* Add new activity flags (#254)

* Add new activity flags

* Add missing commas

* Added support for GUILD_JOIN_REQUEST_DELETE event (#253)

Fixes #247

* Adding BotHTTPInteraction user flag (#252)

* animated guild banner support (#255)

* Docs work (WIP) (#242)

* Main page work

* Metadata logo dir

* More main page edits

* Naming change

* Dnet guide entries pruned

* Add student hub guild directory channel (#256)

* animated guild banner support

* Add guild directory channel

* Fix followup with file overwrite having incorrect parameter locations

* Merge labs 3.x

* Update GUILD_JOIN_REQUEST_DELETE event

* Update head.tmpl.partial

* Removed BannerId and AccentColor  (#260)

* Removed BannerId property, GetBannerURL method, and AccentColor property from IUser and socket entities.

* Fixed errors in IUser.cs

* Added back summary for GetAvatarUrl method in IUser.cs

* Support Guild Boost Progress Bars (#262)

* Support Guild Boost Progress Bars

* Update SocketChannel.cs

* Fix non-optional and unnecessary values.

* Spelling

* Reordering and consistency.

* Remove log for reconnect

* Add missing flags to SystemChannelMessageDeny (#267)

* Fix labs reference in analyzer project and provider project

* Rename new activity flags

* Guild feature revamp and smart gateway intent checks

* Get thread user implementation

* Amend creating slash command guide (#269)

* Adding BotHTTPInteraction user flag

* Added comments explaining the Global command create stipulations.

* Fix numeric type check for options

* Add state checking to ConnectionManager.StartAsync (#272)

* initial interface changes

* Multi file upload + attachment editing

* meta: bump versions

* Update CHANGELOG.md

* Update CHANGELOG.md

* Support Min and Max values on ApplicationCommandOptions (#273)

* Support Min and Max values on ApplicationCommandOptions

* Support decimal min/max values

* Docs imrpovments + use ToNullable

* Logomark, doc settings edit (#258)

* Logomark, doc settings edit

* Replace standard logo

* Bumping docfx plugins to latest release

* Bump version metadata

* Logo svg fix

* Change default sticker behavior and add AlwaysResolveSticker to the config

* Implement rest based interactions. Added ED25519 checks. Updated summaries.

* Update package logo

* Automatically fix ordering of optional command options (#276)

* auto fix optional command option order

* clean up indentation

* Fix maximum number of Select Menu Options (#282)

As of https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure the maximum number of options is 25, not less than 25. Hopefully the change catches all necessary locations

* Add voice region to modify voice channels

* Update summaries on rest interactions

* Interaction Specific Interfaces (#283)

* added interaction specific interfaces

* fix build error

* implement change requests

* Update application

* Add Guild Scheduled Events (#279)

* guild events initial

* sharded events

* Add new gateway intents and fix bugs

* More work on new changes to guild events

* Update guild scheduled events

* Added events to extended guild and add event start event

* Update preconditions

* Implement breaking changes guild guild events. Add guild event permissions

* Update tests and change privacy level requirements

* Update summaries and add docs for guild events

* meta: bump version

* Increment meta version (#285)

* Increment meta version

* Update docfx.json

* Fix #289 and add configureawaits to rest based interactions

* meta: bump version

* Add GUILD_SCHEDULED_EVENT_USER_ADD and GUILD_SCHEDULED_EVENT_USER_REMOVE (#287)

* Remove newline

* Fix autocomplete result value

* meta: bump versions

* Add `GuildScheduledEventUserAdd` and `GuildScheduledEventUserRemove` to sharded client

* Make RestUserCommand public (#292)

* Fix Components not showing on FUWF (#288) (#293)

Adds Components to Payload JSON Generation

* Implement smarter rest resolvable interaction data. Fixes #294

* Add UseInteractionSnowflakeDate to config #286

* Implement Better Discord Errors (#291)

* Initial error parsing

* Implement better errors

* Add missing error codes

* Add voice disconnect opcodes

* Remove unused class, add summaries to discordjsonerror, and remove public constructor of slash command properties

* Add error code summary

* Update error message summary

* Update src/Discord.Net.Core/DiscordJsonError.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Fix autocomplete result value

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Change the minimum length of slash commands to 1 (#284)

* Change the minimum length of slash commands to 1. This is the correct value according to the docs and it has been changed after user feedback.

* Fix the limit in 3 other places

Co-authored-by: quin lynch <lynchquin@gmail.com>

* Add new thread creation properties

* Add role emoji. Fixes #295

* Fix mocked text channel

* Fix precondition checks. Closes #281

* Initial fix (#297)

* meta: bump version

* Update from release/3.x

* Remove more labs references

* Remove doc file for Discord.Net.Analyzers

Co-authored-by: Simon Hjorthøj <sh2@live.dk>
Co-authored-by: drobbins329 <drobbins329@gmail.com>
Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>
Co-authored-by: d4n3436 <dan3436@hotmail.com>
Co-authored-by: Will <WilliamWelsh@users.noreply.github.com>
Co-authored-by: Eugene Garbuzov <kkxo.mail@gmail.com>
Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com>
Co-authored-by: Emily <89871431+emillly-b@users.noreply.github.com>
Co-authored-by: marens101 <marens101@gmail.com>
Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>
Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>
Co-authored-by: Bill <billchirico@gmail.com>
Co-authored-by: Liege72 <65319395+Liege72@users.noreply.github.com>
Co-authored-by: Floowey <floowey@gmx.at>
Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Co-authored-by: exsersewo <exsersewo@systemexit.co.uk>
Co-authored-by: Dennis Fischer <fischer_dennis@live.de>
This commit is contained in:
Quin Lynch
2021-11-23 09:58:05 -04:00
committed by GitHub
parent 3395700720
commit 933ea42eaa
591 changed files with 34402 additions and 1465 deletions

View File

@@ -34,16 +34,19 @@ namespace Discord.WebSocket
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="messageReference">The message references to be included. Used to reply to specific messages.</param>
/// <param name="component">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the message.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null);
new Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[])"/>.
/// Please visit its documentation for more details on this method.
/// </remarks>
/// <param name="filePath">The file path of the file.</param>
@@ -57,16 +60,19 @@ namespace Discord.WebSocket
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="messageReference">The message references to be included. Used to reply to specific messages.</param>
/// <param name="component">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null);
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[])"/>.
/// Please visit its documentation for more details on this method.
/// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@@ -81,11 +87,14 @@ namespace Discord.WebSocket
/// If <c>null</c>, all mentioned roles and users will be notified.
/// </param>
/// <param name="messageReference">The message references to be included. Used to reply to specific messages.</param>
/// <param name="component">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null);
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null);
/// <summary>
/// Gets a cached message from this channel.

View File

@@ -14,6 +14,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel
{
#region SocketCategoryChannel
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuildUser> Users
=> Guild.Users.Where(x => Permissions.GetValue(
@@ -41,8 +42,9 @@ namespace Discord.WebSocket
entity.Update(state, model);
return entity;
}
#endregion
//Users
#region Users
/// <inheritdoc />
public override SocketGuildUser GetUser(ulong id)
{
@@ -59,21 +61,24 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"{Name} ({Id}, Category)";
internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel;
#endregion
// IGuildChannel
#region IGuildChannel
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable();
/// <inheritdoc />
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(GetUser(id));
#endregion
//IChannel
#region IChannel
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable();
/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(id));
#endregion
}
}

View File

@@ -13,6 +13,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class SocketChannel : SocketEntity<ulong>, IChannel
{
#region SocketChannel
/// <summary>
/// Gets when the channel is created.
/// </summary>
@@ -30,19 +31,17 @@ namespace Discord.WebSocket
/// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception>
internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model)
{
switch (model.Type)
return model.Type switch
{
case ChannelType.DM:
return SocketDMChannel.Create(discord, state, model);
case ChannelType.Group:
return SocketGroupChannel.Create(discord, state, model);
default:
throw new InvalidOperationException($"Unexpected channel type: {model.Type}");
}
ChannelType.DM => SocketDMChannel.Create(discord, state, model),
ChannelType.Group => SocketGroupChannel.Create(discord, state, model),
_ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"),
};
}
internal abstract void Update(ClientState state, Model model);
#endregion
//User
#region User
/// <summary>
/// Gets a generic user from this channel.
/// </summary>
@@ -56,8 +55,9 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"Unknown ({Id}, Channel)";
internal SocketChannel Clone() => MemberwiseClone() as SocketChannel;
#endregion
//IChannel
#region IChannel
/// <inheritdoc />
string IChannel.Name => null;
@@ -67,5 +67,6 @@ namespace Discord.WebSocket
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> AsyncEnumerable.Empty<IReadOnlyCollection<IUser>>(); //Overridden
#endregion
}
}

View File

@@ -70,6 +70,7 @@ namespace Discord.WebSocket
{
case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break;
case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break;
case SocketThreadChannel threadChannel: threadChannel.AddMessage(msg); break;
case SocketTextChannel textChannel: textChannel.AddMessage(msg); break;
default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type.");
}
@@ -78,13 +79,13 @@ namespace Discord.WebSocket
public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord,
ulong id)
{
switch (channel)
return channel switch
{
case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id);
case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id);
case SocketTextChannel textChannel: return textChannel.RemoveMessage(id);
default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type.");
}
SocketDMChannel dmChannel => dmChannel.RemoveMessage(id),
SocketGroupChannel groupChannel => groupChannel.RemoveMessage(id),
SocketTextChannel textChannel => textChannel.RemoveMessage(id),
_ => throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."),
};
}
}
}

View File

@@ -16,6 +16,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel
{
#region SocketDMChannel
/// <summary>
/// Gets the recipient of the channel.
/// </summary>
@@ -58,8 +59,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public Task CloseAsync(RequestOptions options = null)
=> ChannelHelper.DeleteAsync(this, Discord, options);
#endregion
//Messages
#region Messages
/// <inheritdoc />
public SocketMessage GetCachedMessage(ulong id)
=> null;
@@ -137,16 +139,25 @@ namespace Discord.WebSocket
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, options);
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> 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)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
=> ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options);
@@ -172,8 +183,9 @@ namespace Discord.WebSocket
{
return null;
}
#endregion
//Users
#region Users
/// <summary>
/// Gets a user in this channel from the provided <paramref name="id"/>.
/// </summary>
@@ -197,26 +209,31 @@ namespace Discord.WebSocket
public override string ToString() => $"@{Recipient}";
private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)";
internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel;
#endregion
//SocketChannel
#region SocketChannel
/// <inheritdoc />
internal override IReadOnlyCollection<SocketUser> GetUsersInternal() => Users;
/// <inheritdoc />
internal override SocketUser GetUserInternal(ulong id) => GetUser(id);
#endregion
//IDMChannel
#region IDMChannel
/// <inheritdoc />
IUser IDMChannel.Recipient => Recipient;
#endregion
//ISocketPrivateChannel
#region ISocketPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<SocketUser> ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient);
#endregion
//IPrivateChannel
#region IPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient);
#endregion
//IMessageChannel
#region IMessageChannel
/// <inheritdoc />
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
{
@@ -238,16 +255,23 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
#endregion
//IChannel
#region IChannel
/// <inheritdoc />
string IChannel.Name => $"@{Recipient}";
@@ -257,5 +281,6 @@ namespace Discord.WebSocket
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable();
#endregion
}
}

View File

@@ -20,6 +20,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel
{
#region SocketGroupChannel
private readonly MessageCache _messages;
private readonly ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
@@ -31,7 +32,15 @@ namespace Discord.WebSocket
/// <inheritdoc />
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();
/// <summary>
/// Returns a collection representing all of the users in the group.
/// </summary>
public new IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection();
/// <summary>
/// Returns a collection representing all users in the group, not including the client.
/// </summary>
public IReadOnlyCollection<SocketGroupUser> Recipients
=> _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1);
@@ -76,8 +85,9 @@ namespace Discord.WebSocket
{
throw new NotSupportedException("Voice is not yet supported for group channels.");
}
#endregion
//Messages
#region Messages
/// <inheritdoc />
public SocketMessage GetCachedMessage(ulong id)
=> _messages?.Get(id);
@@ -163,15 +173,24 @@ namespace Discord.WebSocket
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, options);
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
public Task<RestUserMessage> 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)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -195,8 +214,9 @@ namespace Discord.WebSocket
=> _messages?.Add(msg);
internal SocketMessage RemoveMessage(ulong id)
=> _messages?.Remove(id);
#endregion
//Users
#region Users
/// <summary>
/// Gets a user from this group.
/// </summary>
@@ -231,8 +251,9 @@ namespace Discord.WebSocket
}
return null;
}
#endregion
//Voice States
#region Voice States
internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model)
{
var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel;
@@ -259,22 +280,26 @@ namespace Discord.WebSocket
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, Group)";
internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel;
#endregion
//SocketChannel
#region SocketChannel
/// <inheritdoc />
internal override IReadOnlyCollection<SocketUser> GetUsersInternal() => Users;
/// <inheritdoc />
internal override SocketUser GetUserInternal(ulong id) => GetUser(id);
#endregion
//ISocketPrivateChannel
#region ISocketPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<SocketUser> ISocketPrivateChannel.Recipients => Recipients;
#endregion
//IPrivateChannel
#region IPrivateChannel
/// <inheritdoc />
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => Recipients;
#endregion
//IMessageChannel
#region IMessageChannel
/// <inheritdoc />
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
{
@@ -297,27 +322,37 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
//IAudioChannel
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
#endregion
#region IAudioChannel
/// <inheritdoc />
/// <exception cref="NotSupportedException">Connecting to a group channel is not supported.</exception>
Task<IAudioClient> IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); }
Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); }
#endregion
//IChannel
#region IChannel
/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(id));
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable();
#endregion
}
}

View File

@@ -15,6 +15,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildChannel : SocketChannel, IGuildChannel
{
#region SocketGuildChannel
private ImmutableArray<Overwrite> _overwrites;
/// <summary>
@@ -27,7 +28,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public int Position { get; private set; }
public int Position { get; private set; }
/// <inheritdoc />
public virtual IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites;
@@ -46,27 +47,24 @@ namespace Discord.WebSocket
}
internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model)
{
switch (model.Type)
return model.Type switch
{
case ChannelType.News:
return SocketNewsChannel.Create(guild, state, model);
case ChannelType.Text:
return SocketTextChannel.Create(guild, state, model);
case ChannelType.Voice:
return SocketVoiceChannel.Create(guild, state, model);
case ChannelType.Category:
return SocketCategoryChannel.Create(guild, state, model);
default:
return new SocketGuildChannel(guild.Discord, model.Id, guild);
}
ChannelType.News => SocketNewsChannel.Create(guild, state, model),
ChannelType.Text => SocketTextChannel.Create(guild, state, model),
ChannelType.Voice => SocketVoiceChannel.Create(guild, state, model),
ChannelType.Category => SocketCategoryChannel.Create(guild, state, model),
ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.Create(guild, state, model),
ChannelType.Stage => SocketStageChannel.Create(guild, state, model),
_ => new SocketGuildChannel(guild.Discord, model.Id, guild),
};
}
/// <inheritdoc />
internal override void Update(ClientState state, Model model)
{
Name = model.Name.Value;
Position = model.Position.Value;
var overwrites = model.PermissionOverwrites.Value;
Position = model.Position.GetValueOrDefault(0);
var overwrites = model.PermissionOverwrites.GetValueOrDefault(new API.Overwrite[0]);
var newOverwrites = ImmutableArray.CreateBuilder<Overwrite>(overwrites.Length);
for (int i = 0; i < overwrites.Length; i++)
newOverwrites.Add(overwrites[i].ToEntity());
@@ -176,14 +174,16 @@ namespace Discord.WebSocket
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, Guild)";
internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel;
#endregion
//SocketChannel
#region SocketChannel
/// <inheritdoc />
internal override IReadOnlyCollection<SocketUser> GetUsersInternal() => Users;
/// <inheritdoc />
internal override SocketUser GetUserInternal(ulong id) => GetUser(id);
#endregion
//IGuildChannel
#region IGuildChannel
/// <inheritdoc />
IGuild IGuildChannel.Guild => Guild;
/// <inheritdoc />
@@ -214,13 +214,15 @@ namespace Discord.WebSocket
/// <inheritdoc />
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(GetUser(id));
#endregion
//IChannel
#region IChannel
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice
/// <inheritdoc />
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IUser>(GetUser(id)); //Overridden in Text/Voice
#endregion
}
}

View File

@@ -0,0 +1,158 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using StageInstance = Discord.API.StageInstance;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a stage channel received 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? IsDiscoverableDisabled { get; private set; }
/// <inheritdoc/>
public bool IsLive { get; private set; }
/// <summary>
/// Returns <see langword="true"/> if the current user is a speaker within the stage, otherwise <see langword="false"/>.
/// </summary>
public bool IsSpeaker
=> !Guild.CurrentUser.IsSuppressed;
/// <summary>
/// Gets a collection of users who are speakers within the stage.
/// </summary>
public IReadOnlyCollection<SocketGuildUser> Speakers
=> 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 void Update(StageInstance model, bool isLive = false)
{
IsLive = isLive;
if (isLive)
{
Topic = model.Topic;
PrivacyLevel = model.PrivacyLevel;
IsDiscoverableDisabled = model.DiscoverableDisabled;
}
else
{
Topic = null;
PrivacyLevel = null;
IsDiscoverableDisabled = null;
}
}
/// <inheritdoc/>
public async Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null)
{
var args = new API.Rest.CreateStageInstanceParams
{
ChannelId = Id,
Topic = topic,
PrivacyLevel = privacyLevel
};
var model = await Discord.ApiClient.CreateStageInstanceAsync(args, options).ConfigureAwait(false);
Update(model, true);
}
/// <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 StopStageAsync(RequestOptions options = null)
{
await Discord.ApiClient.DeleteStageInstanceAsync(Id, options);
Update(null);
}
/// <inheritdoc/>
public Task RequestToSpeakAsync(RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams
{
ChannelId = Id,
RequestToSpeakTimestamp = DateTimeOffset.UtcNow
};
return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options);
}
/// <inheritdoc/>
public Task BecomeSpeakerAsync(RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams
{
ChannelId = Id,
Suppressed = false
};
return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options);
}
/// <inheritdoc/>
public Task StopSpeakingAsync(RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams
{
ChannelId = Id,
Suppressed = true
};
return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options);
}
/// <inheritdoc/>
public Task MoveToSpeakerAsync(IGuildUser user, RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams
{
ChannelId = Id,
Suppressed = false
};
return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args);
}
/// <inheritdoc/>
public Task RemoveFromSpeakerAsync(IGuildUser user, RequestOptions options = null)
{
var args = new API.Rest.ModifyVoiceStateParams
{
ChannelId = Id,
Suppressed = true
};
return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args);
}
}
}

View File

@@ -16,6 +16,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel
{
#region SocketTextChannel
private readonly MessageCache _messages;
/// <inheritdoc />
@@ -50,6 +51,12 @@ namespace Discord.WebSocket
Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)),
ChannelPermission.ViewChannel)).ToImmutableArray();
/// <summary>
/// Gets a collection of threads within this text channel.
/// </summary>
public IReadOnlyCollection<SocketThreadChannel> Threads
=> Guild.ThreadChannels.Where(x => x.ParentChannel.Id == Id).ToImmutableArray();
internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
: base(discord, id, guild)
{
@@ -66,16 +73,59 @@ namespace Discord.WebSocket
{
base.Update(state, model);
CategoryId = model.CategoryId;
Topic = model.Topic.Value;
Topic = model.Topic.GetValueOrDefault();
SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet?
_nsfw = model.Nsfw.GetValueOrDefault();
}
/// <inheritdoc />
public Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)
public virtual Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)
=> ChannelHelper.ModifyAsync(this, Discord, func, options);
//Messages
/// <summary>
/// Creates a thread within this <see cref="ITextChannel"/>.
/// </summary>
/// <remarks>
/// When <paramref name="message"/> is <see langword="null"/> the thread type will be based off of the
/// channel its created in. When called on a <see cref="ITextChannel"/>, it creates a <see cref="ThreadType.PublicThread"/>.
/// When called on a <see cref="INewsChannel"/>, it creates a <see cref="ThreadType.NewsThread"/>. The id of the created
/// thread will be the same as the id of the message, and as such a message can only have a
/// single thread created from it.
/// </remarks>
/// <param name="name">The name of the thread.</param>
/// <param name="type">
/// The type of the thread.
/// <para>
/// <b>Note: </b>This parameter is not used if the <paramref name="message"/> parameter is not specified.
/// </para>
/// </param>
/// <param name="autoArchiveDuration">
/// The duration on which this thread archives after.
/// <para>
/// <b>Note: </b> Options <see cref="ThreadArchiveDuration.OneWeek"/> and <see cref="ThreadArchiveDuration.ThreeDays"/>
/// are only available for guilds that are boosted. You can check in the <see cref="IGuild.Features"/> to see if the
/// guild has the <b>THREE_DAY_THREAD_ARCHIVE</b> and <b>SEVEN_DAY_THREAD_ARCHIVE</b>.
/// </para>
/// </param>
/// <param name="message">The message which to start the thread from.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous create operation. The task result contains a <see cref="IThreadChannel"/>
/// </returns>
public async Task<SocketThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread,
ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null)
{
var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options);
var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model);
await thread.DownloadUsersAsync();
return thread;
}
#endregion
#region Messages
/// <inheritdoc />
public SocketMessage GetCachedMessage(ulong id)
=> _messages?.Get(id);
@@ -161,17 +211,27 @@ namespace Discord.WebSocket
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, options);
public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> 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)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, options, isSpoiler);
public Task<RestUserMessage> 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, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null)
=> ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds);
/// <inheritdoc />
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
@@ -202,8 +262,9 @@ namespace Discord.WebSocket
=> _messages?.Add(msg);
internal SocketMessage RemoveMessage(ulong id)
=> _messages?.Remove(id);
#endregion
//Users
#region Users
/// <inheritdoc />
public override SocketGuildUser GetUser(ulong id)
{
@@ -217,8 +278,9 @@ namespace Discord.WebSocket
}
return null;
}
#endregion
//Webhooks
#region Webhooks
/// <summary>
/// Creates a webhook in this text channel.
/// </summary>
@@ -229,7 +291,7 @@ namespace Discord.WebSocket
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// webhook.
/// </returns>
public Task<RestWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null)
public virtual Task<RestWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null)
=> ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options);
/// <summary>
/// Gets a webhook available in this text channel.
@@ -240,7 +302,7 @@ namespace Discord.WebSocket
/// A task that represents the asynchronous get operation. The task result contains a webhook associated
/// with the identifier; <c>null</c> if the webhook is not found.
/// </returns>
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
public virtual Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> ChannelHelper.GetWebhookAsync(this, Discord, id, options);
/// <summary>
/// Gets the webhooks available in this text channel.
@@ -250,21 +312,29 @@ namespace Discord.WebSocket
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of webhooks that is available in this channel.
/// </returns>
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
public virtual Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> ChannelHelper.GetWebhooksAsync(this, Discord, options);
#endregion
//Invites
#region Invites
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);
private string DebuggerDisplay => $"{Name} ({Id}, Text)";
internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel;
#endregion
//ITextChannel
#region ITextChannel
/// <inheritdoc />
async Task<IWebhook> ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options)
=> await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false);
@@ -274,16 +344,21 @@ namespace Discord.WebSocket
/// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> ITextChannel.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IThreadChannel> ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, bool? invitable, int? slowmode, RequestOptions options)
=> await CreateThreadAsync(name, type, autoArchiveDuration, message, invitable, slowmode, options);
#endregion
//IGuildChannel
#region IGuildChannel
/// <inheritdoc />
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(GetUser(id));
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable();
#endregion
//IMessageChannel
#region IMessageChannel
/// <inheritdoc />
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options)
{
@@ -306,18 +381,26 @@ namespace Discord.WebSocket
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds)
=> await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false);
#endregion
// INestedChannel
#region INestedChannel
/// <inheritdoc />
Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult(Category);
#endregion
}
}

View File

@@ -0,0 +1,339 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
using ThreadMember = Discord.API.ThreadMember;
using System.Collections.Concurrent;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a thread channel inside of a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketThreadChannel : SocketTextChannel, IThreadChannel
{
/// <inheritdoc/>
public ThreadType Type { get; private set; }
/// <summary>
/// Gets the owner of the current thread.
/// </summary>
public SocketThreadUser Owner { get; private set; }
/// <summary>
/// Gets the current users within this thread.
/// </summary>
public SocketThreadUser CurrentUser
=> Users.FirstOrDefault(x => x.Id == Discord.CurrentUser.Id);
/// <inheritdoc/>
public bool HasJoined { get; private set; }
/// <summary>
/// <see langword="true"/> if this thread is private, otherwise <see langword="false"/>
/// </summary>
public bool IsPrivateThread
=> Type == ThreadType.PrivateThread;
/// <summary>
/// Gets the parent channel this thread resides in.
/// </summary>
public SocketTextChannel ParentChannel { get; private set; }
/// <inheritdoc/>
public int MessageCount { get; private set; }
/// <inheritdoc/>
public int MemberCount { get; private set; }
/// <inheritdoc/>
public bool IsArchived { get; private set; }
/// <inheritdoc/>
public DateTimeOffset ArchiveTimestamp { get; private set; }
/// <inheritdoc/>
public ThreadArchiveDuration AutoArchiveDuration { get; private set; }
/// <inheritdoc/>
public bool IsLocked { get; private set; }
/// <summary>
/// Gets a collection of cached users within this thread.
/// </summary>
public new IReadOnlyCollection<SocketThreadUser> Users =>
_members.Values.ToImmutableArray();
private readonly ConcurrentDictionary<ulong, SocketThreadUser> _members;
private string DebuggerDisplay => $"{Name} ({Id}, Thread)";
private bool _usersDownloaded;
private readonly object _downloadLock = new object();
internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent)
: base(discord, id, guild)
{
ParentChannel = parent;
_members = new ConcurrentDictionary<ulong, SocketThreadUser>();
}
internal new 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);
Type = (ThreadType)model.Type;
MessageCount = model.MessageCount.GetValueOrDefault(-1);
MemberCount = model.MemberCount.GetValueOrDefault(-1);
if (model.ThreadMetadata.IsSpecified)
{
IsArchived = model.ThreadMetadata.Value.Archived;
ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp;
AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration;
IsLocked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false);
}
if (model.OwnerId.IsSpecified)
{
Owner = GetUser(model.OwnerId.Value);
}
HasJoined = model.ThreadMember.IsSpecified;
}
internal IReadOnlyCollection<SocketThreadUser> RemoveUsers(ulong[] users)
{
List<SocketThreadUser> threadUsers = new();
foreach (var userId in users)
{
if (_members.TryRemove(userId, out var user))
threadUsers.Add(user);
}
return threadUsers.ToImmutableArray();
}
internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuildUser guildMember)
{
if (_members.TryGetValue(model.UserId.Value, out SocketThreadUser member))
member.Update(model);
else
{
member = SocketThreadUser.Create(Guild, this, model, guildMember);
member.GlobalUser.AddRef();
_members[member.Id] = member;
}
return member;
}
/// <inheritdoc />
public new SocketThreadUser GetUser(ulong id)
{
var user = Users.FirstOrDefault(x => x.Id == id);
return user;
}
/// <summary>
/// Gets all users inside this thread.
/// </summary>
/// <remarks>
/// If all users are not downloaded then this method will call <see cref="DownloadUsersAsync(RequestOptions)"/> and return the result.
/// </remarks>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task representing the download operation.</returns>
public async Task<IReadOnlyCollection<SocketThreadUser>> GetUsersAsync(RequestOptions options = null)
{
// download all users if we havent
if (!_usersDownloaded)
{
await DownloadUsersAsync(options);
_usersDownloaded = true;
}
return Users;
}
/// <summary>
/// Downloads all users that have access to this thread.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task representing the asynchronous download operation.</returns>
public async Task DownloadUsersAsync(RequestOptions options = null)
{
var users = await Discord.ApiClient.ListThreadMembersAsync(Id, options);
lock (_downloadLock)
{
foreach (var threadMember in users)
{
var guildUser = Guild.GetUser(threadMember.UserId.Value);
AddOrUpdateThreadMember(threadMember, guildUser);
}
}
}
internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel;
/// <inheritdoc/>
public Task JoinAsync(RequestOptions options = null)
=> Discord.ApiClient.JoinThreadAsync(Id, options);
/// <inheritdoc/>
public Task LeaveAsync(RequestOptions options = null)
=> Discord.ApiClient.LeaveThreadAsync(Id, options);
/// <summary>
/// Adds a user to this thread.
/// </summary>
/// <param name="user">The <see cref="IGuildUser"/> to add.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous operation of adding a member to a thread.
/// </returns>
public Task AddUserAsync(IGuildUser user, RequestOptions options = null)
=> Discord.ApiClient.AddThreadMemberAsync(Id, user.Id, options);
/// <summary>
/// Removes a user from this thread.
/// </summary>
/// <param name="user">The <see cref="IGuildUser"/> to remove from this thread.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous operation of removing a user from this thread.
/// </returns>
public Task RemoveUserAsync(IGuildUser user, RequestOptions options = null)
=> Discord.ApiClient.RemoveThreadMemberAsync(Id, user.Id, options);
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<RestWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override OverwritePermissions? GetPermissionOverwrite(IRole role)
=> ParentChannel.GetPermissionOverwrite(role);
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override OverwritePermissions? GetPermissionOverwrite(IUser user)
=> ParentChannel.GetPermissionOverwrite(user);
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)
=> ThreadHelper.ModifyAsync(this, Discord, func, options);
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override IReadOnlyCollection<Overwrite> PermissionOverwrites
=> throw new NotSupportedException("This method is not supported in threads.");
/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task SyncPermissionsAsync(RequestOptions options = null)
=> throw new NotSupportedException("This method is not supported in threads.");
string IChannel.Name => Name;
}
}

View File

@@ -16,6 +16,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel
{
#region SocketVoiceChannel
/// <inheritdoc />
public int Bitrate { get; private set; }
/// <inheritdoc />
@@ -89,29 +90,39 @@ namespace Discord.WebSocket
return user;
return null;
}
#endregion
//Invites
#region Invites
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);
private string DebuggerDisplay => $"{Name} ({Id}, Voice)";
internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel;
#endregion
//IGuildChannel
#region IGuildChannel
/// <inheritdoc />
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(GetUser(id));
/// <inheritdoc />
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options)
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable();
#endregion
// INestedChannel
#region INestedChannel
/// <inheritdoc />
Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult(Category);
#endregion
}
}

View File

@@ -19,6 +19,9 @@ using PresenceModel = Discord.API.Presence;
using RoleModel = Discord.API.Role;
using UserModel = Discord.API.User;
using VoiceStateModel = Discord.API.VoiceState;
using StickerModel = Discord.API.Sticker;
using EventModel = Discord.API.GuildScheduledEvent;
using System.IO;
namespace Discord.WebSocket
{
@@ -28,16 +31,19 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuild : SocketEntity<ulong>, IGuild, IDisposable
{
#region SocketGuild
#pragma warning disable IDISP002, IDISP006
private readonly SemaphoreSlim _audioLock;
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise;
private TaskCompletionSource<AudioClient> _audioConnectPromise;
private ConcurrentHashSet<ulong> _channels;
private ConcurrentDictionary<ulong, SocketGuildChannel> _channels;
private ConcurrentDictionary<ulong, SocketGuildUser> _members;
private ConcurrentDictionary<ulong, SocketRole> _roles;
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers;
private ConcurrentDictionary<ulong, SocketGuildEvent> _events;
private ImmutableArray<GuildEmote> _emotes;
private ImmutableArray<string> _features;
private AudioClient _audioClient;
#pragma warning restore IDISP002, IDISP006
@@ -118,9 +124,14 @@ namespace Discord.WebSocket
public int? MaxMembers { get; private set; }
/// <inheritdoc />
public int? MaxVideoChannelUsers { get; private set; }
/// <inheritdoc />
public NsfwLevel NsfwLevel { get; private set; }
/// <inheritdoc />
public CultureInfo PreferredCulture { get; private set; }
/// <inheritdoc />
public bool IsBoostProgressBarEnabled { get; private set; }
/// <inheritdoc />
public GuildFeatures Features { get; private set; }
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
@@ -131,7 +142,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId);
/// <inheritdoc />
public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId);
public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto);
/// <summary> Indicates whether the client has all the members downloaded to the local guild cache. </summary>
public bool HasAllMembers => MemberCount <= DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted;
/// <summary> Indicates whether the guild cache is synced to this guild. </summary>
@@ -269,6 +280,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>
@@ -277,6 +296,14 @@ namespace Discord.WebSocket
public IReadOnlyCollection<SocketCategoryChannel> CategoryChannels
=> Channels.OfType<SocketCategoryChannel>().ToImmutableArray();
/// <summary>
/// Gets a collection of all thread channels in this guild.
/// </summary>
/// <returns>
/// A read-only collection of thread channels found within this guild.
/// </returns>
public IReadOnlyCollection<SocketThreadChannel> ThreadChannels
=> Channels.OfType<SocketThreadChannel>().ToImmutableArray();
/// <summary>
/// Gets the current logged-in user.
/// </summary>
public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null;
@@ -299,13 +326,16 @@ namespace Discord.WebSocket
{
var channels = _channels;
var state = Discord.State;
return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels);
return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels);
}
}
/// <inheritdoc />
public IReadOnlyCollection<GuildEmote> Emotes => _emotes;
/// <inheritdoc />
public IReadOnlyCollection<string> Features => _features;
/// <summary>
/// Gets a collection of all custom stickers for this guild.
/// </summary>
public IReadOnlyCollection<SocketCustomSticker> Stickers
=> _stickers.Select(x => x.Value).ToImmutableArray();
/// <summary>
/// Gets a collection of users in this guild.
/// </summary>
@@ -336,12 +366,22 @@ namespace Discord.WebSocket
/// </returns>
public IReadOnlyCollection<SocketRole> Roles => _roles.ToReadOnlyCollection();
/// <summary>
/// Gets a collection of all events within this guild.
/// </summary>
/// <remarks>
/// This field is based off of caching alone, since there is no events returned on the guild model.
/// </remarks>
/// <returns>
/// A read-only collection of guild events found within this guild.
/// </returns>
public IReadOnlyCollection<SocketGuildEvent> Events => _events.ToReadOnlyCollection();
internal SocketGuild(DiscordSocketClient client, ulong id)
: base(client, id)
{
_audioLock = new SemaphoreSlim(1, 1);
_emotes = ImmutableArray.Create<GuildEmote>();
_features = ImmutableArray.Create<string>();
}
internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model)
{
@@ -354,8 +394,10 @@ namespace Discord.WebSocket
IsAvailable = !(model.Unavailable ?? false);
if (!IsAvailable)
{
if(_events == null)
_events = new ConcurrentDictionary<ulong, SocketGuildEvent>();
if (_channels == null)
_channels = new ConcurrentHashSet<ulong>();
_channels = new ConcurrentDictionary<ulong, SocketGuildChannel>();
if (_members == null)
_members = new ConcurrentDictionary<ulong, SocketGuildUser>();
if (_roles == null)
@@ -371,15 +413,23 @@ namespace Discord.WebSocket
Update(state, model as Model);
var channels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05));
var channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05));
{
for (int i = 0; i < model.Channels.Length; i++)
{
var channel = SocketGuildChannel.Create(this, state, model.Channels[i]);
state.AddChannel(channel);
channels.TryAdd(channel.Id);
channels.TryAdd(channel.Id, channel);
}
for(int i = 0; i < model.Threads.Length; i++)
{
var threadChannel = SocketThreadChannel.Create(this, state, model.Threads[i]);
state.AddChannel(threadChannel);
channels.TryAdd(threadChannel.Id, threadChannel);
}
}
_channels = channels;
var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05));
@@ -414,6 +464,17 @@ namespace Discord.WebSocket
}
_voiceStates = voiceStates;
var events = new ConcurrentDictionary<ulong, SocketGuildEvent>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.GuildScheduledEvents.Length * 1.05));
{
for (int i = 0; i < model.GuildScheduledEvents.Length; i++)
{
var guildEvent = SocketGuildEvent.Create(Discord, this, model.GuildScheduledEvents[i]);
events.TryAdd(guildEvent.Id, guildEvent);
}
}
_events = events;
_syncPromise = new TaskCompletionSource<bool>();
_downloaderPromise = new TaskCompletionSource<bool>();
var _ = _syncPromise.TrySetResultAsync(true);
@@ -448,6 +509,7 @@ namespace Discord.WebSocket
SystemChannelFlags = model.SystemChannelFlags;
Description = model.Description;
PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault();
NsfwLevel = model.NsfwLevel;
if (model.MaxPresences.IsSpecified)
MaxPresences = model.MaxPresences.Value ?? 25000;
if (model.MaxMembers.IsSpecified)
@@ -456,7 +518,8 @@ namespace Discord.WebSocket
MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value;
PreferredLocale = model.PreferredLocale;
PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale);
if (model.IsBoostProgressBarEnabled.IsSpecified)
IsBoostProgressBarEnabled = model.IsBoostProgressBarEnabled.Value;
if (model.Emojis != null)
{
var emojis = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length);
@@ -467,10 +530,7 @@ namespace Discord.WebSocket
else
_emotes = ImmutableArray.Create<GuildEmote>();
if (model.Features != null)
_features = model.Features.ToImmutableArray();
else
_features = ImmutableArray.Create<string>();
Features = model.Features;
var roles = new ConcurrentDictionary<ulong, SocketRole>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05));
if (model.Roles != null)
@@ -482,6 +542,25 @@ namespace Discord.WebSocket
}
}
_roles = roles;
if (model.Stickers != null)
{
var stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Stickers.Length * 1.05));
for (int i = 0; i < model.Stickers.Length; i++)
{
var sticker = model.Stickers[i];
if (sticker.User.IsSpecified)
AddOrUpdateUser(sticker.User.Value);
var entity = SocketCustomSticker.Create(Discord, sticker, this, sticker.User.IsSpecified ? sticker.User.Value.Id : null);
stickers.TryAdd(sticker.Id, entity);
}
_stickers = stickers;
}
else
_stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7);
}
/*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related
{
@@ -514,8 +593,9 @@ namespace Discord.WebSocket
emotes.Add(model.Emojis[i].ToEntity());
_emotes = emotes.ToImmutable();
}
#endregion
//General
#region General
/// <inheritdoc />
public Task DeleteAsync(RequestOptions options = null)
=> GuildHelper.DeleteAsync(this, Discord, options);
@@ -539,8 +619,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public Task LeaveAsync(RequestOptions options = null)
=> GuildHelper.LeaveAsync(this, Discord, options);
#endregion
//Bans
#region Bans
/// <summary>
/// Gets a collection of all users banned in this guild.
/// </summary>
@@ -588,8 +669,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public Task RemoveBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.RemoveBanAsync(this, Discord, userId, options);
#endregion
//Channels
#region Channels
/// <summary>
/// Gets a channel in this guild.
/// </summary>
@@ -614,6 +696,16 @@ namespace Discord.WebSocket
public SocketTextChannel GetTextChannel(ulong id)
=> GetChannel(id) as SocketTextChannel;
/// <summary>
/// Gets a thread in this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the thread.</param>
/// <returns>
/// A thread channel associated with the specified <paramref name="id" />; <see langword="null"/> if none is found.
/// </returns>
public SocketThreadChannel GetThreadChannel(ulong id)
=> GetChannel(id) as SocketThreadChannel;
/// <summary>
/// Gets a voice channel in this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the voice channel.</param>
@@ -623,6 +715,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>
@@ -670,6 +771,19 @@ namespace Discord.WebSocket
/// </returns>
public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func);
/// <summary>
/// Creates a new stage channel in this guild.
/// </summary>
/// <param name="name">The new name for the stage channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// stage channel.
/// </returns>
public Task<RestStageChannel> CreateStageChannelAsync(string name, Action<VoiceChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func);
/// <summary>
/// Creates a new channel category in this guild.
/// </summary>
@@ -687,25 +801,40 @@ namespace Discord.WebSocket
internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model)
{
var channel = SocketGuildChannel.Create(this, state, model);
_channels.TryAdd(model.Id);
_channels.TryAdd(model.Id, channel);
state.AddChannel(channel);
return channel;
}
internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model)
{
if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel))
channel.Update(Discord.State, model);
else
{
channel = SocketGuildChannel.Create(this, Discord.State, model);
_channels[channel.Id] = channel;
state.AddChannel(channel);
}
return channel;
}
internal SocketGuildChannel RemoveChannel(ClientState state, ulong id)
{
if (_channels.TryRemove(id))
if (_channels.TryRemove(id, out var _))
return state.RemoveChannel(id) as SocketGuildChannel;
return null;
}
internal void PurgeChannelCache(ClientState state)
{
foreach (var channelId in _channels)
state.RemoveChannel(channelId);
state.RemoveChannel(channelId.Key);
_channels.Clear();
}
#endregion
//Voice Regions
#region Voice Regions
/// <summary>
/// Gets a collection of all the voice regions this guild can access.
/// </summary>
@@ -716,14 +845,124 @@ namespace Discord.WebSocket
/// </returns>
public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null)
=> GuildHelper.GetVoiceRegionsAsync(this, Discord, options);
#endregion
//Integrations
#region Integrations
public Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null)
=> GuildHelper.GetIntegrationsAsync(this, Discord, options);
public Task<RestGuildIntegration> CreateIntegrationAsync(ulong id, string type, RequestOptions options = null)
=> GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options);
#endregion
//Invites
#region Interactions
/// <summary>
/// Deletes all application commands in the current guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous delete operation.
/// </returns>
public Task DeleteApplicationCommandsAsync(RequestOptions options = null)
=> InteractionHelper.DeleteAllGuildCommandsAsync(Discord, Id, options);
/// <summary>
/// Gets a collection of slash commands created by the current user in this guild.
/// </summary>
/// <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
/// slash commands created by the current user.
/// </returns>
public async Task<IReadOnlyCollection<SocketApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null)
{
var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id));
foreach (var command in commands)
{
Discord.State.AddCommand(command);
}
return commands.ToImmutableArray();
}
/// <summary>
/// Gets an application command within this guild with the specified id.
/// </summary>
/// <param name="id">The id of the application command to get.</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 ValueTask that represents the asynchronous get operation. The task result contains a <see cref="IApplicationCommand"/>
/// if found, otherwise <see langword="null"/>.
/// </returns>
public async ValueTask<SocketApplicationCommand> GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
{
var command = Discord.State.GetCommand(id);
if (command != null)
return command;
if (mode == CacheMode.CacheOnly)
return null;
var model = await Discord.ApiClient.GetGlobalApplicationCommandAsync(id, options);
if (model == null)
return null;
command = SocketApplicationCommand.Create(Discord, model, Id);
Discord.State.AddCommand(command);
return command;
}
/// <summary>
/// Creates an application command within this guild.
/// </summary>
/// <param name="properties">The properties to use when creating the command.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the command that was created.
/// </returns>
public async Task<SocketApplicationCommand> CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null)
{
var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options);
var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model));
entity.Update(model);
return entity;
}
/// <summary>
/// Overwrites the application commands within this guild.
/// </summary>
/// <param name="properties">A collection of properties to use when creating the commands.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created.
/// </returns>
public async Task<IReadOnlyCollection<SocketApplicationCommand>> BulkOverwriteApplicationCommandAsync(ApplicationCommandProperties[] properties,
RequestOptions options = null)
{
var models = await InteractionHelper.BulkOverwriteGuildCommandsAsync(Discord, Id, properties, options);
var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x));
Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id);
foreach(var entity in entities)
{
Discord.State.AddCommand(entity);
}
return entities.ToImmutableArray();
}
#endregion
#region Invites
/// <summary>
/// Gets a collection of all invites in this guild.
/// </summary>
@@ -744,8 +983,9 @@ namespace Discord.WebSocket
/// </returns>
public Task<RestInviteMetadata> GetVanityInviteAsync(RequestOptions options = null)
=> GuildHelper.GetVanityInviteAsync(this, Discord, options);
#endregion
//Roles
#region Roles
/// <summary>
/// Gets a role in this guild.
/// </summary>
@@ -794,7 +1034,45 @@ namespace Discord.WebSocket
return null;
}
//Users
internal SocketRole AddOrUpdateRole(RoleModel model)
{
if (_roles.TryGetValue(model.Id, out SocketRole role))
_roles[model.Id].Update(Discord.State, model);
else
role = AddRole(model);
return role;
}
internal SocketCustomSticker AddSticker(StickerModel model)
{
if (model.User.IsSpecified)
AddOrUpdateUser(model.User.Value);
var sticker = SocketCustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null);
_stickers[model.Id] = sticker;
return sticker;
}
internal SocketCustomSticker AddOrUpdateSticker(StickerModel model)
{
if (_stickers.TryGetValue(model.Id, out SocketCustomSticker sticker))
_stickers[model.Id].Update(model);
else
sticker = AddSticker(model);
return sticker;
}
internal SocketCustomSticker RemoveSticker(ulong id)
{
if (_stickers.TryRemove(id, out SocketCustomSticker sticker))
return sticker;
return null;
}
#endregion
#region Users
/// <inheritdoc />
public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null)
=> GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options);
@@ -935,8 +1213,118 @@ namespace Discord.WebSocket
/// </returns>
public Task<IReadOnlyCollection<RestGuildUser>> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null)
=> GuildHelper.SearchUsersAsync(this, Discord, query, limit, options);
#endregion
//Audit logs
#region Guild Events
/// <summary>
/// Gets an event in this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the event.</param>
/// <returns>
/// An event that is associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns>
public SocketGuildEvent GetEvent(ulong id)
{
if (_events.TryGetValue(id, out SocketGuildEvent value))
return value;
return null;
}
internal SocketGuildEvent RemoveEvent(ulong id)
{
if (_events.TryRemove(id, out SocketGuildEvent value))
return value;
return null;
}
internal SocketGuildEvent AddOrUpdateEvent(EventModel model)
{
if (_events.TryGetValue(model.Id, out SocketGuildEvent value))
value.Update(model);
else
{
value = SocketGuildEvent.Create(Discord, this, model);
_events[model.Id] = value;
}
return value;
}
/// <summary>
/// Gets an event within this guild.
/// </summary>
/// <param name="id">The snowflake identifier for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation.
/// </returns>
public Task<RestGuildEvent> GetEventAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetGuildEventAsync(Discord, id, this, options);
/// <summary>
/// Gets all active events within this guild.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation.
/// </returns>
public Task<IReadOnlyCollection<RestGuildEvent>> GetEventsAsync(RequestOptions options = null)
=> GuildHelper.GetGuildEventsAsync(Discord, this, options);
/// <summary>
/// Creates an event within this guild.
/// </summary>
/// <param name="name">The name of the event.</param>
/// <param name="privacyLevel">The privacy level of the event.</param>
/// <param name="startTime">The start time of the event.</param>
/// <param name="type">The type of the event.</param>
/// <param name="description">The description of the event.</param>
/// <param name="endTime">The end time of the event.</param>
/// <param name="channelId">
/// The channel id of the event.
/// <remarks>
/// The event must have a type of <see cref="GuildScheduledEventType.Stage"/> or <see cref="GuildScheduledEventType.Voice"/>
/// in order to use this property.
/// </remarks>
/// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous create operation.
/// </returns>
public Task<RestGuildEvent> CreateEventAsync(
string name,
DateTimeOffset startTime,
GuildScheduledEventType type,
GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private,
string description = null,
DateTimeOffset? endTime = null,
ulong? channelId = null,
string location = null,
RequestOptions options = null)
{
// requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements
switch (type)
{
case GuildScheduledEventType.Stage:
CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ManageChannels | GuildPermission.MuteMembers | GuildPermission.MoveMembers);
break;
case GuildScheduledEventType.Voice:
CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ViewChannel | GuildPermission.Connect);
break;
case GuildScheduledEventType.External:
CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents);
break;
}
return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options);
}
#endregion
#region Audit logs
/// <summary>
/// Gets the specified number of audit log entries for this guild.
/// </summary>
@@ -951,8 +1339,9 @@ namespace Discord.WebSocket
/// </returns>
public IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null)
=> GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType);
#endregion
//Webhooks
#region Webhooks
/// <summary>
/// Gets a webhook found within this guild.
/// </summary>
@@ -974,8 +1363,9 @@ namespace Discord.WebSocket
/// </returns>
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> GuildHelper.GetWebhooksAsync(this, Discord, options);
#endregion
//Emotes
#region Emotes
/// <inheritdoc />
public Task<IReadOnlyCollection<GuildEmote>> GetEmotesAsync(RequestOptions options = null)
=> GuildHelper.GetEmotesAsync(this, Discord, options);
@@ -993,7 +1383,154 @@ namespace Discord.WebSocket
public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null)
=> GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options);
//Voice States
/// <summary>
/// Moves the user to the voice channel.
/// </summary>
/// <param name="user">The user to move.</param>
/// <param name="targetChannel">the channel where the user gets moved to.</param>
/// <returns>A task that represents the asynchronous operation for moving a user.</returns>
public Task MoveAsync(IGuildUser user, IVoiceChannel targetChannel)
=> user.ModifyAsync(x => x.Channel = new Optional<IVoiceChannel>(targetChannel));
/// <summary>
/// Disconnects the user from its current voice channel
/// </summary>
/// <param name="user">The user to disconnect.</param>
/// <returns>A task that represents the asynchronous operation for disconnecting a user.</returns>
async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional<IVoiceChannel>());
#endregion
#region Stickers
/// <summary>
/// Gets a specific sticker within this guild.
/// </summary>
/// <param name="id">The id of the sticker to get.</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 sticker found with the
/// specified <paramref name="id"/>; <see langword="null" /> if none is found.
/// </returns>
public async ValueTask<SocketCustomSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
{
var sticker = _stickers.FirstOrDefault(x => x.Key == id);
if (sticker.Value != null)
return sticker.Value;
if (mode == CacheMode.CacheOnly)
return null;
var model = await Discord.ApiClient.GetGuildStickerAsync(Id, id, options).ConfigureAwait(false);
if (model == null)
return null;
return AddOrUpdateSticker(model);
}
/// <summary>
/// Gets a specific sticker within this guild.
/// </summary>
/// <param name="id">The id of the sticker to get.</param>
/// <returns>A sticker, if none is found then <see langword="null"/>.</returns>
public SocketCustomSticker GetSticker(ulong id)
=> GetStickerAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult();
/// <summary>
/// Gets a collection of all stickers within 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 stickers found within the guild.
/// </returns>
public async ValueTask<IReadOnlyCollection<SocketCustomSticker>> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null)
{
if (Stickers.Count > 0)
return Stickers;
if (mode == CacheMode.CacheOnly)
return ImmutableArray.Create<SocketCustomSticker>();
var models = await Discord.ApiClient.ListGuildStickersAsync(Id, options).ConfigureAwait(false);
List<SocketCustomSticker> stickers = new();
foreach (var model in models)
{
stickers.Add(AddOrUpdateSticker(model));
}
return stickers;
}
/// <summary>
/// Creates a new sticker in this guild.
/// </summary>
/// <param name="name">The name of the sticker.</param>
/// <param name="description">The description of the sticker.</param>
/// <param name="tags">The tags of the sticker.</param>
/// <param name="image">The image of the new emote.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the created sticker.
/// </returns>
public async Task<SocketCustomSticker> CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image,
RequestOptions options = null)
{
var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false);
return AddOrUpdateSticker(model);
}
/// <summary>
/// Creates a new sticker in this guild
/// </summary>
/// <param name="name">The name of the sticker.</param>
/// <param name="description">The description of the sticker.</param>
/// <param name="tags">The tags of the sticker.</param>
/// <param name="path">The path of the file to upload.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the created sticker.
/// </returns>
public Task<SocketCustomSticker> CreateStickerAsync(string name, string description, IEnumerable<string> tags, string path,
RequestOptions options = null)
{
var fs = File.OpenRead(path);
return CreateStickerAsync(name, description, tags, fs, Path.GetFileName(fs.Name), options);
}
/// <summary>
/// Creates a new sticker in this guild
/// </summary>
/// <param name="name">The name of the sticker.</param>
/// <param name="description">The description of the sticker.</param>
/// <param name="tags">The tags of the sticker.</param>
/// <param name="stream">The stream containing the file data.</param>
/// <param name="filename">The name of the file <b>with</b> the extension, ex: image.png.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the created sticker.
/// </returns>
public async Task<SocketCustomSticker> CreateStickerAsync(string name, string description, IEnumerable<string> tags, Stream stream,
string filename, RequestOptions options = null)
{
var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, stream, filename, options).ConfigureAwait(false);
return AddOrUpdateSticker(model);
}
/// <summary>
/// Deletes a sticker within this guild.
/// </summary>
/// <param name="sticker">The sticker to delete.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions options = null)
=> sticker.DeleteAsync(options);
#endregion
#region Voice States
internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model)
{
var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel;
@@ -1037,8 +1574,9 @@ namespace Discord.WebSocket
}
return null;
}
#endregion
//Audio
#region Audio
internal AudioInStream GetAudioStream(ulong userId)
{
return _audioClient?.GetInputStream(userId);
@@ -1143,7 +1681,7 @@ namespace Discord.WebSocket
}
internal async Task FinishConnectAudio(string url, string token)
{
//TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up
//TODO: Mem Leak: Disconnected/Connected handlers aren't cleaned up
var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value;
await _audioLock.WaitAsync().ConfigureAwait(false);
@@ -1192,8 +1730,9 @@ namespace Discord.WebSocket
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
internal SocketGuild Clone() => MemberwiseClone() as SocketGuild;
#endregion
//IGuild
#region IGuild
/// <inheritdoc />
ulong? IGuild.AFKChannelId => AFKChannelId;
/// <inheritdoc />
@@ -1216,7 +1755,17 @@ namespace Discord.WebSocket
int? IGuild.ApproximateMemberCount => null;
/// <inheritdoc />
int? IGuild.ApproximatePresenceCount => null;
/// <inheritdoc />
IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers;
/// <inheritdoc />
async Task<IGuildScheduledEvent> IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options)
=> await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IGuildScheduledEvent> IGuild.GetEventAsync(ulong id, RequestOptions options)
=> await GetEventAsync(id, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildScheduledEvent>> IGuild.GetEventsAsync(RequestOptions options)
=> await GetEventsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false);
@@ -1240,15 +1789,27 @@ namespace Discord.WebSocket
Task<ITextChannel> IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<ITextChannel>(GetTextChannel(id));
/// <inheritdoc />
Task<IThreadChannel> IGuild.GetThreadChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IThreadChannel>(GetThreadChannel(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IThreadChannel>> IGuild.GetThreadChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IThreadChannel>>(ThreadChannels);
/// <inheritdoc />
Task<IReadOnlyCollection<IVoiceChannel>> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IVoiceChannel>>(VoiceChannels);
/// <inheritdoc />
Task<IReadOnlyCollection<ICategoryChannel>> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options)
Task<IReadOnlyCollection<ICategoryChannel>> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<ICategoryChannel>>(CategoryChannels);
/// <inheritdoc />
Task<IVoiceChannel> IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IVoiceChannel>(GetVoiceChannel(id));
/// <inheritdoc />
Task<IStageChannel> IGuild.GetStageChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IStageChannel>(GetStageChannel(id));
/// <inheritdoc />
Task<IReadOnlyCollection<IStageChannel>> IGuild.GetStageChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IStageChannel>>(StageChannels);
/// <inheritdoc />
Task<IVoiceChannel> IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IVoiceChannel>(AFKChannel);
/// <inheritdoc />
@@ -1273,6 +1834,9 @@ namespace Discord.WebSocket
async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options)
=> await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IStageChannel> IGuild.CreateStageChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options)
=> await CreateStageChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, Action<GuildChannelProperties> func, RequestOptions options)
=> await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false);
@@ -1350,6 +1914,37 @@ namespace Discord.WebSocket
/// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options)
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options)
=> await CreateStickerAsync(name, description, tags, image, options);
/// <inheritdoc />
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Stream stream, string filename, RequestOptions options)
=> await CreateStickerAsync(name, description, tags, stream, filename, options);
/// <inheritdoc />
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, string path, RequestOptions options)
=> await CreateStickerAsync(name, description, tags, path, options);
/// <inheritdoc />
async Task<ICustomSticker> IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options)
=> await GetStickerAsync(id, mode, options);
/// <inheritdoc />
async Task<IReadOnlyCollection<ICustomSticker>> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options)
=> await GetStickersAsync(mode, options);
/// <inheritdoc />
Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options)
=> DeleteStickerAsync(_stickers[sticker.Id], options);
/// <inheritdoc />
async Task<IApplicationCommand> IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options)
=> await GetApplicationCommandAsync(id, mode, options);
/// <inheritdoc />
async Task<IApplicationCommand> IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options)
=> await CreateApplicationCommandAsync(properties, options);
/// <inheritdoc />
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties,
RequestOptions options)
=> await BulkOverwriteApplicationCommandAsync(properties, options);
void IDisposable.Dispose()
{
@@ -1357,5 +1952,6 @@ namespace Discord.WebSocket
_audioLock?.Dispose();
_audioClient?.Dispose();
}
#endregion
}
}

View File

@@ -0,0 +1,216 @@
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.GuildScheduledEvent;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based guild event.
/// </summary>
public class SocketGuildEvent : SocketEntity<ulong>, IGuildScheduledEvent
{
/// <summary>
/// Gets the guild of the event.
/// </summary>
public SocketGuild Guild { get; private set; }
/// <summary>
/// Gets the channel of the event.
/// </summary>
public SocketGuildChannel Channel { get; private set; }
/// <summary>
/// Gets the user who created the event.
/// </summary>
public SocketGuildUser Creator { get; private set; }
/// <inheritdoc/>
public string Name { get; private set; }
/// <inheritdoc/>
public string Description { get; private set; }
/// <inheritdoc/>
public DateTimeOffset StartTime { get; private set; }
/// <inheritdoc/>
public DateTimeOffset? EndTime { get; private set; }
/// <inheritdoc/>
public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; }
/// <inheritdoc/>
public GuildScheduledEventStatus Status { get; private set; }
/// <inheritdoc/>
public GuildScheduledEventType Type { get; private set; }
/// <inheritdoc/>
public ulong? EntityId { get; private set; }
/// <inheritdoc/>
public string Location { get; private set; }
/// <inheritdoc/>
public int? UserCount { get; private set; }
internal SocketGuildEvent(DiscordSocketClient client, SocketGuild guild, ulong id)
: base(client, id)
{
Guild = guild;
}
internal static SocketGuildEvent Create(DiscordSocketClient client, SocketGuild guild, Model model)
{
var entity = new SocketGuildEvent(client, guild, model.Id);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
if (model.ChannelId.IsSpecified && model.ChannelId.Value != null)
{
Channel = Guild.GetChannel(model.ChannelId.Value.Value);
}
if (model.CreatorId.IsSpecified)
{
var guildUser = Guild.GetUser(model.CreatorId.Value);
if(guildUser != null)
{
if(model.Creator.IsSpecified)
guildUser.Update(Discord.State, model.Creator.Value);
Creator = guildUser;
}
else if (guildUser == null && model.Creator.IsSpecified)
{
guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value);
Creator = guildUser;
}
}
Name = model.Name;
Description = model.Description.GetValueOrDefault();
EntityId = model.EntityId;
Location = model.EntityMetadata?.Location.GetValueOrDefault();
Type = model.EntityType;
PrivacyLevel = model.PrivacyLevel;
EndTime = model.ScheduledEndTime;
StartTime = model.ScheduledStartTime;
Status = model.Status;
UserCount = model.UserCount.ToNullable();
}
/// <inheritdoc/>
public Task DeleteAsync(RequestOptions options = null)
=> GuildHelper.DeleteEventAsync(Discord, this, options);
/// <inheritdoc/>
public Task StartAsync(RequestOptions options = null)
=> ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active);
/// <inheritdoc/>
public Task EndAsync(RequestOptions options = null)
=> ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled
? GuildScheduledEventStatus.Cancelled
: GuildScheduledEventStatus.Completed);
/// <inheritdoc/>
public async Task ModifyAsync(Action<GuildScheduledEventsProperties> func, RequestOptions options = null)
{
var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false);
Update(model);
}
/// <summary>
/// Gets a collection of users that are interested in this event.
/// </summary>
/// <param name="limit">The amount of users to fetch.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A read-only collection of users.
/// </returns>
public Task<IReadOnlyCollection<RestUser>> GetUsersAsync(int limit = 100, RequestOptions options = null)
=> GuildHelper.GetEventUsersAsync(Discord, this, limit, options);
/// <summary>
/// Gets a collection of N users interested in the event.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// This method will attempt to fetch all users that are interested in the event.
/// The library will attempt to split up the requests according to and <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>.
/// In other words, if there are 300 users, and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant
/// is <c>100</c>, the request will be split into 3 individual requests; thus returning 3 individual asynchronous
/// responses, hence the need of flattening.
/// </remarks>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Paged collection of users.
/// </returns>
public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(RequestOptions options = null)
=> GuildHelper.GetEventUsersAsync(Discord, this, null, null, options);
/// <summary>
/// Gets a collection of N users interested in the event.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual users as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of users specified under <paramref name="limit"/> around
/// the user <paramref name="fromUserId"/> depending on the <paramref name="dir"/>. The library will
/// attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxGuildEventUsersPerBatch"/>. In other words, should the user request 500 users,
/// and the <see cref="Discord.DiscordConfig.MaxGuildEventUsersPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <param name="fromUserId">The ID of the starting user to get the users from.</param>
/// <param name="dir">The direction of the users to be gotten from.</param>
/// <param name="limit">The numbers of users to be gotten from.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Paged collection of users.
/// </returns>
public IAsyncEnumerable<IReadOnlyCollection<RestUser>> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null)
=> GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options);
#region IGuildScheduledEvent
/// <inheritdoc/>
IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(RequestOptions options)
=> GetUsersAsync(options);
/// <inheritdoc/>
IAsyncEnumerable<IReadOnlyCollection<IUser>> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options)
=> GetUsersAsync(fromUserId, dir, limit, options);
/// <inheritdoc/>
IGuild IGuildScheduledEvent.Guild => Guild;
/// <inheritdoc/>
IUser IGuildScheduledEvent.Creator => Creator;
/// <inheritdoc/>
ulong? IGuildScheduledEvent.ChannelId => Channel?.Id;
#endregion
}
}

View File

@@ -0,0 +1,45 @@
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Interaction;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based slash command received over the gateway.
/// </summary>
public class SocketMessageCommand : SocketCommandBase, IMessageCommandInteraction, IDiscordInteraction
{
/// <summary>
/// The data associated with this interaction.
/// </summary>
public new SocketMessageCommandData Data { get; }
internal SocketMessageCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
ulong? guildId = null;
if (Channel is SocketGuildChannel guildChannel)
guildId = guildChannel.Guild.Id;
Data = SocketMessageCommandData.Create(client, dataModel, model.Id, guildId);
}
internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketMessageCommand(client, model, channel);
entity.Update(model);
return entity;
}
//IMessageCommandInteraction
/// <inheritdoc/>
IMessageCommandInteractionData IMessageCommandInteraction.Data => Data;
//IDiscordInteraction
/// <inheritdoc/>
IDiscordInteractionData IDiscordInteraction.Data => Data;
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using Model = Discord.API.ApplicationCommandInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents the data tied with the <see cref="SocketMessageCommand"/> interaction.
/// </summary>
public class SocketMessageCommandData : SocketCommandBaseData, IMessageCommandInteractionData, IDiscordInteractionData
{
/// <summary>
/// Gets the message associated with this message command.
/// </summary>
public SocketMessage Message
=> ResolvableData?.Messages.FirstOrDefault().Value;
/// <inheritdoc/>
/// <remarks>
/// <b>Note</b> Not implemented for <see cref="SocketMessageCommandData"/>
/// </remarks>
public override IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options
=> throw new System.NotImplementedException();
internal SocketMessageCommandData(DiscordSocketClient client, Model model, ulong? guildId)
: base(client, model, guildId) { }
internal new static SocketMessageCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId)
{
var entity = new SocketMessageCommandData(client, model, guildId);
entity.Update(model);
return entity;
}
//IMessageCommandInteractionData
/// <inheritdoc/>
IMessage IMessageCommandInteractionData.Message => Message;
}
}

View File

@@ -0,0 +1,45 @@
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Interaction;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based slash command received over the gateway.
/// </summary>
public class SocketUserCommand : SocketCommandBase, IUserCommandInteraction, IDiscordInteraction
{
/// <summary>
/// The data associated with this interaction.
/// </summary>
public new SocketUserCommandData Data { get; }
internal SocketUserCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
ulong? guildId = null;
if (Channel is SocketGuildChannel guildChannel)
guildId = guildChannel.Guild.Id;
Data = SocketUserCommandData.Create(client, dataModel, model.Id, guildId);
}
internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketUserCommand(client, model, channel);
entity.Update(model);
return entity;
}
//IUserCommandInteraction
/// <inheritdoc/>
IUserCommandInteractionData IUserCommandInteraction.Data => Data;
//IDiscordInteraction
/// <inheritdoc/>
IDiscordInteractionData IDiscordInteraction.Data => Data;
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using Model = Discord.API.ApplicationCommandInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents the data tied with the <see cref="SocketUserCommand"/> interaction.
/// </summary>
public class SocketUserCommandData : SocketCommandBaseData, IUserCommandInteractionData, IDiscordInteractionData
{
/// <summary>
/// Gets the user who this command targets.
/// </summary>
public SocketUser Member
=> (SocketUser)ResolvableData.GuildMembers.Values.FirstOrDefault() ?? ResolvableData.Users.Values.FirstOrDefault();
/// <inheritdoc/>
/// <remarks>
/// <b>Note</b> Not implemented for <see cref="SocketUserCommandData"/>
/// </remarks>
public override IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options
=> throw new System.NotImplementedException();
internal SocketUserCommandData(DiscordSocketClient client, Model model, ulong? guildId)
: base(client, model, guildId) { }
internal new static SocketUserCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId)
{
var entity = new SocketUserCommandData(client, model, guildId);
entity.Update(model);
return entity;
}
//IUserCommandInteractionData
/// <inheritdoc/>
IUser IUserCommandInteractionData.User => Member;
}
}

View File

@@ -0,0 +1,436 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Interaction;
using DataModel = Discord.API.MessageComponentInteractionData;
using Discord.Rest;
using System.Collections.Generic;
using Discord.Net.Rest;
using System.IO;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based interaction type for Message Components.
/// </summary>
public class SocketMessageComponent : SocketInteraction, IComponentInteraction, IDiscordInteraction
{
/// <summary>
/// Gets the data received with this interaction, contains the button that was clicked.
/// </summary>
public new SocketMessageComponentData Data { get; }
/// <summary>
/// Gets the message that contained the trigger for this interaction.
/// </summary>
public SocketUserMessage Message { get; private set; }
private object _lock = new object();
public override bool HasResponded { get; internal set; } = false;
internal SocketMessageComponent(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model.Id, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
Data = new SocketMessageComponentData(dataModel);
}
internal new static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketMessageComponent(client, model, channel);
entity.Update(model);
return entity;
}
internal override void Update(Model model)
{
base.Update(model);
if (model.Message.IsSpecified)
{
if (Message == null)
{
SocketUser author = null;
if (Channel is SocketGuildChannel channel)
{
if (model.Message.Value.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(channel.Guild, Discord.State, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value);
else if (model.Message.Value.Author.IsSpecified)
author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id);
}
else if (model.Message.Value.Author.IsSpecified)
author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id);
Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value);
}
else
{
Message.Update(Discord.State, model.Message.Value);
}
}
}
/// <inheritdoc/>
public override async Task RespondAsync(
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}
var response = new API.InteractionResponse
{
Type = InteractionResponseType.ChannelMessageWithSource,
Data = new API.InteractionCallbackData
{
Content = text ?? Optional<string>.Unspecified,
AllowedMentions = allowedMentions?.ToModel(),
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
TTS = isTTS,
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
}
};
if (ephemeral)
response.Data.Value.Flags = MessageFlags.Ephemeral;
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction");
}
}
await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
/// <summary>
/// Updates the message which this component resides in with the type <see cref="InteractionResponseType.UpdateMessage"/>
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of updating the message.</returns>
public async Task UpdateAsync(Action<MessageProperties> func, RequestOptions options = null)
{
var args = new MessageProperties();
func(args);
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");
if (args.AllowedMentions.IsSpecified)
{
var allowedMentions = args.AllowedMentions.Value;
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed.");
}
var embed = args.Embed;
var embeds = args.Embeds;
bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(Message.Content);
bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || Message.Embeds.Any();
if (!hasText && !hasEmbeds)
Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content));
var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List<API.Embed>() : null;
if (embed.IsSpecified && embed.Value != null)
{
apiEmbeds.Add(embed.Value.ToModel());
}
if (embeds.IsSpecified && embeds.Value != null)
{
apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel()));
}
Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed.");
// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue)
{
var allowedMentions = args.AllowedMentions.Value;
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users)
&& allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions));
}
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles)
&& allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions));
}
}
var response = new API.InteractionResponse
{
Type = InteractionResponseType.UpdateMessage,
Data = new API.InteractionCallbackData
{
Content = args.Content,
AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional<API.AllowedMentions>.Unspecified,
Embeds = apiEmbeds?.ToArray() ?? Optional<API.Embed[]>.Unspecified,
Components = args.Components.IsSpecified
? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty<API.ActionRowComponent>()
: Optional<API.ActionRowComponent[]>.Unspecified,
Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional<MessageFlags>.Unspecified : Optional<MessageFlags>.Unspecified
}
};
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction");
}
}
await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupAsync(
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false);
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFileAsync(
Stream fileStream,
string fileName,
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data");
Preconditions.NotNullOrWhitespace(fileName, nameof(fileName), "File Name must not be empty or null");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional<MultipartFile>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false);
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFileAsync(
string filePath,
string text = null,
string fileName = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.NotNullOrWhitespace(filePath, nameof(filePath), "Path must exist");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional<MultipartFile>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false);
}
/// <summary>
/// Defers an interaction and responds with type 5 (<see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>)
/// </summary>
/// <param name="ephemeral"><see langword="true"/> to send this message ephemerally, otherwise <see langword="false"/>.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns>
public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null)
{
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement");
var response = new API.InteractionResponse
{
Type = InteractionResponseType.DeferredChannelMessageWithSource,
Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified
};
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}
await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
/// <inheritdoc/>
public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
{
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement");
var response = new API.InteractionResponse
{
Type = InteractionResponseType.DeferredUpdateMessage,
Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional<API.InteractionCallbackData>.Unspecified
};
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}
await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
//IComponentInteraction
/// <inheritdoc/>
IComponentInteractionData IComponentInteraction.Data => Data;
/// <inheritdoc/>
IUserMessage IComponentInteraction.Message => Message;
//IDiscordInteraction
/// <inheritdoc/>
IDiscordInteractionData IDiscordInteraction.Data => Data;
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using Model = Discord.API.MessageComponentInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents the data sent with a <see cref="InteractionType.MessageComponent"/>.
/// </summary>
public class SocketMessageComponentData : IComponentInteractionData
{
/// <summary>
/// Gets the components Custom Id that was clicked.
/// </summary>
public string CustomId { get; }
/// <summary>
/// Gets the type of the component clicked.
/// </summary>
public ComponentType Type { get; }
/// <summary>
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response.
/// </summary>
public IReadOnlyCollection<string> Values { get; }
internal SocketMessageComponentData(Model model)
{
CustomId = model.CustomId;
Type = model.ComponentType;
Values = model.Values.GetValueOrDefault();
}
}
}

View File

@@ -0,0 +1,126 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Model = Discord.API.Interaction;
using DataModel = Discord.API.AutocompleteInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a <see cref="InteractionType.ApplicationCommandAutocomplete"/> received over the gateway.
/// </summary>
public class SocketAutocompleteInteraction : SocketInteraction, IAutocompleteInteraction, IDiscordInteraction
{
/// <summary>
/// The autocomplete data of this interaction.
/// </summary>
public new SocketAutocompleteInteractionData Data { get; }
public override bool HasResponded { get; internal set; }
private object _lock = new object();
internal SocketAutocompleteInteraction(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model.Id, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
if (dataModel != null)
Data = new SocketAutocompleteInteractionData(dataModel);
}
internal new static SocketAutocompleteInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketAutocompleteInteraction(client, model, channel);
entity.Update(model);
return entity;
}
/// <summary>
/// Responds to this interaction with a set of choices.
/// </summary>
/// <param name="result">
/// The set of choices for the user to pick from.
/// <remarks>
/// A max of 20 choices are allowed. Passing <see langword="null"/> for this argument will show the executing user that
/// there is no choices for their autocompleted input.
/// </remarks>
/// </param>
/// <param name="options">The request options for this response.</param>
/// <returns>
/// A task that represents the asynchronous operation of responding to this interaction.
/// </returns>
public async Task RespondAsync(IEnumerable<AutocompleteResult> result, RequestOptions options = null)
{
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond twice to the same interaction");
}
}
await InteractionHelper.SendAutocompleteResultAsync(Discord, result, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
/// <summary>
/// Responds to this interaction with a set of choices.
/// </summary>
/// <param name="options">The request options for this response.</param>
/// <param name="result">
/// The set of choices for the user to pick from.
/// <remarks>
/// A max of 20 choices are allowed. Passing <see langword="null"/> for this argument will show the executing user that
/// there is no choices for their autocompleted input.
/// </remarks>
/// </param>
/// <returns>
/// A task that represents the asynchronous operation of responding to this interaction.
/// </returns>
public Task RespondAsync(RequestOptions options = null, params AutocompleteResult[] result)
=> RespondAsync(result, options);
/// <inheritdoc/>
[Obsolete("Autocomplete interactions cannot be deferred!", true)]
public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
=> throw new NotSupportedException("Autocomplete interactions cannot be deferred!");
/// <inheritdoc/>
[Obsolete("Autocomplete interactions cannot have followups!", true)]
public override Task<RestFollowupMessage> FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null)
=> throw new NotSupportedException("Autocomplete interactions cannot be deferred!");
/// <inheritdoc/>
[Obsolete("Autocomplete interactions cannot have followups!", true)]
public override Task<RestFollowupMessage> FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null)
=> throw new NotSupportedException("Autocomplete interactions cannot be deferred!");
/// <inheritdoc/>
[Obsolete("Autocomplete interactions cannot have followups!", true)]
public override Task<RestFollowupMessage> FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null)
=> throw new NotSupportedException("Autocomplete interactions cannot be deferred!");
/// <inheritdoc/>
[Obsolete("Autocomplete interactions cannot have normal responses!", true)]
public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null)
=> throw new NotSupportedException("Autocomplete interactions cannot be deferred!");
//IAutocompleteInteraction
/// <inheritdoc/>
IAutocompleteInteractionData IAutocompleteInteraction.Data => Data;
//IDiscordInteraction
/// <inheritdoc/>
IDiscordInteractionData IDiscordInteraction.Data => Data;
}
}

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using DataModel = Discord.API.AutocompleteInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents data for a slash commands autocomplete interaction.
/// </summary>
public class SocketAutocompleteInteractionData : IAutocompleteInteractionData, IDiscordInteractionData
{
/// <summary>
/// Gets the name of the invoked command.
/// </summary>
public string CommandName { get; }
/// <summary>
/// Gets the id of the invoked command.
/// </summary>
public ulong CommandId { get; }
/// <summary>
/// Gets the type of the invoked command.
/// </summary>
public ApplicationCommandType Type { get; }
/// <summary>
/// Gets the version of the invoked command.
/// </summary>
public ulong Version { get; }
/// <summary>
/// Gets the current autocomplete option that is actively being filled out.
/// </summary>
public AutocompleteOption Current { get; }
/// <summary>
/// Gets a collection of all the other options the executing users has filled out.
/// </summary>
public IReadOnlyCollection<AutocompleteOption> Options { get; }
internal SocketAutocompleteInteractionData(DataModel model)
{
var options = model.Options.SelectMany(GetOptions);
Current = options.FirstOrDefault(x => x.Focused);
Options = options.ToImmutableArray();
if (Options.Count == 1 && Current == null)
Current = Options.FirstOrDefault();
CommandName = model.Name;
CommandId = model.Id;
Type = model.Type;
Version = model.Version;
}
private List<AutocompleteOption> GetOptions(API.AutocompleteInteractionDataOption model)
{
var options = new List<AutocompleteOption>();
options.Add(new AutocompleteOption(model.Type, model.Name, model.Value.GetValueOrDefault(null), model.Focused.GetValueOrDefault(false)));
if (model.Options.IsSpecified)
{
options.AddRange(model.Options.Value.SelectMany(GetOptions));
}
return options;
}
}
}

View File

@@ -0,0 +1,45 @@
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Interaction;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based slash command received over the gateway.
/// </summary>
public class SocketSlashCommand : SocketCommandBase, ISlashCommandInteraction, IDiscordInteraction
{
/// <summary>
/// The data associated with this interaction.
/// </summary>
public new SocketSlashCommandData Data { get; }
internal SocketSlashCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
ulong? guildId = null;
if (Channel is SocketGuildChannel guildChannel)
guildId = guildChannel.Guild.Id;
Data = SocketSlashCommandData.Create(client, dataModel, guildId);
}
internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketSlashCommand(client, model, channel);
entity.Update(model);
return entity;
}
//ISlashCommandInteraction
/// <inheritdoc/>
IApplicationCommandInteractionData ISlashCommandInteraction.Data => Data;
//IDiscordInteraction
/// <inheritdoc/>
IDiscordInteractionData IDiscordInteraction.Data => Data;
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ApplicationCommandInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents the data tied with the <see cref="SocketSlashCommand"/> interaction.
/// </summary>
public class SocketSlashCommandData : SocketCommandBaseData<SocketSlashCommandDataOption>, IDiscordInteractionData
{
internal SocketSlashCommandData(DiscordSocketClient client, Model model, ulong? guildId)
: base(client, model, guildId) { }
internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong? guildId)
{
var entity = new SocketSlashCommandData(client, model, guildId);
entity.Update(model);
return entity;
}
internal override void Update(Model model)
{
base.Update(model);
Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(this, x)).ToImmutableArray()
: ImmutableArray.Create<SocketSlashCommandDataOption>();
}
}
}

View File

@@ -0,0 +1,135 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ApplicationCommandInteractionDataOption;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based <see cref="IApplicationCommandInteractionDataOption"/> received by the gateway.
/// </summary>
public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption
{
#region SocketSlashCommandDataOption
/// <inheritdoc/>
public string Name { get; private set; }
/// <inheritdoc/>
public object Value { get; private set; }
/// <inheritdoc/>
public ApplicationCommandOptionType Type { get; private set; }
/// <summary>
/// The sub command options received for this sub command group.
/// </summary>
public IReadOnlyCollection<SocketSlashCommandDataOption> Options { get; private set; }
internal SocketSlashCommandDataOption() { }
internal SocketSlashCommandDataOption(SocketSlashCommandData data, Model model)
{
Name = model.Name;
Type = model.Type;
if (model.Value.IsSpecified)
{
switch (Type)
{
case ApplicationCommandOptionType.User:
case ApplicationCommandOptionType.Role:
case ApplicationCommandOptionType.Channel:
case ApplicationCommandOptionType.Mentionable:
if (ulong.TryParse($"{model.Value.Value}", out var valueId))
{
switch (Type)
{
case ApplicationCommandOptionType.User:
{
var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value;
if (guildUser != null)
Value = guildUser;
else
Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value;
}
break;
case ApplicationCommandOptionType.Channel:
Value = data.ResolvableData.Channels.FirstOrDefault(x => x.Key == valueId).Value;
break;
case ApplicationCommandOptionType.Role:
Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value;
break;
case ApplicationCommandOptionType.Mentionable:
{
if (data.ResolvableData.GuildMembers.Any(x => x.Key == valueId) || data.ResolvableData.Users.Any(x => x.Key == valueId))
{
var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value;
if (guildUser != null)
Value = guildUser;
else
Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value;
}
else if (data.ResolvableData.Roles.Any(x => x.Key == valueId))
{
Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value;
}
}
break;
default:
Value = model.Value.Value;
break;
}
}
break;
case ApplicationCommandOptionType.String:
Value = model.Value.ToString();
break;
case ApplicationCommandOptionType.Integer:
{
if (model.Value.Value is long val)
Value = val;
else if (long.TryParse(model.Value.Value.ToString(), out long res))
Value = res;
}
break;
case ApplicationCommandOptionType.Boolean:
{
if (model.Value.Value is bool val)
Value = val;
else if (bool.TryParse(model.Value.Value.ToString(), out bool res))
Value = res;
}
break;
case ApplicationCommandOptionType.Number:
{
if (model.Value.Value is int val)
Value = val;
else if (double.TryParse(model.Value.Value.ToString(), out double res))
Value = res;
}
break;
}
}
Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketSlashCommandDataOption(data, x)).ToImmutableArray()
: ImmutableArray.Create<SocketSlashCommandDataOption>();
}
#endregion
#region Converters
public static explicit operator bool(SocketSlashCommandDataOption option)
=> (bool)option.Value;
public static explicit operator int(SocketSlashCommandDataOption option)
=> (int)option.Value;
public static explicit operator string(SocketSlashCommandDataOption option)
=> option.Value.ToString();
#endregion
#region IApplicationCommandInteractionDataOption
IReadOnlyCollection<IApplicationCommandInteractionDataOption> IApplicationCommandInteractionDataOption.Options
=> Options;
#endregion
}
}

View File

@@ -0,0 +1,116 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using GatewayModel = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent;
using Model = Discord.API.ApplicationCommand;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a Websocket-based <see cref="IApplicationCommand"/>.
/// </summary>
public class SocketApplicationCommand : SocketEntity<ulong>, IApplicationCommand
{
#region SocketApplicationCommand
/// <summary>
/// <see langword="true"/> if this command is a global command, otherwise <see langword="false"/>.
/// </summary>
public bool IsGlobalCommand
=> Guild == null;
/// <inheritdoc/>
public ulong ApplicationId { get; private set; }
/// <inheritdoc/>
public string Name { get; private set; }
/// <inheritdoc/>
public ApplicationCommandType Type { get; private set; }
/// <inheritdoc/>
public string Description { get; private set; }
/// <inheritdoc/>
public bool IsDefaultPermission { get; private set; }
/// <summary>
/// A collection of <see cref="SocketApplicationCommandOption"/>'s for this command.
/// </summary>
/// <remarks>
/// If the <see cref="Type"/> is not a slash command, this field will be an empty collection.
/// </remarks>
public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; }
/// <inheritdoc/>
public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(Id);
/// <summary>
/// Returns the guild this command resides in, if this command is a global command then it will return <see langword="null"/>
/// </summary>
public SocketGuild Guild
=> GuildId.HasValue ? Discord.GetGuild(GuildId.Value) : null;
private ulong? GuildId { get; set; }
internal SocketApplicationCommand(DiscordSocketClient client, ulong id, ulong? guildId)
: base(client, id)
{
GuildId = guildId;
}
internal static SocketApplicationCommand Create(DiscordSocketClient client, GatewayModel model)
{
var entity = new SocketApplicationCommand(client, model.Id, model.GuildId.ToNullable());
entity.Update(model);
return entity;
}
internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model, ulong? guildId = null)
{
var entity = new SocketApplicationCommand(client, model.Id, guildId);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
ApplicationId = model.ApplicationId;
Description = model.Description;
Name = model.Name;
IsDefaultPermission = model.DefaultPermissions.GetValueOrDefault(true);
Type = model.Type;
Options = model.Options.IsSpecified
? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray()
: ImmutableArray.Create<SocketApplicationCommandOption>();
}
/// <inheritdoc/>
public Task DeleteAsync(RequestOptions options = null)
=> InteractionHelper.DeleteUnknownApplicationCommandAsync(Discord, GuildId, this, options);
/// <inheritdoc />
public Task ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null)
{
return ModifyAsync<ApplicationCommandProperties>(func, options);
}
/// <inheritdoc />
public async Task ModifyAsync<TArg>(Action<TArg> func, RequestOptions options = null) where TArg : ApplicationCommandProperties
{
var command = IsGlobalCommand
? await InteractionHelper.ModifyGlobalCommandAsync(Discord, this, func, options).ConfigureAwait(false)
: await InteractionHelper.ModifyGuildCommandAsync(Discord, this, GuildId.Value, func, options);
Update(command);
}
#endregion
#region IApplicationCommand
IReadOnlyCollection<IApplicationCommandOption> IApplicationCommand.Options => Options;
#endregion
}
}

View File

@@ -0,0 +1,29 @@
using Model = Discord.API.ApplicationCommandOptionChoice;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a choice for a <see cref="SocketApplicationCommandOption"/>.
/// </summary>
public class SocketApplicationCommandChoice : IApplicationCommandOptionChoice
{
/// <inheritdoc/>
public string Name { get; private set; }
/// <inheritdoc/>
public object Value { get; private set; }
internal SocketApplicationCommandChoice() { }
internal static SocketApplicationCommandChoice Create(Model model)
{
var entity = new SocketApplicationCommandChoice();
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
Name = model.Name;
Value = model.Value;
}
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ApplicationCommandOption;
namespace Discord.WebSocket
{
/// <summary>
/// Represents an option for a <see cref="SocketApplicationCommand"/>.
/// </summary>
public class SocketApplicationCommandOption : IApplicationCommandOption
{
/// <inheritdoc/>
public string Name { get; private set; }
/// <inheritdoc/>
public ApplicationCommandOptionType Type { get; private set; }
/// <inheritdoc/>
public string Description { get; private set; }
/// <inheritdoc/>
public bool? IsDefault { get; private set; }
/// <inheritdoc/>
public bool? IsRequired { get; private set; }
/// <inheritdoc/>
public double? MinValue { get; private set; }
/// <inheritdoc/>
public double? MaxValue { get; private set; }
/// <summary>
/// Choices for string and int types for the user to pick from.
/// </summary>
public IReadOnlyCollection<SocketApplicationCommandChoice> Choices { get; private set; }
/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; }
/// <summary>
/// The allowed channel types for this option.
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; }
internal SocketApplicationCommandOption() { }
internal static SocketApplicationCommandOption Create(Model model)
{
var entity = new SocketApplicationCommandOption();
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
Name = model.Name;
Type = model.Type;
Description = model.Description;
IsDefault = model.Default.ToNullable();
IsRequired = model.Required.ToNullable();
MinValue = model.MinValue.ToNullable();
MaxValue = model.MaxValue.ToNullable();
Choices = model.Choices.IsSpecified
? model.Choices.Value.Select(SocketApplicationCommandChoice.Create).ToImmutableArray()
: ImmutableArray.Create<SocketApplicationCommandChoice>();
Options = model.Options.IsSpecified
? model.Options.Value.Select(Create).ToImmutableArray()
: ImmutableArray.Create<SocketApplicationCommandOption>();
ChannelTypes = model.ChannelTypes.IsSpecified
? model.ChannelTypes.Value.ToImmutableArray()
: ImmutableArray.Create<ChannelType>();
}
IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices;
IReadOnlyCollection<IApplicationCommandOption> IApplicationCommandOption.Options => Options;
}
}

View File

@@ -0,0 +1,300 @@
using Discord.Net.Rest;
using Discord.Rest;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Interaction;
namespace Discord.WebSocket
{
/// <summary>
/// Base class for User, Message, and Slash command interactions.
/// </summary>
public class SocketCommandBase : SocketInteraction
{
/// <summary>
/// Gets the name of the invoked command.
/// </summary>
public string CommandName
=> Data.Name;
/// <summary>
/// Gets the id of the invoked command.
/// </summary>
public ulong CommandId
=> Data.Id;
/// <summary>
/// The data associated with this interaction.
/// </summary>
internal new SocketCommandBaseData Data { get; }
public override bool HasResponded { get; internal set; }
private object _lock = new object();
internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
: base(client, model.Id, channel)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
ulong? guildId = null;
if (Channel is SocketGuildChannel guildChannel)
guildId = guildChannel.Guild.Id;
Data = SocketCommandBaseData.Create(client, dataModel, model.Id, guildId);
}
internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
var entity = new SocketCommandBase(client, model, channel);
entity.Update(model);
return entity;
}
internal override void Update(Model model)
{
var data = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
Data.Update(data);
base.Update(model);
}
/// <inheritdoc/>
public override async Task RespondAsync(
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}
var response = new API.InteractionResponse
{
Type = InteractionResponseType.ChannelMessageWithSource,
Data = new API.InteractionCallbackData
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
TTS = isTTS,
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.Unspecified
}
};
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond twice to the same interaction");
}
}
await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupAsync(
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFileAsync(
Stream fileStream,
string fileName,
string text = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data");
Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional<MultipartFile>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}
/// <inheritdoc/>
public override async Task<RestFollowupMessage> FollowupWithFileAsync(
string filePath,
string text = null,
string fileName = null,
Embed[] embeds = null,
bool isTTS = false,
bool ephemeral = false,
AllowedMentions allowedMentions = null,
RequestOptions options = null,
MessageComponent component = null,
Embed embed = null)
{
if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");
embeds ??= Array.Empty<Embed>();
if (embed != null)
embeds = new[] { embed }.Concat(embeds).ToArray();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist");
fileName ??= Path.GetFileName(filePath);
Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null");
var args = new API.Rest.CreateWebhookMessageParams
{
Content = text,
AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified,
IsTTS = isTTS,
Embeds = embeds.Select(x => x.ToModel()).ToArray(),
Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified,
File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional<MultipartFile>.Unspecified
};
if (ephemeral)
args.Flags = MessageFlags.Ephemeral;
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}
/// <summary>
/// Acknowledges this interaction with the <see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>.
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns>
public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
{
if (!InteractionHelper.CanSendResponse(this))
throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds!");
var response = new API.InteractionResponse
{
Type = InteractionResponseType.DeferredChannelMessageWithSource,
Data = new API.InteractionCallbackData
{
Flags = ephemeral ? MessageFlags.Ephemeral : Optional<MessageFlags>.Unspecified
}
};
lock (_lock)
{
if (HasResponded)
{
throw new InvalidOperationException("Cannot respond or defer twice to the same interaction");
}
}
await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false);
lock (_lock)
{
HasResponded = true;
}
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using Model = Discord.API.ApplicationCommandInteractionData;
namespace Discord.WebSocket
{
/// <summary>
/// Represents the base data tied with the <see cref="SocketCommandBase"/> interaction.
/// </summary>
public class SocketCommandBaseData<TOption> : SocketEntity<ulong>, IApplicationCommandInteractionData where TOption : IApplicationCommandInteractionDataOption
{
/// <inheritdoc/>
public string Name { get; private set; }
/// <summary>
/// The <typeparamref name="TOption"/> received with this interaction.
/// </summary>
public virtual IReadOnlyCollection<TOption> Options { get; internal set; }
internal readonly SocketResolvableData<Model> ResolvableData;
private ApplicationCommandType Type { get; set; }
internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId)
: base(client, model.Id)
{
Type = model.Type;
if (model.Resolved.IsSpecified)
{
ResolvableData = new SocketResolvableData<Model>(client, guildId, model);
}
}
internal static SocketCommandBaseData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId)
{
var entity = new SocketCommandBaseData(client, model, guildId);
entity.Update(model);
return entity;
}
internal virtual void Update(Model model)
{
Name = model.Name;
}
IReadOnlyCollection<IApplicationCommandInteractionDataOption> IApplicationCommandInteractionData.Options
=> (IReadOnlyCollection<IApplicationCommandInteractionDataOption>)Options;
}
/// <summary>
/// Represents the base data tied with the <see cref="SocketCommandBase"/> interaction.
/// </summary>
public class SocketCommandBaseData : SocketCommandBaseData<IApplicationCommandInteractionDataOption>
{
internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId)
: base(client, model, guildId) { }
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
namespace Discord.WebSocket
{
internal class SocketResolvableData<T> where T : API.IResolvable
{
internal readonly Dictionary<ulong, SocketGuildUser> GuildMembers
= new Dictionary<ulong, SocketGuildUser>();
internal readonly Dictionary<ulong, SocketGlobalUser> Users
= new Dictionary<ulong, SocketGlobalUser>();
internal readonly Dictionary<ulong, SocketChannel> Channels
= new Dictionary<ulong, SocketChannel>();
internal readonly Dictionary<ulong, SocketRole> Roles
= new Dictionary<ulong, SocketRole>();
internal readonly Dictionary<ulong, SocketMessage> Messages
= new Dictionary<ulong, SocketMessage>();
internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T model)
{
var guild = guildId.HasValue ? discord.GetGuild(guildId.Value) : null;
var resolved = model.Resolved.Value;
if (resolved.Users.IsSpecified)
{
foreach (var user in resolved.Users.Value)
{
var socketUser = discord.GetOrCreateUser(discord.State, user.Value);
Users.Add(ulong.Parse(user.Key), socketUser);
}
}
if (resolved.Channels.IsSpecified)
{
foreach (var channel in resolved.Channels.Value)
{
SocketChannel socketChannel = guild != null
? guild.GetChannel(channel.Value.Id)
: discord.GetChannel(channel.Value.Id);
if (socketChannel == null)
{
var channelModel = guild != null
? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult()
: discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult();
socketChannel = guild != null
? SocketGuildChannel.Create(guild, discord.State, channelModel)
: (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel);
}
discord.State.AddChannel(socketChannel);
Channels.Add(ulong.Parse(channel.Key), socketChannel);
}
}
if (resolved.Members.IsSpecified)
{
foreach (var member in resolved.Members.Value)
{
member.Value.User = resolved.Users.Value[member.Key];
var user = guild.AddOrUpdateUser(member.Value);
GuildMembers.Add(ulong.Parse(member.Key), user);
}
}
if (resolved.Roles.IsSpecified)
{
foreach (var role in resolved.Roles.Value)
{
var socketRole = guild.AddOrUpdateRole(role.Value);
Roles.Add(ulong.Parse(role.Key), socketRole);
}
}
if (resolved.Messages.IsSpecified)
{
foreach (var msg in resolved.Messages.Value)
{
var channel = discord.GetChannel(msg.Value.ChannelId) as ISocketMessageChannel;
SocketUser author;
if (guild != null)
{
if (msg.Value.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value);
else
author = guild.GetUser(msg.Value.Author.Value.Id);
}
else
author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id);
if (channel == null)
{
if (!msg.Value.GuildId.IsSpecified) // assume it is a DM
{
channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State);
}
}
var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value);
Messages.Add(message.Id, message);
}
}
}
}
}

View File

@@ -0,0 +1,243 @@
using Discord.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.Interaction;
using DataModel = Discord.API.ApplicationCommandInteractionData;
using System.IO;
namespace Discord.WebSocket
{
/// <summary>
/// Represents an Interaction received over the gateway.
/// </summary>
public abstract class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction
{
#region SocketInteraction
/// <summary>
/// The <see cref="ISocketMessageChannel"/> this interaction was used in.
/// </summary>
public ISocketMessageChannel Channel { get; private set; }
/// <summary>
/// The <see cref="SocketUser"/> who triggered this interaction.
/// </summary>
public SocketUser User { get; private set; }
/// <summary>
/// The type of this interaction.
/// </summary>
public InteractionType Type { get; private set; }
/// <summary>
/// The token used to respond to this interaction.
/// </summary>
public string Token { get; private set; }
/// <summary>
/// The data sent with this interaction.
/// </summary>
public IDiscordInteractionData Data { get; private set; }
/// <summary>
/// The version of this interaction.
/// </summary>
public int Version { get; private set; }
/// <inheritdoc/>
public DateTimeOffset CreatedAt { get; private set; }
/// <summary>
/// Gets whether or not this interaction has been responded to.
/// </summary>
/// <remarks>
/// This property is locally set -- if you're running multiple bots
/// off the same token then this property won't be in sync with them.
/// </remarks>
public abstract bool HasResponded { get; internal set; }
/// <summary>
/// <see langword="true"/> if the token is valid for replying to, otherwise <see langword="false"/>.
/// </summary>
public bool IsValidToken
=> InteractionHelper.CanRespondOrFollowup(this);
internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel)
: base(client, id)
{
Channel = channel;
CreatedAt = client.UseInteractionSnowflakeDate
? SnowflakeUtils.FromSnowflake(Id)
: DateTime.UtcNow;
}
internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel)
{
if (model.Type == InteractionType.ApplicationCommand)
{
var dataModel = model.Data.IsSpecified
? (DataModel)model.Data.Value
: null;
if (dataModel == null)
return null;
return dataModel.Type switch
{
ApplicationCommandType.Slash => SocketSlashCommand.Create(client, model, channel),
ApplicationCommandType.Message => SocketMessageCommand.Create(client, model, channel),
ApplicationCommandType.User => SocketUserCommand.Create(client, model, channel),
_ => null
};
}
if (model.Type == InteractionType.MessageComponent)
return SocketMessageComponent.Create(client, model, channel);
if (model.Type == InteractionType.ApplicationCommandAutocomplete)
return SocketAutocompleteInteraction.Create(client, model, channel);
return null;
}
internal virtual void Update(Model model)
{
Data = model.Data.IsSpecified
? model.Data.Value
: null;
Token = model.Token;
Version = model.Version;
Type = model.Type;
if (User == null)
{
if (model.Member.IsSpecified && model.GuildId.IsSpecified)
{
User = SocketGuildUser.Create(Discord.State.GetGuild(model.GuildId.Value), Discord.State, model.Member.Value);
}
else
{
User = SocketGlobalUser.Create(Discord, Discord.State, model.User.Value);
}
}
}
/// <summary>
/// Responds to an Interaction with type <see cref="InteractionResponseType.ChannelMessageWithSource"/>.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid.</exception>
public abstract Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// The sent message.
/// </returns>
public abstract Task<RestFollowupMessage> FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="fileStream">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// The sent message.
/// </returns>
public abstract Task<RestFollowupMessage> FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="filePath">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// The sent message.
/// </returns>
public abstract Task<RestFollowupMessage> FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
/// <summary>
/// Gets the original response for this interaction.
/// </summary>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A <see cref="RestInteractionMessage"/> that represents the initial response.</returns>
public Task<RestInteractionMessage> GetOriginalResponseAsync(RequestOptions options = null)
=> InteractionHelper.GetOriginalResponseAsync(Discord, Channel, this, options);
/// <summary>
/// Edits original response for this interaction.
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A <see cref="RestInteractionMessage"/> that represents the initial response.</returns>
public async Task<RestInteractionMessage> ModifyOriginalResponseAsync(Action<MessageProperties> func, RequestOptions options = null)
{
var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options);
return RestInteractionMessage.Create(Discord, model, Token, Channel);
}
/// <summary>
/// Acknowledges this interaction.
/// </summary>
/// <param name="ephemeral"><see langword="true"/> to send this message ephemerally, otherwise <see langword="false"/>.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns>
public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
#endregion
#region IDiscordInteraction
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions,
RequestOptions options, MessageComponent component, Embed embed)
=> await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component, embed).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.GetOriginalResponseAsync(RequestOptions options)
=> await GetOriginalResponseAsync(options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IUserMessage> IDiscordInteraction.ModifyOriginalResponseAsync(Action<MessageProperties> func, RequestOptions options)
=> await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false);
#endregion
}
}

View File

@@ -6,6 +6,9 @@ using Model = Discord.API.Gateway.InviteCreateEvent;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based invite to a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketInvite : SocketEntity<string>, IInviteMetadata
{
@@ -28,16 +31,16 @@ namespace Discord.WebSocket
{
get
{
switch (Channel)
return Channel switch
{
case IVoiceChannel voiceChannel: return ChannelType.Voice;
case ICategoryChannel categoryChannel: return ChannelType.Category;
case IDMChannel dmChannel: return ChannelType.DM;
case IGroupChannel groupChannel: return ChannelType.Group;
case INewsChannel newsChannel: return ChannelType.News;
case ITextChannel textChannel: return ChannelType.Text;
default: throw new InvalidOperationException("Invalid channel type.");
}
IVoiceChannel voiceChannel => ChannelType.Voice,
ICategoryChannel categoryChannel => ChannelType.Category,
IDMChannel dmChannel => ChannelType.DM,
IGroupChannel groupChannel => ChannelType.Group,
INewsChannel newsChannel => ChannelType.News,
ITextChannel textChannel => ChannelType.Text,
_ => throw new InvalidOperationException("Invalid channel type."),
};
}
}
/// <inheritdoc />

View File

@@ -1,4 +1,5 @@
using Discord.Rest;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -13,8 +14,10 @@ namespace Discord.WebSocket
/// </summary>
public abstract class SocketMessage : SocketEntity<ulong>, IMessage
{
#region SocketMessage
private long _timestampTicks;
private readonly List<SocketReaction> _reactions = new List<SocketReaction>();
private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>();
/// <summary>
/// Gets the author of this message.
@@ -36,6 +39,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string Content { get; private set; }
/// <inheritdoc />
public string CleanContent => MessageHelper.SanitizeMessage(this);
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
@@ -58,6 +64,14 @@ namespace Discord.WebSocket
/// <inheritdoc />
public MessageReference Reference { get; private set; }
/// <inheritdoc/>
public IReadOnlyCollection<ActionRowComponent> Components { get; private set; }
/// <summary>
/// Gets the interaction this message is a response to.
/// </summary>
public MessageInteraction<SocketUser> Interaction { get; private set; }
/// <inheritdoc />
public MessageFlags? Flags { get; private set; }
@@ -92,20 +106,19 @@ namespace Discord.WebSocket
/// Collection of WebSocket-based roles.
/// </returns>
public virtual IReadOnlyCollection<SocketRole> MentionedRoles => ImmutableArray.Create<SocketRole>();
/// <inheritdoc />
public virtual IReadOnlyCollection<ITag> Tags => ImmutableArray.Create<ITag>();
/// <inheritdoc />
public virtual IReadOnlyCollection<SocketSticker> Stickers => ImmutableArray.Create<SocketSticker>();
/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) });
/// <summary>
/// Returns the users mentioned in this message.
/// </summary>
/// <returns>
/// Collection of WebSocket-based users.
/// </returns>
public virtual IReadOnlyCollection<SocketUser> MentionedUsers => ImmutableArray.Create<SocketUser>();
/// <inheritdoc />
public virtual IReadOnlyCollection<ITag> Tags => ImmutableArray.Create<ITag>();
/// <inheritdoc />
public virtual IReadOnlyCollection<Sticker> Stickers => ImmutableArray.Create<Sticker>();
/// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) });
public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions;
/// <inheritdoc />
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks);
@@ -118,7 +131,10 @@ namespace Discord.WebSocket
}
internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model)
{
if (model.Type == MessageType.Default || model.Type == MessageType.Reply)
if (model.Type == MessageType.Default ||
model.Type == MessageType.Reply ||
model.Type == MessageType.ApplicationCommand ||
model.Type == MessageType.ThreadStarterMessage)
return SocketUserMessage.Create(discord, state, author, channel, model);
else
return SocketSystemMessage.Create(discord, state, author, channel, model);
@@ -131,7 +147,9 @@ namespace Discord.WebSocket
_timestampTicks = model.Timestamp.Value.UtcTicks;
if (model.Content.IsSpecified)
{
Content = model.Content.Value;
}
if (model.Application.IsSpecified)
{
@@ -167,6 +185,86 @@ namespace Discord.WebSocket
};
}
if (model.Components.IsSpecified)
{
Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select<IMessageComponent, IMessageComponent>(y =>
{
switch (y.Type)
{
case ComponentType.Button:
{
var parsed = (API.ButtonComponent)y;
return new Discord.ButtonComponent(
parsed.Style,
parsed.Label.GetValueOrDefault(),
parsed.Emote.IsSpecified
? parsed.Emote.Value.Id.HasValue
? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault())
: new Emoji(parsed.Emote.Value.Name)
: null,
parsed.CustomId.GetValueOrDefault(),
parsed.Url.GetValueOrDefault(),
parsed.Disabled.GetValueOrDefault());
}
case ComponentType.SelectMenu:
{
var parsed = (API.SelectMenuComponent)y;
return new SelectMenuComponent(
parsed.CustomId,
parsed.Options.Select(z => new SelectMenuOption(
z.Label,
z.Value,
z.Description.GetValueOrDefault(),
z.Emoji.IsSpecified
? z.Emoji.Value.Id.HasValue
? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault())
: new Emoji(z.Emoji.Value.Name)
: null,
z.Default.ToNullable())).ToList(),
parsed.Placeholder.GetValueOrDefault(),
parsed.MinValues,
parsed.MaxValues,
parsed.Disabled
);
}
default:
return null;
}
}).ToList())).ToImmutableArray();
}
else
Components = new List<ActionRowComponent>();
if (model.UserMentions.IsSpecified)
{
var value = model.UserMentions.Value;
if (value.Length > 0)
{
var newMentions = ImmutableArray.CreateBuilder<SocketUser>(value.Length);
for (int i = 0; i < value.Length; i++)
{
var val = value[i];
if (val != null)
{
var user = Channel.GetUserAsync(val.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser;
if (user != null)
newMentions.Add(user);
else
newMentions.Add(SocketUnknownUser.Create(Discord, state, val));
}
}
_userMentions = newMentions.ToImmutable();
}
}
if (model.Interaction.IsSpecified)
{
Interaction = new MessageInteraction<SocketUser>(model.Interaction.Value.Id,
model.Interaction.Value.Type,
model.Interaction.Value.Name,
SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User));
}
if (model.Flags.IsSpecified)
Flags = model.Flags.Value;
}
@@ -183,8 +281,9 @@ namespace Discord.WebSocket
/// </returns>
public override string ToString() => Content;
internal SocketMessage Clone() => MemberwiseClone() as SocketMessage;
#endregion
//IMessage
#region IMessage
/// <inheritdoc />
IUser IMessage.Author => Author;
/// <inheritdoc />
@@ -199,8 +298,16 @@ namespace Discord.WebSocket
IReadOnlyCollection<ulong> IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc />
IReadOnlyCollection<ulong> IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray();
/// <inheritdoc/>
IReadOnlyCollection<IMessageComponent> IMessage.Components => Components;
/// <inheritdoc/>
IMessageInteraction IMessage.Interaction => Interaction;
/// <inheritdoc />
IReadOnlyCollection<ISticker> IMessage.Stickers => Stickers;
IReadOnlyCollection<IStickerItem> IMessage.Stickers => Stickers;
internal void AddReaction(SocketReaction reaction)
{
@@ -238,5 +345,6 @@ namespace Discord.WebSocket
/// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
#endregion
}
}

View File

@@ -22,8 +22,7 @@ namespace Discord.WebSocket
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>();
private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>();
private ImmutableArray<Sticker> _stickers = ImmutableArray.Create<Sticker>();
private ImmutableArray<SocketSticker> _stickers = ImmutableArray.Create<SocketSticker>();
/// <inheritdoc />
public override bool IsTTS => _isTTS;
@@ -46,9 +45,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override IReadOnlyCollection<SocketRole> MentionedRoles => _roleMentions;
/// <inheritdoc />
public override IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions;
/// <inheritdoc />
public override IReadOnlyCollection<Sticker> Stickers => _stickers;
public override IReadOnlyCollection<SocketSticker> Stickers => _stickers;
/// <inheritdoc />
public IUserMessage ReferencedMessage => _referencedMessage;
@@ -108,32 +105,10 @@ namespace Discord.WebSocket
_embeds = ImmutableArray.Create<Embed>();
}
if (model.UserMentions.IsSpecified)
{
var value = model.UserMentions.Value;
if (value.Length > 0)
{
var newMentions = ImmutableArray.CreateBuilder<SocketUser>(value.Length);
for (int i = 0; i < value.Length; i++)
{
var val = value[i];
if (val.Object != null)
{
var user = Channel.GetUserAsync(val.Object.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser;
if (user != null)
newMentions.Add(user);
else
newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object));
}
}
_userMentions = newMentions.ToImmutable();
}
}
if (model.Content.IsSpecified)
{
var text = model.Content.Value;
_tags = MessageHelper.ParseTags(text, Channel, guild, _userMentions);
_tags = MessageHelper.ParseTags(text, Channel, guild, MentionedUsers);
model.Content = text;
}
@@ -162,18 +137,40 @@ namespace Discord.WebSocket
_referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg);
}
if (model.Stickers.IsSpecified)
if (model.StickerItems.IsSpecified)
{
var value = model.Stickers.Value;
var value = model.StickerItems.Value;
if (value.Length > 0)
{
var stickers = ImmutableArray.CreateBuilder<Sticker>(value.Length);
var stickers = ImmutableArray.CreateBuilder<SocketSticker>(value.Length);
for (int i = 0; i < value.Length; i++)
stickers.Add(Sticker.Create(value[i]));
{
var stickerItem = value[i];
SocketSticker sticker = null;
if (guild != null)
sticker = guild.GetSticker(stickerItem.Id);
if (sticker == null)
sticker = Discord.GetSticker(stickerItem.Id);
// if they want to auto resolve
if (Discord.AlwaysResolveStickers)
{
sticker = Task.Run(async () => await Discord.GetStickerAsync(stickerItem.Id).ConfigureAwait(false)).GetAwaiter().GetResult();
}
// if its still null, create an unknown
if (sticker == null)
sticker = SocketUnknownSticker.Create(Discord, stickerItem);
stickers.Add(sticker);
}
_stickers = stickers.ToImmutable();
}
else
_stickers = ImmutableArray.Create<Sticker>();
_stickers = ImmutableArray.Create<SocketSticker>();
}
}

View File

@@ -1,6 +1,6 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@@ -14,6 +14,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketRole : SocketEntity<ulong>, IRole
{
#region SocketRole
/// <summary>
/// Gets the guild that owns this role.
/// </summary>
@@ -32,6 +33,10 @@ namespace Discord.WebSocket
public bool IsMentionable { get; private set; }
/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc/>
public Emoji Emoji { get; private set; }
/// <inheritdoc />
public string Icon { get; private set; }
/// <inheritdoc />
public GuildPermissions Permissions { get; private set; }
/// <inheritdoc />
@@ -50,7 +55,11 @@ namespace Discord.WebSocket
public bool IsEveryone => Id == Guild.Id;
/// <inheritdoc />
public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id);
public IEnumerable<SocketGuildUser> Members
/// <summary>
/// Returns an IEnumerable containing all <see cref="SocketGuildUser"/> that have this role.
/// </summary>
public IEnumerable<SocketGuildUser> Members
=> Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id));
internal SocketRole(SocketGuild guild, ulong id)
@@ -75,6 +84,16 @@ namespace Discord.WebSocket
Permissions = new GuildPermissions(model.Permissions);
if (model.Tags.IsSpecified)
Tags = model.Tags.Value.ToEntity();
if (model.Icon.IsSpecified)
{
Icon = model.Icon.Value;
}
if (model.Emoji.IsSpecified)
{
Emoji = new Emoji(model.Emoji.Value);
}
}
/// <inheritdoc />
@@ -84,6 +103,10 @@ namespace Discord.WebSocket
public Task DeleteAsync(RequestOptions options = null)
=> RoleHelper.DeleteAsync(this, Discord, options);
/// <inheritdoc />
public string GetIconUrl()
=> CDN.GetGuildRoleIconUrl(Id, Icon);
/// <summary>
/// Gets the name of the role.
/// </summary>
@@ -96,9 +119,11 @@ namespace Discord.WebSocket
/// <inheritdoc />
public int CompareTo(IRole role) => RoleUtils.Compare(this, role);
#endregion
//IRole
#region IRole
/// <inheritdoc />
IGuild IRole.Guild => Guild;
#endregion
}
}

View File

@@ -0,0 +1,81 @@
using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Sticker;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a custom sticker within a guild received over the gateway.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketCustomSticker : SocketSticker, ICustomSticker
{
#region SocketCustomSticker
/// <summary>
/// Gets the user that uploaded the guild sticker.
/// </summary>
/// <remarks>
/// <note>
/// This may return <see langword="null"/> in the WebSocket implementation due to incomplete user collection in
/// large guilds, or the bot doesn't have the MANAGE_EMOJIS_AND_STICKERS permission.
/// </note>
/// </remarks>
public SocketGuildUser Author
=> AuthorId.HasValue ? Guild.GetUser(AuthorId.Value) : null;
/// <summary>
/// Gets the guild the sticker was created in.
/// </summary>
public SocketGuild Guild { get; }
/// <inheritdoc/>
public ulong? AuthorId { get; set; }
internal SocketCustomSticker(DiscordSocketClient client, ulong id, SocketGuild guild, ulong? authorId = null)
: base(client, id)
{
Guild = guild;
AuthorId = authorId;
}
internal static SocketCustomSticker Create(DiscordSocketClient client, Model model, SocketGuild guild, ulong? authorId = null)
{
var entity = new SocketCustomSticker(client, model.Id, guild, authorId);
entity.Update(model);
return entity;
}
/// <inheritdoc/>
public async Task ModifyAsync(Action<StickerProperties> func, RequestOptions options = null)
{
if (!Guild.CurrentUser.GuildPermissions.Has(GuildPermission.ManageEmojisAndStickers))
throw new InvalidOperationException($"Missing permission {nameof(GuildPermission.ManageEmojisAndStickers)}");
var model = await GuildHelper.ModifyStickerAsync(Discord, Guild.Id, this, func, options);
Update(model);
}
/// <inheritdoc/>
public async Task DeleteAsync(RequestOptions options = null)
{
await GuildHelper.DeleteStickerAsync(Discord, Guild.Id, this, options);
Guild.RemoveSticker(Id);
}
internal SocketCustomSticker Clone() => MemberwiseClone() as SocketCustomSticker;
private new string DebuggerDisplay => Guild == null ? base.DebuggerDisplay : $"{Name} in {Guild.Name} ({Id})";
#endregion
#region ICustomSticker
ulong? ICustomSticker.AuthorId
=> AuthorId;
IGuild ICustomSticker.Guild
=> Guild;
#endregion
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Model = Discord.API.Sticker;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a general sticker received over the gateway.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketSticker : SocketEntity<ulong>, ISticker
{
/// <inheritdoc/>
public virtual ulong PackId { get; private set; }
/// <inheritdoc/>
public string Name { get; protected set; }
/// <inheritdoc/>
public virtual string Description { get; private set; }
/// <inheritdoc/>
public virtual IReadOnlyCollection<string> Tags { get; private set; }
/// <inheritdoc/>
public virtual StickerType Type { get; private set; }
/// <inheritdoc/>
public StickerFormatType Format { get; protected set; }
/// <inheritdoc/>
public virtual bool? IsAvailable { get; protected set; }
/// <inheritdoc/>
public virtual int? SortOrder { get; private set; }
/// <inheritdoc/>
public string GetStickerUrl()
=> CDN.GetStickerUrl(Id, Format);
internal SocketSticker(DiscordSocketClient client, ulong id)
: base(client, id) { }
internal static SocketSticker Create(DiscordSocketClient client, Model model)
{
var entity = model.GuildId.IsSpecified
? new SocketCustomSticker(client, model.Id, client.GetGuild(model.GuildId.Value), model.User.IsSpecified ? model.User.Value.Id : null)
: new SocketSticker(client, model.Id);
entity.Update(model);
return entity;
}
internal virtual void Update(Model model)
{
Name = model.Name;
Description = model.Description;
PackId = model.PackId;
IsAvailable = model.Available;
Format = model.FormatType;
Type = model.Type;
SortOrder = model.SortValue;
Tags = model.Tags.IsSpecified
? model.Tags.Value.Split(',').Select(x => x.Trim()).ToImmutableArray()
: ImmutableArray.Create<string>();
}
internal string DebuggerDisplay => $"{Name} ({Id})";
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is Model stickerModel)
{
return stickerModel.Name == Name &&
stickerModel.Description == Description &&
stickerModel.FormatType == Format &&
stickerModel.Id == Id &&
stickerModel.PackId == PackId &&
stickerModel.Type == Type &&
stickerModel.SortValue == SortOrder &&
stickerModel.Available == IsAvailable &&
(!stickerModel.Tags.IsSpecified || stickerModel.Tags.Value == string.Join(", ", Tags));
}
return base.Equals(obj);
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.StickerItem;
namespace Discord.WebSocket
{
/// <summary>
/// Represents an unknown sticker received over the gateway.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketUnknownSticker : SocketSticker
{
/// <inheritdoc/>
public override IReadOnlyCollection<string> Tags
=> null;
/// <inheritdoc/>
public override string Description
=> null;
/// <inheritdoc/>
public override ulong PackId
=> 0;
/// <inheritdoc/>
public override bool? IsAvailable
=> null;
/// <inheritdoc/>
public override int? SortOrder
=> null;
/// <inheritdoc/>
public new StickerType? Type
=> null;
internal SocketUnknownSticker(DiscordSocketClient client, ulong id)
: base(client, id) { }
internal static SocketUnknownSticker Create(DiscordSocketClient client, Model model)
{
var entity = new SocketUnknownSticker(client, model.Id);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
Name = model.Name;
Format = model.FormatType;
}
/// <summary>
/// Attempts to try to find the sticker.
/// </summary>
/// <returns>
/// The sticker representing this unknown stickers Id, if none is found then <see langword="null"/>.
/// </returns>
public Task<SocketSticker> ResolveAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
=> Discord.GetStickerAsync(Id, mode, options);
private new string DebuggerDisplay => $"{Name} ({Id})";
}
}

View File

@@ -47,7 +47,7 @@ namespace Discord.WebSocket
discord.RemoveUser(Id);
}
}
internal void Update(ClientState state, PresenceModel model)
{
Presence = SocketPresence.Create(model);

View File

@@ -1,11 +1,16 @@
using System;
using System.Diagnostics;
using Model = Discord.API.User;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based group user.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class SocketGroupUser : SocketUser, IGroupUser
{
#region SocketGroupUser
/// <summary>
/// Gets the group channel of the user.
/// </summary>
@@ -45,8 +50,9 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)";
internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser;
#endregion
//IVoiceState
#region IVoiceState
/// <inheritdoc />
bool IVoiceState.IsDeafened => false;
/// <inheritdoc />
@@ -63,5 +69,8 @@ namespace Discord.WebSocket
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
#endregion
}
}

View File

@@ -18,6 +18,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildUser : SocketUser, IGuildUser
{
#region SocketGuildUser
private long? _premiumSinceTicks;
private long? _joinedAtTicks;
private ImmutableArray<ulong> _roleIds;
@@ -29,7 +30,8 @@ namespace Discord.WebSocket
public SocketGuild Guild { get; }
/// <inheritdoc />
public string Nickname { get; private set; }
/// <inheritdoc/>
public string GuildAvatarId { get; private set; }
/// <inheritdoc />
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } }
/// <inheritdoc />
@@ -38,6 +40,7 @@ namespace Discord.WebSocket
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } }
/// <inheritdoc />
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } }
/// <inheritdoc />
public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this));
internal override SocketPresence Presence { get; set; }
@@ -57,7 +60,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>
@@ -87,7 +94,7 @@ namespace Discord.WebSocket
/// Returns the position of the user within the role hierarchy.
/// </summary>
/// <remarks>
/// The returned value equal to the position of the highest role the user has, or
/// The returned value equal to the position of the highest role the user has, or
/// <see cref="int.MaxValue"/> if user is the server owner.
/// </remarks>
public int Hierarchy
@@ -144,6 +151,8 @@ namespace Discord.WebSocket
_joinedAtTicks = model.JoinedAt.Value.UtcTicks;
if (model.Nick.IsSpecified)
Nickname = model.Nick.Value;
if (model.Avatar.IsSpecified)
GuildAvatarId = model.Avatar.Value;
if (model.Roles.IsSpecified)
UpdateRoles(model.Roles.Value);
if (model.PremiumSince.IsSpecified)
@@ -208,11 +217,14 @@ namespace Discord.WebSocket
/// <inheritdoc />
public ChannelPermissions GetPermissions(IGuildChannel channel)
=> new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue));
public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format);
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)";
internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser;
#endregion
//IGuildUser
#region IGuildUser
/// <inheritdoc />
IGuild IGuildUser.Guild => Guild;
/// <inheritdoc />
@@ -223,5 +235,6 @@ namespace Discord.WebSocket
//IVoiceState
/// <inheritdoc />
IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel;
#endregion
}
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.ThreadMember;
using System.Collections.Immutable;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a thread user received over the gateway.
/// </summary>
public class SocketThreadUser : SocketUser, IGuildUser
{
/// <summary>
/// Gets the <see cref="SocketThreadChannel"/> this user is in.
/// </summary>
public SocketThreadChannel Thread { get; private set; }
/// <summary>
/// Gets the timestamp for when this user joined this thread.
/// </summary>
public DateTimeOffset ThreadJoinedAt { get; private set; }
/// <summary>
/// Gets the guild this user is in.
/// </summary>
public SocketGuild Guild { get; private set; }
/// <inheritdoc/>
public DateTimeOffset? JoinedAt
=> GuildUser.JoinedAt;
/// <inheritdoc/>
public string Nickname
=> GuildUser.Nickname;
/// <inheritdoc/>
public DateTimeOffset? PremiumSince
=> GuildUser.PremiumSince;
/// <inheritdoc/>
public bool? IsPending
=> GuildUser.IsPending;
/// <inheritdoc />
public int Hierarchy
=> GuildUser.Hierarchy;
/// <inheritdoc/>
public override string AvatarId
{
get => GuildUser.AvatarId;
internal set => GuildUser.AvatarId = value;
}
/// <inheritdoc/>
public string GuildAvatarId
=> GuildUser.GuildAvatarId;
/// <inheritdoc/>
public override ushort DiscriminatorValue
{
get => GuildUser.DiscriminatorValue;
internal set => GuildUser.DiscriminatorValue = value;
}
/// <inheritdoc/>
public override bool IsBot
{
get => GuildUser.IsBot;
internal set => GuildUser.IsBot = value;
}
/// <inheritdoc/>
public override bool IsWebhook
=> GuildUser.IsWebhook;
/// <inheritdoc/>
public override string Username
{
get => GuildUser.Username;
internal set => GuildUser.Username = value;
}
/// <inheritdoc/>
public bool IsDeafened
=> GuildUser.IsDeafened;
/// <inheritdoc/>
public bool IsMuted
=> GuildUser.IsMuted;
/// <inheritdoc/>
public bool IsSelfDeafened
=> GuildUser.IsSelfDeafened;
/// <inheritdoc/>
public bool IsSelfMuted
=> GuildUser.IsSelfMuted;
/// <inheritdoc/>
public bool IsSuppressed
=> GuildUser.IsSuppressed;
/// <inheritdoc/>
public IVoiceChannel VoiceChannel
=> GuildUser.VoiceChannel;
/// <inheritdoc/>
public string VoiceSessionId
=> GuildUser.VoiceSessionId;
/// <inheritdoc/>
public bool IsStreaming
=> GuildUser.IsStreaming;
/// <inheritdoc/>
public DateTimeOffset? RequestToSpeakTimestamp
=> GuildUser.RequestToSpeakTimestamp;
private SocketGuildUser GuildUser { get; set; }
internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member)
: base(guild.Discord, member.Id)
{
Thread = thread;
Guild = guild;
GuildUser = member;
}
internal static 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)
{
ThreadJoinedAt = model.JoinTimestamp;
if (model.Presence.IsSpecified)
{
GuildUser.Update(Discord.State, model.Presence.Value, true);
}
if (model.Member.IsSpecified)
{
GuildUser.Update(Discord.State, model.Member.Value);
}
}
/// <inheritdoc/>
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel);
/// <inheritdoc/>
public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options);
/// <inheritdoc/>
public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options);
/// <inheritdoc/>
public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options);
/// <inheritdoc/>
public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options);
/// <inheritdoc/>
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options);
/// <inheritdoc/>
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options);
/// <inheritdoc/>
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options);
/// <inheritdoc/>
public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options);
/// <inheritdoc/>
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options);
/// <inheritdoc/>
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options);
/// <inheritdoc/>
GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions;
/// <inheritdoc/>
IGuild IGuildUser.Guild => Guild;
/// <inheritdoc/>
ulong IGuildUser.GuildId => Guild.Id;
/// <inheritdoc/>
IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray();
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size);
internal override SocketGlobalUser GlobalUser => GuildUser.GlobalUser;
internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; }
/// <summary>
/// Gets the guild user of this thread user.
/// </summary>
/// <param name="user"></param>
public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser;
}
}

View File

@@ -19,9 +19,10 @@ namespace Discord.WebSocket
public override ushort DiscriminatorValue { get; internal set; }
/// <inheritdoc />
public override string AvatarId { get; internal set; }
/// <inheritdoc />
public override bool IsBot { get; internal set; }
/// <inheritdoc />
public override bool IsWebhook => false;
/// <inheritdoc />

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>

View File

@@ -13,6 +13,7 @@ namespace Discord.WebSocket
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketWebhookUser : SocketUser, IWebhookUser
{
#region SocketWebhookUser
/// <summary> Gets the guild of this webhook. </summary>
public SocketGuild Guild { get; }
/// <inheritdoc />
@@ -24,6 +25,8 @@ namespace Discord.WebSocket
public override ushort DiscriminatorValue { get; internal set; }
/// <inheritdoc />
public override string AvatarId { get; internal set; }
/// <inheritdoc />
public override bool IsBot { get; internal set; }
@@ -49,9 +52,9 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)";
internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser;
#endregion
//IGuildUser
#region IGuildUser
/// <inheritdoc />
IGuild IGuildUser.Guild => Guild;
/// <inheritdoc />
@@ -63,10 +66,16 @@ namespace Discord.WebSocket
/// <inheritdoc />
string IGuildUser.Nickname => null;
/// <inheritdoc />
string IGuildUser.GuildAvatarId => null;
/// <inheritdoc />
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null;
/// <inheritdoc />
DateTimeOffset? IGuildUser.PremiumSince => null;
/// <inheritdoc />
bool? IGuildUser.IsPending => null;
/// <inheritdoc />
int IGuildUser.Hierarchy => 0;
/// <inheritdoc />
GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook;
/// <inheritdoc />
@@ -120,8 +129,9 @@ namespace Discord.WebSocket
/// <exception cref="NotSupportedException">Roles are not supported on webhook users.</exception>
Task IGuildUser.RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options) =>
throw new NotSupportedException("Roles are not supported on webhook users.");
#endregion
//IVoiceState
#region IVoiceState
/// <inheritdoc />
bool IVoiceState.IsDeafened => false;
/// <inheritdoc />
@@ -138,5 +148,8 @@ namespace Discord.WebSocket
string IVoiceState.VoiceSessionId => null;
/// <inheritdoc />
bool IVoiceState.IsStreaming => false;
/// <inheritdoc />
DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null;
#endregion
}
}