commitpull/1958/headff0bbbd4d3
Merge:41b4686b
19a66bf8
Author: 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 commit19a66bf878
Author: 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> commitb9274d115d
Author: 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> commit51e06e9ce1
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri Nov 26 11:30:19 2021 -0400 feature: warn on invalid gateway intents (#1948) commit82276e351a
Author: 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 commit4f1fe2b084
Merge:9d6dc627
3cd9f399
Author: quin lynch <lynchquin@gmail.com> Date: Fri Nov 26 11:23:32 2021 -0400 Merge branch 'siscodeorg-commands/validate-get-best-match' into dev commit3cd9f39918
Merge:9d6dc627
adf3a9c4
Author: 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 commitadf3a9c459
Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 26 09:26:53 2021 -0300 Fix incorrect casing on `HandleCommandPipeline` commita92ec56d88
Author: 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 commitd1b31c8f52
Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Thu Nov 25 15:31:48 2021 -0300 Add `MatchResult` commit9d6dc6279d
Author: 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) commit10afd96e6e
Author: 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 commit143ca6db43
Author: 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) commitd5f5ae132c
Author: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu Nov 25 18:22:50 2021 +0300 fix sharded client current user (#1947) commitb5c150dc16
Author: 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 commitbc440abd44
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed Nov 24 12:52:55 2021 -0400 Implement multi-file upload to webhooks (#1942) commitf7a07aec02
Author: Paulo <pnmanjos@hotmail.com> Date: Wed Nov 24 09:57:06 2021 -0300 Add default nullable enum typereader (#1518) commit6abdfcbf87
Author: 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 commite0dbe7c695
Author: 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> commit3cb662ff7a
Author: d4n <dan3436@hotmail.com> Date: Tue Nov 23 10:49:31 2021 -0500 Add null check to AllowedMentions.ToModel() (#1865) commit900c1f4385
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Nov 23 11:46:18 2021 -0400 Fix emoto try parse (#1941) commit933ea42eaa
Author: 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: commitdca41a348e
Author: quin lynch <lynchquin@gmail.com> Date: Thu Sep 23 07:02:19 2021 -0300 Autocomplete commands * meta: xml. closes #171 * Revert user agent and $device to dnet * meta: bump version * meta: bump vers * Fix sticker args * Grammer fix (#179) * Made IVoiceChannel mentionable * Embeds array for send message async (#181) * meta: bump version * meta: bump vers * Fix sticker args * Grammer fix (#179) * Added embeds for SendMessageAsync * [JsonProperty("embed")] forgot to remove this public Optional<Embed> Embed { get; set; } * It has been done as requested. * Changed the old way of handeling single embeds * Moved embeds param and added options param * xmls Co-authored-by: quin lynch <lynchquin@gmail.com> * Fix thread permissions (#183) * Update GuildPermissionsTests.cs * Update GuildPermissions.cs * Use compound assignment (#186) * Used compound assignment * -||- * -||- * Remove unnecessary suppression (#188) * Inlined variable declarations (#185) * Fixed some warnings (#184) * Fixed some warnings * Another fixed warning * Changed the SSendFileAsync to SendFileAsync * Removed para AlwaysAcknowledgeInteractions * Moved it back to the previous version * Added periods to the end like quin requested!! :(( Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> * Object initialization can be simplified fixed (#189) * Conditional-expression-simplification (#193) * Capitlazation fixes (#192) * Removed-this. (#191) * Use 'switch' expression (#187) * Use 'switch' expression * Reverted it to the old switch case * Fixed-compiler-error (#194) * Submitting updates to include new permissions. (#195) * Submitting updates to include new permissions. * Make old permissions obsolete and update tests Co-authored-by: quin lynch <lynchquin@gmail.com> * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Add support for long in autocomplete option * Add support for sending files with multiple embeds (#196) * Add support for sending files with multiple embeds * Simplify prepending single embed to embed array * Consistency for embeds endpoints (#197) * Changed the way of handling prepending of embeds. For consistency. * reformatted the summary * Revert pipeline * Fix duplicate merge conflicts * Changed minimum slash command name length to 1 per Discord API docs (#198) * Channel endpoints requirements correction (#199) * Added some requirements to channels for topic * Changed check from NotNullOrEmpty to NotNullOrEmpty * Added some requirements to channels for name Preconditions.LessThan * Formatting of file * Added restriction for description not being null (#200) * Update azure-pipelines.yml * Update deploy.yml * Remove version tag from proj * Update deploy.yml * Removed versions from project files * Removed style of the nuget badge and added logo (#201) The style was not properly added to it and the plastic version does not look good with the discord badge. I thought it would look better with a logo * Fix Type not being set in SocketApplicationCommand * Remove useless GuildId property * meta: update XML * Add Autocomplete to SlashCommandOptionBuilder * Added autocomplete in SlashCommandOptionBuilder. (#206) Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Fix duplicate autocomplete * Fix #208 * Fix sub commands being interpreted as a parameter for autocomplete * Fix exposed optional * Support the discord:// protocol in buttons (#207) * Update UrlValidation.cs * Update ComponentBuilder.cs * Add docs and better error messages. * Fix wonky intentation * Add competing activity status type (#205) * Update GuildPermissionsTests.cs * Update GuildPermissions.cs * Add competing status type * Add Icons to IRole (#204) * Added icon field to IRole * Added GetGuildRoleIconUrl() * Added Clean Content Function (#174) * Added Clean Content Function * Fixed Spelling problems and bad var handling * Add StripMarkDown Method * Clean Content Expanded (#212) * Implement CleanContent In IMessage & RestMessage * Update Spelling and Documentation * Add SanatizeMessage to MessageHelper and Refactor Rest and Socket Message * Add event for autocomplete interaction (#214) * Spelling corrections (#215) * Remove null collections * Followup with file async warnings (#216) * Changed from NotNullOrWhitespace to NotNullOrEmpty * Added NotNullOrEmpty on filename * Added system to interpret from the path * Added a check for if it contains a period * It has been done, how ever it will break stuff * Changed to use ??= how ever still added error check * Added space under check * Changed from with a period to valid file extension * Added checks for SendFileAsync * Removed filename != null && * Add channel types in application command options. (#217) * add channel types in application command options * Indent Docs * Stage instance audit logs as well as thread audit log type * Update azure-pipelines.yml * Update azure-pipelines.yml * Fix system messages not including mentioned users. Added ContextMenuCommand message type * Remove file extension check (#218) * Fix NRE in modify guild channel * Fix 429's not being accounted for in ratelimit updates * meta: add net5 framework Co-Authored-By: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> * Proper doc logos (#221) * Update GuildPermissionsTests.cs * Update GuildPermissions.cs * Add competing activity status type * logo changes * logo text as path * add missing logo * Update package logo and favicon * Update docfx references * Remove XML files and use original pipeline format * Remove console writeline * Remove Console.WriteLine * Remove useless log * Rename Available sticker field to IsAvailable * Rename Available to IsAvailable in stickers * Add summary indent for role members * Add summary indent to SocketInvite * Rename DefaultPermission to IsDefaultPermission * Rename Default to IsDefault and Required to IsRequired in IApplicationCommandOption * Rename Default and Required to IsDefault and IsRequired in IApplicationCommandOption. Rename DefaultPermission to IsDefaultPermission in IApplicationCommand * Remove extra white spaces * Renamed Joined, Archived, and Locked to HasJoined, IsArchived, and IsLocked * Rename Live and DiscoverableDisabled to IsDiscoverableDisabled and IsLive in IStageChannel * Remove newline * Add indent to summaries * Remove unnecessary json serializer field * Fix ToEntity for roletags incorrectly using IsPremiumSubscriber * Update RestChannel for new channel types * Fix different rest channels not deserializing properly * fully qualify internal for UrlValidation and add indent to summary * Add missing periods to InteractionResponseType * Fix summary in IApplicationCommandOptionChoice * Update IApplicationCommandOption summaries * Update IApplicationCommandInteractionDataOption summaries * Update IApplicationCommandInteractionData summaries * Update IApplicationCommand summaries * Update ApplicationCommandType summaries * rename DefaultPermission to IsDefaultPermission in ApplicationCommandProperties * update ApplicationCommandOptionChoiceProperties summaries * Rename Default, Required, and Autocomplete to IsDefault, IsRequired, and IsAutocomplete in ApplicationCommandOptionProperties * Update SlashCommandProperties summaries * update SlashCommandBuilder boolean field names, summaries, and choice parameters * Update SelectMenuOption summaries, Rename Default to IsDefault in SelectMenuOption * update SelectMenuComponent summaries. Rename Disabled to IsDisabled in SelectMenuComponent * update ComponentBuilder summaries and boolean fields. * Update ButtonComponent summaries and boolean fields * update ActionRowComponent summaries * Update UserCommandBuilder * Update MessageCommandBuilder summaries and boolean properties * Update IGuild summary * Update IGuild summaries * Update StagePrivacyLevel summary * update IThreadChannel summaries * Update IStageChannel summaries * Refactor summaries and boolean property names * General cleanup (#223) * General cleanup * Add Async suffix to SendAutocompleteResult * Fix more formatting * Fix unused RequestOptions in GetActiveThreadsAsync * Add message to ArgumentNullException * Ephemeral attachments * Add missing jsonproperty attribute * Add IMessage.Interaction * Update attachment checks for embed urls * meta: bump version * Remove old package configs and update image * Update package logos * Fix logo reference for azure * Deprecate old package definitions in favor for target file * Deprecate old package definitions in favor for target file Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update package ids * Fix url validation * meta: bump version * Fix assignment of UserMentions (#233) * Fix CleanContent (#231) * Fix SocketSlashCommandData access modifier. (#237) Fixes #229 * Update README with better header (#232) * Update README with better header Adds HTML elements that implement the main logo & improve the redirection tag positions. * Resolving border issue in light-mode * Update sponsor section * Implement checks for interaction respond times and multiple interaction responses. closes #236, #235 * Add response check to socket auto complete * meta: bump versions * Fix #239 * meta: bump version * meta: update logo * meta: bump versions * Revert received at time, confirmed by discord staff to be accurate * Merge branch 'release/3.x' of https://github.com/Discord-Net-Labs/Discord.Net-Labs into merger-labs Update requested changes of obsolete and references to labs. Added `Interaction` to `IMessage` Fixed grammar Fixed bugs relating to interactions. * Update docs * Update CHANGELOG.md * meta: docs building * Update docs.yml * Update docs.yml * Fix docfx version * Update docs.yml * Update docs.bat * Rename docs repo for clone * update docfx version * Update docs.bat * Update docfx version * Remove docs from pipeline * FAQ revamped, metadata updated (#241) * FAQ revamped, metadata updated * Update FAQ.md * Update README.md * Docs index improvement * Fix InvalidOperationException in modify channel * feature: guild avatars, closes #238 * feature: modify role icons * meta: changelog * meta: bump version * Update README.md * Fix non value type options not being included in autocomplete * Add new activity flags (#254) * Add new activity flags * Add missing commas * Added support for GUILD_JOIN_REQUEST_DELETE event (#253) Fixes #247 * Adding BotHTTPInteraction user flag (#252) * animated guild banner support (#255) * Docs work (WIP) (#242) * Main page work * Metadata logo dir * More main page edits * Naming change * Dnet guide entries pruned * Add student hub guild directory channel (#256) * animated guild banner support * Add guild directory channel * Fix followup with file overwrite having incorrect parameter locations * Merge labs 3.x * Update GUILD_JOIN_REQUEST_DELETE event * Update head.tmpl.partial * Removed BannerId and AccentColor (#260) * Removed BannerId property, GetBannerURL method, and AccentColor property from IUser and socket entities. * Fixed errors in IUser.cs * Added back summary for GetAvatarUrl method in IUser.cs * Support Guild Boost Progress Bars (#262) * Support Guild Boost Progress Bars * Update SocketChannel.cs * Fix non-optional and unnecessary values. * Spelling * Reordering and consistency. * Remove log for reconnect * Add missing flags to SystemChannelMessageDeny (#267) * Fix labs reference in analyzer project and provider project * Rename new activity flags * Guild feature revamp and smart gateway intent checks * Get thread user implementation * Amend creating slash command guide (#269) * Adding BotHTTPInteraction user flag * Added comments explaining the Global command create stipulations. * Fix numeric type check for options * Add state checking to ConnectionManager.StartAsync (#272) * initial interface changes * Multi file upload + attachment editing * meta: bump versions * Update CHANGELOG.md * Update CHANGELOG.md * Support Min and Max values on ApplicationCommandOptions (#273) * Support Min and Max values on ApplicationCommandOptions * Support decimal min/max values * Docs imrpovments + use ToNullable * Logomark, doc settings edit (#258) * Logomark, doc settings edit * Replace standard logo * Bumping docfx plugins to latest release * Bump version metadata * Logo svg fix * Change default sticker behavior and add AlwaysResolveSticker to the config * Implement rest based interactions. Added ED25519 checks. Updated summaries. * Update package logo * Automatically fix ordering of optional command options (#276) * auto fix optional command option order * clean up indentation * Fix maximum number of Select Menu Options (#282) As of https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure the maximum number of options is 25, not less than 25. Hopefully the change catches all necessary locations * Add voice region to modify voice channels * Update summaries on rest interactions * Interaction Specific Interfaces (#283) * added interaction specific interfaces * fix build error * implement change requests * Update application * Add Guild Scheduled Events (#279) * guild events initial * sharded events * Add new gateway intents and fix bugs * More work on new changes to guild events * Update guild scheduled events * Added events to extended guild and add event start event * Update preconditions * Implement breaking changes guild guild events. Add guild event permissions * Update tests and change privacy level requirements * Update summaries and add docs for guild events * meta: bump version * Increment meta version (#285) * Increment meta version * Update docfx.json * Fix #289 and add configureawaits to rest based interactions * meta: bump version * Add GUILD_SCHEDULED_EVENT_USER_ADD and GUILD_SCHEDULED_EVENT_USER_REMOVE (#287) * Remove newline * Fix autocomplete result value * meta: bump versions * Add `GuildScheduledEventUserAdd` and `GuildScheduledEventUserRemove` to sharded client * Make RestUserCommand public (#292) * Fix Components not showing on FUWF (#288) (#293) Adds Components to Payload JSON Generation * Implement smarter rest resolvable interaction data. Fixes #294 * Add UseInteractionSnowflakeDate to config #286 * Implement Better Discord Errors (#291) * Initial error parsing * Implement better errors * Add missing error codes * Add voice disconnect opcodes * Remove unused class, add summaries to discordjsonerror, and remove public constructor of slash command properties * Add error code summary * Update error message summary * Update src/Discord.Net.Core/DiscordJsonError.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Fix autocomplete result value Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Change the minimum length of slash commands to 1 (#284) * Change the minimum length of slash commands to 1. This is the correct value according to the docs and it has been changed after user feedback. * Fix the limit in 3 other places Co-authored-by: quin lynch <lynchquin@gmail.com> * Add new thread creation properties * Add role emoji. Fixes #295 * Fix mocked text channel * Fix precondition checks. Closes #281 * Initial fix (#297) * meta: bump version * Update from release/3.x * Remove more labs references * Remove doc file for Discord.Net.Analyzers Co-authored-by: Simon Hjorthøj <sh2@live.dk> Co-authored-by: drobbins329 <drobbins329@gmail.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: d4n3436 <dan3436@hotmail.com> Co-authored-by: Will <WilliamWelsh@users.noreply.github.com> Co-authored-by: Eugene Garbuzov <kkxo.mail@gmail.com> Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Co-authored-by: Emily <89871431+emillly-b@users.noreply.github.com> Co-authored-by: marens101 <marens101@gmail.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: Bill <billchirico@gmail.com> Co-authored-by: Liege72 <65319395+Liege72@users.noreply.github.com> Co-authored-by: Floowey <floowey@gmx.at> Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: exsersewo <exsersewo@systemexit.co.uk> Co-authored-by: Dennis Fischer <fischer_dennis@live.de> commit3395700720
Author: Nikon <47792796+INikonI@users.noreply.github.com> Date: Mon Aug 23 02:00:18 2021 +0500 feature: IVoiceChannel implements IMentionable (#1896) commit41b4686b5e
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Aug 3 20:43:10 2021 -0300 Update README.md commit5fc31451a1
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue Aug 3 20:28:15 2021 -0300 Update README.md commit56d16397f7
Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 18:42:23 2020 -0300 Fixes Azure linux build failing due to a CS8652. commitc455b50331
Author: roridev <t3ctotalmenterandom1@outlook.com> Date: Fri Nov 27 14:10:39 2020 -0300 Make use of new ValidateAndGetBestMatch api on ExecuteAsync commit7955a09090
Author: 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. commit574b503e9e
Author: 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 | |