commitpull/1958/headff0bbbd4d3Merge:41b4686b19a66bf8Author: quin lynch <lynchquin@gmail.com> Date: Sat Nov 27 08:39:35 2021 -0400 Merge branch 'dev' of https://github.com/discord-net/Discord.Net into dev commit19a66bf878Author: Daniel Baynton <49287178+230Daniel@users.noreply.github.com> Date: Fri Nov 26 15:41:55 2021 +0000 feature: Add method to clear guild user cache (#1767) * Add method to clear a SocketGuild's user cache * Add optional predicate * Compress overload to be consistant * Fix global user not clearing (may cause other issues) * Remove debug code and add param documentation * Standardise doc string * Remove old hack-fix * Rename new method for consistency * Add missing line to reset downloaderPromise * Undo accidental whitespace changes * Rider better actually keep the tab this time Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> commitb9274d115dAuthor: Monica S <FiniteReality@users.noreply.github.com> Date: Fri Nov 26 15:41:08 2021 +0000 Add characters commonly use in links to Sanitize (#1152) Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> commit51e06e9ce1Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri Nov 26 11:30:19 2021 -0400 feature: warn on invalid gateway intents (#1948) commit82276e351aAuthor: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri Nov 26 11:29:53 2021 -0400 feature: default application games (#1949) * Initial implementation * Add missing summary commit4f1fe2b084Merge:9d6dc6273cd9f399Author: quin lynch <lynchquin@gmail.com> Date: Fri Nov 26 11:23:32 2021 -0400 Merge branch 'siscodeorg-commands/validate-get-best-match' into dev commit3cd9f39918Merge:9d6dc627adf3a9c4Author: quin lynch <lynchquin@gmail.com> Date: Fri Nov 26 11:23:05 2021 -0400 Merge branch 'commands/validate-get-best-match' of https://github.com/siscodeorg/Discord.Net into siscodeorg-commands/validate-get-best-match commitadf3a9c459Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 26 09:26:53 2021 -0300 Fix incorrect casing on `HandleCommandPipeline` commita92ec56d88Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Thu Nov 25 16:42:18 2021 -0300 Add requested changes Changes: - Use IResult instead of Optional CommandMatch - Rework branching workflow commitd1b31c8f52Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Thu Nov 25 15:31:48 2021 -0300 Add `MatchResult` commit9d6dc6279dAuthor: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu Nov 25 11:25:19 2021 -0400 Update socket presence and add new presence event (#1945) commit10afd96e6eAuthor: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu Nov 25 11:24:44 2021 -0400 feature: Handle bidirectional usernames (#1943) * Initial implementation * Update summary commit143ca6db43Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu Nov 25 11:23:33 2021 -0400 fix NRE when adding parameters thru builders (#1946) commitd5f5ae132cAuthor: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu Nov 25 18:22:50 2021 +0300 fix sharded client current user (#1947) commitb5c150dc16Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed Nov 24 12:53:39 2021 -0400 Add Voice binaries (#1944) * Add binaries and read me * Update sending voice docs * Undo markdown formatting commitbc440abd44Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed Nov 24 12:52:55 2021 -0400 Implement multi-file upload to webhooks (#1942) commitf7a07aec02Author: Paulo <pnmanjos@hotmail.com> Date: Wed Nov 24 09:57:06 2021 -0300 Add default nullable enum typereader (#1518) commit6abdfcbf87Author: Slate <kristian.f@hotmail.co.uk> Date: Wed Nov 24 12:55:07 2021 +0000 Added negative TimeSpan handling (#1666) - Added unit tests for the TimeSpanTypeReader - Fixes https://github.com/discord-net/Discord.Net/issues/1657 commite0dbe7c695Author: Paulo <pnmanjos@hotmail.com> Date: Wed Nov 24 09:43:57 2021 -0300 Add MaxBitrate to the interface (#1861) Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> commit3cb662ff7aAuthor: d4n <dan3436@hotmail.com> Date: Tue Nov 23 10:49:31 2021 -0500 Add null check to AllowedMentions.ToModel() (#1865) commit900c1f4385Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Nov 23 11:46:18 2021 -0400 Fix emoto try parse (#1941) commit933ea42eaaAuthor: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Nov 23 09:58:05 2021 -0400 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: commitdca41a348eAuthor: 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> commit3395700720Author: Nikon <47792796+INikonI@users.noreply.github.com> Date: Mon Aug 23 02:00:18 2021 +0500 feature: IVoiceChannel implements IMentionable (#1896) commit41b4686b5eAuthor: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Aug 3 20:43:10 2021 -0300 Update README.md commit5fc31451a1Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Aug 3 20:28:15 2021 -0300 Update README.md commit56d16397f7Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 18:42:23 2020 -0300 Fixes Azure linux build failing due to a CS8652. commitc455b50331Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 14:10:39 2020 -0300 Make use of new ValidateAndGetBestMatch api on ExecuteAsync commit7955a09090Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 13:52:53 2020 -0300 Creates ValidateAndGetBestMatch function This function will validate all commands from a SearchResult and return the result of said validation, along with the command matched, if a valid match was found. commit574b503e9eAuthor: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 13:38:00 2020 -0300 Moves CalculateScore function to outer scope.
| @@ -1,6 +1,6 @@ | |||||
| # Ratelimits | # Ratelimits | ||||
| Ratelimits are a core concept of the discord api, each verified library must follow the ratelimit guidelines. Labs introduces a ratelimit exposure system to help you follow these ratelimits in your own code. | |||||
| Ratelimits are a core concept of any API - Discords API is no exception. each verified library must follow the ratelimit guidelines. | |||||
| ### Using the ratelimit callback | ### Using the ratelimit callback | ||||
| @@ -6,7 +6,7 @@ title: Introduction to slash commands | |||||
| # Getting started with application commands. | # Getting started with application commands. | ||||
| Welcome! This guide will show you how to use application commands. If you have extra questions that aren't covered here you can come to our [Discord](https://discord.com/invite/dvSfUTet3K) server and ask around there. | |||||
| Welcome! This guide will show you how to use application commands. | |||||
| ## What is an application command? | ## What is an application command? | ||||
| @@ -18,7 +18,7 @@ when developing on .NET Core, this is where you execute `dotnet run` | |||||
| from; typically the same directory as your csproj). | from; typically the same directory as your csproj). | ||||
| For Windows Users, precompiled binaries are available for your | For Windows Users, precompiled binaries are available for your | ||||
| convienence [here](https://discord.foxbot.me/binaries/). | |||||
| convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives). | |||||
| For Linux Users, you will need to compile [Sodium] and [Opus] from | For Linux Users, you will need to compile [Sodium] and [Opus] from | ||||
| source, or install them from your package manager. | source, or install them from your package manager. | ||||
| @@ -54,7 +54,7 @@ namespace Discord.Commands.Builders | |||||
| if (type.GetTypeInfo().IsValueType) | if (type.GetTypeInfo().IsValueType) | ||||
| DefaultValue = Activator.CreateInstance(type); | DefaultValue = Activator.CreateInstance(type); | ||||
| else if (type.IsArray) | else if (type.IsArray) | ||||
| type = ParameterType.GetElementType(); | |||||
| DefaultValue = Array.CreateInstance(type.GetElementType(), 0); | |||||
| ParameterType = type; | ParameterType = type; | ||||
| } | } | ||||
| @@ -438,6 +438,13 @@ namespace Discord.Commands | |||||
| _defaultTypeReaders[type] = reader; | _defaultTypeReaders[type] = reader; | ||||
| return reader; | return reader; | ||||
| } | } | ||||
| var underlyingType = Nullable.GetUnderlyingType(type); | |||||
| if (underlyingType != null && underlyingType.IsEnum) | |||||
| { | |||||
| reader = NullableTypeReader.Create(underlyingType, EnumTypeReader.GetReader(underlyingType)); | |||||
| _defaultTypeReaders[type] = reader; | |||||
| return reader; | |||||
| } | |||||
| //Is this an entity? | //Is this an entity? | ||||
| for (int i = 0; i < _entityTypeReaders.Count; i++) | for (int i = 0; i < _entityTypeReaders.Count; i++) | ||||
| @@ -510,19 +517,83 @@ namespace Discord.Commands | |||||
| services ??= EmptyServiceProvider.Instance; | services ??= EmptyServiceProvider.Instance; | ||||
| var searchResult = Search(input); | var searchResult = Search(input); | ||||
| if (!searchResult.IsSuccess) | |||||
| var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); | |||||
| if (validationResult is SearchResult result) | |||||
| { | |||||
| await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, result).ConfigureAwait(false); | |||||
| return result; | |||||
| } | |||||
| if (validationResult is MatchResult matchResult) | |||||
| { | |||||
| return await HandleCommandPipeline(matchResult, context, services); | |||||
| } | |||||
| return validationResult; | |||||
| } | |||||
| private async Task<IResult> HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) | |||||
| { | |||||
| if (!matchResult.IsSuccess) | |||||
| return matchResult; | |||||
| if (matchResult.Pipeline is ParseResult parseResult) | |||||
| { | |||||
| var executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services); | |||||
| if (!executeResult.IsSuccess && !(executeResult is RuntimeResult || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) | |||||
| await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult); | |||||
| return executeResult; | |||||
| } | |||||
| if (matchResult.Pipeline is PreconditionResult preconditionResult) | |||||
| { | |||||
| await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); | |||||
| } | |||||
| return matchResult; | |||||
| } | |||||
| // Calculates the 'score' of a command given a parse result | |||||
| float CalculateScore(CommandMatch match, ParseResult parseResult) | |||||
| { | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| { | { | ||||
| await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false); | |||||
| return searchResult; | |||||
| var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| argValuesScore = argValuesSum / match.Command.Parameters.Count; | |||||
| paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||||
| } | } | ||||
| var totalArgsScore = (argValuesScore + paramValuesScore) / 2; | |||||
| return match.Command.Priority + totalArgsScore * 0.99f; | |||||
| } | |||||
| /// <summary> | |||||
| /// Validates and gets the best <see cref="CommandMatch"/> from a specified <see cref="SearchResult"/> | |||||
| /// </summary> | |||||
| /// <param name="matches">The SearchResult.</param> | |||||
| /// <param name="context">The context of the command.</param> | |||||
| /// <param name="provider">The service provider to be used on the command's dependency injection.</param> | |||||
| /// <param name="multiMatchHandling">The handling mode when multiple command matches are found.</param> | |||||
| /// <returns>A task that represents the asynchronous validation operation. The task result contains the result of the | |||||
| /// command validation as a <see cref="MatchResult"/> or a <see cref="SearchResult"/> if no matches were found.</returns> | |||||
| public async Task<IResult> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
| { | |||||
| if (!matches.IsSuccess) | |||||
| return matches; | |||||
| var commands = searchResult.Commands; | |||||
| var commands = matches.Commands; | |||||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | ||||
| foreach (var match in commands) | |||||
| foreach (var command in commands) | |||||
| { | { | ||||
| preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||||
| preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider); | |||||
| } | } | ||||
| var successfulPreconditions = preconditionResults | var successfulPreconditions = preconditionResults | ||||
| @@ -533,19 +604,16 @@ namespace Discord.Commands | |||||
| { | { | ||||
| //All preconditions failed, return the one from the highest priority command | //All preconditions failed, return the one from the highest priority command | ||||
| var bestCandidate = preconditionResults | var bestCandidate = preconditionResults | ||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); | |||||
| return bestCandidate.Value; | |||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value); | |||||
| } | } | ||||
| //If we get this far, at least one precondition was successful. | |||||
| var parseResults = new Dictionary<CommandMatch, ParseResult>(); | |||||
| var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||||
| foreach (var pair in successfulPreconditions) | foreach (var pair in successfulPreconditions) | ||||
| { | { | ||||
| var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); | |||||
| var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false); | |||||
| if (parseResult.Error == CommandError.MultipleMatches) | if (parseResult.Error == CommandError.MultipleMatches) | ||||
| { | { | ||||
| @@ -560,51 +628,27 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| parseResultsDict[pair.Key] = parseResult; | |||||
| parseResults[pair.Key] = parseResult; | |||||
| } | } | ||||
| // Calculates the 'score' of a command given a parse result | |||||
| float CalculateScore(CommandMatch match, ParseResult parseResult) | |||||
| { | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| { | |||||
| var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var weightedParseResults = parseResults | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| argValuesScore = argValuesSum / match.Command.Parameters.Count; | |||||
| paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||||
| } | |||||
| var totalArgsScore = (argValuesScore + paramValuesScore) / 2; | |||||
| return match.Command.Priority + totalArgsScore * 0.99f; | |||||
| } | |||||
| //Order the parse results by their score so that we choose the most likely result to execute | |||||
| var parseResults = parseResultsDict | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| var successfulParses = parseResults | |||||
| var successfulParses = weightedParseResults | |||||
| .Where(x => x.Value.IsSuccess) | .Where(x => x.Value.IsSuccess) | ||||
| .ToArray(); | .ToArray(); | ||||
| if (successfulParses.Length == 0) | |||||
| if(successfulParses.Length == 0) | |||||
| { | { | ||||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||||
| var bestMatch = parseResults | var bestMatch = parseResults | ||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | .FirstOrDefault(x => !x.Value.IsSuccess); | ||||
| await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); | |||||
| return bestMatch.Value; | |||||
| return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); | |||||
| } | } | ||||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||||
| var chosenOverload = successfulParses[0]; | var chosenOverload = successfulParses[0]; | ||||
| var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); | |||||
| if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // successful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deferred execution) | |||||
| await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | |||||
| return result; | |||||
| return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); | |||||
| } | } | ||||
| #endregion | #endregion | ||||
| @@ -6,30 +6,50 @@ namespace Discord.Commands | |||||
| { | { | ||||
| internal class TimeSpanTypeReader : TypeReader | internal class TimeSpanTypeReader : TypeReader | ||||
| { | { | ||||
| private static readonly string[] Formats = { | |||||
| "%d'd'%h'h'%m'm'%s's'", //4d3h2m1s | |||||
| "%d'd'%h'h'%m'm'", //4d3h2m | |||||
| "%d'd'%h'h'%s's'", //4d3h 1s | |||||
| "%d'd'%h'h'", //4d3h | |||||
| "%d'd'%m'm'%s's'", //4d 2m1s | |||||
| "%d'd'%m'm'", //4d 2m | |||||
| "%d'd'%s's'", //4d 1s | |||||
| "%d'd'", //4d | |||||
| "%h'h'%m'm'%s's'", // 3h2m1s | |||||
| "%h'h'%m'm'", // 3h2m | |||||
| "%h'h'%s's'", // 3h 1s | |||||
| "%h'h'", // 3h | |||||
| "%m'm'%s's'", // 2m1s | |||||
| "%m'm'", // 2m | |||||
| "%s's'", // 1s | |||||
| /// <summary> | |||||
| /// TimeSpan try parse formats. | |||||
| /// </summary> | |||||
| private static readonly string[] Formats = | |||||
| { | |||||
| "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s | |||||
| "%d'd'%h'h'%m'm'", // 4d3h2m | |||||
| "%d'd'%h'h'%s's'", // 4d3h 1s | |||||
| "%d'd'%h'h'", // 4d3h | |||||
| "%d'd'%m'm'%s's'", // 4d 2m1s | |||||
| "%d'd'%m'm'", // 4d 2m | |||||
| "%d'd'%s's'", // 4d 1s | |||||
| "%d'd'", // 4d | |||||
| "%h'h'%m'm'%s's'", // 3h2m1s | |||||
| "%h'h'%m'm'", // 3h2m | |||||
| "%h'h'%s's'", // 3h 1s | |||||
| "%h'h'", // 3h | |||||
| "%m'm'%s's'", // 2m1s | |||||
| "%m'm'", // 2m | |||||
| "%s's'", // 1s | |||||
| }; | }; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) | ||||
| { | { | ||||
| return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) | |||||
| ? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) | |||||
| : Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); | |||||
| if (string.IsNullOrEmpty(input)) | |||||
| throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input)); | |||||
| var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign | |||||
| if (isNegative) | |||||
| { | |||||
| input = input.Substring(1); | |||||
| } | |||||
| if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) | |||||
| { | |||||
| return isNegative | |||||
| ? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan)) | |||||
| : Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,47 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public class MatchResult : IResult | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the command that may have matched during the command execution. | |||||
| /// </summary> | |||||
| public CommandMatch? Match { get; } | |||||
| /// <summary> | |||||
| /// Gets on which pipeline stage the command may have matched or failed. | |||||
| /// </summary> | |||||
| public IResult? Pipeline { get; } | |||||
| /// <inheritdoc /> | |||||
| public CommandError? Error { get; } | |||||
| /// <inheritdoc /> | |||||
| public string ErrorReason { get; } | |||||
| /// <inheritdoc /> | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) | |||||
| { | |||||
| Match = match; | |||||
| Error = error; | |||||
| Pipeline = pipeline; | |||||
| ErrorReason = errorReason; | |||||
| } | |||||
| public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) | |||||
| => new MatchResult(match,pipeline,null, null); | |||||
| public static MatchResult FromError(CommandError error, string reason) | |||||
| => new MatchResult(null,null,error, reason); | |||||
| public static MatchResult FromError(Exception ex) | |||||
| => FromError(CommandError.Exception, ex.Message); | |||||
| public static MatchResult FromError(IResult result) | |||||
| => new MatchResult(null, null,result.Error, result.ErrorReason); | |||||
| public static MatchResult FromError(IResult pipeline, CommandError error, string reason) | |||||
| => new MatchResult(null, pipeline, error, reason); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -26,4 +26,4 @@ | |||||
| <ItemGroup Condition=" '$(TargetFramework)' == 'net461' "> | <ItemGroup Condition=" '$(TargetFramework)' == 'net461' "> | ||||
| <PackageReference Include="System.ValueTuple" Version="4.4.0" /> | <PackageReference Include="System.ValueTuple" Version="4.4.0" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | |||||
| </Project> | |||||
| @@ -0,0 +1,86 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public enum DefaultApplications : ulong | |||||
| { | |||||
| /// <summary> | |||||
| /// Watch youtube together. | |||||
| /// </summary> | |||||
| Youtube = 880218394199220334, | |||||
| /// <summary> | |||||
| /// Youtube development application. | |||||
| /// </summary> | |||||
| YoutubeDev = 880218832743055411, | |||||
| /// <summary> | |||||
| /// Poker! | |||||
| /// </summary> | |||||
| Poker = 755827207812677713, | |||||
| /// <summary> | |||||
| /// Betrayal: A Party Adventure. Betrayal is a social deduction game inspired by Werewolf, Town of Salem, and Among Us. | |||||
| /// </summary> | |||||
| Betrayal = 773336526917861400, | |||||
| /// <summary> | |||||
| /// Sit back, relax, and do some fishing! | |||||
| /// </summary> | |||||
| Fishing = 814288819477020702, | |||||
| /// <summary> | |||||
| /// The queens gambit. | |||||
| /// </summary> | |||||
| Chess = 832012774040141894, | |||||
| /// <summary> | |||||
| /// Development version of chess. | |||||
| /// </summary> | |||||
| ChessDev = 832012586023256104, | |||||
| /// <summary> | |||||
| /// LetterTile is a version of scrabble. | |||||
| /// </summary> | |||||
| LetterTile = 879863686565621790, | |||||
| /// <summary> | |||||
| /// Find words in a jumble of letters in coffee. | |||||
| /// </summary> | |||||
| WordSnack = 879863976006127627, | |||||
| /// <summary> | |||||
| /// It's like skribbl.io. | |||||
| /// </summary> | |||||
| DoodleCrew = 878067389634314250, | |||||
| /// <summary> | |||||
| /// It's like cards against humanity. | |||||
| /// </summary> | |||||
| Awkword = 879863881349087252, | |||||
| /// <summary> | |||||
| /// A word-search like game where you unscramble words and score points in a scrabble fashion. | |||||
| /// </summary> | |||||
| SpellCast = 852509694341283871, | |||||
| /// <summary> | |||||
| /// Classic checkers | |||||
| /// </summary> | |||||
| Checkers = 832013003968348200, | |||||
| /// <summary> | |||||
| /// The development version of poker. | |||||
| /// </summary> | |||||
| PokerDev = 763133495793942528, | |||||
| /// <summary> | |||||
| /// SketchyArtist. | |||||
| /// </summary> | |||||
| SketchyArtist = 879864070101172255 | |||||
| } | |||||
| } | |||||
| @@ -60,13 +60,6 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new invite to this channel. | /// Creates a new invite to this channel. | ||||
| /// </summary> | /// </summary> | ||||
| /// <example> | |||||
| /// <para>The following example creates a new invite to this channel; the invite lasts for 12 hours and can only | |||||
| /// be used 3 times throughout its lifespan.</para> | |||||
| /// <code language="cs"> | |||||
| /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="applicationId">The id of the embedded application to open for this invite.</param> | /// <param name="applicationId">The id of the embedded application to open for this invite.</param> | ||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | ||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | ||||
| @@ -79,6 +72,21 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <param name="application">The application to open for this invite.</param> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new invite to this channel. | /// Creates a new invite to this channel. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -17,7 +17,6 @@ namespace Discord | |||||
| string Topic { get; } | string Topic { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The <see cref="StagePrivacyLevel"/> of the current stage. | |||||
| /// Gets the <see cref="StagePrivacyLevel"/> of the current stage. | /// Gets the <see cref="StagePrivacyLevel"/> of the current stage. | ||||
| /// </summary> | /// </summary> | ||||
| /// <remarks> | /// <remarks> | ||||
| @@ -19,12 +19,12 @@ namespace Discord | |||||
| bool HasJoined { get; } | bool HasJoined { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// <see langword="true"/> if the current thread is archived, otherwise <see langword="false"/>. | |||||
| /// Gets whether or not the current thread is archived. | |||||
| /// </summary> | /// </summary> | ||||
| bool IsArchived { get; } | bool IsArchived { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets whether or not the current thread is archived. | |||||
| /// Gets the duration of time before the thread is automatically archived after no activity. | |||||
| /// </summary> | /// </summary> | ||||
| ThreadArchiveDuration AutoArchiveDuration { get; } | ThreadArchiveDuration AutoArchiveDuration { get; } | ||||
| @@ -1,7 +1,7 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Specifies the privacy levels of a Stage instance. | |||||
| /// Represents the privacy level of a stage. | |||||
| /// </summary> | /// </summary> | ||||
| public enum StagePrivacyLevel | public enum StagePrivacyLevel | ||||
| { | { | ||||
| @@ -74,6 +74,10 @@ namespace Discord | |||||
| public static bool TryParse(string text, out Emote result) | public static bool TryParse(string text, out Emote result) | ||||
| { | { | ||||
| result = null; | result = null; | ||||
| if (text == null) | |||||
| return false; | |||||
| if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') | if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') | ||||
| { | { | ||||
| bool animated = text[1] == 'a'; | bool animated = text[1] == 'a'; | ||||
| @@ -313,6 +313,13 @@ namespace Discord | |||||
| /// The approximate number of non-offline members in this guild. | /// The approximate number of non-offline members in this guild. | ||||
| /// </returns> | /// </returns> | ||||
| int? ApproximatePresenceCount { get; } | int? ApproximatePresenceCount { get; } | ||||
| /// <summary> | |||||
| /// Gets the max bitrate for voice channels in this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A <see cref="int"/> representing the maximum bitrate value allowed by Discord in this guild. | |||||
| /// </returns> | |||||
| int MaxBitrate { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the preferred locale of this guild in IETF BCP 47 | /// Gets the preferred locale of this guild in IETF BCP 47 | ||||
| @@ -11,7 +11,7 @@ namespace Discord | |||||
| private object _value; | private object _value; | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the name of this choice. | |||||
| /// Gets or sets the name of this choice. | |||||
| /// </summary> | /// </summary> | ||||
| public string Name | public string Name | ||||
| { | { | ||||
| @@ -1,22 +1,22 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// ApplicationCommandType is enum of current valid Application Command Types: Slash, User, Message | |||||
| /// Represents the types of application commands. | |||||
| /// </summary> | /// </summary> | ||||
| public enum ApplicationCommandType : byte | public enum ApplicationCommandType : byte | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// ApplicationCommandType.Slash is Slash command type | |||||
| /// A Slash command type | |||||
| /// </summary> | /// </summary> | ||||
| Slash = 1, | Slash = 1, | ||||
| /// <summary> | /// <summary> | ||||
| /// ApplicationCommandType.User is Context Menu User command type | |||||
| /// A Context Menu User command type | |||||
| /// </summary> | /// </summary> | ||||
| User = 2, | User = 2, | ||||
| /// <summary> | /// <summary> | ||||
| /// ApplicationCommandType.Message is Context Menu Message command type | |||||
| /// A Context Menu Message command type | |||||
| /// </summary> | /// </summary> | ||||
| Message = 3 | Message = 3 | ||||
| } | } | ||||
| @@ -45,7 +45,7 @@ namespace Discord | |||||
| public object Value | public object Value | ||||
| { | { | ||||
| get => _value; | get => _value; | ||||
| set | |||||
| set | |||||
| { | { | ||||
| if (value is not string && !value.IsNumericType()) | if (value is not string && !value.IsNumericType()) | ||||
| throw new ArgumentException($"{nameof(value)} must be a numeric type or a string!"); | throw new ArgumentException($"{nameof(value)} must be a numeric type or a string!"); | ||||
| @@ -18,7 +18,7 @@ namespace Discord | |||||
| string Name { get; } | string Name { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the params + values from the user. | |||||
| /// Gets the options that the user has provided. | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | ||||
| } | } | ||||
| @@ -26,7 +26,7 @@ namespace Discord | |||||
| ApplicationCommandOptionType Type { get; } | ApplicationCommandOptionType Type { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the options for this command. | |||||
| /// Gets the nested options of this option. | |||||
| /// </summary> | /// </summary> | ||||
| IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; } | ||||
| } | } | ||||
| @@ -18,18 +18,6 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| Pong = 1, | Pong = 1, | ||||
| /// <summary> | |||||
| /// ACK a command without sending a message, eating the user's input. | |||||
| /// </summary> | |||||
| [Obsolete("This response type has been deprecated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)] | |||||
| Acknowledge = 2, | |||||
| /// <summary> | |||||
| /// Respond with a message, showing the user's input. | |||||
| /// </summary> | |||||
| [Obsolete("This response type has been deprecated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)] | |||||
| ChannelMessage = 3, | |||||
| /// <summary> | /// <summary> | ||||
| /// Respond to an interaction with a message. | /// Respond to an interaction with a message. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -76,7 +76,7 @@ namespace Discord | |||||
| AddComponent(cmp, row); | AddComponent(cmp, row); | ||||
| break; | break; | ||||
| case SelectMenuComponent menu: | case SelectMenuComponent menu: | ||||
| WithSelectMenu(menu.CustomId, menu.Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row); | |||||
| WithSelectMenu(menu.CustomId, menu.Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row); | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -715,7 +715,7 @@ namespace Discord | |||||
| MinValues = selectMenu.MinValues; | MinValues = selectMenu.MinValues; | ||||
| IsDisabled = selectMenu.IsDisabled; | IsDisabled = selectMenu.IsDisabled; | ||||
| Options = selectMenu.Options? | Options = selectMenu.Options? | ||||
| .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default)) | |||||
| .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) | |||||
| .ToList(); | .ToList(); | ||||
| } | } | ||||
| @@ -969,7 +969,7 @@ namespace Discord | |||||
| Value = value; | Value = value; | ||||
| Description = description; | Description = description; | ||||
| Emote = emote; | Emote = emote; | ||||
| this.IsDefault = isDefault; | |||||
| IsDefault = isDefault; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -981,7 +981,7 @@ namespace Discord | |||||
| Value = option.Value; | Value = option.Value; | ||||
| Description = option.Description; | Description = option.Description; | ||||
| Emote = option.Emote; | Emote = option.Emote; | ||||
| IsDefault = option.Default; | |||||
| IsDefault = option.IsDefault; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -48,7 +48,7 @@ namespace Discord | |||||
| public SelectMenuBuilder ToBuilder() | public SelectMenuBuilder ToBuilder() | ||||
| => new SelectMenuBuilder( | => new SelectMenuBuilder( | ||||
| CustomId, | CustomId, | ||||
| Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default)).ToList(), | |||||
| Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), | |||||
| Placeholder, | Placeholder, | ||||
| MaxValues, | MaxValues, | ||||
| MinValues, | MinValues, | ||||
| @@ -6,29 +6,29 @@ namespace Discord | |||||
| public class SelectMenuOption | public class SelectMenuOption | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// The user-facing name of the option, max 25 characters. | |||||
| /// Gets the user-facing name of the option. | |||||
| /// </summary> | /// </summary> | ||||
| public string Label { get; } | public string Label { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// The dev-define value of the option, max 100 characters. | |||||
| /// Gets the dev-define value of the option. | |||||
| /// </summary> | /// </summary> | ||||
| public string Value { get; } | public string Value { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// An additional description of the option, max 50 characters. | |||||
| /// Gets a description of the option. | |||||
| /// </summary> | /// </summary> | ||||
| public string Description { get; } | public string Description { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// A <see cref="IEmote"/> that will be displayed with this menu option. | |||||
| /// Gets the <see cref="IEmote"/> displayed with this menu option. | |||||
| /// </summary> | /// </summary> | ||||
| public IEmote Emote { get; } | public IEmote Emote { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Will render this option as selected by default. | |||||
| /// Gets whether or not this option will render as selected by default. | |||||
| /// </summary> | /// </summary> | ||||
| public bool? Default { get; } | |||||
| public bool? IsDefault { get; } | |||||
| internal SelectMenuOption(string label, string value, string description, IEmote emote, bool? defaultValue) | internal SelectMenuOption(string label, string value, string description, IEmote emote, bool? defaultValue) | ||||
| { | { | ||||
| @@ -36,7 +36,7 @@ namespace Discord | |||||
| Value = value; | Value = value; | ||||
| Description = description; | Description = description; | ||||
| Emote = emote; | Emote = emote; | ||||
| Default = defaultValue; | |||||
| IsDefault = defaultValue; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -519,7 +519,7 @@ namespace Discord | |||||
| Preconditions.AtLeast(name.Length, 1, nameof(name)); | Preconditions.AtLeast(name.Length, 1, nameof(name)); | ||||
| Preconditions.AtMost(name.Length, 100, nameof(name)); | Preconditions.AtMost(name.Length, 100, nameof(name)); | ||||
| if (value is string str) | |||||
| if(value is string str) | |||||
| { | { | ||||
| Preconditions.AtLeast(str.Length, 1, nameof(value)); | Preconditions.AtLeast(str.Length, 1, nameof(value)); | ||||
| Preconditions.AtMost(str.Length, 100, nameof(value)); | Preconditions.AtMost(str.Length, 100, nameof(value)); | ||||
| @@ -614,7 +614,7 @@ namespace Discord | |||||
| MinValue = value; | MinValue = value; | ||||
| return this; | return this; | ||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Sets the current builders max value field. | /// Sets the current builders max value field. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -110,12 +110,6 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| ManageEmojis = 0x00_40_00_00_00, | ManageEmojis = 0x00_40_00_00_00, | ||||
| /// <summary> | |||||
| /// Allows members to use slash commands in text channels. | |||||
| /// </summary> | |||||
| [Obsolete("UseSlashCommands has been replaced by UseApplicationCommands", true)] | |||||
| UseSlashCommands = 0x00_80_00_00_00, | |||||
| /// <summary> | /// <summary> | ||||
| /// Allows members to use slash commands in text channels. | /// Allows members to use slash commands in text channels. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -131,17 +125,6 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| ManageThreads = 0x04_00_00_00_00, | ManageThreads = 0x04_00_00_00_00, | ||||
| /// <summary> | |||||
| /// Allows for creating and participating in threads | |||||
| /// </summary> | |||||
| [Obsolete("UsePublicThreads has been replaced by CreatePublicThreads and SendMessagesInThreads", true)] | |||||
| UsePublicThreads = 0x08_00_00_00_00, | |||||
| /// <summary> | |||||
| /// Allows for creating and participating in private threads | |||||
| /// </summary> | |||||
| [Obsolete("UsePrivateThreads has been replaced by CreatePrivateThreads and SendMessagesInThreads", true)] | |||||
| UsePrivateThreads = 0x10_00_00_00_00, | |||||
| /// <summary> | /// <summary> | ||||
| /// Allows for creating public threads. | /// Allows for creating public threads. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -176,11 +176,6 @@ namespace Discord | |||||
| /// </remarks> | /// </remarks> | ||||
| ManageEmojisAndStickers = 0x40_00_00_00, | ManageEmojisAndStickers = 0x40_00_00_00, | ||||
| /// <summary> | /// <summary> | ||||
| /// Allows members to use slash commands in text channels. | |||||
| /// </summary> | |||||
| [Obsolete("UseSlashCommands has been replaced by UseApplicationCommands", true)] | |||||
| UseSlashCommands = 0x80_00_00_00, | |||||
| /// <summary> | |||||
| /// Allows members to use application commands like slash commands and context menus in text channels. | /// Allows members to use application commands like slash commands and context menus in text channels. | ||||
| /// </summary> | /// </summary> | ||||
| UseApplicationCommands = 0x80_00_00_00, | UseApplicationCommands = 0x80_00_00_00, | ||||
| @@ -209,16 +204,6 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| CreatePrivateThreads = 0x10_00_00_00_00, | CreatePrivateThreads = 0x10_00_00_00_00, | ||||
| /// <summary> | /// <summary> | ||||
| /// Allows for creating public threads. | |||||
| /// </summary> | |||||
| [Obsolete("UsePublicThreads has been replaced by CreatePublicThreads and SendMessagesInThreads", true)] | |||||
| UsePublicThreads = 0x08_00_00_00_00, | |||||
| /// <summary> | |||||
| /// Allows for creating private threads. | |||||
| /// </summary> | |||||
| [Obsolete("UsePrivateThreads has been replaced by CreatePrivateThreads and SendMessagesInThreads", true)] | |||||
| UsePrivateThreads = 0x10_00_00_00_00, | |||||
| /// <summary> | |||||
| /// Allows the usage of custom stickers from other servers. | /// Allows the usage of custom stickers from other servers. | ||||
| /// </summary> | /// </summary> | ||||
| UseExternalStickers = 0x20_00_00_00_00, | UseExternalStickers = 0x20_00_00_00_00, | ||||
| @@ -58,7 +58,7 @@ namespace Discord | |||||
| /// A string containing the hash of this role's icon. | /// A string containing the hash of this role's icon. | ||||
| /// </returns> | /// </returns> | ||||
| string Icon { get; } | string Icon { get; } | ||||
| /// <summary> | |||||
| /// <summary> | |||||
| /// Gets the unicode emoji of this role. | /// Gets the unicode emoji of this role. | ||||
| /// </summary> | /// </summary> | ||||
| /// <remarks> | /// <remarks> | ||||
| @@ -1,4 +1,4 @@ | |||||
| using System.Collections.Immutable; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -14,10 +14,10 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the set of clients where this user is currently active. | /// Gets the set of clients where this user is currently active. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableSet<ClientType> ActiveClients { get; } | |||||
| IReadOnlyCollection<ClientType> ActiveClients { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the list of activities that this user currently has available. | /// Gets the list of activities that this user currently has available. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableList<IActivity> Activities { get; } | |||||
| IReadOnlyCollection<IActivity> Activities { get; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,7 +7,8 @@ namespace Discord | |||||
| public static class Format | public static class Format | ||||
| { | { | ||||
| // Characters which need escaping | // Characters which need escaping | ||||
| private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" }; | |||||
| private static readonly string[] SensitiveCharacters = { | |||||
| "\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" }; | |||||
| /// <summary> Returns a markdown-formatted string with bold formatting. </summary> | /// <summary> Returns a markdown-formatted string with bold formatting. </summary> | ||||
| public static string Bold(string text) => $"**{text}**"; | public static string Bold(string text) => $"**{text}**"; | ||||
| @@ -104,5 +105,15 @@ namespace Discord | |||||
| var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); | var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); | ||||
| return newText; | return newText; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Formats a user's username + discriminator while maintaining bidirectional unicode | |||||
| /// </summary> | |||||
| /// <param name="user">The user whos username and discriminator to format</param> | |||||
| /// <returns>The username + discriminator</returns> | |||||
| public static string UsernameAndDiscriminator(IUser user) | |||||
| { | |||||
| return $"\u2066{user.Username}\u2069#{user.Discriminator}"; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,7 +5,7 @@ namespace Discord.Utils | |||||
| internal static class UrlValidation | internal static class UrlValidation | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Not full URL validation right now. Just ensures protocol is present and that it's either http or https | |||||
| /// Not full URL validation right now. Just ensures protocol is present and that it's either http or https | |||||
| /// <see cref="ValidateButton(string)"/> should be used for url buttons. | /// <see cref="ValidateButton(string)"/> should be used for url buttons. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="url">The URL to validate before sending to Discord.</param> | /// <param name="url">The URL to validate before sending to Discord.</param> | ||||
| @@ -22,7 +22,7 @@ namespace Discord.Utils | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord | |||||
| /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord | |||||
| /// <see cref="Validate(string)"/> should be used everything other than url buttons. | /// <see cref="Validate(string)"/> should be used everything other than url buttons. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="url">The URL to validate before sending to discord.</param> | /// <param name="url">The URL to validate before sending to discord.</param> | ||||
| @@ -47,7 +47,7 @@ namespace Discord.API | |||||
| } | } | ||||
| } | } | ||||
| Default = option.Default ?? Optional<bool>.Unspecified; | |||||
| Default = option.IsDefault ?? Optional<bool>.Unspecified; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -11,9 +11,8 @@ namespace Discord.API.Rest | |||||
| { | { | ||||
| private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | ||||
| public Stream File { get; } | |||||
| public FileAttachment[] Files { get; } | |||||
| public Optional<string> Filename { get; set; } | |||||
| public Optional<string> Content { get; set; } | public Optional<string> Content { get; set; } | ||||
| public Optional<string> Nonce { get; set; } | public Optional<string> Nonce { get; set; } | ||||
| public Optional<bool> IsTTS { get; set; } | public Optional<bool> IsTTS { get; set; } | ||||
| @@ -21,22 +20,16 @@ namespace Discord.API.Rest | |||||
| public Optional<string> AvatarUrl { get; set; } | public Optional<string> AvatarUrl { get; set; } | ||||
| public Optional<Embed[]> Embeds { get; set; } | public Optional<Embed[]> Embeds { get; set; } | ||||
| public Optional<AllowedMentions> AllowedMentions { get; set; } | public Optional<AllowedMentions> AllowedMentions { get; set; } | ||||
| public Optional<ActionRowComponent[]> MessageComponents { get; set; } | |||||
| public bool IsSpoiler { get; set; } = false; | |||||
| public UploadWebhookFileParams(Stream file) | |||||
| public UploadWebhookFileParams(params FileAttachment[] files) | |||||
| { | { | ||||
| File = file; | |||||
| Files = files; | |||||
| } | } | ||||
| public IReadOnlyDictionary<string, object> ToDictionary() | public IReadOnlyDictionary<string, object> ToDictionary() | ||||
| { | { | ||||
| var d = new Dictionary<string, object>(); | var d = new Dictionary<string, object>(); | ||||
| var filename = Filename.GetValueOrDefault("unknown.dat"); | |||||
| if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) | |||||
| filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); | |||||
| d["file"] = new MultipartFile(File, filename); | |||||
| var payload = new Dictionary<string, object>(); | var payload = new Dictionary<string, object>(); | ||||
| if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
| @@ -49,11 +42,34 @@ namespace Discord.API.Rest | |||||
| payload["username"] = Username.Value; | payload["username"] = Username.Value; | ||||
| if (AvatarUrl.IsSpecified) | if (AvatarUrl.IsSpecified) | ||||
| payload["avatar_url"] = AvatarUrl.Value; | payload["avatar_url"] = AvatarUrl.Value; | ||||
| if (MessageComponents.IsSpecified) | |||||
| payload["components"] = MessageComponents.Value; | |||||
| if (Embeds.IsSpecified) | if (Embeds.IsSpecified) | ||||
| payload["embeds"] = Embeds.Value; | payload["embeds"] = Embeds.Value; | ||||
| if (AllowedMentions.IsSpecified) | if (AllowedMentions.IsSpecified) | ||||
| payload["allowed_mentions"] = AllowedMentions.Value; | payload["allowed_mentions"] = AllowedMentions.Value; | ||||
| List<object> attachments = new(); | |||||
| for (int n = 0; n != Files.Length; n++) | |||||
| { | |||||
| var attachment = Files[n]; | |||||
| var filename = attachment.FileName ?? "unknown.dat"; | |||||
| if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) | |||||
| filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); | |||||
| d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); | |||||
| attachments.Add(new | |||||
| { | |||||
| id = (ulong)n, | |||||
| filename = filename, | |||||
| description = attachment.Description ?? Optional<string>.Unspecified | |||||
| }); | |||||
| } | |||||
| payload["attachments"] = attachments; | |||||
| var json = new StringBuilder(); | var json = new StringBuilder(); | ||||
| using (var text = new StringWriter(json)) | using (var text = new StringWriter(json)) | ||||
| using (var writer = new JsonTextWriter(text)) | using (var writer = new JsonTextWriter(text)) | ||||
| @@ -55,7 +55,7 @@ namespace Discord.API | |||||
| _restClientProvider = restClientProvider; | _restClientProvider = restClientProvider; | ||||
| UserAgent = userAgent; | UserAgent = userAgent; | ||||
| DefaultRetryMode = defaultRetryMode; | DefaultRetryMode = defaultRetryMode; | ||||
| _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver(), NullValueHandling = NullValueHandling.Include }; | |||||
| _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
| UseSystemClock = useSystemClock; | UseSystemClock = useSystemClock; | ||||
| RequestQueue = new RequestQueue(); | RequestQueue = new RequestQueue(); | ||||
| @@ -227,8 +227,11 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual 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); | => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); | ||||
| public virtual Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => throw new NotImplementedException(); | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); | |||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); | |||||
| public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -21,8 +21,10 @@ namespace Discord.Rest | |||||
| public int? UserLimit { get; private set; } | public int? UserLimit { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ulong? CategoryId { get; private set; } | public ulong? CategoryId { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Mention => MentionUtils.MentionChannel(Id); | public string Mention => MentionUtils.MentionChannel(Id); | ||||
| internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) | internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) | ||||
| : base(discord, guild, id) | : base(discord, guild, id) | ||||
| { | { | ||||
| @@ -76,6 +78,9 @@ namespace Discord.Rest | |||||
| public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -85,6 +85,20 @@ namespace Discord.Rest | |||||
| public int? ApproximateMemberCount { get; private set; } | public int? ApproximateMemberCount { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public int? ApproximatePresenceCount { get; private set; } | public int? ApproximatePresenceCount { get; private set; } | ||||
| /// <inheritdoc/> | |||||
| public int MaxBitrate | |||||
| { | |||||
| get | |||||
| { | |||||
| return PremiumTier switch | |||||
| { | |||||
| PremiumTier.Tier1 => 128000, | |||||
| PremiumTier.Tier2 => 256000, | |||||
| PremiumTier.Tier3 => 384000, | |||||
| _ => 96000, | |||||
| }; | |||||
| } | |||||
| } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public NsfwLevel NsfwLevel { get; private set; } | public NsfwLevel NsfwLevel { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -47,7 +47,6 @@ namespace Discord.Rest | |||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; } | ||||
| internal RestApplicationCommandOption() { } | internal RestApplicationCommandOption() { } | ||||
| internal static RestApplicationCommandOption Create(Model model) | internal static RestApplicationCommandOption Create(Model model) | ||||
| @@ -51,6 +51,7 @@ namespace Discord.Rest | |||||
| AllowedMentions allowedMentions = args.AllowedMentions.Value; | AllowedMentions allowedMentions = args.AllowedMentions.Value; | ||||
| Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); | 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(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); | ||||
| Preconditions.AtMost(args.Embeds.Value?.Length ?? 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 | // 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 != null && allowedMentions.AllowedTypes.HasValue) | ||||
| @@ -115,7 +115,6 @@ namespace Discord.Rest | |||||
| throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); | throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); | ||||
| } | } | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| } | } | ||||
| } | } | ||||
| @@ -5,6 +5,7 @@ using System.Globalization; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using EventUserModel = Discord.API.GuildScheduledEventUser; | using EventUserModel = Discord.API.GuildScheduledEventUser; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| @@ -41,9 +42,9 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual UserStatus Status => UserStatus.Offline; | public virtual UserStatus Status => UserStatus.Offline; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| public virtual IReadOnlyCollection<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| public virtual IReadOnlyCollection<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual bool IsWebhook => false; | public virtual bool IsWebhook => false; | ||||
| @@ -128,8 +129,8 @@ namespace Discord.Rest | |||||
| /// <returns> | /// <returns> | ||||
| /// A string that resolves to Username#Discriminator of the user. | /// A string that resolves to Username#Discriminator of the user. | ||||
| /// </returns> | /// </returns> | ||||
| public override string ToString() => $"{Username}#{Discriminator}"; | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | |||||
| public override string ToString() => Format.UsernameAndDiscriminator(this); | |||||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; | |||||
| #endregion | #endregion | ||||
| #region IUser | #region IUser | ||||
| @@ -71,6 +71,7 @@ namespace Discord.Rest | |||||
| public static API.AllowedMentions ToModel(this AllowedMentions entity) | public static API.AllowedMentions ToModel(this AllowedMentions entity) | ||||
| { | { | ||||
| if (entity == null) return null; | |||||
| return new API.AllowedMentions() | return new API.AllowedMentions() | ||||
| { | { | ||||
| Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), | Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), | ||||
| @@ -36,7 +36,6 @@ namespace Discord.Net.Converters | |||||
| return new GuildFeatures(features, experimental.ToArray()); | return new GuildFeatures(features, experimental.ToArray()); | ||||
| } | } | ||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
| { | { | ||||
| throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
| @@ -502,6 +502,18 @@ namespace Discord.WebSocket | |||||
| internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | ||||
| #endregion | #endregion | ||||
| #region Presence | |||||
| /// <summary> Fired when a users presence is updated. </summary> | |||||
| public event Func<SocketUser, SocketPresence, SocketPresence, Task> PresenceUpdated | |||||
| { | |||||
| add { _presenceUpdated.Add(value); } | |||||
| remove { _presenceUpdated.Remove(value); } | |||||
| } | |||||
| internal readonly AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>> _presenceUpdated = new AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>>(); | |||||
| #endregion | |||||
| #region Invites | #region Invites | ||||
| /// <summary> | /// <summary> | ||||
| /// Fired when an invite is created. | /// Fired when an invite is created. | ||||
| @@ -115,7 +115,7 @@ namespace Discord.WebSocket | |||||
| if (_guilds.TryRemove(id, out SocketGuild guild)) | if (_guilds.TryRemove(id, out SocketGuild guild)) | ||||
| { | { | ||||
| guild.PurgeChannelCache(this); | guild.PurgeChannelCache(this); | ||||
| guild.PurgeGuildUserCache(); | |||||
| guild.PurgeUserCache(); | |||||
| return guild; | return guild; | ||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -140,7 +140,35 @@ namespace Discord.WebSocket | |||||
| internal void PurgeUsers() | internal void PurgeUsers() | ||||
| { | { | ||||
| foreach (var guild in _guilds.Values) | foreach (var guild in _guilds.Values) | ||||
| guild.PurgeGuildUserCache(); | |||||
| guild.PurgeUserCache(); | |||||
| } | |||||
| 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 _); | |||||
| } | } | ||||
| internal SocketApplicationCommand GetCommand(ulong id) | internal SocketApplicationCommand GetCommand(ulong id) | ||||
| @@ -1,4 +1,4 @@ | |||||
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <Import Project="../../Discord.Net.targets" /> | <Import Project="../../Discord.Net.targets" /> | ||||
| <Import Project="../../StyleAnalyzer.targets" /> | <Import Project="../../StyleAnalyzer.targets" /> | ||||
| <PropertyGroup> | <PropertyGroup> | ||||
| @@ -495,9 +495,12 @@ namespace Discord.WebSocket | |||||
| client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2); | client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2); | ||||
| client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2); | client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2); | ||||
| } | } | ||||
| #endregion | |||||
| #endregion | |||||
| #region IDiscordClient | #region IDiscordClient | ||||
| /// <inheritdoc /> | |||||
| ISelfUser IDiscordClient.CurrentUser => CurrentUser; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | ||||
| => await GetApplicationInfoAsync().ConfigureAwait(false); | => await GetApplicationInfoAsync().ConfigureAwait(false); | ||||
| @@ -76,6 +76,7 @@ namespace Discord.WebSocket | |||||
| internal int? HandlerTimeout { get; private set; } | internal int? HandlerTimeout { get; private set; } | ||||
| internal bool AlwaysDownloadDefaultStickers { get; private set; } | internal bool AlwaysDownloadDefaultStickers { get; private set; } | ||||
| internal bool AlwaysResolveStickers { get; private set; } | internal bool AlwaysResolveStickers { get; private set; } | ||||
| internal bool LogGatewayIntentWarnings { get; private set; } | |||||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient; | internal new DiscordSocketApiClient ApiClient => base.ApiClient; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | ||||
| @@ -147,6 +148,7 @@ namespace Discord.WebSocket | |||||
| AlwaysDownloadUsers = config.AlwaysDownloadUsers; | AlwaysDownloadUsers = config.AlwaysDownloadUsers; | ||||
| AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; | AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; | ||||
| AlwaysResolveStickers = config.AlwaysResolveStickers; | AlwaysResolveStickers = config.AlwaysResolveStickers; | ||||
| LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; | |||||
| HandlerTimeout = config.HandlerTimeout; | HandlerTimeout = config.HandlerTimeout; | ||||
| State = new ClientState(0, 0); | State = new ClientState(0, 0); | ||||
| Rest = new DiscordSocketRestClient(config, ApiClient); | Rest = new DiscordSocketRestClient(config, ApiClient); | ||||
| @@ -238,6 +240,9 @@ namespace Discord.WebSocket | |||||
| _defaultStickers = builder.ToImmutable(); | _defaultStickers = builder.ToImmutable(); | ||||
| } | } | ||||
| if(LogGatewayIntentWarnings) | |||||
| await LogGatewayIntentsWarning().ConfigureAwait(false); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -708,6 +713,52 @@ namespace Discord.WebSocket | |||||
| game); | game); | ||||
| } | } | ||||
| private async Task LogGatewayIntentsWarning() | |||||
| { | |||||
| if(_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && !_presenceUpdated.HasSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using the GuildPresences intent without listening to the PresenceUpdate event, consider removing the intent from your config.").ConfigureAwait(false); | |||||
| } | |||||
| if(!_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && _presenceUpdated.HasSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using the PresenceUpdate event without specifying the GuildPresences intent, consider adding the intent to your config.").ConfigureAwait(false); | |||||
| } | |||||
| bool hasGuildScheduledEventsSubscribers = | |||||
| _guildScheduledEventCancelled.HasSubscribers || | |||||
| _guildScheduledEventUserRemove.HasSubscribers || | |||||
| _guildScheduledEventCompleted.HasSubscribers || | |||||
| _guildScheduledEventCreated.HasSubscribers || | |||||
| _guildScheduledEventStarted.HasSubscribers || | |||||
| _guildScheduledEventUpdated.HasSubscribers || | |||||
| _guildScheduledEventUserAdd.HasSubscribers; | |||||
| if(_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && !hasGuildScheduledEventsSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using the GuildScheduledEvents gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); | |||||
| } | |||||
| if(!_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && hasGuildScheduledEventsSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using events related to the GuildScheduledEvents gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); | |||||
| } | |||||
| bool hasInviteEventSubscribers = | |||||
| _inviteCreatedEvent.HasSubscribers || | |||||
| _inviteDeletedEvent.HasSubscribers; | |||||
| if (_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && !hasInviteEventSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using the GuildInvites gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); | |||||
| } | |||||
| if (!_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && hasInviteEventSubscribers) | |||||
| { | |||||
| await _gatewayLogger.WarningAsync("You're using events related to the GuildInvites gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| #region ProcessMessageAsync | #region ProcessMessageAsync | ||||
| private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) | private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) | ||||
| { | { | ||||
| @@ -1858,6 +1909,8 @@ namespace Discord.WebSocket | |||||
| var data = (payload as JToken).ToObject<API.Presence>(_serializer); | var data = (payload as JToken).ToObject<API.Presence>(_serializer); | ||||
| SocketUser user = null; | |||||
| if (data.GuildId.IsSpecified) | if (data.GuildId.IsSpecified) | ||||
| { | { | ||||
| var guild = State.GetGuild(data.GuildId.Value); | var guild = State.GetGuild(data.GuildId.Value); | ||||
| @@ -1872,7 +1925,7 @@ namespace Discord.WebSocket | |||||
| return; | return; | ||||
| } | } | ||||
| var user = guild.GetUser(data.User.Id); | |||||
| user = guild.GetUser(data.User.Id); | |||||
| if (user == null) | if (user == null) | ||||
| { | { | ||||
| if (data.Status == UserStatus.Offline) | if (data.Status == UserStatus.Offline) | ||||
| @@ -1890,26 +1943,21 @@ namespace Discord.WebSocket | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | ||||
| } | } | ||||
| } | } | ||||
| var before = user.Clone(); | |||||
| user.Update(State, data, true); | |||||
| var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => Task.FromResult(user)); | |||||
| await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| var globalUser = State.GetUser(data.User.Id); | |||||
| if (globalUser == null) | |||||
| user = State.GetUser(data.User.Id); | |||||
| if (user == null) | |||||
| { | { | ||||
| await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); | await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); | ||||
| return; | return; | ||||
| } | } | ||||
| var before = globalUser.Clone(); | |||||
| globalUser.Update(State, data.User); | |||||
| globalUser.Update(State, data); | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); | |||||
| } | } | ||||
| var before = user.Presence.Clone(); | |||||
| user.Update(State, data.User); | |||||
| user.Update(data); | |||||
| await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); | |||||
| } | } | ||||
| break; | break; | ||||
| case "TYPING_START": | case "TYPING_START": | ||||
| @@ -183,6 +183,11 @@ namespace Discord.WebSocket | |||||
| /// </remarks> | /// </remarks> | ||||
| public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; | public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not to log warnings related to guild intents and events. | |||||
| /// </summary> | |||||
| public bool LogGatewayIntentWarnings { get; set; } = true; | |||||
| /// <summary> | /// <summary> | ||||
| /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -34,12 +34,12 @@ namespace Discord.WebSocket | |||||
| public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | ||||
| /// <summary> | /// <summary> | ||||
| /// Returns a collection representing all of the users in the group. | |||||
| /// Returns a collection representing all of the users in the group. | |||||
| /// </summary> | /// </summary> | ||||
| public new IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection(); | public new IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection(); | ||||
| /// <summary> | /// <summary> | ||||
| /// Returns a collection representing all users in the group, not including the client. | |||||
| /// Returns a collection representing all users in the group, not including the client. | |||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketGroupUser> Recipients | public IReadOnlyCollection<SocketGroupUser> Recipients | ||||
| => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | ||||
| @@ -324,6 +324,9 @@ namespace Discord.WebSocket | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); | |||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -21,6 +21,7 @@ namespace Discord.WebSocket | |||||
| public int Bitrate { get; private set; } | public int Bitrate { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public int? UserLimit { get; private set; } | public int? UserLimit { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ulong? CategoryId { get; private set; } | public ulong? CategoryId { get; private set; } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -31,6 +32,10 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public ICategoryChannel Category | public ICategoryChannel Category | ||||
| => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; | ||||
| /// <inheritdoc /> | |||||
| public string Mention => MentionUtils.MentionChannel(Id); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Mention => MentionUtils.MentionChannel(Id); | public string Mention => MentionUtils.MentionChannel(Id); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -97,6 +102,9 @@ namespace Discord.WebSocket | |||||
| public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||||
| => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -185,24 +185,18 @@ namespace Discord.WebSocket | |||||
| return id.HasValue ? GetVoiceChannel(id.Value) : null; | return id.HasValue ? GetVoiceChannel(id.Value) : null; | ||||
| } | } | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Gets the max bitrate for voice channels in this guild. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A <see cref="int"/> representing the maximum bitrate value allowed by Discord in this guild. | |||||
| /// </returns> | |||||
| /// <inheritdoc/> | |||||
| public int MaxBitrate | public int MaxBitrate | ||||
| { | { | ||||
| get | get | ||||
| { | { | ||||
| var maxBitrate = PremiumTier switch | |||||
| return PremiumTier switch | |||||
| { | { | ||||
| PremiumTier.Tier1 => 128000, | PremiumTier.Tier1 => 128000, | ||||
| PremiumTier.Tier2 => 256000, | PremiumTier.Tier2 => 256000, | ||||
| PremiumTier.Tier3 => 384000, | PremiumTier.Tier3 => 384000, | ||||
| _ => 96000, | _ => 96000, | ||||
| }; | }; | ||||
| return maxBitrate; | |||||
| } | } | ||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -1150,22 +1144,29 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void PurgeGuildUserCache() | |||||
| /// <summary> | |||||
| /// Purges this guild's user cache. | |||||
| /// </summary> | |||||
| public void PurgeUserCache() => PurgeUserCache(_ => true); | |||||
| /// <summary> | |||||
| /// Purges this guild's user cache. | |||||
| /// </summary> | |||||
| /// <param name="predicate">The predicate used to select which users to clear.</param> | |||||
| public void PurgeUserCache(Func<SocketGuildUser, bool> predicate) | |||||
| { | { | ||||
| var members = Users; | |||||
| var self = CurrentUser; | |||||
| _members.Clear(); | |||||
| if (self != null) | |||||
| _members.TryAdd(self.Id, self); | |||||
| var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); | |||||
| var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); | |||||
| foreach (var member in membersToPurge) | |||||
| if(_members.TryRemove(member.Id, out _)) | |||||
| member.GlobalUser.RemoveRef(Discord); | |||||
| foreach (var member in membersToKeep) | |||||
| _members.TryAdd(member.Id, member); | |||||
| _downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| DownloadedMemberCount = _members.Count; | DownloadedMemberCount = _members.Count; | ||||
| foreach (var member in members) | |||||
| { | |||||
| if (member.Id != self?.Id) | |||||
| member.GlobalUser.RemoveRef(Discord); | |||||
| } | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -7,7 +7,7 @@ using Model = Discord.API.Gateway.InviteCreateEvent; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Represents a WebSocket-based invite to a guild. | |||||
| /// Represents a WebSocket-based invite to a guild. | |||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketInvite : SocketEntity<string>, IInviteMetadata | public class SocketInvite : SocketEntity<string>, IInviteMetadata | ||||
| @@ -57,7 +57,7 @@ namespace Discord.WebSocket | |||||
| public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); | public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); | ||||
| /// <summary> | /// <summary> | ||||
| /// Returns an IEnumerable containing all <see cref="SocketGuildUser"/> that have this role. | |||||
| /// Returns an IEnumerable containing all <see cref="SocketGuildUser"/> that have this role. | |||||
| /// </summary> | /// </summary> | ||||
| public IEnumerable<SocketGuildUser> Members | public IEnumerable<SocketGuildUser> Members | ||||
| => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | ||||
| @@ -1,7 +1,6 @@ | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -48,11 +47,6 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| internal void Update(ClientState state, PresenceModel model) | |||||
| { | |||||
| Presence = SocketPresence.Create(model); | |||||
| } | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | ||||
| } | } | ||||
| @@ -164,8 +164,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (updatePresence) | if (updatePresence) | ||||
| { | { | ||||
| Presence = SocketPresence.Create(model); | |||||
| GlobalUser.Update(state, model); | |||||
| Update(model); | |||||
| } | } | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| Nickname = model.Nick.Value; | Nickname = model.Nick.Value; | ||||
| @@ -174,6 +173,13 @@ namespace Discord.WebSocket | |||||
| if (model.PremiumSince.IsSpecified) | if (model.PremiumSince.IsSpecified) | ||||
| _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | ||||
| } | } | ||||
| internal override void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| GlobalUser.Update(model); | |||||
| } | |||||
| private void UpdateRoles(ulong[] roleIds) | private void UpdateRoles(ulong[] roleIds) | ||||
| { | { | ||||
| var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | ||||
| @@ -11,26 +11,37 @@ namespace Discord.WebSocket | |||||
| /// Represents the WebSocket user's presence status. This may include their online status and their activity. | /// Represents the WebSocket user's presence status. This may include their online status and their activity. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct SocketPresence : IPresence | |||||
| public class SocketPresence : IPresence | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status { get; } | |||||
| public UserStatus Status { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients { get; } | |||||
| public IReadOnlyCollection<ClientType> ActiveClients { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities { get; } | |||||
| public IReadOnlyCollection<IActivity> Activities { get; private set; } | |||||
| internal SocketPresence() { } | |||||
| internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | ||||
| { | { | ||||
| Status = status; | Status = status; | ||||
| ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ||||
| Activities = activities ?? ImmutableList<IActivity>.Empty; | Activities = activities ?? ImmutableList<IActivity>.Empty; | ||||
| } | } | ||||
| internal static SocketPresence Create(Model model) | internal static SocketPresence Create(Model model) | ||||
| { | { | ||||
| var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); | |||||
| var activities = ConvertActivitiesList(model.Activities); | |||||
| return new SocketPresence(model.Status, clients, activities); | |||||
| var entity = new SocketPresence(); | |||||
| entity.Update(model); | |||||
| return entity; | |||||
| } | |||||
| internal void Update(Model model) | |||||
| { | |||||
| Status = model.Status; | |||||
| ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray<ClientType>.Empty; | |||||
| Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | ||||
| /// where a user is active from the data supplied in the Presence update frame. | /// where a user is active from the data supplied in the Presence update frame. | ||||
| @@ -42,7 +53,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A collection of all <see cref="ClientType"/>s that this user is active. | /// A collection of all <see cref="ClientType"/>s that this user is active. | ||||
| /// </returns> | /// </returns> | ||||
| private static IImmutableSet<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| private static IReadOnlyCollection<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| { | { | ||||
| if (clientTypesDict == null || clientTypesDict.Count == 0) | if (clientTypesDict == null || clientTypesDict.Count == 0) | ||||
| return ImmutableHashSet<ClientType>.Empty; | return ImmutableHashSet<ClientType>.Empty; | ||||
| @@ -84,6 +95,6 @@ namespace Discord.WebSocket | |||||
| public override string ToString() => Status.ToString(); | public override string ToString() => Status.ToString(); | ||||
| private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | ||||
| internal SocketPresence Clone() => this; | |||||
| internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,6 +7,7 @@ using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -40,9 +41,9 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status => Presence.Status; | public UserStatus Status => Presence.Status; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets mutual guilds shared with this user. | /// Gets mutual guilds shared with this user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -91,6 +92,11 @@ namespace Discord.WebSocket | |||||
| return hasChanges; | return hasChanges; | ||||
| } | } | ||||
| internal virtual void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | ||||
| => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | ||||
| @@ -109,8 +115,8 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// The full name of the user. | /// The full name of the user. | ||||
| /// </returns> | /// </returns> | ||||
| public override string ToString() => $"{Username}#{Discriminator}"; | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | |||||
| public override string ToString() => Format.UsernameAndDiscriminator(this); | |||||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; | |||||
| internal SocketUser Clone() => MemberwiseClone() as SocketUser; | internal SocketUser Clone() => MemberwiseClone() as SocketUser; | ||||
| } | } | ||||
| } | } | ||||
| @@ -123,14 +123,35 @@ namespace Discord.Webhook | |||||
| /// <returns> Returns the ID of the created message. </returns> | /// <returns> Returns the ID of the created message. </returns> | ||||
| public Task<ulong> SendFileAsync(string filePath, string text, bool isTTS = false, | public Task<ulong> SendFileAsync(string filePath, string text, bool isTTS = false, | ||||
| IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | ||||
| RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) | |||||
| => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); | |||||
| RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, | |||||
| MessageComponent components = null) | |||||
| => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, | |||||
| allowedMentions, options, isSpoiler, components); | |||||
| /// <summary> Sends a message to the channel for this webhook with an attachment. </summary> | /// <summary> Sends a message to the channel for this webhook with an attachment. </summary> | ||||
| /// <returns> Returns the ID of the created message. </returns> | /// <returns> Returns the ID of the created message. </returns> | ||||
| public Task<ulong> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, | public Task<ulong> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, | ||||
| IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | ||||
| RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) | |||||
| => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); | |||||
| RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, | |||||
| MessageComponent components = null) | |||||
| => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, | |||||
| avatarUrl, allowedMentions, options, isSpoiler, components); | |||||
| /// <summary> Sends a message to the channel for this webhook with an attachment. </summary> | |||||
| /// <returns> Returns the ID of the created message. </returns> | |||||
| public Task<ulong> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, | |||||
| IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | |||||
| RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) | |||||
| => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, | |||||
| avatarUrl, allowedMentions, components, options); | |||||
| /// <summary> Sends a message to the channel for this webhook with an attachment. </summary> | |||||
| /// <returns> Returns the ID of the created message. </returns> | |||||
| public Task<ulong> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, | |||||
| IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null, | |||||
| RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) | |||||
| => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, | |||||
| allowedMentions, components, options); | |||||
| /// <summary> Modifies the properties of this webhook. </summary> | /// <summary> Modifies the properties of this webhook. </summary> | ||||
| public Task ModifyWebhookAsync(Action<WebhookProperties> func, RequestOptions options = null) | public Task ModifyWebhookAsync(Action<WebhookProperties> func, RequestOptions options = null) | ||||
| @@ -97,24 +97,51 @@ namespace Discord.Webhook | |||||
| await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); | await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); | ||||
| } | } | ||||
| public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, | public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, | ||||
| IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) | |||||
| IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components) | |||||
| { | { | ||||
| string filename = Path.GetFileName(filePath); | string filename = Path.GetFileName(filePath); | ||||
| using (var file = File.OpenRead(filePath)) | using (var file = File.OpenRead(filePath)) | ||||
| return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler).ConfigureAwait(false); | |||||
| return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false); | |||||
| } | } | ||||
| public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, | |||||
| IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) | |||||
| public static Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, | |||||
| IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, | |||||
| MessageComponent components) | |||||
| => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); | |||||
| public static Task<ulong> SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) | |||||
| => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); | |||||
| public static async Task<ulong> SendFilesAsync(DiscordWebhookClient client, | |||||
| IEnumerable<FileAttachment> attachments, string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) | |||||
| { | { | ||||
| var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler }; | |||||
| if (username != null) | |||||
| args.Username = username; | |||||
| if (avatarUrl != null) | |||||
| args.AvatarUrl = avatarUrl; | |||||
| if (embeds != null) | |||||
| args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); | |||||
| if(allowedMentions != null) | |||||
| args.AllowedMentions = allowedMentions.ToModel(); | |||||
| embeds ??= Array.Empty<Embed>(); | |||||
| 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.Count(), 10, nameof(embeds), "A max of 10 embeds are allowed."); | |||||
| foreach (var attachment in attachments) | |||||
| { | |||||
| Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); | |||||
| } | |||||
| // 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 args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified }; | |||||
| var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); | var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); | ||||
| return msg.Id; | return msg.Id; | ||||
| } | } | ||||
| @@ -47,4 +47,4 @@ | |||||
| </group> | </group> | ||||
| </dependencies> | </dependencies> | ||||
| </metadata> | </metadata> | ||||
| </package> | |||||
| </package> | |||||
| @@ -10,7 +10,6 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public class ColorTests | public class ColorTests | ||||
| { | { | ||||
| [Fact] | |||||
| public void Color_New() | public void Color_New() | ||||
| { | { | ||||
| Assert.Equal(0u, new Color().RawValue); | Assert.Equal(0u, new Color().RawValue); | ||||
| @@ -214,5 +214,6 @@ namespace Discord | |||||
| public Task<IUserMessage> SendFileAsync(FileAttachment attachment, 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) => throw new NotImplementedException(); | public Task<IUserMessage> SendFileAsync(FileAttachment attachment, 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) => throw new NotImplementedException(); | ||||
| public Task<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, 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) => throw new NotImplementedException(); | public Task<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, 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) => throw new NotImplementedException(); | ||||
| public Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); | public Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); | ||||
| public Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -12,6 +12,8 @@ namespace Discord | |||||
| public int? UserLimit => throw new NotImplementedException(); | public int? UserLimit => throw new NotImplementedException(); | ||||
| public string Mention => throw new NotImplementedException(); | |||||
| public ulong? CategoryId => throw new NotImplementedException(); | public ulong? CategoryId => throw new NotImplementedException(); | ||||
| public int Position => throw new NotImplementedException(); | public int Position => throw new NotImplementedException(); | ||||
| @@ -25,7 +27,6 @@ namespace Discord | |||||
| public string Name => throw new NotImplementedException(); | public string Name => throw new NotImplementedException(); | ||||
| public DateTimeOffset CreatedAt => throw new NotImplementedException(); | public DateTimeOffset CreatedAt => throw new NotImplementedException(); | ||||
| public string Mention => throw new NotImplementedException(); | |||||
| public ulong Id => throw new NotImplementedException(); | public ulong Id => throw new NotImplementedException(); | ||||
| public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) | public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) | ||||
| @@ -49,6 +50,7 @@ namespace Discord | |||||
| } | } | ||||
| public Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||
| public Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); | |||||
| public Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||
| @@ -0,0 +1,70 @@ | |||||
| using Discord.Commands; | |||||
| using System; | |||||
| using Xunit; | |||||
| namespace Discord | |||||
| { | |||||
| public class TimeSpanTypeReaderTests | |||||
| { | |||||
| [Theory] | |||||
| [InlineData("4d3h2m1s", false)] // tests format "%d'd'%h'h'%m'm'%s's'" | |||||
| [InlineData("4d3h2m", false)] // tests format "%d'd'%h'h'%m'm'" | |||||
| [InlineData("4d3h1s", false)] // tests format "%d'd'%h'h'%s's'" | |||||
| [InlineData("4d3h", false)] // tests format "%d'd'%h'h'" | |||||
| [InlineData("4d2m1s", false)] // tests format "%d'd'%m'm'%s's'" | |||||
| [InlineData("4d2m", false)] // tests format "%d'd'%m'm'" | |||||
| [InlineData("4d1s", false)] // tests format "%d'd'%s's'" | |||||
| [InlineData("4d", false)] // tests format "%d'd'" | |||||
| [InlineData("3h2m1s", false)] // tests format "%h'h'%m'm'%s's'" | |||||
| [InlineData("3h2m", false)] // tests format "%h'h'%m'm'" | |||||
| [InlineData("3h1s", false)] // tests format "%h'h'%s's'" | |||||
| [InlineData("3h", false)] // tests format "%h'h'" | |||||
| [InlineData("2m1s", false)] // tests format "%m'm'%s's'" | |||||
| [InlineData("2m", false)] // tests format "%m'm'" | |||||
| [InlineData("1s", false)] // tests format "%s's'" | |||||
| // Negatives | |||||
| [InlineData("-4d3h2m1s", true)] // tests format "-%d'd'%h'h'%m'm'%s's'" | |||||
| [InlineData("-4d3h2m", true)] // tests format "-%d'd'%h'h'%m'm'" | |||||
| [InlineData("-4d3h1s", true)] // tests format "-%d'd'%h'h'%s's'" | |||||
| [InlineData("-4d3h", true)] // tests format "-%d'd'%h'h'" | |||||
| [InlineData("-4d2m1s", true)] // tests format "-%d'd'%m'm'%s's'" | |||||
| [InlineData("-4d2m", true)] // tests format "-%d'd'%m'm'" | |||||
| [InlineData("-4d1s", true)] // tests format "-%d'd'%s's'" | |||||
| [InlineData("-4d", true)] // tests format "-%d'd'" | |||||
| [InlineData("-3h2m1s", true)] // tests format "-%h'h'%m'm'%s's'" | |||||
| [InlineData("-3h2m", true)] // tests format "-%h'h'%m'm'" | |||||
| [InlineData("-3h1s", true)] // tests format "-%h'h'%s's'" | |||||
| [InlineData("-3h", true)] // tests format "-%h'h'" | |||||
| [InlineData("-2m1s", true)] // tests format "-%m'm'%s's'" | |||||
| [InlineData("-2m", true)] // tests format "-%m'm'" | |||||
| [InlineData("-1s", true)] // tests format "-%s's'" | |||||
| public void TestTimeSpanParse(string input, bool isNegative) | |||||
| { | |||||
| var reader = new TimeSpanTypeReader(); | |||||
| var result = reader.ReadAsync(null, input, null).Result; | |||||
| Assert.True(result.IsSuccess); | |||||
| var actual = (TimeSpan)result.BestMatch; | |||||
| Assert.True(actual != TimeSpan.Zero); | |||||
| if (isNegative) | |||||
| { | |||||
| Assert.True(actual < TimeSpan.Zero); | |||||
| Assert.True(actual.Seconds == 0 || actual.Seconds == -1); | |||||
| Assert.True(actual.Minutes == 0 || actual.Minutes == -2); | |||||
| Assert.True(actual.Hours == 0 || actual.Hours == -3); | |||||
| Assert.True(actual.Days == 0 || actual.Days == -4); | |||||
| } | |||||
| else | |||||
| { | |||||
| Assert.True(actual > TimeSpan.Zero); | |||||
| Assert.True(actual.Seconds == 0 || actual.Seconds == 1); | |||||
| Assert.True(actual.Minutes == 0 || actual.Minutes == 2); | |||||
| Assert.True(actual.Hours == 0 || actual.Hours == 3); | |||||
| Assert.True(actual.Days == 0 || actual.Days == 4); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| # Voice binaries | |||||
| These binaries were taken from the [DSharpPlus](https://dsharpplus.github.io/natives/index.html) website and are temporary until we resolve the old url for them. | |||||
| **NOTE**: You need to rename libopus.dll to opus.dll before use, otherwise audio client will complain about missing libraries. | |||||
| #### Licenses | |||||
| | Library | License | | |||||
| | :-------: | :-------------------------------------------------------- | | |||||
| | Opus | https://opus-codec.org/license/ | | |||||
| | libsodium | https://github.com/jedisct1/libsodium/blob/master/LICENSE | | |||||