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

@@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class ApplicationCommandCreatedUpdatedEvent : ApplicationCommand
{
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;
@@ -8,18 +7,29 @@ namespace Discord.API.Gateway
{
[JsonProperty("unavailable")]
public bool? Unavailable { get; set; }
[JsonProperty("member_count")]
public int MemberCount { get; set; }
[JsonProperty("large")]
public bool Large { get; set; }
[JsonProperty("presences")]
public Presence[] Presences { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
[JsonProperty("channels")]
public Channel[] Channels { get; set; }
[JsonProperty("joined_at")]
public DateTimeOffset JoinedAt { get; set; }
[JsonProperty("threads")]
public new Channel[] Threads { get; set; }
[JsonProperty("guild_scheduled_events")]
public GuildScheduledEvent[] GuildScheduledEvents { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
namespace Discord.API.Gateway
{
internal enum GatewayOpCode : byte
@@ -10,7 +9,7 @@ namespace Discord.API.Gateway
/// <summary> C→S - Used to associate a connection with a token and specify configuration. </summary>
Identify = 2,
/// <summary> C→S - Used to update client's status and current game id. </summary>
StatusUpdate = 3,
PresenceUpdate = 3,
/// <summary> C→S - Used to join a particular voice channel. </summary>
VoiceStateUpdate = 4,
/// <summary> C→S - Used to ensure the guild's voice server is alive. </summary>

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class GuildJoinRequestDeleteEvent
{
[JsonProperty("user_id")]
public ulong UserId { get; set; }
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,10 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;
namespace Discord.API.Gateway
{
internal class GuildMemberUpdateEvent : GuildMember
{
[JsonProperty("joined_at")]
public new DateTimeOffset? JoinedAt { get; set; }
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord.API.Gateway
{
internal class GuildScheduledEventUserAddRemoveEvent
{
[JsonProperty("guild_scheduled_event_id")]
public ulong EventId { get; set; }
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("user_id")]
public ulong UserId { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class GuildStickerUpdateEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("stickers")]
public Sticker[] Stickers { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;
@@ -16,7 +15,7 @@ namespace Discord.API.Gateway
[JsonProperty("shard")]
public Optional<int[]> ShardingParams { get; set; }
[JsonProperty("presence")]
public Optional<StatusUpdateParams> Presence { get; set; }
public Optional<PresenceUpdateParams> Presence { get; set; }
[JsonProperty("intents")]
public Optional<int> Intents { get; set; }
}

View File

@@ -0,0 +1,32 @@
using Discord.API;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord.API.Gateway
{
internal class InviteCreatedEvent
{
[JsonProperty("channel_id")]
public ulong ChannelID { get; set; }
[JsonProperty("code")]
public string InviteCode { get; set; }
[JsonProperty("timestamp")]
public Optional<DateTimeOffset> RawTimestamp { get; set; }
[JsonProperty("guild_id")]
public ulong? GuildID { get; set; }
[JsonProperty("inviter")]
public Optional<User> Inviter { get; set; }
[JsonProperty("max_age")]
public int RawAge { get; set; }
[JsonProperty("max_uses")]
public int MaxUsers { get; set; }
[JsonProperty("temporary")]
public bool TempInvite { get; set; }
[JsonProperty("uses")]
public int Uses { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord.WebSocket
{
internal class InviteDeletedEvent
{
[JsonProperty("channel_id")]
public ulong ChannelID { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildID { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,18 +1,18 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class StatusUpdateParams
internal class PresenceUpdateParams
{
[JsonProperty("status")]
public UserStatus Status { get; set; }
[JsonProperty("since"), Int53]
[JsonProperty("since", NullValueHandling = NullValueHandling.Include), Int53]
public long? IdleSince { get; set; }
[JsonProperty("afk")]
public bool IsAFK { get; set; }
[JsonProperty("game")]
public Game Game { get; set; }
[JsonProperty("activities")]
public object[] Activities { get; set; } // TODO, change to interface later
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class ThreadListSyncEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("channel_ids")]
public Optional<ulong[]> ChannelIds { get; set; }
[JsonProperty("threads")]
public Channel[] Threads { get; set; }
[JsonProperty("members")]
public ThreadMember[] Members { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
internal class ThreadMembersUpdated
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("member_count")]
public int MemberCount { get; set; }
[JsonProperty("added_members")]
public Optional<ThreadMember[]> AddedMembers { get; set; }
[JsonProperty("removed_member_ids")]
public Optional<ulong[]> RemovedMemberIds { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;
@@ -15,7 +14,7 @@ namespace Discord.API.Voice
[JsonProperty("modes")]
public string[] Modes { get; set; }
[JsonProperty("heartbeat_interval")]
[Obsolete("This field is errorneous and should not be used", true)]
[Obsolete("This field is erroneous and should not be used", true)]
public int HeartbeatInterval { get; set; }
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord.API.Voice
{
/// <summary>
/// Represents generic op codes for voice disconnect.
/// </summary>
public enum VoiceCloseCode
{
/// <summary>
/// You sent an invalid opcode.
/// </summary>
UnknownOpcode = 4001,
/// <summary>
/// You sent an invalid payload in your identifying to the Gateway.
/// </summary>
DecodeFailure = 4002,
/// <summary>
/// You sent a payload before identifying with the Gateway.
/// </summary>
NotAuthenticated = 4003,
/// <summary>
/// The token you sent in your identify payload is incorrect.
/// </summary>
AuthenticationFailed = 4004,
/// <summary>
/// You sent more than one identify payload. Stahp.
/// </summary>
AlreadyAuthenticated = 4005,
/// <summary>
/// Your session is no longer valid.
/// </summary>
SessionNolongerValid = 4006,
/// <summary>
/// Your session has timed out.
/// </summary>
SessionTimeout = 4009,
/// <summary>
/// We can't find the server you're trying to connect to.
/// </summary>
ServerNotFound = 4011,
/// <summary>
/// We didn't recognize the protocol you sent.
/// </summary>
UnknownProtocol = 4012,
/// <summary>
/// Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect.
/// </summary>
Disconnected = 4014,
/// <summary>
/// The server crashed. Our bad! Try resuming.
/// </summary>
VoiceServerCrashed = 4015,
/// <summary>
/// We didn't recognize your encryption.
/// </summary>
UnknownEncryptionMode = 4016,
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
namespace Discord.API.Voice
{
internal enum VoiceOpCode : byte

View File

@@ -125,7 +125,7 @@ namespace Discord.Audio.Streams
timestamp += OpusEncoder.FrameSamplesPerChannel;
}
#if DEBUG
var _ = _logger?.DebugAsync("Buffer underrun");
var _ = _logger?.DebugAsync("Buffer under run");
#endif
}
}

View File

@@ -1,3 +1,4 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -6,7 +7,7 @@ namespace Discord.WebSocket
{
public partial class BaseSocketClient
{
//Channels
#region Channels
/// <summary> Fired when a channel is created. </summary>
/// <remarks>
/// <para>
@@ -23,7 +24,7 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelCreated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelCreated
public event Func<SocketChannel, Task> ChannelCreated
{
add { _channelCreatedEvent.Add(value); }
remove { _channelCreatedEvent.Remove(value); }
@@ -45,7 +46,8 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelDestroyed"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelDestroyed {
public event Func<SocketChannel, Task> ChannelDestroyed
{
add { _channelDestroyedEvent.Add(value); }
remove { _channelDestroyedEvent.Remove(value); }
}
@@ -67,13 +69,15 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelUpdated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated {
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated
{
add { _channelUpdatedEvent.Add(value); }
remove { _channelUpdatedEvent.Remove(value); }
}
}
internal readonly AsyncEvent<Func<SocketChannel, SocketChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<SocketChannel, SocketChannel, Task>>();
#endregion
//Messages
#region Messages
/// <summary> Fired when a message is received. </summary>
/// <remarks>
/// <para>
@@ -92,7 +96,8 @@ namespace Discord.WebSocket
/// <code language="cs" region="MessageReceived"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketMessage, Task> MessageReceived {
public event Func<SocketMessage, Task> MessageReceived
{
add { _messageReceivedEvent.Add(value); }
remove { _messageReceivedEvent.Remove(value); }
}
@@ -124,7 +129,9 @@ namespace Discord.WebSocket
/// <code language="cs" region="MessageDeleted"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs" />
/// </example>
public event Func<Cacheable<IMessage, ulong>, Cacheable<IMessageChannel, ulong>, Task> MessageDeleted {
public event Func<Cacheable<IMessage, ulong>, Cacheable<IMessageChannel, ulong>, Task> MessageDeleted
{
add { _messageDeletedEvent.Add(value); }
remove { _messageDeletedEvent.Remove(value); }
}
@@ -182,7 +189,8 @@ namespace Discord.WebSocket
/// <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// </remarks>
public event Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task> MessageUpdated {
public event Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task> MessageUpdated
{
add { _messageUpdatedEvent.Add(value); }
remove { _messageUpdatedEvent.Remove(value); }
}
@@ -217,19 +225,22 @@ namespace Discord.WebSocket
/// <code language="cs" region="ReactionAdded"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task> ReactionAdded {
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task> ReactionAdded
{
add { _reactionAddedEvent.Add(value); }
remove { _reactionAddedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task>>();
/// <summary> Fired when a reaction is removed from a message. </summary>
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task> ReactionRemoved {
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task> ReactionRemoved
{
add { _reactionRemovedEvent.Add(value); }
remove { _reactionRemovedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, SocketReaction, Task>>();
/// <summary> Fired when all reactions to a message are cleared. </summary>
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, Task> ReactionsCleared {
public event Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, Task> ReactionsCleared
{
add { _reactionsClearedEvent.Add(value); }
remove { _reactionsClearedEvent.Remove(value); }
}
@@ -256,104 +267,200 @@ namespace Discord.WebSocket
remove { _reactionsRemovedForEmoteEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, Cacheable<IMessageChannel, ulong>, IEmote, Task>>();
#endregion
//Roles
#region Roles
/// <summary> Fired when a role is created. </summary>
public event Func<SocketRole, Task> RoleCreated {
public event Func<SocketRole, Task> RoleCreated
{
add { _roleCreatedEvent.Add(value); }
remove { _roleCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketRole, Task>> _roleCreatedEvent = new AsyncEvent<Func<SocketRole, Task>>();
/// <summary> Fired when a role is deleted. </summary>
public event Func<SocketRole, Task> RoleDeleted {
public event Func<SocketRole, Task> RoleDeleted
{
add { _roleDeletedEvent.Add(value); }
remove { _roleDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketRole, Task>> _roleDeletedEvent = new AsyncEvent<Func<SocketRole, Task>>();
/// <summary> Fired when a role is updated. </summary>
public event Func<SocketRole, SocketRole, Task> RoleUpdated {
public event Func<SocketRole, SocketRole, Task> RoleUpdated
{
add { _roleUpdatedEvent.Add(value); }
remove { _roleUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketRole, SocketRole, Task>> _roleUpdatedEvent = new AsyncEvent<Func<SocketRole, SocketRole, Task>>();
#endregion
//Guilds
#region Guilds
/// <summary> Fired when the connected account joins a guild. </summary>
public event Func<SocketGuild, Task> JoinedGuild {
public event Func<SocketGuild, Task> JoinedGuild
{
add { _joinedGuildEvent.Add(value); }
remove { _joinedGuildEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _joinedGuildEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when the connected account leaves a guild. </summary>
public event Func<SocketGuild, Task> LeftGuild {
public event Func<SocketGuild, Task> LeftGuild
{
add { _leftGuildEvent.Add(value); }
remove { _leftGuildEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _leftGuildEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild becomes available. </summary>
public event Func<SocketGuild, Task> GuildAvailable {
public event Func<SocketGuild, Task> GuildAvailable
{
add { _guildAvailableEvent.Add(value); }
remove { _guildAvailableEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildAvailableEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild becomes unavailable. </summary>
public event Func<SocketGuild, Task> GuildUnavailable {
public event Func<SocketGuild, Task> GuildUnavailable
{
add { _guildUnavailableEvent.Add(value); }
remove { _guildUnavailableEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildUnavailableEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when offline guild members are downloaded. </summary>
public event Func<SocketGuild, Task> GuildMembersDownloaded {
public event Func<SocketGuild, Task> GuildMembersDownloaded
{
add { _guildMembersDownloadedEvent.Add(value); }
remove { _guildMembersDownloadedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildMembersDownloadedEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild is updated. </summary>
public event Func<SocketGuild, SocketGuild, Task> GuildUpdated {
public event Func<SocketGuild, SocketGuild, Task> GuildUpdated
{
add { _guildUpdatedEvent.Add(value); }
remove { _guildUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, SocketGuild, Task>> _guildUpdatedEvent = new AsyncEvent<Func<SocketGuild, SocketGuild, Task>>();
/// <summary>Fired when a user leaves without agreeing to the member screening </summary>
public event Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task> GuildJoinRequestDeleted
{
add { _guildJoinRequestDeletedEvent.Add(value); }
remove { _guildJoinRequestDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>> _guildJoinRequestDeletedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuild, Task>>();
#endregion
//Users
#region Guild Events
/// <summary>
/// Fired when a guild event is created.
/// </summary>
public event Func<SocketGuildEvent, Task> GuildScheduledEventCreated
{
add { _guildScheduledEventCreated.Add(value); }
remove { _guildScheduledEventCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCreated = new AsyncEvent<Func<SocketGuildEvent, Task>>();
/// <summary>
/// Fired when a guild event is updated.
/// </summary>
public event Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task> GuildScheduledEventUpdated
{
add { _guildScheduledEventUpdated.Add(value); }
remove { _guildScheduledEventUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task>> _guildScheduledEventUpdated = new AsyncEvent<Func<Cacheable<SocketGuildEvent, ulong>, SocketGuildEvent, Task>>();
/// <summary>
/// Fired when a guild event is cancelled.
/// </summary>
public event Func<SocketGuildEvent, Task> GuildScheduledEventCancelled
{
add { _guildScheduledEventCancelled.Add(value); }
remove { _guildScheduledEventCancelled.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCancelled = new AsyncEvent<Func<SocketGuildEvent, Task>>();
/// <summary>
/// Fired when a guild event is completed.
/// </summary>
public event Func<SocketGuildEvent, Task> GuildScheduledEventCompleted
{
add { _guildScheduledEventCompleted.Add(value); }
remove { _guildScheduledEventCompleted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventCompleted = new AsyncEvent<Func<SocketGuildEvent, Task>>();
/// <summary>
/// Fired when a guild event is started.
/// </summary>
public event Func<SocketGuildEvent, Task> GuildScheduledEventStarted
{
add { _guildScheduledEventStarted.Add(value); }
remove { _guildScheduledEventStarted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildEvent, Task>> _guildScheduledEventStarted = new AsyncEvent<Func<SocketGuildEvent, Task>>();
public event Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task> GuildScheduledEventUserAdd
{
add { _guildScheduledEventUserAdd.Add(value); }
remove { _guildScheduledEventUserAdd.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task>> _guildScheduledEventUserAdd = new AsyncEvent<Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task>>();
public event Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task> GuildScheduledEventUserRemove
{
add { _guildScheduledEventUserRemove.Add(value); }
remove { _guildScheduledEventUserRemove.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task>> _guildScheduledEventUserRemove = new AsyncEvent<Func<Cacheable<SocketUser, RestUser, IUser, ulong>, SocketGuildEvent, Task>>();
#endregion
#region Users
/// <summary> Fired when a user joins a guild. </summary>
public event Func<SocketGuildUser, Task> UserJoined {
public event Func<SocketGuildUser, Task> UserJoined
{
add { _userJoinedEvent.Add(value); }
remove { _userJoinedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildUser, Task>> _userJoinedEvent = new AsyncEvent<Func<SocketGuildUser, Task>>();
/// <summary> Fired when a user leaves a guild. </summary>
public event Func<SocketGuildUser, Task> UserLeft {
public event Func<SocketGuildUser, Task> UserLeft
{
add { _userLeftEvent.Add(value); }
remove { _userLeftEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildUser, Task>> _userLeftEvent = new AsyncEvent<Func<SocketGuildUser, Task>>();
/// <summary> Fired when a user is banned from a guild. </summary>
public event Func<SocketUser, SocketGuild, Task> UserBanned {
public event Func<SocketUser, SocketGuild, Task> UserBanned
{
add { _userBannedEvent.Add(value); }
remove { _userBannedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketGuild, Task>> _userBannedEvent = new AsyncEvent<Func<SocketUser, SocketGuild, Task>>();
/// <summary> Fired when a user is unbanned from a guild. </summary>
public event Func<SocketUser, SocketGuild, Task> UserUnbanned {
public event Func<SocketUser, SocketGuild, Task> UserUnbanned
{
add { _userUnbannedEvent.Add(value); }
remove { _userUnbannedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketGuild, Task>> _userUnbannedEvent = new AsyncEvent<Func<SocketUser, SocketGuild, Task>>();
/// <summary> Fired when a user is updated. </summary>
public event Func<SocketUser, SocketUser, Task> UserUpdated {
public event Func<SocketUser, SocketUser, Task> UserUpdated
{
add { _userUpdatedEvent.Add(value); }
remove { _userUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketUser, Task>> _userUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketUser, Task>>();
/// <summary> Fired when a guild member is updated, or a member presence is updated. </summary>
public event Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task> GuildMemberUpdated {
public event Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task> GuildMemberUpdated
{
add { _guildMemberUpdatedEvent.Add(value); }
remove { _guildMemberUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>>();
internal readonly AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task>>();
/// <summary> Fired when a user joins, leaves, or moves voice channels. </summary>
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated {
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated
{
add { _userVoiceStateUpdatedEvent.Add(value); }
remove { _userVoiceStateUpdatedEvent.Remove(value); }
}
@@ -361,36 +468,41 @@ namespace Discord.WebSocket
/// <summary> Fired when the bot connects to a Discord voice server. </summary>
public event Func<SocketVoiceServer, Task> VoiceServerUpdated
{
add { _voiceServerUpdatedEvent.Add(value); }
add { _voiceServerUpdatedEvent.Add(value); }
remove { _voiceServerUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>();
/// <summary> Fired when the connected account is updated. </summary>
public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated {
public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated
{
add { _selfUpdatedEvent.Add(value); }
remove { _selfUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketSelfUser, SocketSelfUser, Task>> _selfUpdatedEvent = new AsyncEvent<Func<SocketSelfUser, SocketSelfUser, Task>>();
/// <summary> Fired when a user starts typing. </summary>
public event Func<Cacheable<IUser, ulong>, Cacheable<IMessageChannel, ulong>, Task> UserIsTyping {
public event Func<Cacheable<IUser, ulong>, Cacheable<IMessageChannel, ulong>, Task> UserIsTyping
{
add { _userIsTypingEvent.Add(value); }
remove { _userIsTypingEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUser, ulong>, Cacheable<IMessageChannel, ulong>, Task>> _userIsTypingEvent = new AsyncEvent<Func<Cacheable<IUser, ulong>, Cacheable<IMessageChannel, ulong>, Task>>();
/// <summary> Fired when a user joins a group channel. </summary>
public event Func<SocketGroupUser, Task> RecipientAdded {
public event Func<SocketGroupUser, Task> RecipientAdded
{
add { _recipientAddedEvent.Add(value); }
remove { _recipientAddedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientAddedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();
/// <summary> Fired when a user is removed from a group channel. </summary>
public event Func<SocketGroupUser, Task> RecipientRemoved {
public event Func<SocketGroupUser, Task> RecipientRemoved
{
add { _recipientRemovedEvent.Add(value); }
remove { _recipientRemovedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();
#endregion
//Invites
#region Invites
/// <summary>
/// Fired when an invite is created.
/// </summary>
@@ -431,5 +543,292 @@ namespace Discord.WebSocket
remove { _inviteDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>();
#endregion
#region Interactions
/// <summary>
/// Fired when an Interaction is created. This event covers all types of interactions including but not limited to: buttons, select menus, slash commands, autocompletes.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an interaction is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketInteraction"/> as its parameter.
/// </para>
/// <para>
/// The interaction created will be passed into the <see cref="SocketInteraction"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketInteraction, Task> InteractionCreated
{
add { _interactionCreatedEvent.Add(value); }
remove { _interactionCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketInteraction, Task>> _interactionCreatedEvent = new AsyncEvent<Func<SocketInteraction, Task>>();
/// <summary>
/// Fired when a button is clicked and its interaction is received.
/// </summary>
public event Func<SocketMessageComponent, Task> ButtonExecuted
{
add => _buttonExecuted.Add(value);
remove => _buttonExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketMessageComponent, Task>> _buttonExecuted = new AsyncEvent<Func<SocketMessageComponent, Task>>();
/// <summary>
/// Fired when a select menu is used and its interaction is received.
/// </summary>
public event Func<SocketMessageComponent, Task> SelectMenuExecuted
{
add => _selectMenuExecuted.Add(value);
remove => _selectMenuExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketMessageComponent, Task>> _selectMenuExecuted = new AsyncEvent<Func<SocketMessageComponent, Task>>();
/// <summary>
/// Fired when a slash command is used and its interaction is received.
/// </summary>
public event Func<SocketSlashCommand, Task> SlashCommandExecuted
{
add => _slashCommandExecuted.Add(value);
remove => _slashCommandExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketSlashCommand, Task>> _slashCommandExecuted = new AsyncEvent<Func<SocketSlashCommand, Task>>();
/// <summary>
/// Fired when a user command is used and its interaction is received.
/// </summary>
public event Func<SocketUserCommand, Task> UserCommandExecuted
{
add => _userCommandExecuted.Add(value);
remove => _userCommandExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketUserCommand, Task>> _userCommandExecuted = new AsyncEvent<Func<SocketUserCommand, Task>>();
/// <summary>
/// Fired when a message command is used and its interaction is received.
/// </summary>
public event Func<SocketMessageCommand, Task> MessageCommandExecuted
{
add => _messageCommandExecuted.Add(value);
remove => _messageCommandExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketMessageCommand, Task>> _messageCommandExecuted = new AsyncEvent<Func<SocketMessageCommand, Task>>();
/// <summary>
/// Fired when an autocomplete is used and its interaction is received.
/// </summary>
public event Func<SocketAutocompleteInteraction, Task> AutocompleteExecuted
{
add => _autocompleteExecuted.Add(value);
remove => _autocompleteExecuted.Remove(value);
}
internal readonly AsyncEvent<Func<SocketAutocompleteInteraction, Task>> _autocompleteExecuted = new AsyncEvent<Func<SocketAutocompleteInteraction, Task>>();
/// <summary>
/// Fired when a guild application command is created.
///</summary>
///<remarks>
/// <para>
/// This event is fired when an application command is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandCreated
{
add { _applicationCommandCreated.Add(value); }
remove { _applicationCommandCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandCreated = new AsyncEvent<Func<SocketApplicationCommand, Task>>();
/// <summary>
/// Fired when a guild application command is updated.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an application command is updated. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandUpdated
{
add { _applicationCommandUpdated.Add(value); }
remove { _applicationCommandUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandUpdated = new AsyncEvent<Func<SocketApplicationCommand, Task>>();
/// <summary>
/// Fired when a guild application command is deleted.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an application command is deleted. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandDeleted
{
add { _applicationCommandDeleted.Add(value); }
remove { _applicationCommandDeleted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandDeleted = new AsyncEvent<Func<SocketApplicationCommand, Task>>();
/// <summary>
/// Fired when a thread is created within a guild, or when the current user is added to a thread.
/// </summary>
public event Func<SocketThreadChannel, Task> ThreadCreated
{
add { _threadCreated.Add(value); }
remove { _threadCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketThreadChannel, Task>> _threadCreated = new AsyncEvent<Func<SocketThreadChannel, Task>>();
/// <summary>
/// Fired when a thread is updated within a guild.
/// </summary>
public event Func<Cacheable<SocketThreadChannel, ulong>, SocketThreadChannel, Task> ThreadUpdated
{
add { _threadUpdated.Add(value); }
remove { _threadUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketThreadChannel, ulong>, SocketThreadChannel, Task>> _threadUpdated = new();
/// <summary>
/// Fired when a thread is deleted.
/// </summary>
public event Func<Cacheable<SocketThreadChannel, ulong>, Task> ThreadDeleted
{
add { _threadDeleted.Add(value); }
remove { _threadDeleted.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<SocketThreadChannel, ulong>, Task>> _threadDeleted = new AsyncEvent<Func<Cacheable<SocketThreadChannel, ulong>, Task>>();
/// <summary>
/// Fired when a user joins a thread
/// </summary>
public event Func<SocketThreadUser, Task> ThreadMemberJoined
{
add { _threadMemberJoined.Add(value); }
remove { _threadMemberJoined.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketThreadUser, Task>> _threadMemberJoined = new AsyncEvent<Func<SocketThreadUser, Task>>();
/// <summary>
/// Fired when a user leaves a thread
/// </summary>
public event Func<SocketThreadUser, Task> ThreadMemberLeft
{
add { _threadMemberLeft.Add(value); }
remove { _threadMemberLeft.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketThreadUser, Task>> _threadMemberLeft = new AsyncEvent<Func<SocketThreadUser, Task>>();
/// <summary>
/// Fired when a stage is started.
/// </summary>
public event Func<SocketStageChannel, Task> StageStarted
{
add { _stageStarted.Add(value); }
remove { _stageStarted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, Task>> _stageStarted = new AsyncEvent<Func<SocketStageChannel, Task>>();
/// <summary>
/// Fired when a stage ends.
/// </summary>
public event Func<SocketStageChannel, Task> StageEnded
{
add { _stageEnded.Add(value); }
remove { _stageEnded.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, Task>> _stageEnded = new AsyncEvent<Func<SocketStageChannel, Task>>();
/// <summary>
/// Fired when a stage is updated.
/// </summary>
public event Func<SocketStageChannel, SocketStageChannel, Task> StageUpdated
{
add { _stageUpdated.Add(value); }
remove { _stageUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketStageChannel, Task>> _stageUpdated = new AsyncEvent<Func<SocketStageChannel, SocketStageChannel, Task>>();
/// <summary>
/// Fired when a user requests to speak within a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> RequestToSpeak
{
add { _requestToSpeak.Add(value); }
remove { _requestToSpeak.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _requestToSpeak = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();
/// <summary>
/// Fired when a speaker is added in a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> SpeakerAdded
{
add { _speakerAdded.Add(value); }
remove { _speakerAdded.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _speakerAdded = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();
/// <summary>
/// Fired when a speaker is removed from a stage channel.
/// </summary>
public event Func<SocketStageChannel, SocketGuildUser, Task> SpeakerRemoved
{
add { _speakerRemoved.Add(value); }
remove { _speakerRemoved.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>> _speakerRemoved = new AsyncEvent<Func<SocketStageChannel, SocketGuildUser, Task>>();
/// <summary>
/// Fired when a sticker in a guild is created.
/// </summary>
public event Func<SocketCustomSticker, Task> GuildStickerCreated
{
add { _guildStickerCreated.Add(value); }
remove { _guildStickerCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketCustomSticker, Task>> _guildStickerCreated = new AsyncEvent<Func<SocketCustomSticker, Task>>();
/// <summary>
/// Fired when a sticker in a guild is updated.
/// </summary>
public event Func<SocketCustomSticker, SocketCustomSticker, Task> GuildStickerUpdated
{
add { _guildStickerUpdated.Add(value); }
remove { _guildStickerUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketCustomSticker, SocketCustomSticker, Task>> _guildStickerUpdated = new AsyncEvent<Func<SocketCustomSticker, SocketCustomSticker, Task>>();
/// <summary>
/// Fired when a sticker in a guild is deleted.
/// </summary>
public event Func<SocketCustomSticker, Task> GuildStickerDeleted
{
add { _guildStickerDeleted.Add(value); }
remove { _guildStickerDeleted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketCustomSticker, Task>> _guildStickerDeleted = new AsyncEvent<Func<SocketCustomSticker, Task>>();
#endregion
}
}

View File

@@ -12,6 +12,7 @@ namespace Discord.WebSocket
/// </summary>
public abstract partial class BaseSocketClient : BaseDiscordClient, IDiscordClient
{
#region BaseSocketClient
protected readonly DiscordSocketConfig BaseConfig;
/// <summary>
@@ -44,6 +45,10 @@ namespace Discord.WebSocket
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
/// <summary>
/// Gets a collection of default stickers.
/// </summary>
public abstract IReadOnlyCollection<StickerPack<SocketSticker>> DefaultStickerPacks { get; }
/// <summary>
/// Gets the current logged-in user.
/// </summary>
@@ -75,7 +80,7 @@ namespace Discord.WebSocket
: base(config, client) => BaseConfig = config;
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
useSystemClock: config.UseSystemClock);
useSystemClock: config.UseSystemClock);
/// <summary>
/// Gets a Discord application information for the logged-in user.
@@ -268,8 +273,19 @@ namespace Discord.WebSocket
/// </returns>
public Task<RestInviteMetadata> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default);
// IDiscordClient
/// <summary>
/// Gets a sticker.
/// </summary>
/// <param name="mode">Whether or not to allow downloading from the api.</param>
/// <param name="id">The id of the sticker to get.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A <see cref="SocketSticker"/> if found, otherwise <see langword="null"/>.
/// </returns>
public abstract Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
#endregion
#region IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync(options).ConfigureAwait(false);
@@ -317,5 +333,6 @@ namespace Discord.WebSocket
{
return await GetVoiceRegionsAsync().ConfigureAwait(false);
}
#endregion
}
}

View File

@@ -16,12 +16,14 @@ namespace Discord.WebSocket
private readonly ConcurrentDictionary<ulong, SocketGuild> _guilds;
private readonly ConcurrentDictionary<ulong, SocketGlobalUser> _users;
private readonly ConcurrentHashSet<ulong> _groupChannels;
private readonly ConcurrentDictionary<ulong, SocketApplicationCommand> _commands;
internal IReadOnlyCollection<SocketChannel> Channels => _channels.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketGroupChannel> GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels);
internal IReadOnlyCollection<SocketGuild> Guilds => _guilds.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketGlobalUser> Users => _users.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketApplicationCommand> Commands => _commands.ToReadOnlyCollection();
internal IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels =>
_dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat(
@@ -37,6 +39,7 @@ namespace Discord.WebSocket
_guilds = new ConcurrentDictionary<ulong, SocketGuild>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(guildCount * CollectionMultiplier));
_users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
_groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier));
_commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>();
}
internal SocketChannel GetChannel(ulong id)
@@ -139,5 +142,33 @@ namespace Discord.WebSocket
foreach (var guild in _guilds.Values)
guild.PurgeGuildUserCache();
}
internal SocketApplicationCommand GetCommand(ulong id)
{
if (_commands.TryGetValue(id, out SocketApplicationCommand command))
return command;
return null;
}
internal void AddCommand(SocketApplicationCommand command)
{
_commands[command.Id] = command;
}
internal SocketApplicationCommand GetOrAddCommand(ulong id, Func<ulong, SocketApplicationCommand> commandFactory)
{
return _commands.GetOrAdd(id, commandFactory);
}
internal SocketApplicationCommand RemoveCommand(ulong id)
{
if (_commands.TryRemove(id, out SocketApplicationCommand command))
return command;
return null;
}
internal void PurgeCommands(Func<SocketApplicationCommand, bool> precondition)
{
var ids = _commands.Where(x => precondition(x.Value)).Select(x => x.Key);
foreach (var id in ids)
_commands.TryRemove(id, out var _);
}
}
}

View File

@@ -5,6 +5,7 @@ namespace Discord.Commands
/// <summary> The sharded variant of <see cref="ICommandContext"/>, which may contain the client, user, guild, channel, and message. </summary>
public class ShardedCommandContext : SocketCommandContext, ICommandContext
{
#region ShardedCommandContext
/// <summary> Gets the <see cref="DiscordShardedClient"/> that the command is executed with. </summary>
public new DiscordShardedClient Client { get; }
@@ -17,9 +18,11 @@ namespace Discord.Commands
/// <summary> Gets the shard ID of the command context. </summary>
private static int GetShardId(DiscordShardedClient client, IGuild guild)
=> guild == null ? 0 : client.GetShardIdFor(guild);
#endregion
//ICommandContext
#region ICommandContext
/// <inheritdoc />
IDiscordClient ICommandContext.Client => Client;
#endregion
}
}

View File

@@ -7,6 +7,7 @@ namespace Discord.Commands
/// </summary>
public class SocketCommandContext : ICommandContext
{
#region SocketCommandContext
/// <summary>
/// Gets the <see cref="DiscordSocketClient" /> that the command is executed with.
/// </summary>
@@ -46,8 +47,9 @@ namespace Discord.Commands
User = msg.Author;
Message = msg;
}
#endregion
//ICommandContext
#region ICommandContext
/// <inheritdoc/>
IDiscordClient ICommandContext.Client => Client;
/// <inheritdoc/>
@@ -58,5 +60,6 @@ namespace Discord.Commands
IUser ICommandContext.User => User;
/// <inheritdoc/>
IUserMessage ICommandContext.Message => Message;
#endregion
}
}

View File

@@ -57,6 +57,9 @@ namespace Discord
public virtual async Task StartAsync()
{
if (State != ConnectionState.Disconnected)
throw new InvalidOperationException("Cannot start an already running client.");
await AcquireConnectionLock().ConfigureAwait(false);
var reconnectCancelToken = new CancellationTokenSource();
_reconnectCancelToken?.Dispose();

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<PropertyGroup>
@@ -13,4 +13,4 @@
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,11 +1,11 @@
using System;
using System;
using System.Threading.Tasks;
namespace Discord.WebSocket
{
public partial class DiscordShardedClient
{
//General
#region General
/// <summary> Fired when a shard is connected to the Discord gateway. </summary>
public event Func<DiscordSocketClient, Task> ShardConnected
{
@@ -34,5 +34,6 @@ namespace Discord.WebSocket
remove { _shardLatencyUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<int, int, DiscordSocketClient, Task>> _shardLatencyUpdatedEvent = new AsyncEvent<Func<int, int, DiscordSocketClient, Task>>();
#endregion
}
}
}

View File

@@ -6,16 +6,19 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Immutable;
namespace Discord.WebSocket
{
public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient
{
#region DiscordShardedClient
private readonly DiscordSocketConfig _baseConfig;
private readonly Dictionary<int, int> _shardIdsToIndex;
private readonly bool _automaticShards;
private int[] _shardIds;
private DiscordSocketClient[] _shards;
private ImmutableArray<StickerPack<SocketSticker>> _defaultStickers;
private int _totalShards;
private SemaphoreSlim[] _identifySemaphores;
private object _semaphoreResetLock;
@@ -30,7 +33,20 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override IActivity Activity { get => _shards[0].Activity; protected set { } }
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
internal new DiscordSocketApiClient ApiClient
{
get
{
if (base.ApiClient.CurrentUserId == null)
base.ApiClient.CurrentUserId = CurrentUser?.Id;
return base.ApiClient;
}
}
/// <inheritdoc/>
public override IReadOnlyCollection<StickerPack<SocketSticker>> DefaultStickerPacks
=> _defaultStickers.ToReadOnlyCollection();
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => GetGuilds().ToReadOnlyCollection(GetGuildCount);
/// <inheritdoc />
@@ -68,6 +84,7 @@ namespace Discord.WebSocket
_shardIdsToIndex = new Dictionary<int, int>();
config.DisplayInitialLog = false;
_baseConfig = config;
_defaultStickers = ImmutableArray.Create<StickerPack<SocketSticker>>();
if (config.TotalShards == null)
_automaticShards = true;
@@ -146,6 +163,10 @@ namespace Discord.WebSocket
//Assume thread safe: already in a connection lock
for (int i = 0; i < _shards.Length; i++)
await _shards[i].LoginAsync(tokenType, token);
if(_defaultStickers.Length == 0 && _baseConfig.AlwaysDownloadDefaultStickers)
await DownloadDefaultStickersAsync().ConfigureAwait(false);
}
internal override async Task OnLogoutAsync()
{
@@ -238,6 +259,67 @@ namespace Discord.WebSocket
result += _shards[i].Guilds.Count;
return result;
}
/// <inheritdoc/>
public override async Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null)
{
var sticker = _defaultStickers.FirstOrDefault(x => x.Stickers.Any(y => y.Id == id))?.Stickers.FirstOrDefault(x => x.Id == id);
if (sticker != null)
return sticker;
foreach (var guild in Guilds)
{
sticker = await guild.GetStickerAsync(id, CacheMode.CacheOnly).ConfigureAwait(false);
if (sticker != null)
return sticker;
}
if (mode == CacheMode.CacheOnly)
return null;
var model = await ApiClient.GetStickerAsync(id, options).ConfigureAwait(false);
if (model == null)
return null;
if (model.GuildId.IsSpecified)
{
var guild = GetGuild(model.GuildId.Value);
sticker = guild.AddOrUpdateSticker(model);
return sticker;
}
else
{
return SocketSticker.Create(_shards[0], model);
}
}
private async Task DownloadDefaultStickersAsync()
{
var models = await ApiClient.ListNitroStickerPacksAsync().ConfigureAwait(false);
var builder = ImmutableArray.CreateBuilder<StickerPack<SocketSticker>>();
foreach (var model in models.StickerPacks)
{
var stickers = model.Stickers.Select(x => SocketSticker.Create(_shards[0], x));
var pack = new StickerPack<SocketSticker>(
model.Name,
model.Id,
model.SkuId,
model.CoverStickerId.ToNullable(),
model.Description,
model.BannerAssetId,
stickers
);
builder.Add(pack);
}
_defaultStickers = builder.ToImmutable();
}
/// <inheritdoc />
public override SocketUser GetUser(ulong id)
@@ -377,9 +459,45 @@ namespace Discord.WebSocket
client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite);
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite);
}
//IDiscordClient
client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction);
client.ButtonExecuted += (arg) => _buttonExecuted.InvokeAsync(arg);
client.SelectMenuExecuted += (arg) => _selectMenuExecuted.InvokeAsync(arg);
client.SlashCommandExecuted += (arg) => _slashCommandExecuted.InvokeAsync(arg);
client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg);
client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg);
client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg);
client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2);
client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread);
client.ThreadDeleted += (thread) => _threadDeleted.InvokeAsync(thread);
client.ThreadMemberJoined += (user) => _threadMemberJoined.InvokeAsync(user);
client.ThreadMemberLeft += (user) => _threadMemberLeft.InvokeAsync(user);
client.StageEnded += (stage) => _stageEnded.InvokeAsync(stage);
client.StageStarted += (stage) => _stageStarted.InvokeAsync(stage);
client.StageUpdated += (stage1, stage2) => _stageUpdated.InvokeAsync(stage1, stage2);
client.RequestToSpeak += (stage, user) => _requestToSpeak.InvokeAsync(stage, user);
client.SpeakerAdded += (stage, user) => _speakerAdded.InvokeAsync(stage, user);
client.SpeakerRemoved += (stage, user) => _speakerRemoved.InvokeAsync(stage, user);
client.GuildStickerCreated += (sticker) => _guildStickerCreated.InvokeAsync(sticker);
client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker);
client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after);
client.GuildJoinRequestDeleted += (userId, guildId) => _guildJoinRequestDeletedEvent.InvokeAsync(userId, guildId);
client.GuildScheduledEventCancelled += (arg) => _guildScheduledEventCancelled.InvokeAsync(arg);
client.GuildScheduledEventCompleted += (arg) => _guildScheduledEventCompleted.InvokeAsync(arg);
client.GuildScheduledEventCreated += (arg) => _guildScheduledEventCreated.InvokeAsync(arg);
client.GuildScheduledEventUpdated += (arg1, arg2) => _guildScheduledEventUpdated.InvokeAsync(arg1, arg2);
client.GuildScheduledEventStarted += (arg) => _guildScheduledEventStarted.InvokeAsync(arg);
client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2);
client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2);
}
#endregion
#region IDiscordClient
/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync().ConfigureAwait(false);
@@ -426,7 +544,9 @@ namespace Discord.WebSocket
{
return await GetVoiceRegionAsync(id).ConfigureAwait(false);
}
#endregion
#region Dispose
internal override void Dispose(bool disposing)
{
if (!_isDisposed)
@@ -445,5 +565,6 @@ namespace Discord.WebSocket
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Discord.API.Gateway;
using Discord.Net.Queue;
using Discord.Net.Rest;
@@ -75,8 +74,15 @@ namespace Discord.API
using (var jsonReader = new JsonTextReader(reader))
{
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
if (msg != null)
{
#if DEBUG_PACKETS
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}");
#endif
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
}
}
}
};
@@ -87,11 +93,21 @@ namespace Discord.API
{
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
if (msg != null)
{
#if DEBUG_PACKETS
Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}");
#endif
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
}
}
};
WebSocketClient.Closed += async ex =>
{
#if DEBUG_PACKETS
Console.WriteLine(ex);
#endif
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
@@ -153,6 +169,11 @@ namespace Discord.API
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}&compress=zlib-stream";
}
#if DEBUG_PACKETS
Console.WriteLine("Connecting to gateway: " + _gatewayUrl);
#endif
await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false);
ConnectionState = ConnectionState.Connected;
@@ -195,7 +216,7 @@ namespace Discord.API
ConnectionState = ConnectionState.Disconnected;
}
//Core
#region Core
public Task SendGatewayAsync(GatewayOpCode opCode, object payload, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, options);
private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, RequestOptions options)
@@ -213,6 +234,10 @@ namespace Discord.API
options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id;
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, opCode == GatewayOpCode.Heartbeat, options)).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
#if DEBUG_PACKETS
Console.WriteLine($"-> {opCode}:\n{SerializeJson(payload)}");
#endif
}
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, GatewayIntents gatewayIntents = GatewayIntents.AllUnprivileged, (UserStatus, bool, long?, GameModel)? presence = null, RequestOptions options = null)
@@ -220,7 +245,9 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);
var props = new Dictionary<string, string>
{
["$device"] = "Discord.Net"
["$device"] = "Discord.Net",
["$os"] = Environment.OSVersion.Platform.ToString(),
[$"browser"] = "Discord.Net"
};
var msg = new IdentifyParams()
{
@@ -237,12 +264,12 @@ namespace Discord.API
if (presence.HasValue)
{
msg.Presence = new StatusUpdateParams
msg.Presence = new PresenceUpdateParams
{
Status = presence.Value.Item1,
IsAFK = presence.Value.Item2,
IdleSince = presence.Value.Item3,
Game = presence.Value.Item4,
Activities = new object[] { presence.Value.Item4 }
};
}
@@ -264,18 +291,18 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);
await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}
public async Task SendStatusUpdateAsync(UserStatus status, bool isAFK, long? since, GameModel game, RequestOptions options = null)
public async Task SendPresenceUpdateAsync(UserStatus status, bool isAFK, long? since, GameModel game, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);
var args = new StatusUpdateParams
var args = new PresenceUpdateParams
{
Status = status,
IdleSince = since,
IsAFK = isAFK,
Game = game
Activities = new object[] { game }
};
options.BucketId = GatewayBucket.Get(GatewayBucketType.PresenceUpdate).Id;
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
await SendGatewayAsync(GatewayOpCode.PresenceUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
@@ -299,5 +326,6 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);
await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false);
}
#endregion
}
}

View File

@@ -6,7 +6,7 @@ namespace Discord.WebSocket
{
public partial class DiscordSocketClient
{
//General
#region General
/// <summary> Fired when connected to the Discord gateway. </summary>
public event Func<Task> Connected
{
@@ -45,5 +45,6 @@ namespace Discord.WebSocket
internal DiscordSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) : base(config, client)
{
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -49,8 +49,31 @@ namespace Discord.WebSocket
/// <summary>
/// Gets or sets the total number of shards for this application.
/// </summary>
/// <remarks>
/// If this is left <see langword="null"/> in a sharded client the bot will get the recommended shard
/// count from discord and use that.
/// </remarks>
public int? TotalShards { get; set; } = null;
/// <summary>
/// Gets or sets whether or not the client should download the default stickers on startup.
/// </summary>
/// <remarks>
/// When this is set to <see langword="false"/> default stickers arn't present and cannot be resolved by the client.
/// This will make all default stickers have the type of <see cref="SocketUnknownSticker"/>.
/// </remarks>
public bool AlwaysDownloadDefaultStickers { get; set; } = false;
/// <summary>
/// Gets or sets whether or not the client should automatically resolve the stickers sent on a message.
/// </summary>
/// <remarks>
/// Note if a sticker isn't cached the client will preform a rest request to resolve it. This
/// may be very rest heavy depending on your bots size, it isn't recommended to use this with large scale bots as you
/// can get ratelimited easily.
/// </remarks>
public bool AlwaysResolveStickers { get; set; } = false;
/// <summary>
/// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero
/// disables the message cache entirely.
@@ -79,7 +102,7 @@ namespace Discord.WebSocket
/// <remarks>
/// <para>
/// By default, the Discord gateway will only send offline members if a guild has less than a certain number
/// of members (determined by <see cref="LargeThreshold"/> in this library). This behaviour is why
/// of members (determined by <see cref="LargeThreshold"/> in this library). This behavior is why
/// sometimes a user may be missing from the WebSocket cache for collections such as
/// <see cref="Discord.WebSocket.SocketGuild.Users"/>.
/// </para>
@@ -137,13 +160,13 @@ namespace Discord.WebSocket
{
get
{
return this.maxWaitForGuildAvailable;
return maxWaitForGuildAvailable;
}
set
{
Preconditions.AtLeast(value, 0, nameof(this.MaxWaitBetweenGuildAvailablesBeforeReady));
this.maxWaitForGuildAvailable = value;
Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady));
maxWaitForGuildAvailable = value;
}
}

View File

@@ -1,4 +1,3 @@
#pragma warning disable CS1591
using Discord.API;
using Discord.API.Voice;
using Discord.Net.Converters;
@@ -18,6 +17,7 @@ namespace Discord.Audio
{
internal class DiscordVoiceAPIClient : IDisposable
{
#region DiscordVoiceAPIClient
public const int MaxBitrate = 128 * 1024;
public const string Mode = "xsalsa20_poly1305";
@@ -126,8 +126,9 @@ namespace Discord.Audio
await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false);
await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false);
}
#endregion
//WebSocket
#region WebSocket
public async Task SendHeartbeatAsync(RequestOptions options = null)
{
await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false);
@@ -208,10 +209,12 @@ namespace Discord.Audio
}
private async Task DisconnectInternalAsync()
{
if (ConnectionState == ConnectionState.Disconnected) return;
if (ConnectionState == ConnectionState.Disconnected)
return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
try
{ _connectCancelToken?.Cancel(false); }
catch { }
//Wait for tasks to complete
@@ -220,8 +223,9 @@ namespace Discord.Audio
ConnectionState = ConnectionState.Disconnected;
}
#endregion
//Udp
#region Udp
public async Task SendDiscoveryAsync(uint ssrc)
{
var packet = new byte[70];
@@ -252,8 +256,9 @@ namespace Discord.Audio
{
_udp.SetDestination(ip, port);
}
#endregion
//Helpers
#region Helpers
private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
private string SerializeJson(object value)
{
@@ -269,5 +274,6 @@ namespace Discord.Audio
using (JsonReader reader = new JsonTextReader(text))
return _serializer.Deserialize<T>(reader);
}
#endregion
}
}

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;
}
}

Some files were not shown because too many files have changed in this diff Show More