Browse Source

Update from Discord .Net Labs 3.4.8 (#1958)

* 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

* 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 Console.WriteLine

* Rename Available to IsAvailable in stickers

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

* Fix different rest channels not deserializing properly

* 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

* 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

* 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)

* 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 property names and summaries

* Add Audit Log Data classes for Threads (#301)

* Add ThreadDeleteAuditLogData

* Add ThreadCreateAuditLogData

* Fix ThreadCreateAuditLogData using old instead of new value

* Create ThreadInfo Class

* Fix Thread not being a property

* Add ThreadUpdateAuditLogData

* Cleanup usings

* Add RateLimit to ThreadAuditLogData classese

Co-authored-by: Playwo <eliaswolf2001@t-online.de>

* Fix #300

* Interaction Command Service (#52)

* init

* attribute rename

* added docs

* Revert "added docs"

This reverts commit 30aa0c4ef7.

* added basic docs

* Switched to nested modules  for method grouping, changed command traversal method

* interface now declares the helper methods

* added new method with predicate parameter

* added config option for deleting the "thinking" state of unhandled commands

* Slash Module Base now exposes helper methods for interacting with the underlying Interaction

* Revert "interface now declares the helper methods"

This reverts commit 541b0be935.

* IDiscordInteraction now declares the helper methods

* new cancelable wait interaction method

* added support for user created command types

* added option type 'number', added write method to typereaders

* added enum and timespan typereaders

* revert

* added interface method declarations

* inline docs

* revert interface changes

* current user id assignment in sharded client

* added wildcards to interactions, tweaks

* tweaks on interaction wild card pattern

* Pre-app menu

* fixed CurrentUserId and added application command events

* made event listener persistent

* Sharded Client Application Command Events and CurrentUserId Issue (#105)

* added interface method declarations

* inline docs

* current user id assignment in sharded client

* fixed CurrentUserId and added application command events

* made event listener persistent

* removed option type converter, task offloaded to typereaders

* added "deleteOGResponse" method to module base

* Upstream fetch for Discord-Net-Labs/release/3.x

* solved merge conflicts

* removed merge artifacts

* added new Context Command attributes

* added Conxtext Command info classes and changed the naming scheme for the existing classes

* added IgnoreGroupNames prop to command attributes

* added ContextCommand builder

* moved command builders to internal

* added ContextCommand methods to the command service

* command service now uses InteractionHelper to register commands

* bug fixes and refactorings

* docs update

* added inline docs to public members

* added inline docs

* added method name property to command infos

* added inline docs

* changed the execution callback to a declared delegate

* createInstance delegate is now created only once per module

* declared the ExecuteCallback delegate

* introduced a way to modify the command permissions

* changed method names

* added optional compiled lambda module builder

* added the missing sync execution option

* moved run mode selection to the base class

* info class refactorings

* switched to compiled lambda based method invoke

* command refactorings

* added docs

* removed untended class

* bug fixes

* minor refactorings

* reflection changes

* bug fix for interaction parameters

* commands and modules now groups the preconditons on building

* added default permission to context commands

* added DontAutoRegister attribute

* renamed TypeReader to TypeConverter

* added docs to TypeConverterResult, made ISlashModuleBase public

* namespace and project change

* added inline docs file

* renamed ExecuteComponentCommand method

* added scoped service support to the dependency injection model

* fixed premature disposal of scoped services

* extended the scope to cover the precondition checking methods

* removed slash command related preconditions from core lib

* added sample application

* precondition checks are now executed according to the command RunMode

* reverting the conflicting changes

* reverted SocketInteraction

* reverting more conflicts

* added indentations to inline docs

* implemented change requests

* updated the sample app

* moved builders to public

* added indentations to typeconverter docs

* renamed old componentCommandExecuted event

* bug fix for generic typeconverters

* Revert "bug fix for generic typeconverters"

This reverts commit fcc61957de.

* bug fix for context commands

* code cleanup

* removed public module build method

* modev OnModuleBuilding execution inside the module build method

* added try-catch blocks encapsulating arg generation

* fixed parameter preconditions not raising commandExecuted event

* removed OnModuleBuilding execution from ModuleClassBuilder

* removed setters from Precondition ErrorMessages

* added methods to interaction service for creating user defined modules

* added IParameterInfo parameter to TypeConverter.Write

* changed the target frameworks

* DefaultValueConverter bug fix

* GenerateArgs refactorings

* WaitForMessageComponent now relies message id

* added ChannelTypes support

* added ChannelTypes support

* fix build error for new lib version

* added ToString method to CommandInfo

* added ToString method to CommandInfo

* Fix index out of bounds error for new non-null slash command option collection

* enum converter rework

* added user extendable types to command context and module base

* added regex anchors to ensure pattern matches the whole string

* incomplete guides

* add missing ignoreGroupNames assignment for ComponentInteraction

* typeconverters now seperate pascal casing parameter names

* fix missing IServiceScopefactory ?

* Revert "typeconverters now seperate pascal casing parameter names"

This reverts commit 141300f3d2.

* moved the option name pascal casing seperator to RestUtils

* fix component command arg generation

* removed , from default command delimiters

* updated the regex to match every non whitespace value

* added Autocomplete interaction support and updated the regex to match every non whitespace value

* replaced the posix class with range exps in pascal casing seperator

* added inline docs to autocompleter related classes

* added regex metacharacter escape to wildcard patterns

* added null check to Regex EscapeExcluding

* added .net5.0 support and updated the project package

* added .net5.0 support and updated the project package

* add dependency injection to autocompleters

* add net6.0

* bump versions

* bug fix: pascal casing parameters are not assigned

* rework autocomplete commands to accept command and parameter names seperatly

* rename *InteractionCommandContext to *InteractionContext

* add max-min value support to number type slash command options

* add hide attribute to deafult enum converter

* add inline docs

* guides update: min/max value and autocomplete interactions

* remove net6.0 support

* add inline doc to Config.EnableAutocompleters

* add autocompleters guide

* added attribute usage to min/max value

* implement rest based interactions

* add handling logic for rest based interactions

* update default TypeConverters to accommodate rest based interactions

* added interaction specific interfaces

* fix build error

* implement change requests

* bump metapackage version

* replace concrete interface types with interfaces in command execution logic

* fix min/max value attribute target

* add rest specific interaction module for creating interaction responses for rest based interactions within the module

* update rest callback to accept an interaction context parameter

* clean up RestResponseCallback implementation artifacts

* fix command registration bug when using the sharded socket client

* update docs

* fix build errors

* fix slash command depth check

* implement requested changes

* fix build error

* the grand finale

* the grand finale

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

* Remove XML doc

* Add Interactions service to azure build

* Add DocFX refs to interaction framework docs

* meta: bump docfx version

* meta: bump versions

* Remove versioning metadata in csproj

* Fix user command mismatch in docs

* Fix parameter in message commands

* Fix SocketVoiceChannel options are created as generic mentionables in Interaction service (#308)

* added interaction specific interfaces

* fix build error

* implement change requests

* add autocomplete respond method to IAutocompleteInteraction

* fix sharded client current user

* fix generic typeconverter picking priority

* Revert "fix sharded client current user"

This reverts commit a9c15ffd6a.

* Revert "add autocomplete respond method to IAutocompleteInteraction"

This reverts commit f2fc50f1f1.

* meta: bump version

* Improve the `GuildFeatures` converter (#311)

* Fix Message/User commands are not being executed when their name have spaces on it  (#310)

* added interaction specific interfaces

* fix build error

* implement change requests

* add autocomplete respond method to IAutocompleteInteraction

* fix sharded client current user

* fix generic typeconverter picking priority

* Revert "fix sharded client current user"

This reverts commit a9c15ffd6a.

* Revert "add autocomplete respond method to IAutocompleteInteraction"

This reverts commit f2fc50f1f1.

* fix command parsing for names with spaces

* meta: bump version

* fix minor spelling mistake

* add missing $ on docs

Co-Authored-By: Liege72 <65319395+Liege72@users.noreply.github.com>

* Squashed commit of the following:

commit ff0bbbd4d3
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

commit 19a66bf878
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>

commit b9274d115d
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>

commit 51e06e9ce1
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)

commit 82276e351a
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

commit 4f1fe2b084
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

commit 3cd9f39918
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

commit adf3a9c459
Author: roridev <t3ctotalmenterandom1@outlook.com>
Date:   Fri Nov 26 09:26:53 2021 -0300

    Fix incorrect casing on `HandleCommandPipeline`

commit a92ec56d88
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

commit d1b31c8f52
Author: roridev <t3ctotalmenterandom1@outlook.com>
Date:   Thu Nov 25 15:31:48 2021 -0300

    Add `MatchResult`

commit 9d6dc6279d
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)

commit 10afd96e6e
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

commit 143ca6db43
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)

commit d5f5ae132c
Author: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date:   Thu Nov 25 18:22:50 2021 +0300

    fix sharded client current user (#1947)

commit b5c150dc16
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

commit bc440abd44
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)

commit f7a07aec02
Author: Paulo <pnmanjos@hotmail.com>
Date:   Wed Nov 24 09:57:06 2021 -0300

    Add default nullable enum typereader (#1518)

commit 6abdfcbf87
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

commit e0dbe7c695
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>

commit 3cb662ff7a
Author: d4n <dan3436@hotmail.com>
Date:   Tue Nov 23 10:49:31 2021 -0500

    Add null check to AllowedMentions.ToModel() (#1865)

commit 900c1f4385
Author: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Date:   Tue Nov 23 11:46:18 2021 -0400

    Fix emoto try parse (#1941)

commit 933ea42eaa
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:

    commit dca41a348e
    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>

commit 3395700720
Author: Nikon <47792796+INikonI@users.noreply.github.com>
Date:   Mon Aug 23 02:00:18 2021 +0500

    feature: IVoiceChannel implements IMentionable (#1896)

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

    Update README.md

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

    Update README.md

commit 56d16397f7
Author: roridev <t3ctotalmenterandom1@outlook.com>
Date:   Fri Nov 27 18:42:23 2020 -0300

    Fixes Azure linux build failing due to a CS8652.

commit c455b50331
Author: roridev <t3ctotalmenterandom1@outlook.com>
Date:   Fri Nov 27 14:10:39 2020 -0300

    Make use of new ValidateAndGetBestMatch api on ExecuteAsync

commit 7955a09090
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.

commit 574b503e9e
Author: roridev <t3ctotalmenterandom1@outlook.com>
Date:   Fri Nov 27 13:38:00 2020 -0300

    Moves CalculateScore function to outer scope.

* Update from Discord.Net dev - fix merge conflicts

* meta: .net6 support

* meta: pipelines use .net 6

* meta: bump tests framework versions

* Invoke SlashCommandExecuted event on failed type conversion (#314)

* added interaction specific interfaces

* fix build error

* implement change requests

* add autocomplete respond method to IAutocompleteInteraction

* fix sharded client current user

* fix generic typeconverter picking priority

* Revert "fix sharded client current user"

This reverts commit a9c15ffd6a.

* Revert "add autocomplete respond method to IAutocompleteInteraction"

This reverts commit f2fc50f1f1.

* fix command parsing for names with spaces

* add SlashCommandExecuted event invoke to failed type conversion

* update interactions sample app

* Revert "update interactions sample app"

This reverts commit 6ac8cd0da6.

* meta: bump to exclusive net.5 and net6 versions

Co-Authored-By: JT <Hawxy@users.noreply.github.com>

* meta: bump versions

* meta: add net461 support back

* Add System.Collections.Immutable back to core

* Fix presence NRE

* meta: bump versions

* Fix presence NRE again...

* meta: bump version

* Fix current user presence

* meta: bump version

* Fix NRE on service providerless command execution (#322)


* fix not set to an instance of an object exception for service providerless command execution

* add methods for manually registering global comands (#325)

* fix dependency injection link in docs (#326)

* Add back netstandard2.0 / 2.1. Closes #324

* meta: bump version

* Add not supported exception on news channels when creating threads, #296

* Revert thread block for news channel. Add check for news channel

Co-Authored-By: Nova Fox <novamaday@gmail.com>

* Make autocomplete log virtual (#328)

* Fix #300 again

* Update GUILD_SCHEDULED_EVENT_CREATE (#330)

Changed _guildScheduledEventCancelled to _guildScheduledEventCreated in GUILD_SCHEDULED_EVENT_CREATE

* correct the number of allowed autocomplete choices (#333)

* correct the number of allowed autocomplete choices (#334)

* Add FollowupWithFileAsync to IDiscordInteraction (#336)

* Add uppercase character check to SlashCommandBuilder and ApplicationCommandOptionProperties (#339)

* Make IModuleBase and IInteractionModuleBase public (#341)

* fix command validation (#335)

* fix autocomplete command traversal and use IList<string> in command map instead of stirng[] (#342)

* Refactor Interactions (#340)

* Refactor Interactions

* Remove ApplicationCommandException

* Fix Module Preconditions (#343)

* fix module preconditions

* fix module preconditions

* meta: bump version

* Update autocomplete docs

* Initial preps

* Fix #347

This comit makes `Content` optional for webhook execution. This comit also adds null checks to content when creating the api args to properly specify the optional struct to the model. This is done so the message entity doesn't try to parse a null string.

* Fix merge errors

* meta: net5 and 6 support

* Update README.md

Co-authored-by: Simon Hjorthøj <sh2@live.dk>
Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>
Co-authored-by: drobbins329 <drobbins329@gmail.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>
Co-authored-by: PoolPirate <94938310+PoolPirate@users.noreply.github.com>
Co-authored-by: Playwo <eliaswolf2001@t-online.de>
Co-authored-by: JT <Hawxy@users.noreply.github.com>
Co-authored-by: Nova Fox <novamaday@gmail.com>
Co-authored-by: Daan van den Hoek <28300783+daanvandenhoek@users.noreply.github.com>
Co-authored-by: nev-r <gh@f-m.fm>
tags/3.0.0
Quin Lynch GitHub 3 years ago
parent
commit
aa6bb5e293
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 7084 additions and 58 deletions
  1. +47
    -34
      Discord.Net.sln
  2. +1
    -1
      README.md
  3. +7
    -0
      azure/build.yml
  4. +1
    -0
      azure/deploy.yml
  5. +27
    -0
      docs/guides/interactions_framework/autocompleters.md
  6. +27
    -0
      docs/guides/interactions_framework/dependency-injection.md
  7. +360
    -0
      docs/guides/interactions_framework/intro.md
  8. +73
    -0
      docs/guides/interactions_framework/post_execution.md
  9. +8
    -0
      docs/guides/interactions_framework/preconditions.md
  10. +130
    -0
      docs/guides/interactions_framework/typeconverters.md
  11. +8
    -0
      docs/guides/toc.yml
  12. +1
    -1
      samples/01_basic_ping_bot/01_basic_ping_bot.csproj
  13. +1
    -1
      samples/02_commands_framework/02_commands_framework.csproj
  14. +1
    -1
      samples/03_sharded_client/03_sharded_client.csproj
  15. +25
    -0
      samples/04_interactions_framework/04_interactions_framework.csproj
  16. +146
    -0
      samples/04_interactions_framework/CommandHandler.cs
  17. +16
    -0
      samples/04_interactions_framework/ExampleEnum.cs
  18. +93
    -0
      samples/04_interactions_framework/Modules/UtilityModule.cs
  19. +85
    -0
      samples/04_interactions_framework/Program.cs
  20. +27
    -0
      samples/04_interactions_framework/RequireOwnerAttribute.cs
  21. +1
    -1
      samples/idn/idn.csproj
  22. +2
    -2
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  23. +22
    -2
      src/Discord.Net.Commands/IModuleBase.cs
  24. +1
    -0
      src/Discord.Net.Core/AssemblyInfo.cs
  25. +3
    -3
      src/Discord.Net.Core/Discord.Net.Core.csproj
  26. +4
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
  27. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteraction.cs
  28. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteraction.cs
  29. +19
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteraction.cs
  30. +99
    -8
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  31. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/ISlashCommandInteraction.cs
  32. +3
    -0
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  33. +36
    -0
      src/Discord.Net.Core/Interactions/IInteractionContext.cs
  34. +1
    -1
      src/Discord.Net.Examples/Discord.Net.Examples.csproj
  35. +36
    -0
      src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs
  36. +30
    -0
      src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs
  37. +64
    -0
      src/Discord.Net.Interactions/Attributes/ChoiceAttribute.cs
  38. +39
    -0
      src/Discord.Net.Interactions/Attributes/Commands/AutocompleteCommandAttribute.cs
  39. +44
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
  40. +36
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ContextCommandAttribute.cs
  41. +29
    -0
      src/Discord.Net.Interactions/Attributes/Commands/MessageCommandAttribute.cs
  42. +49
    -0
      src/Discord.Net.Interactions/Attributes/Commands/SlashCommandAttribute.cs
  43. +29
    -0
      src/Discord.Net.Interactions/Attributes/Commands/UserCommandAttribute.cs
  44. +25
    -0
      src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
  45. +13
    -0
      src/Discord.Net.Interactions/Attributes/DontAutoRegisterAttribute.cs
  46. +35
    -0
      src/Discord.Net.Interactions/Attributes/GroupAttribute.cs
  47. +25
    -0
      src/Discord.Net.Interactions/Attributes/MaxValueAttribute.cs
  48. +25
    -0
      src/Discord.Net.Interactions/Attributes/MinValueAttribute.cs
  49. +34
    -0
      src/Discord.Net.Interactions/Attributes/ParameterPreconditionAttribute.cs
  50. +42
    -0
      src/Discord.Net.Interactions/Attributes/PreconditionAttribute.cs
  51. +32
    -0
      src/Discord.Net.Interactions/Attributes/SummaryAttribute.cs
  52. +102
    -0
      src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs
  53. +43
    -0
      src/Discord.Net.Interactions/AutocompleteHandlers/IAutocompleteHandler.cs
  54. +76
    -0
      src/Discord.Net.Interactions/Builders/Commands/AutocompleteCommandBuilder.cs
  55. +174
    -0
      src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
  56. +40
    -0
      src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs
  57. +76
    -0
      src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
  58. +111
    -0
      src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
  59. +76
    -0
      src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
  60. +279
    -0
      src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
  61. +469
    -0
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  62. +25
    -0
      src/Discord.Net.Interactions/Builders/Parameters/CommandParameterBuilder.cs
  63. +105
    -0
      src/Discord.Net.Interactions/Builders/Parameters/IParameterBuilder.cs
  64. +162
    -0
      src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs
  65. +178
    -0
      src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs
  66. +25
    -0
      src/Discord.Net.Interactions/Discord.Net.Interactions.csproj
  67. +24
    -0
      src/Discord.Net.Interactions/Entities/ParameterChoice.cs
  68. +21
    -0
      src/Discord.Net.Interactions/Entities/SlashCommandChoiceType.cs
  69. +34
    -0
      src/Discord.Net.Interactions/Extensions/AutocompleteOptionComparer.cs
  70. +53
    -0
      src/Discord.Net.Interactions/Extensions/WebSocketExtensions.cs
  71. +33
    -0
      src/Discord.Net.Interactions/IInteractionModuleBase.cs
  72. +89
    -0
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  73. +247
    -0
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  74. +132
    -0
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  75. +51
    -0
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
  76. +41
    -0
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs
  77. +41
    -0
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs
  78. +112
    -0
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  79. +23
    -0
      src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
  80. +83
    -0
      src/Discord.Net.Interactions/Info/ICommandInfo.cs
  81. +57
    -0
      src/Discord.Net.Interactions/Info/IParameterInfo.cs
  82. +217
    -0
      src/Discord.Net.Interactions/Info/ModuleInfo.cs
  83. +62
    -0
      src/Discord.Net.Interactions/Info/Parameters/CommandParameterInfo.cs
  84. +72
    -0
      src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs
  85. +43
    -0
      src/Discord.Net.Interactions/InteractionCommandError.cs
  86. +34
    -0
      src/Discord.Net.Interactions/InteractionContext.cs
  87. +68
    -0
      src/Discord.Net.Interactions/InteractionModuleBase.cs
  88. +949
    -0
      src/Discord.Net.Interactions/InteractionService.cs
  89. +70
    -0
      src/Discord.Net.Interactions/InteractionServiceConfig.cs
  90. +87
    -0
      src/Discord.Net.Interactions/Map/CommandMap.cs
  91. +113
    -0
      src/Discord.Net.Interactions/Map/CommandMapNode.cs
  92. +59
    -0
      src/Discord.Net.Interactions/RestInteractionModuleBase.cs
  93. +99
    -0
      src/Discord.Net.Interactions/Results/AutocompletionResult.cs
  94. +86
    -0
      src/Discord.Net.Interactions/Results/ExecuteResult.cs
  95. +33
    -0
      src/Discord.Net.Interactions/Results/IResult.cs
  96. +51
    -0
      src/Discord.Net.Interactions/Results/PreconditionGroupResult.cs
  97. +59
    -0
      src/Discord.Net.Interactions/Results/PreconditionResult.cs
  98. +37
    -0
      src/Discord.Net.Interactions/Results/RuntimeResult.cs
  99. +37
    -0
      src/Discord.Net.Interactions/Results/SearchResult.cs
  100. +61
    -0
      src/Discord.Net.Interactions/Results/TypeConverterResult.cs

+ 47
- 34
Discord.Net.sln View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28407.52
# Visual Studio Version 17
VisualStudioVersion = 17.1.31903.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}"
EndProject
@@ -12,16 +12,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.WS4Net", "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj", "{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}"
@@ -42,6 +36,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Interactions", "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj", "{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "04_interactions_framework", "samples\04_interactions_framework\04_interactions_framework.csproj", "{A23E46D2-1610-4AE5-820F-422D34810887}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -100,18 +100,6 @@ Global
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.Build.0 = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -124,18 +112,6 @@ Global
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.ActiveCfg = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.Build.0 = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.ActiveCfg = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.Build.0 = Debug|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.Build.0 = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.ActiveCfg = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -232,6 +208,42 @@ Global
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|x64.ActiveCfg = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|x64.Build.0 = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|x86.ActiveCfg = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Debug|x86.Build.0 = Debug|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|Any CPU.Build.0 = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|x64.ActiveCfg = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|x64.Build.0 = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|x86.ActiveCfg = Release|Any CPU
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9}.Release|x86.Build.0 = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|x64.ActiveCfg = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|x64.Build.0 = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|x86.ActiveCfg = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Debug|x86.Build.0 = Debug|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|Any CPU.Build.0 = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|x64.ActiveCfg = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|x64.Build.0 = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|x86.ActiveCfg = Release|Any CPU
{A23E46D2-1610-4AE5-820F-422D34810887}.Release|x86.Build.0 = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|x64.ActiveCfg = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|x64.Build.0 = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|x86.ActiveCfg = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Debug|x86.Build.0 = Debug|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|Any CPU.Build.0 = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|x64.ActiveCfg = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|x64.Build.0 = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|x86.ActiveCfg = Release|Any CPU
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -240,9 +252,7 @@ Global
{BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
@@ -251,6 +261,9 @@ Global
{FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71}
{47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{137DB209-B357-4EE8-A6EE-4B6127F6DEE9} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{A23E46D2-1610-4AE5-820F-422D34810887} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{24C231FD-8CF3-444A-9E7C-45C18BAD4A0D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 1
- 1
README.md View File

@@ -64,4 +64,4 @@ Due to the nature of the Discord API, we will oftentimes need to add a property

Furthermore, while we will never break the API (outside of interface changes) on minor builds, we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord. As such, a minor version increment may require you to recompile your code, and dependencies, such as addons, may also need to be recompiled and republished on the newer version. When a binary breaking change is made, the change will be noted in the release notes.

An increment of the MAJOR component indicates that breaking changes have been made to the library; consumers should check the release notes to determine what changes need to be made.
An increment of the MAJOR component indicates that breaking changes have been made to the library; consumers should check the release notes to determine what changes need to be made.

+ 7
- 0
azure/build.yml View File

@@ -1,4 +1,11 @@
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2
inputs:
command: 'restore'


+ 1
- 0
azure/deploy.yml View File

@@ -7,6 +7,7 @@ steps:
dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Pack projects

- task: NuGetCommand@2


+ 27
- 0
docs/guides/interactions_framework/autocompleters.md View File

@@ -0,0 +1,27 @@
---
uid: Guides.InteractionsFramework.Autocompleters
title: Autocompleters
---

# Autocompleters

Autocompleters provide a similar pattern to TypeConverters. Autocompleters are cached, singleton services and they are used by the Interaction Service to handle Autocomplete Interations targeted to a specific Slash Command parameter.

To start using Autocompleters, use the `[AutocompleteAttribute(Type autocompleterType)]` overload of the `[AutocompleteAttribute]`. This will dynamically link the parameter to the Autocompleter type.

## Creating Autocompleters

A valid Autocompleter must inherit `AutocompleteHandler` base type and implement all of its abstract methods.

### GenerateSuggestionsAsync()

Interactions Service uses this method to generate a response to a Autocomplete Interaction. This method should return `AutocompletionResult.FromSuccess(IEnumerable<AutocompleteResult>)` to display parameter sugesstions to the user. If there are no suggestions to be presented to the user, you have two options:

1. Returning the parameterless `AutocompletionResult.FromSuccess()` will display "No options match your search." message to the user.
2. Returning `AutocompleteResult.FromError()` will make the Interaction Service not respond to the interation, consequently displaying the user "Loading options failed." message.

## Resolving Autocompleter Dependencies

Autocompleter dependencies are resolved using the same dependency injection pattern as the Interaction Modules. Property injection and constructor injection are both valid ways to get service dependencies.

Because Autocompleters are constructed at service startup, class dependencies are resolved only once. If you need to access per-request dependencies you can use the IServiceProvider parameter of the `GenerateSuggestionsAsync()` method.

+ 27
- 0
docs/guides/interactions_framework/dependency-injection.md View File

@@ -0,0 +1,27 @@
---
uid: Guides.InteractionsFramework.DependencyInjection
title: Dependency Injection
---

# Dependency Injection

Interaction Service uses dependency injection to perform most of its operations. This way, you can access service dependencies throughout the framework.

## Setup

1. Create a `Microsoft.Extensions.DependencyInjection.ServiceCollection`.
2. Add the dependencies you wish to use in the modules.
3. Build a `IServiceProvider` using the `BuildServiceProvider()` method of the `ServiceCollection`.
4. Pass the `IServiceProvider` to `AddModulesAsync()`, `AddModuleAsync()` and `ExecuteAsync()` methods.

## Accessing the Dependencies

Services of a `IServiceProvider` can be accessed using *Contructor Injection* and *Property Injection*.

Interaction Service will populate the constructor parameters using the provided `IServiceProvider`. Any public settable class Property will also be populated in the same manner.

## Service Scopes

Interaction Service has built-in support for scoped service types. Scoped lifetime services are instantiated once per command execution. Including the Preconditon checks, every module operation is executed within a single service scope (which is sepearate from the global service scope).

> For more in-depth information about service lifetimes check out [Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0#service-lifetimes-1).

+ 360
- 0
docs/guides/interactions_framework/intro.md View File

@@ -0,0 +1,360 @@
---
uid: Guides.InteractionsFramework.Intro
title: Introduction to the Interaction Framework
---

# Getting Started

Interaction Service provides an attribute based framework for creating Discord Interaction handlers.

To start using the Interaction Service, you need to create a service instance. Optionally you can provide the `InterctionService` constructor with a `InteractionServiceConfig` to change the services behaviour to suit your needs.

```csharp
...

var commands = new InteractionService(discord);

...
```

## Modules

Attribute based Interaction handlers must be defined within a command module class. Command modules are responsible for executing the Interaction handlers and providing them with the necessary execution info and helper functions.

Command modules are transient objects. A new module instance is created before a command execution starts then it will be disposed right after the method returns.

Every module class must:

- be public
- inherit `InteractionModuleBase`

Optionally you can override the included :

- OnModuleBuilding (executed after the module is built)
- BeforeExecute (executed before a command execution starts)
- AfterExecute (executed after a command execution concludes)

methods to configure the modules behaviour.

Every command module exposes a set of helper methods, namely:

- `RespondAsync()` => Respond to the interaction
- `FollowupAsync()` => Create a followup message for an interaction
- `ReplyAsync()` => Send a message to the origin channel of the interaction
- `DeleteOriginalResponseAsync()` => Delete the original interaction response

## Commands

Valid **Interaction Commands** must comply with the following requirements:

| | return type | max parameter count | allowed parameter types | attribute |
|-------------------------------|------------------------------|---------------------|-------------------------------|--------------------------|
|[Slash Command](#slash-commands)| `Task`/`Task<RuntimeResult>` | 25 | any* | `[SlashCommand]` |
|[User Command](#user-commands) | `Task`/`Task<RuntimeResult>` | 1 | Implementations of `IUser` | `[UserCommand]` |
|[Message Command](#message-commands)| `Task`/`Task<RuntimeResult>` | 1 | Implementations of `IMessage` | `[MessageCommand]` |
|[Component Interaction Command](#component-interaction-commands)| `Task`/`Task<RuntimeResult>` | inf | `string` or `string[]` | `[ComponentInteraction]` |
|[Autocomplete Command](#autocomplete-commands)| `Task`/`Task<RuntimeResult>` | - | - | `[AutocompleteCommand]`|

> [!NOTE]
> a `TypeConverter` that is capable of parsing type in question must be registered to the `InteractionService` instance.

You should avoid using long running code in your command module. Depending on your setup, long running code may block the Gateway thread of your bot, interrupting its connection to Discord.

### Slash Commands

Slash Commands are created using the `[SlashCommandAttribute]`. Every Slash Command must declare a name and a description. You can check Discords **Application Command Naming Guidelines** [here](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming).

```csharp
[SlashCommand("echo", "Echo an input")]
public async Task Echo(string input)
{
await RespondAsync(input);
}
```

#### Parameters

Slash Commands can have up to 25 method parameters. You must name your parameters in accordance with [Discords Naming Guidelines](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming). Interaction Service also features a pascal casing seperator for formatting parameter names with pascal casing into Discord compliant parameter names('parameterName' => 'parameter-name'). By default, your methods can feature the following parameter types:

- Implementations of `IUser`
- Implementations of `IChannel`*
- Implementations of `IRole`
- Implementations of `IMentionable`
- `string`
- `float`, `double`, `decimal`
- `bool`
- `char`
- `sbyte`, `byte`
- `int16`, `int32`, `int64`
- `uint16`, `uint32`, `uint64`
- `enum` (Values are registered as multiple choice options and are enforced by Discord. Use `[HideAttribute]' on enum values to prevent them from getting registered.)
- `DateTime`
- `TimeSpan`

---

**You can use more specialized implementations of `IChannel` to restrict the allowed channel types for a channel type option.*
| interface | Channel Type |
|---------------------|-------------------------------|
| `IStageChannel` | Stage Channels |
| `IVoiceChannel` | Voice Channels |
| `IDMChannel` | DM Channels |
| `IGroupChannel` | Group Channels |
| `ICategory Channel` | Category Channels |
| `INewsChannel` | News Channels |
| `IThreadChannel` | Public, Private, News Threads |
| `ITextChannel` | Text Channels |

---

##### Optional Parameters

Parameters with default values (ie. `int count = 0`) will be displayed as optional parameters on Discord Client.

##### Parameter Summary

By using the `[SummaryAttribute]` you can customize the displayed name and description of a parameter

```csharp
[Summary(description: "this is a parameter description")] string input
```

##### Parameter Choices

`[ChoiceAttribute]` can be used to add choices to a parameter.

```csharp
[SlashCommand("blep", "Send a random adorable animal photo")]
public async Task Blep([Choice("Dog","dog"), Choice("Cat", "cat"), Choice("Penguin", "penguin")] string animal)
{
...
}
```

In most cases, instead of relying on this attribute, you should use an `Enum` to create multiple choice parameters. Ex.

```csharp
public enum Animal
{
Cat,
Dog,
Penguin
}

[SlashCommand("blep", "Send a random adorable animal photo")]
public async Task Blep(Animal animal)
{
...
}
```

This Slash Command will be displayed exactly the same as the previous example.

##### Channel Types

Channel types for an `IChannel` parameter can also be restricted using the `[ChannelTypesAttribute]`.

```csharp
[SlashCommand("name", "Description")]
public async Task Command([ChannelTypes(ChannelType.Stage, ChannelType.Text)]IChannel channel)
{
...
}
```

In this case, user can only input Stage Channels and Text Channels to this parameter.

##### Autocomplete

You can enable Autocomple Interactions for a Slash Command parameter using the `[AutocompleteAttribute]`. To handle the Autocomplete Interactions raised by this parameter you can either create [Autocomplete Commands](#autocomplete-commands) or you can opt to use the [Autocompleters Pattern](./autocompleters)

##### Min/Max Value

You can specify the permitted max/min value for a number type parameter using the `[MaxValueAttribute]` and `[MinValueAttribute]`.

### User Commands

A valid User Command must have the following structure:

```csharp
[UserCommand("Say Hello")]
public async Task SayHello(IUser user)
{
...
}
```

User commands can only have one parameter and its type must be an implementation of `IUser`.

### Message Commands

A valid Message Command must have the following structure:

```csharp
[MessageCommand("Bookmark")]
public async Task Bookmark(IMessage user)
{
...
}
```

Message commands can only have one parameter and its type must be an implementation of `IMessage`.

### Component Interaction Commands

Component Interaction Commands are used to handle interactions that originate from **Discord Message Component**s. This pattern is particularly useful if you will be reusing a set a **Custom ID**s.

```csharp
[ComponentInteraction("custom_id")]
public async Task RoleSelection()
{
...
}
```

Component Interaction Commands support wild card matching, by default `*` character can be used to create a wild card pattern. Interaction Service will use lazy matching to capture the words corresponding to the wild card character. And the captured words will be passed on to the command method in the same order they were captured.

*Ex.*

If Interaction Service recieves a component interaction with **player:play,rickroll** custom id, `op` will be *play* and `name` will be *rickroll*

```csharp
[ComponentInteraction("player:*,*")]
public async Task Play(string op, string name)
{
...
}
```

You may use as many wild card characters as you want.

#### Select Menus

Unlike button interactions, select menu interactions also contain the values of the selected menu items. In this case, you should structure your method to accept a string array.

```csharp
[ComponentInteraction("role_selection")]
public async Task RoleSelection(string[] selectedRoles)
{
...
}
```

Wild card pattern can also be used to match select menu custom ids but remember that the array containing the select menu values should be the last parameter.

```csharp
[ComponentInteraction("role_selection_*")]
public async Task RoleSelection(string id, string[] selectedRoles)
{
...
}
```

### Autocomplete Commands

Autocomplete commands must be parameterless methods. A valid Autocomplete command must have the following structure:

```csharp
[AutocompleteCommand("command_name", "parameter_name")]
public async Task Autocomplete()
{
IEnumerable<AutocompleteResult> results;

...

await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results);
}
```

Alternatively, you can use the *Autocompleters* to simplify this workflow.

## Interaction Context

Every command module provides its commands with an execution context. This context property includes general information about the underlying interaction that triggered the command execution. The base command context.

You can design your modules to work with different implementation types of `IInteractionContext`. To achieve this, make sure your module classes inherit from the generic variant of the `InteractionModuleBase`.

> Context type must be consistent throughout the project, or you will run into issues during runtime.

Interaction Service ships with 4 different kinds of `InteractionContext`s:

1. InteractionContext: A bare-bones execution context consisting of only implementation netural interfaces
2. SocketInteractionContext: An execution context for use with `DiscordSocketClient`. Socket entities are exposed in this context without the need of casting them.
3. ShardedInteractionContext: `DiscordShardedClient` variant of the `SocketInteractionContext`
4. RestInteractionContext: An execution context designed to be used with a `DiscordRestClient` and webhook based interactions pattern

You can create custom Interaction Contexts by implementing the `IInteracitonContext` interface.

One problem with using the concrete type InteractionContexts is that you cannot access the information that is specific to different interaction types without casting. Concrete type interaction contexts are great for creating shared interaction modules but you can also use the generic variants of the built-in interaction contexts to create interaction specific interaction modules.

Ex.
Message component interactions have access to a special method called `UpdateAsync()` to update the body of the method the interaction originated from. Normally this wouldn't be accessable without casting the `Context.Interaction`.

```csharp
discordClient.ButtonExecuted += async (interaction) =>
{
var ctx = new SocketInteractionContext<SocketMessageComponent>(discordClient, interaction);
await interactionService.ExecuteAsync(ctx, serviceProvider);
};

public class MessageComponentModule : InteractionModuleBase<SocketInteractionContext<SocketMessageComponent>>
{
[ComponentInteraction("custom_id")]
public async Command()
{
Context.Interaction.UpdateAsync(...);
}
}
```

## Loading Modules

Interaction Service can automatically discover and load modules that inherit `InteractionModuleBase` from an `Assembly`. Call `InteractionService.AddModulesAsync()` to use this functionality.

You can also manually add Interaction modules using the `InteractionService.AddModuleAsync()` method by providing the module type you want to load.

## Resolving Module Dependencies

Module dependencies are resolved using the Constructor Injection and Property Injection patterns. Meaning, the constructor parameters and public settable properties of a module will be assigned using the `IServiceProvider`. For more information on dependency injection, check out [Dependency Injection](./dependency-injection.md)

## Module Groups

Module groups allow you to create sub-commands and sub-commands groups. By nesting commands inside a module that is tagged with `[GroupAttribute]` you can create prefixed commands.

Although creating nested module stuctures are allowed, you are not permitted to use more than 2 `[GroupAttribute]`s in module hierarchy.

## Executing Commands

Any of the following socket events can be used to execute commands:

- InteractionCreated
- ButtonExecuted
- SelectMenuExecuted
- AutocompleteExecuted
- UserCommandExecuted
- MessageCommandExecuted

Commands can be either executed on the gateway thread or on a seperate thread from the thread pool. This behaviour can be configured by changing the *RunMode* property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute.

You can also configure the way `InteractionService` executes the commands. By default, commands are executed using `ConstructorInfo.Invoke()` to create module instances and `MethodInfo.Invoke()` method for executing the method bodies. By setting, `InteractionServiceConfig.UseCompiledLambda` to `true`, you can make `InteractionService` create module instances and execute commands using *Compiled Lambda* expressions. This cuts down on command execution time but it might add some memory overhead.

Time it takes to create a module instance and execute a `Task.Delay(0)` method using the Reflection methods compared to Compiled Lambda expressions:

| Method | Mean | Error | StdDev |
|----------------- |----------:|---------:|---------:|
| ReflectionInvoke | 225.93 ns | 4.522 ns | 7.040 ns |
| CompiledLambda | 48.79 ns | 0.981 ns | 1.276 ns |

## Registering Commands to Discord

Application commands loaded to the Interaction Service can be registered to Discord using a number of different methods. In most cases `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` are the methods to use. Command registration methods can only be used after the gateway client is ready or the rest client is logged in.

In debug environment, since Global commands can take up to 1 hour to register/update, you should register your commands to a test guild for your changes to take effect immediately. You can use the preprocessor directives to create a simple logic for registering commands:

```csharp
#if DEBUG
await interactionService.RegisterCommandsToGuildAsync(<test_guild_id>);
#else
await interactionService.RegisterCommandsGloballyAsync();
#endif
```

+ 73
- 0
docs/guides/interactions_framework/post_execution.md View File

@@ -0,0 +1,73 @@
---
uid: Guides.InteractionsFramework.PostEx
title: Post-Execution
---

# Post-Execution Logic

Interaction Service uses `IResult`s to provide information about the state of command execution. These can be used to log internal exceptions or provide some insight to the command user.

If you are running your commands using `RunMode.Sync` these command results can be retrieved from the return value of `InteractionService.ExecuteCommandAsync()` method or by registering delegates to Interaction Service events.

If you are using the `RunMode.Async` to run your commands, you must use the Interaction Service events to get the execution results. When using `RunMode.Async`, `InteractionService.ExecuteCommandAsync()` will always return a successful result.

## Results

Interaction Result come in a handful of different flavours:

1. `AutocompletionResult`: returned by Autocompleters
2. `ExecuteResult`: contains the result of method body execution process
3. `PreconditionGroupResult`: returned by Precondition groups
4. `PreconditionResult`: returned by preconditions
5. `RuntimeResult`: a user implementable result for returning user defined results
6. `SearchResult`: returned by command lookup map
7. `TypeConverterResult`: returned by TypeConverters

You can either use the `IResult.Error` property of an Interaction result or create type check for the afformentioned result types to branch out your post-execution logic to handle different situations.

## CommandExecuted Events

Every time a command gets executed, Interaction Service raises a *CommandExecuted event. These events can be used to create a post-execution pipeline.

```csharp
interactionService.SlashCommandExecuted += SlashCommandExecuted;

async Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
await arg2.Interaction.RespondAsync($"Unmet Precondition: {arg3.ErrorReason}");
break;
case InteractionCommandError.UnknownCommand:
await arg2.Interaction.RespondAsync("Unknown command");
break;
case InteractionCommandError.BadArgs:
await arg2.Interaction.RespondAsync("Invalid number or arguments");
break;
case InteractionCommandError.Exception:
await arg2.Interaction.RespondAsync($"Command exception:{arg3.ErrorReason}");
break;
case InteractionCommandError.Unsuccessful:
await arg2.Interaction.RespondAsync("Command could not be executed");
break;
default:
break;
}
}
}
```

## Log Event

InteractionService regularly outputs information about the occuring events to keep the developer informed.

## Runtime Result

Interaction commands allow you to return `Task<RuntimeResult>` to pass on additional information about the command execution process back to your post-execution logic.

Custom `RuntimeResult` classes can be created by inheriting the base `RuntimeResult` class.

If command execution process reaches the method body of the command and no exceptions are thrown during the execution of the method body, `RuntimeResult` returned by your command will be accessible by casting/type-checking the `IResult` parameter of the *CommandExecuted event delegate.

+ 8
- 0
docs/guides/interactions_framework/preconditions.md View File

@@ -0,0 +1,8 @@
---
uid: Guides.InteractionsFramework.Preconditions
title: Preconditions
---

# Preconditions

Preconditions in Interaction Service work exactly the same as they do in ***Discord.Net.Commands***. For more information, check out [Preconditions](../commands/preconditions.md)

+ 130
- 0
docs/guides/interactions_framework/typeconverters.md View File

@@ -0,0 +1,130 @@
---
uid: Guides.InteractionsFramework.TypeConverters
title: Type Converters
---

# TypeConverters

TypeConverters are responsible for registering command parameters to Discord and parsing the user inputs into method parameters.

By default, TypeConverters for the following types are provided with `Discord.Net.Interactions` library.

- Implementations of `IUser`
- Implementations of `IChannel`
- Implementations of `IRole`
- Implementations of `IMentionable`
- `string`
- `float`, `double`, `decimal`
- `bool`
- `char`
- `sbyte`, `byte`
- `int16`, `int32`, `int64`
- `uint16`, `uint32`, `uint64`
- `enum`
- `DateTime`
- `TimeSpan`

## Creating TypeConverters

Depending on your needs, there are two types of `TypeConverter`s you can create:

- Concrete type
- Generic type

A valid converter must inherit `TypeConverter` base type. And override the abstract base methods.

### CanConvertTo() Method

This method is used by Interaction Service to search for alternative Type Converters.

Interaction Services determines the most suitable `TypeConverter` for a parameter type in the following order:

1. It searches for a `TypeConverter` that is registered to specifically target that parameter type
2. It searches for a generic `TypeConverter` with a matching type constraint. If there are more multiple matches, the one whose type constraint is the most specialized will be chosen.
3. It searches for a `TypeConverter` that returns `true` when its `CanConvertTo()` method is invoked for thaty parameter type.

> Alternatively, you can use the generic variant (`TypeConverter<T>`) of the `TypeConverter` base class which implements the following method body for `CanConvertTo()` method

```csharp
public sealed override bool CanConvertTo (Type type) =>
typeof(T).IsAssignableFrom(type);
```

### GetDiscordType() Method

This method is used by Interaction Service to determine the [Discord Application Command Option type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type) of a parameter type.

### ReadAsync() Method

This method is used by Interaction Service to parse the user input. This method should return `TypeConverterResult.FromSuccess` if the parsing operation is successful, otherwise it should return `TypeConverterResult.FromError`. The inner logic of this method is totally up to you, however you should avoid using long running code.

### Write() Method

This method is used to configure the **Discord Application Command Option** before it gets registered to Discord. Command Option is configured by modifying the `ApplicationCommandOptionProperties` instance.

The default parameter building pipeline is isolated and will not be disturbed by the `TypeConverter` workflow. But changes made in this method will override the values generated by the Interaction Service for a **Discord Application Command Option**.

---

### Example Enum TypeConverter

```csharp
internal sealed class EnumConverter<T> : TypeConverter<T> where T : struct, Enum
{
public override ApplicationCommandOptionType GetDiscordType ( ) => ApplicationCommandOptionType.String;

public override Task<TypeConverterResult> ReadAsync (IInteractionCommandContext context, SocketSlashCommandDataOption option, IServiceProvider services)
{
if (Enum.TryParse<T>((string)option.Value, out var result))
return Task.FromResult(TypeConverterResult.FromSuccess(result));
else
return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {nameof(T)}"));
}

public override void Write (ApplicationCommandOptionProperties properties, IParameterInfo parameterInfo)
{
var names = Enum.GetNames(typeof(T));
if (names.Length <= 25)
{
var choices = new List<ApplicationCommandOptionChoiceProperties>();

foreach (var name in names)
choices.Add(new ApplicationCommandOptionChoiceProperties
{
Name = name,
Value = name
});

properties.Choices = choices;
}
}
}
```

---

## Registering TypeConverters

> TypeConverters must be registered prior to module discovery. If Interaction Service encounters a parameter type that doesn't belong to any of the registered `TypeConverter`s during this phase, it will throw an exception.

### Concrete TypeConverters

Registering Concrete TypeConverters are as simple as creating an instance of your custom converter and invoking `AddTypeConverter()` method.

```csharp
interactionService.AddTypeConverter<string[]>(new StringArrayConverter());
```

### Generic TypeConverters

To register a generic TypeConverter, you need to invoke the `AddGenericTypeConverter()` method of the Interaction Service class. You need to pass the type of your `TypeConverter` and a target base type to this method.

For instance, to register the previously mentioned [Example Enum Converter](#example-enum-converter) the following can be used:

```csharp
interactionService.AddGenericTypeConverter<Enum>(typeof(EnumConverter<>));
```

Interaction service checks if the target base type satisfies the type constraints of the Generic TypeConverter class.

> Dependencies of Generic TypeConverters are also resolved using the Dependency Injection pattern.

+ 8
- 0
docs/guides/toc.yml View File

@@ -60,6 +60,14 @@
topicUid: Guides.MessageComponents.SelectMenus
- name: Advanced Concepts
topicUid: Guides.MessageComponents.Advanced
- name: Interaction Framework
items:
- name: Getting started
topicUid: Guides.InteractionsFramework.Intro
- name: Dependency Injection
topicUid: Guides.Commands.DI
- name: Post-execution Handling
topicUid: Guides.Commands.PostExecution
- name: Emoji
topicUid: Guides.Emoji
- name: Voice


+ 1
- 1
samples/01_basic_ping_bot/01_basic_ping_bot.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 1
- 1
samples/02_commands_framework/02_commands_framework.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 1
- 1
samples/03_sharded_client/03_sharded_client.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>_03_sharded_client</RootNamespace>
</PropertyGroup>



+ 25
- 0
samples/04_interactions_framework/04_interactions_framework.csproj View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>_04_interactions_framework</RootNamespace>
<StartupObject></StartupObject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>

</Project>

+ 146
- 0
samples/04_interactions_framework/CommandHandler.cs View File

@@ -0,0 +1,146 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace _04_interactions_framework
{
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _commands;
private readonly IServiceProvider _services;

public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services)
{
_client = client;
_commands = commands;
_services = services;
}

public async Task InitializeAsync ( )
{
// Add the public modules that inherit InteractionModuleBase<T> to the InteractionService
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);

// Process the InteractionCreated payloads to execute Interactions commands
_client.InteractionCreated += HandleInteraction;

// Process the command execution results
_commands.SlashCommandExecuted += SlashCommandExecuted;
_commands.ContextCommandExecuted += ContextCommandExecuted;
_commands.ComponentCommandExecuted += ComponentCommandExecuted;
}

private Task ComponentCommandExecuted (ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task ContextCommandExecuted (ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private async Task HandleInteraction (SocketInteraction arg)
{
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules
var ctx = new SocketInteractionContext(_client, arg);
await _commands.ExecuteCommandAsync(ctx, _services);
}
catch (Exception ex)
{
Console.WriteLine(ex);

// If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if(arg.Type == InteractionType.ApplicationCommand)
await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
}
}
}
}

+ 16
- 0
samples/04_interactions_framework/ExampleEnum.cs View File

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

namespace _04_interactions_framework
{
public enum ExampleEnum
{
First,
Second,
Third,
Fourth
}
}

+ 93
- 0
samples/04_interactions_framework/Modules/UtilityModule.cs View File

@@ -0,0 +1,93 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _04_interactions_framework.Modules
{
// Interation modules must be public and inherit from an IInterationModuleBase
public class UtilityModule : InteractionModuleBase<SocketInteractionContext>
{
// Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider
public InteractionService Commands { get; set; }

private CommandHandler _handler;

// Constructor injection is also a valid way to access the dependecies
public UtilityModule ( CommandHandler handler )
{
_handler = handler;
}

// Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines
[SlashCommand("ping", "Recieve a pong")]
// By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission
[DefaultPermission(false)]
public async Task Ping ( )
{
await RespondAsync("pong");
}

// You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally,
// you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation.
// Optional method parameters(parameters with a default value) also will be displayed as optional on Discord.

// [Summary] lets you customize the name and the description of a parameter
[SlashCommand("echo", "Repeat the input")]
public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false)
{
await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty));
}

// [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix
[Group("test_group", "This is a command group")]
public class GroupExample : InteractionModuleBase<SocketInteractionContext>
{
// You can create command choices either by using the [Choice] attribute or by creating an enum. Every enum with 25 or less values will be registered as a multiple
// choice option
[SlashCommand("choice_example", "Enums create choices")]
public async Task ChoiceExample(ExampleEnum input)
{
await RespondAsync(input.ToString());
}
}

// User Commands can only have one parameter, which must be a type of SocketUser
[UserCommand("SayHello")]
public async Task SayHello(IUser user)
{
await RespondAsync($"Hello, {user.Mention}");
}

// Message Commands can only have one parameter, which must be a type of SocketMessage
[MessageCommand("Delete")]
[RequireOwner]
public async Task DeleteMesage(IMessage message)
{
await message.DeleteAsync();
await RespondAsync("Deleted message.");
}

// Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed.
// Alternatively, you can create a wild card pattern using the '*' character. Interaction Service will perform a lazy regex search and capture the matching strings.
// You can then access these capture groups from the method parameters, in the order they were captured. Using the wild card pattern, you can cherry pick component interactions.
[ComponentInteraction("musicSelect:*,*")]
public async Task ButtonPress(string id, string name)
{
// ...
await RespondAsync($"Playing song: {name}/{id}");
}

// Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters.
// You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids.
[ComponentInteraction("roleSelect")]
public async Task RoleSelect(params string[] selections)
{
// implement
}
}
}

+ 85
- 0
samples/04_interactions_framework/Program.cs View File

@@ -0,0 +1,85 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace _04_interactions_framework
{
class Program
{
static void Main ( string[] args )
{
// One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model,
// this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here.
IConfiguration config = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();

RunAsync(config).GetAwaiter().GetResult();
}

static async Task RunAsync (IConfiguration configuration )
{
// Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime.
using var services = ConfigureServices(configuration);

var client = services.GetRequiredService<DiscordSocketClient>();
var commands = services.GetRequiredService<InteractionService>();

client.Log += LogAsync;
commands.Log += LogAsync;

// Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should
// register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild.
client.Ready += async ( ) =>
{
if (IsDebug())
// Id of the test guild can be provided from the Configuration object
await commands.RegisterCommandsToGuildAsync(configuration.GetValue<ulong>("testGuild"), true);
else
await commands.RegisterCommandsGloballyAsync(true);
};

// Here we can initialize the service that will register and execute our commands
await services.GetRequiredService<CommandHandler>().InitializeAsync();

// Bot token can be provided from the Configuration object we set up earlier
await client.LoginAsync(TokenType.Bot, configuration["token"]);
await client.StartAsync();

await Task.Delay(Timeout.Infinite);
}

static Task LogAsync(LogMessage message)
{
Console.WriteLine(message.ToString());
return Task.CompletedTask;
}

static ServiceProvider ConfigureServices ( IConfiguration configuration )
{
return new ServiceCollection()
.AddSingleton(configuration)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
}

static bool IsDebug ( )
{
#if DEBUG
return true;
#else
return false;
#endif
}
}
}

+ 27
- 0
samples/04_interactions_framework/RequireOwnerAttribute.cs View File

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

namespace _04_interactions_framework
{
public class RequireOwnerAttribute : PreconditionAttribute
{
public override async Task<PreconditionResult> CheckRequirementsAsync (IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
switch (context.Client.TokenType)
{
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
if (context.User.Id != application.Owner.Id)
return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.");
return PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
}
}
}

+ 1
- 1
samples/idn/idn.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 2
- 2
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -5,8 +5,8 @@
<AssemblyName>Discord.Net.Commands</AssemblyName>
<RootNamespace>Discord.Commands</RootNamespace>
<Description>A Discord.Net extension adding support for bot commands.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />


+ 22
- 2
src/Discord.Net.Commands/IModuleBase.cs View File

@@ -2,14 +2,34 @@ using Discord.Commands.Builders;

namespace Discord.Commands
{
internal interface IModuleBase
/// <summary>
/// Represents a generic module base.
/// </summary>
public interface IModuleBase
{
/// <summary>
/// Sets the context of this module base.
/// </summary>
/// <param name="context">The context to set.</param>
void SetContext(ICommandContext context);

/// <summary>
/// Executed before a command is run in this module base.
/// </summary>
/// <param name="command">The command thats about to run.</param>
void BeforeExecute(CommandInfo command);

/// <summary>
/// Executed after a command is ran in this module base.
/// </summary>
/// <param name="command">The command that ran.</param>
void AfterExecute(CommandInfo command);

/// <summary>
/// Executed when this module is building.
/// </summary>
/// <param name="commandService">The command service that is building this module.</param>
/// <param name="builder">The builder constructing this module.</param>
void OnModuleBuilding(CommandService commandService, ModuleBuilder builder);
}
}

+ 1
- 0
src/Discord.Net.Core/AssemblyInfo.cs View File

@@ -8,3 +8,4 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
[assembly: InternalsVisibleTo("Discord.Net.Interactions")]

+ 3
- 3
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Core</AssemblyName>
<RootNamespace>Discord</RootNamespace>
<Description>The core components for the Discord.Net library.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />


+ 4
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord
@@ -29,6 +30,9 @@ namespace Discord
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$");

if (value.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");

_name = value;
}
}


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteraction.cs View File

@@ -3,7 +3,7 @@ namespace Discord
/// <summary>
/// Represents a Message Command interaction.
/// </summary>
public interface IMessageCommandInteraction : IDiscordInteraction
public interface IMessageCommandInteraction : IApplicationCommandInteraction
{
/// <summary>
/// Gets the data associated with this interaction.


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteraction.cs View File

@@ -3,7 +3,7 @@ namespace Discord
/// <summary>
/// Represents a User Command interaction.
/// </summary>
public interface IUserCommandInteraction : IDiscordInteraction
public interface IUserCommandInteraction : IApplicationCommandInteraction
{
/// <summary>
/// Gets the data associated with this interaction.


+ 19
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteraction.cs View File

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

namespace Discord
{
/// <summary>
/// Represents an application command interaction.
/// </summary>
public interface IApplicationCommandInteraction : IDiscordInteraction
{
/// <summary>
/// Gets the data of the application command interaction
/// </summary>
new IApplicationCommandInteractionData Data { get; }
}
}

+ 99
- 8
src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Discord
@@ -33,6 +35,11 @@ namespace Discord
/// </summary>
int Version { get; }

/// <summary>
/// Gets the user who invoked the interaction.
/// </summary>
IUser User { get; }

/// <summary>
/// Responds to an Interaction with type <see cref="InteractionResponseType.ChannelMessageWithSource"/>.
/// </summary>
@@ -42,10 +49,14 @@ namespace Discord
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false,
bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false,
bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);

/// <summary>
/// Sends a followup message for this interaction.
@@ -56,13 +67,90 @@ namespace Discord
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// The sent message.
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null);
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);

/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="fileStream">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
public Task<IUserMessage> FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);

/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="filePath">The file to upload.</param>
/// <param name="fileName">The file name of the attachment.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
public Task<IUserMessage> FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="attachment">The attachment containing the file and description.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);
/// <summary>
/// Sends a followup message for this interaction.
/// </summary>
/// <param name="attachments">A collection of attachments to upload.</param>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="components">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> FollowupWithFilesAsync(IEnumerable<FileAttachment> attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null);

/// <summary>
/// Gets the original response for this interaction.
@@ -76,14 +164,17 @@ namespace Discord
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A <see cref="IUserMessage"/> that represents the initial response.</returns>
/// <returns>
/// A task that represents an asynchronous modification operation. The task result
/// contains the updated message.
/// </returns>
Task<IUserMessage> ModifyOriginalResponseAsync(Action<MessageProperties> func, RequestOptions options = null);

/// <summary>
/// Acknowledges this interaction.
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// A task that represents the asynchronous operation of deferring the interaction.
/// </returns>
Task DeferAsync(bool ephemeral = false, RequestOptions options = null);
}


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/SlashCommands/ISlashCommandInteraction.cs View File

@@ -3,7 +3,7 @@ namespace Discord
/// <summary>
/// Represents a slash command interaction.
/// </summary>
public interface ISlashCommandInteraction : IDiscordInteraction
public interface ISlashCommandInteraction : IApplicationCommandInteraction
{
/// <summary>
/// Gets the data associated with this interaction.


+ 3
- 0
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -40,6 +40,9 @@ namespace Discord
if (!Regex.IsMatch(value, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value));

if (value.Any(x => char.IsUpper(x)))
throw new FormatException("Name cannot contain any uppercase characters.");

_name = value;
}
}


+ 36
- 0
src/Discord.Net.Core/Interactions/IInteractionContext.cs View File

@@ -0,0 +1,36 @@
namespace Discord
{
/// <summary>
/// Represents the context of an Interaction.
/// </summary>
public interface IInteractionContext
{
/// <summary>
/// Gets the client that will be used to handle this interaction.
/// </summary>
IDiscordClient Client { get; }

/// <summary>
/// Gets the guild the interaction originated from.
/// </summary>
/// <remarks>
/// Will be <see langword="null"/> if the interaction originated from a DM channel or the interaction was a Context Command interaction.
/// </remarks>
IGuild Guild { get; }

/// <summary>
/// Gets the channel the interaction originated from.
/// </summary>
IMessageChannel Channel { get; }

/// <summary>
/// Gets the user who invoked the interaction event.
/// </summary>
IUser User { get; }

/// <summary>
/// Gets the underlying interaction.
/// </summary>
IDiscordInteraction Interaction { get; }
}
}

+ 1
- 1
src/Discord.Net.Examples/Discord.Net.Examples.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 36
- 0
src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs View File

@@ -0,0 +1,36 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class AutocompleteAttribute : Attribute
{
/// <summary>
/// Type of the <see cref="AutocompleteHandler"/>.
/// </summary>
public Type AutocompleteHandlerType { get; }

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle
/// Autocomplete interactions targeting the parameter this <see cref="Attribute"/> is applied to.
/// </summary>
/// <remarks>
/// <see cref="InteractionServiceConfig.EnableAutocompleteHandlers"/> must be set to <see langword="true"/> to use this constructor.
/// </remarks>
public AutocompleteAttribute(Type autocompleteHandlerType)
{
if (!typeof(IAutocompleteHandler).IsAssignableFrom(autocompleteHandlerType))
throw new InvalidOperationException($"{autocompleteHandlerType.FullName} isn't a valid {nameof(IAutocompleteHandler)} type");

AutocompleteHandlerType = autocompleteHandlerType;
}

/// <summary>
/// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>.
/// </summary>
public AutocompleteAttribute() { }
}
}

+ 30
- 0
src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Discord.Interactions
{
/// <summary>
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class ChannelTypesAttribute : Attribute
{
/// <summary>
/// Gets the allowed channel types for this option.
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }

/// <summary>
/// Specify the target channel types for a <see cref="ApplicationCommandOptionType.Channel"/> option.
/// </summary>
/// <param name="channelTypes">The allowed channel types for this option.</param>
public ChannelTypesAttribute (params ChannelType[] channelTypes)
{
if (channelTypes is null)
throw new ArgumentNullException(nameof(channelTypes));

ChannelTypes = channelTypes.ToImmutableArray();
}
}
}

+ 64
- 0
src/Discord.Net.Interactions/Attributes/ChoiceAttribute.cs View File

@@ -0,0 +1,64 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Add a pre-determined argument value to a command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
public class ChoiceAttribute : Attribute
{
/// <summary>
/// Gets the name of the choice.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the type of this choice.
/// </summary>
public SlashCommandChoiceType Type { get; }

/// <summary>
/// Gets the value that will be used whenever this choice is selected.
/// </summary>
public object Value { get; }

private ChoiceAttribute (string name)
{
Name = name;
}

/// <summary>
/// Create a parameter choice with type <see cref="SlashCommandChoiceType.String"/>.
/// </summary>
/// <param name="name">Name of the choice.</param>
/// <param name="value">Predefined value of the choice.</param>
public ChoiceAttribute (string name, string value) : this(name)
{
Type = SlashCommandChoiceType.String;
Value = value;
}

/// <summary>
/// Create a parameter choice with type <see cref="SlashCommandChoiceType.Integer"/>.
/// </summary>
/// <param name="name">Name of the choice.</param>
/// <param name="value">Predefined value of the choice.</param>
public ChoiceAttribute (string name, int value) : this(name)
{
Type = SlashCommandChoiceType.Integer;
Value = value;
}

/// <summary>
/// Create a parameter choice with type <see cref="SlashCommandChoiceType.Number"/>.
/// </summary>
/// <param name="name">Name of the choice.</param>
/// <param name="value">Predefined value of the choice.</param>
public ChoiceAttribute (string name, double value) : this(name)
{
Type = SlashCommandChoiceType.Number;
Value = value;
}
}
}

+ 39
- 0
src/Discord.Net.Interactions/Attributes/Commands/AutocompleteCommandAttribute.cs View File

@@ -0,0 +1,39 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Create an Autocomplete Command.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AutocompleteCommandAttribute : Attribute
{
/// <summary>
/// Gets the name of the target parameter.
/// </summary>
public string ParameterName { get; }

/// <summary>
/// Gets the name of the target command.
/// </summary>
public string CommandName { get; }

/// <summary>
/// Get the run mode this command gets executed with.
/// </summary>
public RunMode RunMode { get; }

/// <summary>
/// Create a command for Autocomplete interaction handling.
/// </summary>
/// <param name="parameterName">Name of the target parameter.</param>
/// <param name="commandName">Name of the target command.</param>
/// <param name="runMode">Set the run mode of the command.</param>
public AutocompleteCommandAttribute(string parameterName, string commandName, RunMode runMode = RunMode.Default)
{
ParameterName = parameterName;
CommandName = commandName;
RunMode = runMode;
}
}
}

+ 44
- 0
src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs View File

@@ -0,0 +1,44 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Create a Message Component interaction handler, CustomId represents
/// the CustomId of the Message Component that will be handled.
/// </summary>
/// <remarks>
/// <see cref="GroupAttribute"/>s will add prefixes to this command if <see cref="IgnoreGroupNames"/> is set to <see langword="false"/>
/// CustomID supports a Wild Card pattern where you can use the <see cref="InteractionServiceConfig.WildCardExpression"/> to match a set of CustomIDs.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ComponentInteractionAttribute : Attribute
{
/// <summary>
/// Gets the string to compare the Message Component CustomIDs with.
/// </summary>
public string CustomId { get; }

/// <summary>
/// Gets <see langword="true"/> if <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.
/// </summary>
public bool IgnoreGroupNames { get; }

/// <summary>
/// Gets the run mode this command gets executed with.
/// </summary>
public RunMode RunMode { get; }

/// <summary>
/// Create a command for component interaction handling.
/// </summary>
/// <param name="customId">String to compare the Message Component CustomIDs with.</param>
/// <param name="ignoreGroupNames">If <see langword="true"/> <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.</param>
/// <param name="runMode">Set the run mode of the command.</param>
public ComponentInteractionAttribute (string customId, bool ignoreGroupNames = false, RunMode runMode = RunMode.Default)
{
CustomId = customId;
IgnoreGroupNames = ignoreGroupNames;
RunMode = runMode;
}
}
}

+ 36
- 0
src/Discord.Net.Interactions/Attributes/Commands/ContextCommandAttribute.cs View File

@@ -0,0 +1,36 @@
using System;
using System.Reflection;

namespace Discord.Interactions
{
/// <summary>
/// Base attribute for creating a Context Commands.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ContextCommandAttribute : Attribute
{
/// <summary>
/// Gets the name of this Context Command.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the type of this Context Command.
/// </summary>
public ApplicationCommandType CommandType { get; }

/// <summary>
/// Gets the run mode this command gets executed with.
/// </summary>
public RunMode RunMode { get; }

internal ContextCommandAttribute (string name, ApplicationCommandType commandType, RunMode runMode = RunMode.Default)
{
Name = name;
CommandType = commandType;
RunMode = runMode;
}

internal virtual void CheckMethodDefinition (MethodInfo methodInfo) { }
}
}

+ 29
- 0
src/Discord.Net.Interactions/Attributes/Commands/MessageCommandAttribute.cs View File

@@ -0,0 +1,29 @@
using System;
using System.Reflection;

namespace Discord.Interactions
{
/// <summary>
/// Create a Message Context Command.
/// </summary>
/// <remarks>
/// <see cref="GroupAttribute"/>s won't add prefixes to this command.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MessageCommandAttribute : ContextCommandAttribute
{
/// <summary>
/// Register a method as a Message Context Command.
/// </summary>
/// <param name="name">Name of the context command.</param>
public MessageCommandAttribute (string name) : base(name, ApplicationCommandType.Message) { }

internal override void CheckMethodDefinition (MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();

if (parameters.Length != 1 || !typeof(IMessage).IsAssignableFrom(parameters[0].ParameterType))
throw new InvalidOperationException($"Message Commands must have only one parameter that is a type of {nameof(IMessage)}");
}
}
}

+ 49
- 0
src/Discord.Net.Interactions/Attributes/Commands/SlashCommandAttribute.cs View File

@@ -0,0 +1,49 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Create an Slash Application Command.
/// </summary>
/// <remarks>
/// <see cref="GroupAttribute"/> prefix will be used to created nested Slash Application Commands.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SlashCommandAttribute : Attribute
{
/// <summary>
/// Gets the name of the Slash Command.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the description of the Slash Command.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets <see langword="true"/> if <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.
/// </summary>
public bool IgnoreGroupNames { get; }

/// <summary>
/// Gets the run mode this command gets executed with.
/// </summary>
public RunMode RunMode { get; }

/// <summary>
/// Register a method as a Slash Command.
/// </summary>
/// <param name="name">Name of the command.</param>
/// <param name="description">Description of the command.</param>
/// <param name="ignoreGroupNames"> If <see langword="true"/>, <see cref="GroupAttribute"/>s will be ignored while creating this command and this method will be treated as a top level command.</param>
/// <param name="runMode">Set the run mode of the command.</param>
public SlashCommandAttribute (string name, string description, bool ignoreGroupNames = false, RunMode runMode = RunMode.Default)
{
Name = name;
Description = description;
IgnoreGroupNames = ignoreGroupNames;
RunMode = runMode;
}
}
}

+ 29
- 0
src/Discord.Net.Interactions/Attributes/Commands/UserCommandAttribute.cs View File

@@ -0,0 +1,29 @@
using System;
using System.Reflection;

namespace Discord.Interactions
{
/// <summary>
/// Create an User Context Command.
/// </summary>
/// <remarks>
/// <see cref="GroupAttribute"/>s won't add prefixes to this command.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class UserCommandAttribute : ContextCommandAttribute
{
/// <summary>
/// Register a command as a User Context Command.
/// </summary>
/// <param name="name">Name of this User Context Command.</param>
public UserCommandAttribute (string name) : base(name, ApplicationCommandType.User) { }

internal override void CheckMethodDefinition (MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();

if (parameters.Length != 1 || !typeof(IUser).IsAssignableFrom(parameters[0].ParameterType))
throw new InvalidOperationException($"User Commands must have only one parameter that is a type of {nameof(IUser)}");
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Set the "Default Permission" property of an Application Command.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class DefaultPermissionAttribute : Attribute
{
/// <summary>
/// Gets whether the users are allowed to use a Slash Command by default or not.
/// </summary>
public bool IsDefaultPermission { get; }

/// <summary>
/// Set the default permission of a Slash Command.
/// </summary>
/// <param name="isDefaultPermission"><see langword="true"/> if the users are allowed to use this command.</param>
public DefaultPermissionAttribute (bool isDefaultPermission)
{
IsDefaultPermission = isDefaultPermission;
}
}
}

+ 13
- 0
src/Discord.Net.Interactions/Attributes/DontAutoRegisterAttribute.cs View File

@@ -0,0 +1,13 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// <see cref="InteractionModuleBase{T}"/>s with this attribute will not be registered by the <see cref="InteractionService.RegisterCommandsGloballyAsync(bool)"/> or
/// <see cref="InteractionService.RegisterCommandsToGuildAsync(ulong, bool)"/> methods.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DontAutoRegisterAttribute : Attribute
{
}
}

+ 35
- 0
src/Discord.Net.Interactions/Attributes/GroupAttribute.cs View File

@@ -0,0 +1,35 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Create nested Slash Commands by marking a module as a command group.
/// </summary>
/// <remarks>
/// <see cref="ContextCommandAttribute"/> commands wil not be affected by this.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class GroupAttribute : Attribute
{
/// <summary>
/// Gets the name of the group.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the description of the group.
/// </summary>
public string Description { get; }

/// <summary>
/// Create a command group.
/// </summary>
/// <param name="name">Name of the group.</param>
/// <param name="description">Description of the group.</param>
public GroupAttribute (string name, string description)
{
Name = name;
Description = description;
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/Attributes/MaxValueAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Set the maximum value permitted for a number type parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class MaxValueAttribute : Attribute
{
/// <summary>
/// Gets the maximum value permitted.
/// </summary>
public double Value { get; }

/// <summary>
/// Set the maximum value permitted for a number type parameter.
/// </summary>
/// <param name="value">The maximum value permitted.</param>
public MaxValueAttribute(double value)
{
Value = value;
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/Attributes/MinValueAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Set the minimum value permitted for a number type parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class MinValueAttribute : Attribute
{
/// <summary>
/// Gets the minimum value permitted.
/// </summary>
public double Value { get; }

/// <summary>
/// Set the minimum value permitted for a number type parameter.
/// </summary>
/// <param name="value">The minimum value permitted.</param>
public MinValueAttribute(double value)
{
Value = value;
}
}
}

+ 34
- 0
src/Discord.Net.Interactions/Attributes/ParameterPreconditionAttribute.cs View File

@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the parameter to pass the specified precondition before execution can begin.
/// </summary>
/// <seealso cref="PreconditionAttribute"/>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
public abstract class ParameterPreconditionAttribute : Attribute
{
/// <summary>
/// Gets the error message to be returned if execution context doesn't pass the precondition check.
/// </summary>
/// <remarks>
/// When overridden in a derived class, uses the supplied string
/// as the error message if the precondition doesn't pass.
/// Setting this for a class that doesn't override
/// this property is a no-op.
/// </remarks>
public virtual string ErrorMessage { get; }

/// <summary>
/// Checks whether the condition is met before execution of the command.
/// </summary>
/// <param name="context">The context of the command.</param>
/// <param name="parameterInfo">The parameter of the command being checked against.</param>
/// <param name="value">The raw value of the parameter.</param>
/// <param name="services">The service collection used for dependency injection.</param>
public abstract Task<PreconditionResult> CheckRequirementsAsync (IInteractionContext context, IParameterInfo parameterInfo, object value,
IServiceProvider services);
}
}

+ 42
- 0
src/Discord.Net.Interactions/Attributes/PreconditionAttribute.cs View File

@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Requires the module or class to pass the specified precondition before execution can begin.
/// </summary>
/// <seealso cref="ParameterPreconditionAttribute"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class PreconditionAttribute : Attribute
{
/// <summary>
/// Gets the group that this precondition belongs to.
/// </summary>
/// <remarks>
/// <see cref="Preconditions" /> of the same group require only one of the preconditions to pass in order to
/// be successful (A || B). Specifying <see cref="Group" /> = <c>null</c> or not at all will
/// require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// </remarks>
public string Group { get; set; } = null;

/// <summary>
/// Gets the error message to be returned if execution context doesn't pass the precondition check.
/// </summary>
/// <remarks>
/// When overridden in a derived class, uses the supplied string
/// as the error message if the precondition doesn't pass.
/// Setting this for a class that doesn't override
/// this property is a no-op.
/// </remarks>
public virtual string ErrorMessage { get; }

/// <summary>
/// Checks if the <paramref name="commandInfo"/> command to be executed meets the precondition requirements.
/// </summary>
/// <param name="context">The context of the command.</param>
/// <param name="commandInfo">The command being executed.</param>
/// <param name="services">The service collection used for dependency injection.</param>
public abstract Task<PreconditionResult> CheckRequirementsAsync (IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services);
}
}

+ 32
- 0
src/Discord.Net.Interactions/Attributes/SummaryAttribute.cs View File

@@ -0,0 +1,32 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Customize the name and description of an Slash Application Command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class SummaryAttribute : Attribute
{
/// <summary>
/// Gets the name of the parameter.
/// </summary>
public string Name { get; } = null;

/// <summary>
/// Gets the description of the parameter.
/// </summary>
public string Description { get; } = null;

/// <summary>
/// Modify the default name and description values of a Slash Command parameter.
/// </summary>
/// <param name="name">Name of the parameter.</param>
/// <param name="description">Description of the parameter.</param>
public SummaryAttribute (string name = null, string description = null)
{
Name = name;
Description = description;
}
}
}

+ 102
- 0
src/Discord.Net.Interactions/AutocompleteHandlers/AutocompleteHandler.cs View File

@@ -0,0 +1,102 @@
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Base class for creating Autocompleters. <see cref="InteractionService"/> uses Autocompleters to generate parameter suggestions.
/// </summary>
public abstract class AutocompleteHandler : IAutocompleteHandler
{
/// <inheritdoc/>
public InteractionService InteractionService { get; set; }

/// <inheritdoc/>
public abstract Task<AutocompletionResult> GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter,
IServiceProvider services);

protected virtual string GetLogString(IInteractionContext context)
{
var interaction = (context.Interaction as IAutocompleteInteraction);
return $"{interaction.Data.CommandName}: {interaction.Data.Current.Name} Autocomplete";
}

/// <inheritdoc/>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter,
IServiceProvider services)
{
switch (InteractionService._runMode)
{
case RunMode.Sync:
{
return await ExecuteInternalAsync(context, autocompleteInteraction, parameter, services).ConfigureAwait(false);
}
case RunMode.Async:
_ = Task.Run(async () =>
{
await ExecuteInternalAsync(context, autocompleteInteraction, parameter, services).ConfigureAwait(false);
});
break;
default:
throw new InvalidOperationException($"RunMode {InteractionService._runMode} is not supported.");
}

return ExecuteResult.FromSuccess();
}

private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter,
IServiceProvider services)
{
try
{
var result = await GenerateSuggestionsAsync(context, autocompleteInteraction, parameter, services).ConfigureAwait(false);

if (result.IsSuccess)
switch (autocompleteInteraction)
{
case RestAutocompleteInteraction restAutocomplete:
var payload = restAutocomplete.Respond(result.Suggestions);
await InteractionService._restResponseCallback(context, payload).ConfigureAwait(false);
break;
case SocketAutocompleteInteraction socketAutocomplete:
await socketAutocomplete.RespondAsync(result.Suggestions).ConfigureAwait(false);
break;
}

await InteractionService._autocompleteHandlerExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
return result;
}
catch (Exception ex)
{
var originalEx = ex;
while (ex is TargetInvocationException)
ex = ex.InnerException;

await InteractionService._cmdLogger.ErrorAsync(ex).ConfigureAwait(false);

var result = ExecuteResult.FromError(ex);
await InteractionService._autocompleteHandlerExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);

if (InteractionService._throwOnError)
{
if (ex == originalEx)
throw;
else
ExceptionDispatchInfo.Capture(ex).Throw();
}

return result;
}
finally
{
await InteractionService._cmdLogger.VerboseAsync($"Executed {GetLogString(context)}").ConfigureAwait(false);
}
}
}
}



+ 43
- 0
src/Discord.Net.Interactions/AutocompleteHandlers/IAutocompleteHandler.cs View File

@@ -0,0 +1,43 @@
using Discord.WebSocket;
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represent a Autocomplete handler object that can be executed to generate parameter suggestions.
/// </summary>
public interface IAutocompleteHandler
{
/// <summary>
/// Gets the the underlying command service.
/// </summary>
InteractionService InteractionService { get; }

/// <summary>
/// Will be used to generate parameter suggestions.
/// </summary>
/// <param name="context">Command execution context.</param>
/// <param name="autocompleteInteraction">Autocomplete Interaction payload.</param>
/// <param name="parameter">Parameter information of the target parameter.</param>
/// <param name="services">Dependencies that will be used to create the module instance.</param>
/// <returns>
/// A task representing the execution process. The task result contains the Autocompletion result.
/// </returns>
Task<AutocompletionResult> GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter,
IServiceProvider services);

/// <summary>
/// Executes the <see cref="IAutocompleteHandler"/> with the provided context.
/// </summary>
/// <param name="context">The execution context.</param>
/// <param name="autocompleteInteraction">AutocompleteInteraction payload.</param>
/// <param name="parameter">Parameter information of the target parameter.</param>
/// <param name="services">Dependencies that will be used to create the module instance.</param>
/// <returns>
/// A task representing the execution process. The task result contains the execution result.
/// </returns>
Task<IResult> ExecuteAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter,
IServiceProvider services);
}
}

+ 76
- 0
src/Discord.Net.Interactions/Builders/Commands/AutocompleteCommandBuilder.cs View File

@@ -0,0 +1,76 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="AutocompleteCommandInfo"/>.
/// </summary>
public sealed class AutocompleteCommandBuilder : CommandBuilder<AutocompleteCommandInfo, AutocompleteCommandBuilder, CommandParameterBuilder>
{
/// <summary>
/// Gets the name of the target parameter.
/// </summary>
public string ParameterName { get; set; }

/// <summary>
/// Gets the name of the target command.
/// </summary>
public string CommandName { get; set; }

protected override AutocompleteCommandBuilder Instance => this;

internal AutocompleteCommandBuilder(ModuleBuilder module) : base(module) { }

/// <summary>
/// Initializes a new <see cref="AutocompleteCommandBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this command.</param>
/// <param name="name">Name of this command.</param>
/// <param name="callback">Execution callback of this command.</param>
public AutocompleteCommandBuilder(ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }

/// <summary>
/// Sets <see cref="ParameterName"/>.
/// </summary>
/// <param name="name">New value of the <see cref="ParameterName"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public AutocompleteCommandBuilder WithParameterName(string name)
{
ParameterName = name;
return this;
}

/// <summary>
/// Sets <see cref="CommandName"/>.
/// </summary>
/// <param name="name">New value of the <see cref="CommandName"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public AutocompleteCommandBuilder WithCommandName(string name)
{
CommandName = name;
return this;
}

/// <summary>
/// Adds a command parameter to the parameters collection.
/// </summary>
/// <param name="configure"><see cref="CommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public override AutocompleteCommandBuilder AddParameter(Action<CommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
}

internal override AutocompleteCommandInfo Build(ModuleInfo module, InteractionService commandService) =>
new AutocompleteCommandInfo(this, module, commandService);
}
}

+ 174
- 0
src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents the base builder class for creating <see cref="CommandInfo{TParameter}"/>.
/// </summary>
/// <typeparam name="TInfo">The <see cref="CommandInfo{TParameter}"/> this builder yields when built.</typeparam>
/// <typeparam name="TBuilder">Inherited <see cref="CommandBuilder{TInfo, TBuilder, TParamBuilder}"/> type.</typeparam>
/// <typeparam name="TParamBuilder">Builder type for this commands parameters.</typeparam>
public abstract class CommandBuilder<TInfo, TBuilder, TParamBuilder> : ICommandBuilder
where TInfo : class, ICommandInfo
where TBuilder : CommandBuilder<TInfo, TBuilder, TParamBuilder>
where TParamBuilder : class, IParameterBuilder
{
private readonly List<Attribute> _attributes;
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<TParamBuilder> _parameters;

protected abstract TBuilder Instance { get; }

/// <inheritdoc/>
public ModuleBuilder Module { get; }

//// <inheritdoc/>
public ExecuteCallback Callback { get; internal set; }

/// <inheritdoc/>
public string Name { get; internal set; }

/// <inheritdoc/>
public string MethodName { get; set; }

/// <inheritdoc/>
public bool IgnoreGroupNames { get; set; }

/// <inheritdoc/>
public RunMode RunMode { get; set; }

/// <inheritdoc/>
public IReadOnlyList<Attribute> Attributes => _attributes;

/// <inheritdoc/>
public IReadOnlyList<TParamBuilder> Parameters => _parameters;

/// <inheritdoc/>
public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;

/// <inheritdoc/>
IReadOnlyList<IParameterBuilder> ICommandBuilder.Parameters => Parameters;

internal CommandBuilder (ModuleBuilder module)
{
_attributes = new List<Attribute>();
_preconditions = new List<PreconditionAttribute>();
_parameters = new List<TParamBuilder>();

Module = module;
}

protected CommandBuilder (ModuleBuilder module, string name, ExecuteCallback callback) : this(module)
{
Name = name;
Callback = callback;
}

/// <summary>
/// Sets <see cref="Name"/>.
/// </summary>
/// <param name="name">New value of the <see cref="Name"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithName (string name)
{
Name = name;
return Instance;
}

/// <summary>
/// Sets <see cref="MethodName"/>.
/// </summary>
/// <param name="name">New value of the <see cref="MethodName"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithMethodName (string name)
{
MethodName = name;
return Instance;
}

/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithAttributes (params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return Instance;
}

/// <summary>
/// Sets <see cref="RunMode"/>.
/// </summary>
/// <param name="runMode">New value of the <see cref="RunMode"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder SetRunMode (RunMode runMode)
{
RunMode = runMode;
return Instance;
}

/// <summary>
/// Adds parameter builders to <see cref="Parameters"/>.
/// </summary>
/// <param name="parameters">New parameter builders to be added to <see cref="Parameters"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder AddParameters (params TParamBuilder[] parameters)
{
_parameters.AddRange(parameters);
return Instance;
}

/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>.
/// </summary>
/// <param name="preconditions">New preconditions to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithPreconditions (params PreconditionAttribute[] preconditions)
{
_preconditions.AddRange(preconditions);
return Instance;
}

/// <inheritdoc/>
public abstract TBuilder AddParameter (Action<TParamBuilder> configure);

internal abstract TInfo Build (ModuleInfo module, InteractionService commandService);

//ICommandBuilder
/// <inheritdoc/>
ICommandBuilder ICommandBuilder.WithName (string name) =>
WithName(name);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.WithMethodName (string name) =>
WithMethodName(name);
ICommandBuilder ICommandBuilder.WithAttributes (params Attribute[] attributes) =>
WithAttributes(attributes);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) =>
SetRunMode(runMode);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) =>
AddParameters(parameters as TParamBuilder);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.WithPreconditions (params PreconditionAttribute[] preconditions) =>
WithPreconditions(preconditions);
}
}

+ 40
- 0
src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs View File

@@ -0,0 +1,40 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ComponentCommandInfo"/>.
/// </summary>
public sealed class ComponentCommandBuilder : CommandBuilder<ComponentCommandInfo, ComponentCommandBuilder, CommandParameterBuilder>
{
protected override ComponentCommandBuilder Instance => this;

internal ComponentCommandBuilder (ModuleBuilder module) : base(module) { }

/// <summary>
/// Initializes a new <see cref="ComponentBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this command.</param>
/// <param name="name">Name of this command.</param>
/// <param name="callback">Execution callback of this command.</param>
public ComponentCommandBuilder (ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }

/// <summary>
/// Adds a command parameter to the parameters collection.
/// </summary>
/// <param name="configure"><see cref="CommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public override ComponentCommandBuilder AddParameter (Action<CommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
}

internal override ComponentCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
new ComponentCommandInfo(this, module, commandService);
}
}

+ 76
- 0
src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs View File

@@ -0,0 +1,76 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ContextCommandInfo"/>.
/// </summary>
public sealed class ContextCommandBuilder : CommandBuilder<ContextCommandInfo, ContextCommandBuilder, CommandParameterBuilder>
{
protected override ContextCommandBuilder Instance => this;

/// <summary>
/// Gets the type of this command.
/// </summary>
public ApplicationCommandType CommandType { get; set; }

/// <summary>
/// Gets the default permission of this command.
/// </summary>
public bool DefaultPermission { get; set; } = true;

internal ContextCommandBuilder (ModuleBuilder module) : base(module) { }

/// <summary>
/// Initializes a new <see cref="ContextCommandBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this command.</param>
/// <param name="name">Name of this command.</param>
/// <param name="callback">Execution callback of this command.</param>
public ContextCommandBuilder (ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }

/// <summary>
/// Sets <see cref="CommandType"/>.
/// </summary>
/// <param name="commandType">New value of the <see cref="CommandType"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ContextCommandBuilder SetType (ApplicationCommandType commandType)
{
CommandType = commandType;
return this;
}

/// <summary>
/// Sets <see cref="DefaultPermission"/>.
/// </summary>
/// <param name="defaultPermision">New value of the <see cref="DefaultPermission"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ContextCommandBuilder SetDefaultPermission (bool defaultPermision)
{
DefaultPermission = defaultPermision;
return this;
}

/// <summary>
/// Adds a command parameter to the parameters collection.
/// </summary>
/// <param name="configure"><see cref="CommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public override ContextCommandBuilder AddParameter (Action<CommandParameterBuilder> configure)
{
var parameter = new CommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
}

internal override ContextCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
ContextCommandInfo.Create(this, module, commandService);
}
}

+ 111
- 0
src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represent a command builder for creating <see cref="ICommandInfo"/>.
/// </summary>
public interface ICommandBuilder
{
/// <summary>
/// Gets the execution delegate of this command.
/// </summary>
ExecuteCallback Callback { get; }

/// <summary>
/// Gets the parent module of this command.
/// </summary>
ModuleBuilder Module { get; }

/// <summary>
/// Gets the name of this command.
/// </summary>
string Name { get; }

/// <summary>
/// Gets or sets the method name of this command.
/// </summary>
string MethodName { get; set; }

/// <summary>
/// Gets or sets <see langword="true"/> if this command will be registered and executed as a standalone command, unaffected by the <see cref="GroupAttribute"/>s of
/// of the commands parents.
/// </summary>
bool IgnoreGroupNames { get; set; }

/// <summary>
/// Gets or sets the run mode this command gets executed with.
/// </summary>
RunMode RunMode { get; set; }

/// <summary>
/// Gets a collection of the attributes of this command.
/// </summary>
IReadOnlyList<Attribute> Attributes { get; }

/// <summary>
/// Gets a collection of the parameters of this command.
/// </summary>
IReadOnlyList<IParameterBuilder> Parameters { get; }

/// <summary>
/// Gets a collection of the preconditions of this command.
/// </summary>
IReadOnlyList<PreconditionAttribute> Preconditions { get; }

/// <summary>
/// Sets <see cref="Name"/>.
/// </summary>
/// <param name="name">New value of the <see cref="Name"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithName (string name);

/// <summary>
/// Sets <see cref="MethodName"/>.
/// </summary>
/// <param name="name">New value of the <see cref="MethodName"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithMethodName (string name);

/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithAttributes (params Attribute[] attributes);

/// <summary>
/// Sets <see cref="RunMode"/>.
/// </summary>
/// <param name="runMode">New value of the <see cref="RunMode"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder SetRunMode (RunMode runMode);

/// <summary>
/// Adds parameter builders to <see cref="Parameters"/>.
/// </summary>
/// <param name="parameters">New parameter builders to be added to <see cref="Parameters"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder AddParameters (params IParameterBuilder[] parameters);

/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>.
/// </summary>
/// <param name="preconditions">New preconditions to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithPreconditions (params PreconditionAttribute[] preconditions);
}
}

+ 76
- 0
src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs View File

@@ -0,0 +1,76 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="SlashCommandInfo"/>.
/// </summary>
public sealed class SlashCommandBuilder : CommandBuilder<SlashCommandInfo, SlashCommandBuilder, SlashCommandParameterBuilder>
{
protected override SlashCommandBuilder Instance => this;

/// <summary>
/// Gets and sets the description of this command.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets and sets the default permission of this command.
/// </summary>
public bool DefaultPermission { get; set; } = true;

internal SlashCommandBuilder (ModuleBuilder module) : base(module) { }

/// <summary>
/// Initializes a new <see cref="SlashCommandBuilder"/>.
/// </summary>
/// <param name="module">Parent module of this command.</param>
/// <param name="name">Name of this command.</param>
/// <param name="callback">Execution callback of this command.</param>
public SlashCommandBuilder (ModuleBuilder module, string name, ExecuteCallback callback) : base(module, name, callback) { }

/// <summary>
/// Sets <see cref="Description"/>.
/// </summary>
/// <param name="description">New value of the <see cref="Description"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandBuilder WithDescription (string description)
{
Description = description;
return this;
}

/// <summary>
/// Sets <see cref="DefaultPermission"/>.
/// </summary>
/// <param name="defaultPermision">New value of the <see cref="DefaultPermission"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
return Instance;
}

/// <summary>
/// Adds a command parameter to the parameters collection.
/// </summary>
/// <param name="configure"><see cref="SlashCommandParameterBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public override SlashCommandBuilder AddParameter (Action<SlashCommandParameterBuilder> configure)
{
var parameter = new SlashCommandParameterBuilder(this);
configure(parameter);
AddParameters(parameter);
return this;
}

internal override SlashCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
new SlashCommandInfo(this, module, commandService);
}
}

+ 279
- 0
src/Discord.Net.Interactions/Builders/ModuleBuilder.cs View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="ModuleInfo"/>.
/// </summary>
public class ModuleBuilder
{
private readonly List<Attribute> _attributes;
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<ModuleBuilder> _subModules;
private readonly List<SlashCommandBuilder> _slashCommands;
private readonly List<ContextCommandBuilder> _contextCommands;
private readonly List<ComponentCommandBuilder> _componentCommands;
private readonly List<AutocompleteCommandBuilder> _autocompleteCommands;

/// <summary>
/// Gets the underlying Interaction Service.
/// </summary>
public InteractionService InteractionService { get; }

/// <summary>
/// Gets the parent module if this module is a sub-module.
/// </summary>
public ModuleBuilder Parent { get; }

/// <summary>
/// Gets the name of this module.
/// </summary>
public string Name { get; internal set; }

/// <summary>
/// Gets and sets the group name of this module.
/// </summary>
public string SlashGroupName { get; set; }

/// <summary>
/// Gets whether this has a <see cref="GroupAttribute"/>.
/// </summary>
public bool IsSlashGroup => !string.IsNullOrEmpty(SlashGroupName);

/// <summary>
/// Gets and sets the description of this module.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets and sets the default permission of this module.
/// </summary>
public bool DefaultPermission { get; set; } = true;

/// <summary>
/// Gets and sets whether this has a <see cref="DontAutoRegisterAttribute"/>.
/// </summary>
public bool DontAutoRegister { get; set; } = false;

/// <summary>
/// Gets a collection of the attributes of this module.
/// </summary>
public IReadOnlyList<Attribute> Attributes => _attributes;

/// <summary>
/// Gets a collection of the preconditions of this module.
/// </summary>
public IReadOnlyCollection<PreconditionAttribute> Preconditions => _preconditions;

/// <summary>
/// Gets a collection of the sub-modules of this module.
/// </summary>
public IReadOnlyList<ModuleBuilder> SubModules => _subModules;

/// <summary>
/// Gets a collection of the Slash Commands of this module.
/// </summary>
public IReadOnlyList<SlashCommandBuilder> SlashCommands => _slashCommands;

/// <summary>
/// Gets a collection of the Context Commands of this module.
/// </summary>
public IReadOnlyList<ContextCommandBuilder> ContextCommands => _contextCommands;

/// <summary>
/// Gets a collection of the Component Commands of this module.
/// </summary>
public IReadOnlyList<ComponentCommandBuilder> ComponentCommands => _componentCommands;

/// <summary>
/// Gets a collection of the Autocomplete Commands of this module.
/// </summary>
public IReadOnlyList<AutocompleteCommandBuilder> AutocompleteCommands => _autocompleteCommands;

internal TypeInfo TypeInfo { get; set; }

internal ModuleBuilder (InteractionService interactionService, ModuleBuilder parent = null)
{
InteractionService = interactionService;
Parent = parent;

_attributes = new List<Attribute>();
_subModules = new List<ModuleBuilder>();
_slashCommands = new List<SlashCommandBuilder>();
_contextCommands = new List<ContextCommandBuilder>();
_componentCommands = new List<ComponentCommandBuilder>();
_autocompleteCommands = new List<AutocompleteCommandBuilder>();
_preconditions = new List<PreconditionAttribute>();
}

/// <summary>
/// Initializes a new <see cref="ModuleBuilder"/>.
/// </summary>
/// <param name="interactionService">The underlying Interaction Service.</param>
/// <param name="name">Name of this module.</param>
/// <param name="parent">Parent module of this sub-module.</param>
public ModuleBuilder (InteractionService interactionService, string name, ModuleBuilder parent = null) : this(interactionService, parent)
{
Name = name;
}

/// <summary>
/// Sets <see cref="SlashGroupName"/>.
/// </summary>
/// <param name="name">New value of the <see cref="SlashGroupName"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder WithGroupName (string name)
{
SlashGroupName = name;
return this;
}

/// <summary>
/// Sets <see cref="Description"/>.
/// </summary>
/// <param name="description">New value of the <see cref="Description"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder WithDescription (string description)
{
Description = description;
return this;
}

/// <summary>
/// Sets <see cref="DefaultPermission"/>.
/// </summary>
/// <param name="permission">New value of the <see cref="DefaultPermission"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder WithDefaultPermision (bool permission)
{
DefaultPermission = permission;
return this;
}

/// <summary>
/// Adds attributes to <see cref="Attributes"/>
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddAttributes (params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}

/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>
/// </summary>
/// <param name="preconditions">New preconditions to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddPreconditions (params PreconditionAttribute[] preconditions)
{
_preconditions.AddRange(preconditions);
return this;
}

/// <summary>
/// Adds slash command builder to <see cref="SlashCommands"/>
/// </summary>
/// <param name="configure"><see cref="SlashCommandBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddSlashCommand (Action<SlashCommandBuilder> configure)
{
var command = new SlashCommandBuilder(this);
configure(command);
_slashCommands.Add(command);
return this;
}

/// <summary>
/// Adds context command builder to <see cref="ContextCommands"/>
/// </summary>
/// <param name="configure"><see cref="ContextCommandBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddContextCommand (Action<ContextCommandBuilder> configure)
{
var command = new ContextCommandBuilder(this);
configure(command);
_contextCommands.Add(command);
return this;
}

/// <summary>
/// Adds component command builder to <see cref="ComponentCommands"/>
/// </summary>
/// <param name="configure"><see cref="ComponentCommandBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddComponentCommand (Action<ComponentCommandBuilder> configure)
{
var command = new ComponentCommandBuilder(this);
configure(command);
_componentCommands.Add(command);
return this;
}

/// <summary>
/// Adds autocomplete command builder to <see cref="AutocompleteCommands"/>
/// </summary>
/// <param name="configure"><see cref="AutocompleteCommands"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddAutocompleteCommand(Action<AutocompleteCommandBuilder> configure)
{
var command = new AutocompleteCommandBuilder(this);
configure(command);
_autocompleteCommands.Add(command);
return this;
}

/// <summary>
/// Adds sub-module builder to <see cref="SubModules"/>
/// </summary>
/// <param name="configure"><see cref="ModuleBuilder"/> factory.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder AddModule (Action<ModuleBuilder> configure)
{
var subModule = new ModuleBuilder(InteractionService, this);
configure(subModule);
_subModules.Add(subModule);
return this;
}

internal ModuleInfo Build (InteractionService interactionService, IServiceProvider services, ModuleInfo parent = null)
{
var moduleInfo = new ModuleInfo(this, interactionService, services, parent);

IInteractionModuleBase instance = ReflectionUtils<IInteractionModuleBase>.CreateObject(TypeInfo, interactionService, services);
try
{
instance.OnModuleBuilding(interactionService, moduleInfo);
}
finally
{
( instance as IDisposable )?.Dispose();
}

return moduleInfo;
}
}
}

+ 469
- 0
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

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

namespace Discord.Interactions.Builders
{
internal static class ModuleClassBuilder
{
private static readonly TypeInfo ModuleTypeInfo = typeof(IInteractionModuleBase).GetTypeInfo();

public const int MaxCommandDepth = 3;

public static async Task<IEnumerable<TypeInfo>> SearchAsync (Assembly assembly, InteractionService commandService)
{
static bool IsLoadableModule (TypeInfo info)
{
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<SlashCommandAttribute>() != null);
}

var result = new List<TypeInfo>();

foreach (var type in assembly.DefinedTypes)
{
if (( type.IsPublic || type.IsNestedPublic ) && IsValidModuleDefinition(type))
{
result.Add(type);
}
else if (IsLoadableModule(type))
{
await commandService._cmdLogger.WarningAsync($"Class {type.FullName} is not public and cannot be loaded.").ConfigureAwait(false);
}
}
return result;
}

public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync (IEnumerable<TypeInfo> validTypes, InteractionService commandService,
IServiceProvider services)
{
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));
var built = new List<TypeInfo>();

var result = new Dictionary<Type, ModuleInfo>();

foreach (var type in topLevelGroups)
{
var builder = new ModuleBuilder(commandService);

BuildModule(builder, type, commandService, services);
BuildSubModules(builder, type.DeclaredNestedTypes, built, commandService, services);
built.Add(type);

var moduleInfo = builder.Build(commandService, services);

result.Add(type.AsType(), moduleInfo);
}

await commandService._cmdLogger.DebugAsync($"Successfully built {built.Count} Slash Command modules.").ConfigureAwait(false);

return result;
}

private static void BuildModule (ModuleBuilder builder, TypeInfo typeInfo, InteractionService commandService,
IServiceProvider services)
{
var attributes = typeInfo.GetCustomAttributes();

builder.Name = typeInfo.Name;
builder.TypeInfo = typeInfo;

foreach (var attribute in attributes)
{
switch (attribute)
{
case GroupAttribute group:
{
builder.SlashGroupName = group.Name;
builder.Description = group.Description;
}
break;
case DefaultPermissionAttribute defPermission:
{
builder.DefaultPermission = defPermission.IsDefaultPermission;
}
break;
case PreconditionAttribute precondition:
builder.AddPreconditions(precondition);
break;
case DontAutoRegisterAttribute dontAutoRegister:
builder.DontAutoRegister = true;
break;
default:
builder.AddAttributes(attribute);
break;
}
}

var methods = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

var validSlashCommands = methods.Where(IsValidSlashCommandDefinition);
var validContextCommands = methods.Where(IsValidContextCommandDefinition);
var validInteractions = methods.Where(IsValidComponentCommandDefinition);
var validAutocompleteCommands = methods.Where(IsValidAutocompleteCommandDefinition);

Func<IServiceProvider, IInteractionModuleBase> createInstance = commandService._useCompiledLambda ?
ReflectionUtils<IInteractionModuleBase>.CreateLambdaBuilder(typeInfo, commandService) : ReflectionUtils<IInteractionModuleBase>.CreateBuilder(typeInfo, commandService);

foreach (var method in validSlashCommands)
builder.AddSlashCommand(x => BuildSlashCommand(x, createInstance, method, commandService, services));

foreach (var method in validContextCommands)
builder.AddContextCommand(x => BuildContextCommand(x, createInstance, method, commandService, services));

foreach (var method in validInteractions)
builder.AddComponentCommand(x => BuildComponentCommand(x, createInstance, method, commandService, services));

foreach(var method in validAutocompleteCommands)
builder.AddAutocompleteCommand(x => BuildAutocompleteCommand(x, createInstance, method, commandService, services));
}

private static void BuildSubModules (ModuleBuilder parent, IEnumerable<TypeInfo> subModules, IList<TypeInfo> builtTypes, InteractionService commandService,
IServiceProvider services, int slashGroupDepth = 0)
{
foreach (var submodule in subModules.Where(IsValidModuleDefinition))
{
if (builtTypes.Contains(submodule))
continue;

parent.AddModule((builder) =>
{
BuildModule(builder, submodule, commandService, services);

if (slashGroupDepth >= MaxCommandDepth - 1)
throw new InvalidOperationException($"Slash Commands only support {MaxCommandDepth - 1} command prefixes for sub-commands");

BuildSubModules(builder, submodule.DeclaredNestedTypes, builtTypes, commandService, services, builder.IsSlashGroup ? slashGroupDepth + 1 : slashGroupDepth);
});
builtTypes.Add(submodule);
}
}

private static void BuildSlashCommand (SlashCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;

foreach (var attribute in attributes)
{
switch (attribute)
{
case SlashCommandAttribute command:
{
builder.Name = command.Name;
builder.Description = command.Description;
builder.IgnoreGroupNames = command.IgnoreGroupNames;
builder.RunMode = command.RunMode;
}
break;
case DefaultPermissionAttribute defaultPermission:
{
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
default:
builder.WithAttributes(attribute);
break;
}
}

var parameters = methodInfo.GetParameters();

foreach (var parameter in parameters)
builder.AddParameter(x => BuildSlashParameter(x, parameter, services));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}

private static void BuildContextCommand (ContextCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;

foreach (var attribute in attributes)
{
switch (attribute)
{
case ContextCommandAttribute command:
{
builder.Name = command.Name;
builder.CommandType = command.CommandType;
builder.RunMode = command.RunMode;

command.CheckMethodDefinition(methodInfo);
}
break;
case DefaultPermissionAttribute defaultPermission:
{
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
default:
builder.WithAttributes(attribute);
break;
}
}

var parameters = methodInfo.GetParameters();

foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}

private static void BuildComponentCommand (ComponentCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[])))
throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}");

var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;

foreach (var attribute in attributes)
{
switch (attribute)
{
case ComponentInteractionAttribute interaction:
{
builder.Name = interaction.CustomId;
builder.RunMode = interaction.RunMode;
builder.IgnoreGroupNames = interaction.IgnoreGroupNames;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
default:
builder.WithAttributes(attribute);
break;
}
}

var parameters = methodInfo.GetParameters();

foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}

private static void BuildAutocompleteCommand(AutocompleteCommandBuilder builder, Func<IServiceProvider, IInteractionModuleBase> createInstance, MethodInfo methodInfo,
InteractionService commandService, IServiceProvider services)
{
var attributes = methodInfo.GetCustomAttributes();

builder.MethodName = methodInfo.Name;

foreach(var attribute in attributes)
{
switch (attribute)
{
case AutocompleteCommandAttribute autocomplete:
{
builder.ParameterName = autocomplete.ParameterName;
builder.CommandName = autocomplete.CommandName;
builder.Name = autocomplete.CommandName + " " + autocomplete.ParameterName;
builder.RunMode = autocomplete.RunMode;
}
break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
default:
builder.WithAttributes(attribute);
break;
}
}

var parameters = methodInfo.GetParameters();

foreach (var parameter in parameters)
builder.AddParameter(x => BuildParameter(x, parameter));

builder.Callback = CreateCallback(createInstance, methodInfo, commandService);
}

private static ExecuteCallback CreateCallback (Func<IServiceProvider, IInteractionModuleBase> createInstance,
MethodInfo methodInfo, InteractionService commandService)
{
Func<IInteractionModuleBase, object[], Task> commandInvoker = commandService._useCompiledLambda ?
ReflectionUtils<IInteractionModuleBase>.CreateMethodInvoker(methodInfo) : (module, args) => methodInfo.Invoke(module, args) as Task;

async Task<IResult> ExecuteCallback (IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo)
{
var instance = createInstance(serviceProvider);
instance.SetContext(context);

try
{
instance.BeforeExecute(commandInfo);
var task = commandInvoker(instance, args) ?? Task.Delay(0);

if (task is Task<RuntimeResult> runtimeTask)
{
return await runtimeTask.ConfigureAwait(false);
}
else
{
await task.ConfigureAwait(false);
return ExecuteResult.FromSuccess();

}
}
catch (Exception ex)
{
await commandService._cmdLogger.ErrorAsync(ex).ConfigureAwait(false);
return ExecuteResult.FromError(ex);
}
finally
{
instance.AfterExecute(commandInfo);
( instance as IDisposable )?.Dispose();
}
}

return ExecuteCallback;
}

#region Parameters
private static void BuildSlashParameter (SlashCommandParameterBuilder builder, ParameterInfo paramInfo, IServiceProvider services)
{
var attributes = paramInfo.GetCustomAttributes();
var paramType = paramInfo.ParameterType;

builder.Name = paramInfo.Name;
builder.Description = paramInfo.Name;
builder.IsRequired = !paramInfo.IsOptional;
builder.DefaultValue = paramInfo.DefaultValue;
builder.SetParameterType(paramType, services);

foreach (var attribute in attributes)
{
switch (attribute)
{
case SummaryAttribute description:
{
if (!string.IsNullOrEmpty(description.Name))
builder.Name = description.Name;

if (!string.IsNullOrEmpty(description.Description))
builder.Description = description.Description;
}
break;
case ChoiceAttribute choice:
builder.WithChoices(new ParameterChoice(choice.Name, choice.Value));
break;
case ParamArrayAttribute _:
builder.IsParameterArray = true;
break;
case ParameterPreconditionAttribute precondition:
builder.AddPreconditions(precondition);
break;
case ChannelTypesAttribute channelTypes:
builder.WithChannelTypes(channelTypes.ChannelTypes);
break;
case AutocompleteAttribute autocomplete:
builder.Autocomplete = true;
if(autocomplete.AutocompleteHandlerType is not null)
builder.WithAutocompleteHandler(autocomplete.AutocompleteHandlerType, services);
break;
case MaxValueAttribute maxValue:
builder.MaxValue = maxValue.Value;
break;
case MinValueAttribute minValue:
builder.MinValue = minValue.Value;
break;
default:
builder.AddAttributes(attribute);
break;
}
}

// Replace pascal casings with '-'
builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower();
}

private static void BuildParameter (CommandParameterBuilder builder, ParameterInfo paramInfo)
{
var attributes = paramInfo.GetCustomAttributes();
var paramType = paramInfo.ParameterType;

builder.Name = paramInfo.Name;
builder.IsRequired = !paramInfo.IsOptional;
builder.DefaultValue = paramInfo.DefaultValue;
builder.SetParameterType(paramType);

foreach (var attribute in attributes)
{
switch (attribute)
{
case ParameterPreconditionAttribute precondition:
builder.AddPreconditions(precondition);
break;
case ParamArrayAttribute _:
builder.IsParameterArray = true;
break;
default:
builder.AddAttributes(attribute);
break;
}
}
}
#endregion

internal static bool IsValidModuleDefinition (TypeInfo typeInfo)
{
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
!typeInfo.IsAbstract &&
!typeInfo.ContainsGenericParameters;
}

private static bool IsValidSlashCommandDefinition (MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(SlashCommandAttribute)) &&
( methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>) ) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
}

private static bool IsValidContextCommandDefinition (MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(ContextCommandAttribute)) &&
( methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>) ) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
}

private static bool IsValidComponentCommandDefinition (MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(ComponentInteractionAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
}

private static bool IsValidAutocompleteCommandDefinition (MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(AutocompleteCommandAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod &&
methodInfo.GetParameters().Length == 0;
}
}
}

+ 25
- 0
src/Discord.Net.Interactions/Builders/Parameters/CommandParameterBuilder.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="CommandParameterInfo"/>.
/// </summary>
public sealed class CommandParameterBuilder : ParameterBuilder<CommandParameterInfo, CommandParameterBuilder>
{
protected override CommandParameterBuilder Instance => this;

internal CommandParameterBuilder (ICommandBuilder command) : base(command) { }

/// <summary>
/// Initializes a new <see cref="CommandParameterInfo"/>.
/// </summary>
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public CommandParameterBuilder (ICommandBuilder command, string name, Type type) : base(command, name, type) { }

internal override CommandParameterInfo Build (ICommandInfo command) =>
new CommandParameterInfo(this, command);
}
}

+ 105
- 0
src/Discord.Net.Interactions/Builders/Parameters/IParameterBuilder.cs View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represent a command builder for creating <see cref="IParameterInfo"/>.
/// </summary>
public interface IParameterBuilder
{
/// <summary>
/// Gets the parent command of this parameter.
/// </summary>
ICommandBuilder Command { get; }

/// <summary>
/// Gets the name of this parameter.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the type of this parameter.
/// </summary>
Type ParameterType { get; }

/// <summary>
/// Gets whether this parameter is required.
/// </summary>
bool IsRequired { get; }

/// <summary>
/// Gets whether this parameter is <see langword="params"/>.
/// </summary>
bool IsParameterArray { get; }

/// <summary>
/// Gets the deafult value of this parameter.
/// </summary>
object DefaultValue { get; }

/// <summary>
/// Gets a collection of the attributes of this command.
/// </summary>
IReadOnlyCollection<Attribute> Attributes { get; }

/// <summary>
/// Gets a collection of the preconditions of this command.
/// </summary>
IReadOnlyCollection<ParameterPreconditionAttribute> Preconditions { get; }

/// <summary>
/// Sets <see cref="Name"/>.
/// </summary>
/// <param name="name">New value of the <see cref="Name"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder WithName (string name);

/// <summary>
/// Sets <see cref="ParameterType"/>.
/// </summary>
/// <param name="type">New value of the <see cref="ParameterType"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder SetParameterType (Type type);

/// <summary>
/// Sets <see cref="IsRequired"/>.
/// </summary>
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder SetRequired (bool isRequired);

/// <summary>
/// Sets <see cref="DefaultValue"/>.
/// </summary>
/// <param name="defaultValue">New value of the <see cref="DefaultValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder SetDefaultValue (object defaultValue);

/// <summary>
/// Adds attributes to <see cref="Attributes"/>.
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder AddAttributes (params Attribute[] attributes);

/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>.
/// </summary>
/// <param name="preconditions">New attributes to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
IParameterBuilder AddPreconditions (params ParameterPreconditionAttribute[] preconditions);
}
}

+ 162
- 0
src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs View File

@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents the base builder class for creating <see cref="IParameterInfo"/>.
/// </summary>
/// <typeparam name="TInfo">The <see cref="IParameterInfo"/> this builder yields when built.</typeparam>
/// <typeparam name="TBuilder">Inherited <see cref="ParameterBuilder{TInfo, TBuilder}"/> type.</typeparam>
public abstract class ParameterBuilder<TInfo, TBuilder> : IParameterBuilder
where TInfo : class, IParameterInfo
where TBuilder : ParameterBuilder<TInfo, TBuilder>
{
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;

/// <inheritdoc/>
public ICommandBuilder Command { get; }

/// <inheritdoc/>
public string Name { get; internal set; }

/// <inheritdoc/>
public Type ParameterType { get; private set; }

/// <inheritdoc/>
public bool IsRequired { get; set; } = true;

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

/// <inheritdoc/>
public object DefaultValue { get; set; }

/// <inheritdoc/>
public IReadOnlyCollection<Attribute> Attributes => _attributes;

/// <inheritdoc/>
public IReadOnlyCollection<ParameterPreconditionAttribute> Preconditions => _preconditions;
protected abstract TBuilder Instance { get; }

internal ParameterBuilder (ICommandBuilder command)
{
_attributes = new List<Attribute>();
_preconditions = new List<ParameterPreconditionAttribute>();

Command = command;
}

protected ParameterBuilder (ICommandBuilder command, string name, Type type) : this(command)
{
Name = name;
SetParameterType(type);
}

/// <summary>
/// Sets <see cref="Name"/>.
/// </summary>
/// <param name="name">New value of the <see cref="Name"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder WithName (string name)
{
Name = name;
return Instance;
}

/// <summary>
/// Sets <see cref="ParameterType"/>.
/// </summary>
/// <param name="type">New value of the <see cref="ParameterType"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder SetParameterType (Type type)
{
ParameterType = type;
return Instance;
}

/// <summary>
/// Sets <see cref="IsRequired"/>.
/// </summary>
/// <param name="isRequired">New value of the <see cref="IsRequired"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder SetRequired (bool isRequired)
{
IsRequired = isRequired;
return Instance;
}

/// <summary>
/// Sets <see cref="DefaultValue"/>.
/// </summary>
/// <param name="defaultValue">New value of the <see cref="DefaultValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder SetDefaultValue (object defaultValue)
{
DefaultValue = defaultValue;
return Instance;
}

/// <summary>
/// Adds attributes to <see cref="Attributes"/>
/// </summary>
/// <param name="attributes">New attributes to be added to <see cref="Attributes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder AddAttributes (params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return Instance;
}

/// <summary>
/// Adds preconditions to <see cref="Preconditions"/>
/// </summary>
/// <param name="preconditions">New attributes to be added to <see cref="Preconditions"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public virtual TBuilder AddPreconditions (params ParameterPreconditionAttribute[] attributes)
{
_preconditions.AddRange(attributes);
return Instance;
}

internal abstract TInfo Build (ICommandInfo command);

//IParameterBuilder
/// <inheritdoc/>
IParameterBuilder IParameterBuilder.WithName (string name) =>
WithName(name);

/// <inheritdoc/>
IParameterBuilder IParameterBuilder.SetParameterType (Type type) =>
SetParameterType(type);

/// <inheritdoc/>
IParameterBuilder IParameterBuilder.SetRequired (bool isRequired) =>
SetRequired(isRequired);

/// <inheritdoc/>
IParameterBuilder IParameterBuilder.SetDefaultValue (object defaultValue) =>
SetDefaultValue(defaultValue);

/// <inheritdoc/>
IParameterBuilder IParameterBuilder.AddAttributes (params Attribute[] attributes) =>
AddAttributes(attributes);

/// <inheritdoc/>
IParameterBuilder IParameterBuilder.AddPreconditions (params ParameterPreconditionAttribute[] preconditions) =>
AddPreconditions(preconditions);
}
}

+ 178
- 0
src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;

namespace Discord.Interactions.Builders
{
/// <summary>
/// Represents a builder for creating <see cref="SlashCommandParameterInfo"/>.
/// </summary>
public sealed class SlashCommandParameterBuilder : ParameterBuilder<SlashCommandParameterInfo, SlashCommandParameterBuilder>
{
private readonly List<ParameterChoice> _choices = new();
private readonly List<ChannelType> _channelTypes = new();

/// <summary>
/// Gets or sets the description of this parameter.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets the max value of this parameter.
/// </summary>
public double? MaxValue { get; set; }

/// <summary>
/// Gets or sets the min value of this parameter.
/// </summary>
public double? MinValue { get; set; }

/// <summary>
/// Gets a collection of the choices of this command.
/// </summary>
public IReadOnlyCollection<ParameterChoice> Choices => _choices;

/// <summary>
/// Gets a collection of the channel types of this command.
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes => _channelTypes;

/// <summary>
/// Gets or sets whether this parameter should be configured for Autocomplete Interactions.
/// </summary>
public bool Autocomplete { get; set; }

/// <summary>
/// Gets or sets the <see cref="TypeConverter"/> of this parameter.
/// </summary>
public TypeConverter TypeConverter { get; private set; }

/// <summary>
/// Gets or sets the <see cref="IAutocompleteHandler"/> of this parameter.
/// </summary>
public IAutocompleteHandler AutocompleteHandler { get; set; }
protected override SlashCommandParameterBuilder Instance => this;

internal SlashCommandParameterBuilder(ICommandBuilder command) : base(command) { }

/// <summary>
/// Initializes a new <see cref="SlashCommandParameterBuilder"/>.
/// </summary>
/// <param name="command">Parent command of this parameter.</param>
/// <param name="name">Name of this command.</param>
/// <param name="type">Type of this parameter.</param>
public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { }

/// <summary>
/// Sets <see cref="Description"/>.
/// </summary>
/// <param name="description">New value of the <see cref="Description"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithDescription(string description)
{
Description = description;
return this;
}

/// <summary>
/// Sets <see cref="MinValue"/>.
/// </summary>
/// <param name="value">New value of the <see cref="MinValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithMinValue(double value)
{
MinValue = value;
return this;
}

/// <summary>
/// Sets <see cref="MaxValue"/>.
/// </summary>
/// <param name="value">New value of the <see cref="MaxValue"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithMaxValue(double value)
{
MaxValue = value;
return this;
}

/// <summary>
/// Adds parameter choices to <see cref="Choices"/>.
/// </summary>
/// <param name="options">New choices to be added to <see cref="Choices"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithChoices(params ParameterChoice[] options)
{
_choices.AddRange(options);
return this;
}

/// <summary>
/// Adds channel types to <see cref="ChannelTypes"/>.
/// </summary>
/// <param name="channelTypes">New channel types to be added to <see cref="ChannelTypes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithChannelTypes(params ChannelType[] channelTypes)
{
_channelTypes.AddRange(channelTypes);
return this;
}

/// <summary>
/// Adds channel types to <see cref="ChannelTypes"/>.
/// </summary>
/// <param name="channelTypes">New channel types to be added to <see cref="ChannelTypes"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithChannelTypes(IEnumerable<ChannelType> channelTypes)
{
_channelTypes.AddRange(channelTypes);
return this;
}

/// <summary>
/// Sets <see cref="AutocompleteHandler"/>.
/// </summary>
/// <param name="autocompleteHandlerType">Type of the <see cref="IAutocompleteHandler"/>.</param>
/// <param name="services">Service container to be used to resolve the dependencies of this parameters <see cref="TypeConverter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder WithAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
{
AutocompleteHandler = Command.Module.InteractionService.GetAutocompleteHandler(autocompleteHandlerType, services);
return this;
}

/// <inheritdoc/>
public override SlashCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null);

/// <summary>
/// Sets <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.
/// </summary>
/// <param name="type">New value of the <see cref="ParameterBuilder{TInfo, TBuilder}.ParameterType"/>.</param>
/// <param name="services">Service container to be used to resolve the dependencies of this parameters <see cref="TypeConverter"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null)
{
base.SetParameterType(type);
TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services);
return this;
}

internal override SlashCommandParameterInfo Build(ICommandInfo command) =>
new SlashCommandParameterInfo(this, command as SlashCommandInfo);
}
}

+ 25
- 0
src/Discord.Net.Interactions/Discord.Net.Interactions.csproj View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<RootNamespace>Discord.Interactions</RootNamespace>
<AssemblyName>Discord.Net.Interactions</AssemblyName>
<PackageId>Discord.Net.Labs.Interactions</PackageId>
<Description>A Discord.Net extension adding support for Application Commands.</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>

</Project>

+ 24
- 0
src/Discord.Net.Interactions/Entities/ParameterChoice.cs View File

@@ -0,0 +1,24 @@
namespace Discord.Interactions
{
/// <summary>
/// Represents a Slash Command parameter choice.
/// </summary>
public class ParameterChoice
{
/// <summary>
/// Gets the name of the choice.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the value of the choice.
/// </summary>
public object Value { get; }

internal ParameterChoice (string name, object value)
{
Name = name;
Value = value;
}
}
}

+ 21
- 0
src/Discord.Net.Interactions/Entities/SlashCommandChoiceType.cs View File

@@ -0,0 +1,21 @@
namespace Discord.Interactions
{
/// <summary>
/// Supported types of pre-defined parameter choices.
/// </summary>
public enum SlashCommandChoiceType
{
/// <summary>
/// Discord type for <see cref="string"/>.
/// </summary>
String,
/// <summary>
/// Discord type for <see cref="int"/>.
/// </summary>
Integer,
/// <summary>
/// Discord type for <see cref="double"/>.
/// </summary>
Number
}
}

+ 34
- 0
src/Discord.Net.Interactions/Extensions/AutocompleteOptionComparer.cs View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;

namespace Discord.Interactions
{
internal class AutocompleteOptionComparer : IComparer<ApplicationCommandOptionType>
{
public int Compare(ApplicationCommandOptionType x, ApplicationCommandOptionType y)
{
if (x == ApplicationCommandOptionType.SubCommandGroup)
{
if (y == ApplicationCommandOptionType.SubCommandGroup)
return 0;
else
return 1;
}
else if (x == ApplicationCommandOptionType.SubCommand)
{
if (y == ApplicationCommandOptionType.SubCommandGroup)
return -1;
else if (y == ApplicationCommandOptionType.SubCommand)
return 0;
else
return 1;
}
else
{
if (y == ApplicationCommandOptionType.SubCommand || y == ApplicationCommandOptionType.SubCommandGroup)
return -1;
else
return 0;
}
}
}
}

+ 53
- 0
src/Discord.Net.Interactions/Extensions/WebSocketExtensions.cs View File

@@ -0,0 +1,53 @@
using Discord.Rest;
using System.Collections.Generic;
using System.Linq;

namespace Discord.WebSocket
{
internal static class WebSocketExtensions
{
/// <summary>
/// Get the name of the executed command and its parents in hierarchical order.
/// </summary>
/// <param name="data"></param>
/// <returns>
/// The name of the executed command and its parents in hierarchical order.
/// </returns>
public static IList<string> GetCommandKeywords(this IApplicationCommandInteractionData data)
{
var keywords = new List<string> { data.Name };

var child = data.Options?.ElementAtOrDefault(0);

while (child?.Type == ApplicationCommandOptionType.SubCommandGroup || child?.Type == ApplicationCommandOptionType.SubCommand)
{
keywords.Add(child.Name);
child = child.Options?.ElementAtOrDefault(0);
}

return keywords;
}

/// <summary>
/// Get the name of the executed command and its parents in hierarchical order.
/// </summary>
/// <param name="data"></param>
/// <returns>
/// The name of the executed command and its parents in hierarchical order.
/// </returns>
public static IList<string> GetCommandKeywords(this IAutocompleteInteractionData data)
{
var keywords = new List<string> { data.CommandName };

var group = data.Options?.FirstOrDefault(x => x.Type == ApplicationCommandOptionType.SubCommandGroup);
if (group is not null)
keywords.Add(group.Name);

var subcommand = data.Options?.FirstOrDefault(x => x.Type == ApplicationCommandOptionType.SubCommand);
if (subcommand is not null)
keywords.Add(subcommand.Name);

return keywords;
}
}
}

+ 33
- 0
src/Discord.Net.Interactions/IInteractionModuleBase.cs View File

@@ -0,0 +1,33 @@
namespace Discord.Interactions
{
/// <summary>
/// Represents a generic interaction module base.
/// </summary>
public interface IInteractionModuleBase
{
/// <summary>
/// Sets the context of this module.
/// </summary>
/// <param name="context"></param>
void SetContext (IInteractionContext context);

/// <summary>
/// Method body to be executed before executing an application command.
/// </summary>
/// <param name="command">Command information related to the Discord Application Command.</param>
void BeforeExecute (ICommandInfo command);

/// <summary>
/// Method body to be executed after an application command execution.
/// </summary>
/// <param name="command">Command information related to the Discord Application Command.</param>
void AfterExecute (ICommandInfo command);

/// <summary>
/// Method body to be executed before the derived module is built.
/// </summary>
/// <param name="commandService">Command Service instance that built this module.</param>
/// <param name="module">Info class of this module.</param>
void OnModuleBuilding (InteractionService commandService, ModuleInfo module);
}
}

+ 89
- 0
src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs View File

@@ -0,0 +1,89 @@
using Discord.Interactions.Builders;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the info class of an attribute based method for handling Autocomplete Interaction events.
/// </summary>
public sealed class AutocompleteCommandInfo : CommandInfo<CommandParameterInfo>
{
/// <summary>
/// Gets the name of the target parameter.
/// </summary>
public string ParameterName { get; }

/// <summary>
/// Gets the name of the target command.
/// </summary>
public string CommandName { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;

internal AutocompleteCommandInfo(AutocompleteCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
ParameterName = builder.ParameterName;
CommandName = builder.CommandName;
}

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
if (context.Interaction is not IAutocompleteInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction");

try
{
return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

/// <inheritdoc/>
protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) =>
CommandService._autocompleteCommandExecutedEvent.InvokeAsync(this, context, result);

/// <inheritdoc/>
protected override string GetLogString(IInteractionContext context)
{
if (context.Guild != null)
return $"Autocomplete Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
else
return $"Autocomplete Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}

internal IList<string> GetCommandKeywords()
{
var keywords = new List<string>() { ParameterName, CommandName };

if(!IgnoreGroupNames)
{
var currentParent = Module;

while (currentParent != null)
{
if (!string.IsNullOrEmpty(currentParent.SlashGroupName))
keywords.Add(currentParent.SlashGroupName);

currentParent = currentParent.Parent;
}
}

keywords.Reverse();

return keywords;
}
}
}

+ 247
- 0
src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs View File

@@ -0,0 +1,247 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents a cached method execution delegate.
/// </summary>
/// <param name="context">Execution context that will be injected into the module class.</param>
/// <param name="args">Method arguments array.</param>
/// <param name="serviceProvider">Service collection for initializing the module.</param>
/// <param name="commandInfo">Command info class of the executed method.</param>
/// <returns>
/// A task representing the execution operation.
/// </returns>
public delegate Task ExecuteCallback (IInteractionContext context, object[] args, IServiceProvider serviceProvider, ICommandInfo commandInfo);

/// <summary>
/// The base information class for <see cref="InteractionService"/> commands.
/// </summary>
/// <typeparam name="TParameter">The type of <see cref="IParameterInfo"/> that is used by this command type.</typeparam>
public abstract class CommandInfo<TParameter> : ICommandInfo where TParameter : class, IParameterInfo
{
private readonly ExecuteCallback _action;
private readonly ILookup<string, PreconditionAttribute> _groupedPreconditions;

/// <inheritdoc/>
public ModuleInfo Module { get; }

/// <inheritdoc/>
public InteractionService CommandService { get; }

/// <inheritdoc/>
public string Name { get; }

/// <inheritdoc/>
public string MethodName { get; }

/// <inheritdoc/>
public virtual bool IgnoreGroupNames { get; }

/// <inheritdoc/>
public abstract bool SupportsWildCards { get; }

/// <inheritdoc/>
public bool IsTopLevelCommand => IgnoreGroupNames || !Module.IsTopLevelGroup;

/// <inheritdoc/>
public RunMode RunMode { get; }

/// <inheritdoc/>
public IReadOnlyCollection<Attribute> Attributes { get; }

/// <inheritdoc/>
public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; }

/// <inheritdoc cref="ICommandInfo.Parameters"/>
public abstract IReadOnlyCollection<TParameter> Parameters { get; }

internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
{
CommandService = commandService;
Module = module;

Name = builder.Name;
MethodName = builder.MethodName;
IgnoreGroupNames = builder.IgnoreGroupNames;
RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode;
Attributes = builder.Attributes.ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray();

_action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
}

/// <inheritdoc/>
public abstract Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services);
protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result);
protected abstract string GetLogString(IInteractionContext context);

/// <inheritdoc/>
public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services)
{
async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type)
{
foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions)
{
if (preconditionGroup.Key == null)
{
foreach (PreconditionAttribute precondition in preconditionGroup)
{
var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}
}
else
{
var results = new List<PreconditionResult>();
foreach (PreconditionAttribute precondition in preconditionGroup)
results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false));

if (!results.Any(p => p.IsSuccess))
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
}
}
return PreconditionGroupResult.FromSuccess();
}

var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false);
if (!moduleResult.IsSuccess)
return moduleResult;

var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
if (!commandResult.IsSuccess)
return commandResult;

return PreconditionResult.FromSuccess();
}

protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services)
{
switch (RunMode)
{
case RunMode.Sync:
{
using var scope = services?.CreateScope();
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
}
case RunMode.Async:
_ = Task.Run(async () =>
{
using var scope = services?.CreateScope();
await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
});
break;
default:
throw new InvalidOperationException($"RunMode {RunMode} is not supported.");
}

return ExecuteResult.FromSuccess();
}

private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, object[] args, IServiceProvider services)
{
await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false);

try
{
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false);
return preconditionResult;
}

var index = 0;
foreach (var parameter in Parameters)
{
var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false);
if (!result.IsSuccess)
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}
}

var task = _action(context, args, services, this);

if (task is Task<IResult> resultTask)
{
var result = await resultTask.ConfigureAwait(false);
await InvokeModuleEvent(context, result).ConfigureAwait(false);
if (result is RuntimeResult || result is ExecuteResult)
return result;
}
else
{
await task.ConfigureAwait(false);
var result = ExecuteResult.FromSuccess();
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;
}

var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason");
await InvokeModuleEvent(context, failResult).ConfigureAwait(false);
return failResult;
}
catch (Exception ex)
{
var originalEx = ex;
while (ex is TargetInvocationException)
ex = ex.InnerException;

await Module.CommandService._cmdLogger.ErrorAsync(ex).ConfigureAwait(false);

var result = ExecuteResult.FromError(ex);
await InvokeModuleEvent(context, result).ConfigureAwait(false);

if (Module.CommandService._throwOnError)
{
if (ex == originalEx)
throw;
else
ExceptionDispatchInfo.Capture(ex).Throw();
}

return result;
}
finally
{
await CommandService._cmdLogger.VerboseAsync($"Executed {GetLogString(context)}").ConfigureAwait(false);
}
}

// ICommandInfo

/// <inheritdoc/>
IReadOnlyCollection<IParameterInfo> ICommandInfo.Parameters => Parameters;

/// <inheritdoc/>
public override string ToString()
{
StringBuilder builder = new();

var currentParent = Module;

while (currentParent != null)
{
if (currentParent.IsSlashGroup)
builder.AppendFormat(" {0}", currentParent.SlashGroupName);

currentParent = currentParent.Parent;
}
builder.AppendFormat(" {0}", Name);

return builder.ToString();
}
}
}

+ 132
- 0
src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs View File

@@ -0,0 +1,132 @@
using Discord.Interactions.Builders;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the info class of an attribute based method for handling Component Interaction events.
/// </summary>
public class ComponentCommandInfo : CommandInfo<CommandParameterInfo>
{
/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => true;

internal ComponentCommandInfo(ComponentCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
}

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
=> await ExecuteAsync(context, services, null).ConfigureAwait(false);

/// <summary>
/// Execute this command using dependency injection.
/// </summary>
/// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
/// <returns>
/// A task representing the asyncronous command execution process.
/// </returns>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
{
if (context.Interaction is not IComponentInteraction componentInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction");

var args = new List<string>();

if (additionalArgs is not null)
args.AddRange(additionalArgs);

if (componentInteraction.Data?.Values is not null)
args.AddRange(componentInteraction.Data.Values);

return await ExecuteAsync(context, Parameters, args, services);
}

/// <inheritdoc/>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> values,
IServiceProvider services)
{
if (context.Interaction is not SocketMessageComponent messageComponent)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction");

try
{
var strCount = Parameters.Count(x => x.ParameterType == typeof(string));

if (strCount > values?.Count())
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters");

var componentValues = messageComponent.Data?.Values;

var args = new object[Parameters.Count];

if (componentValues is not null)
{
if (Parameters.Last().ParameterType == typeof(string[]))
args[args.Length - 1] = componentValues.ToArray();
else
return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter");
}

for (var i = 0; i < strCount; i++)
args[i] = values.ElementAt(i);

return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

private static object[] GenerateArgs(IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> argList)
{
var result = new object[paramList.Count()];

for (var i = 0; i < paramList.Count(); i++)
{
var parameter = paramList.ElementAt(i);

if (argList?.ElementAt(i) == null)
{
if (!parameter.IsRequired)
result[i] = parameter.DefaultValue;
else
throw new InvalidOperationException($"Component Interaction handler is executed with too few args.");
}
else if (parameter.IsParameterArray)
{
string[] paramArray = new string[argList.Count() - i];
argList.ToArray().CopyTo(paramArray, i);
result[i] = paramArray;
}
else
result[i] = argList?.ElementAt(i);
}

return result;
}

protected override Task InvokeModuleEvent(IInteractionContext context, IResult result)
=> CommandService._componentCommandExecutedEvent.InvokeAsync(this, context, result);

protected override string GetLogString(IInteractionContext context)
{
if (context.Guild != null)
return $"Component Interaction: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
else
return $"Component Interaction: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}
}
}

+ 51
- 0
src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Base information class for attribute based context command handlers.
/// </summary>
public abstract class ContextCommandInfo : CommandInfo<CommandParameterInfo>, IApplicationCommandInfo
{
/// <inheritdoc/>
public ApplicationCommandType CommandType { get; }

/// <inheritdoc/>
public bool DefaultPermission { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;

/// <inheritdoc/>
public override bool IgnoreGroupNames => true;

internal ContextCommandInfo (Builders.ContextCommandBuilder builder, ModuleInfo module, InteractionService commandService)
: base(builder, module, commandService)
{
CommandType = builder.CommandType;
DefaultPermission = builder.DefaultPermission;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
}

internal static ContextCommandInfo Create (Builders.ContextCommandBuilder builder, ModuleInfo module, InteractionService commandService)
{
return builder.CommandType switch
{
ApplicationCommandType.User => new UserCommandInfo(builder, module, commandService),
ApplicationCommandType.Message => new MessageCommandInfo(builder, module, commandService),
_ => throw new InvalidOperationException("This command type is not a supported Context Command"),
};
}

/// <inheritdoc/>
protected override Task InvokeModuleEvent (IInteractionContext context, IResult result)
=> CommandService._contextCommandExecutedEvent.InvokeAsync(this, context, result);
}
}

+ 41
- 0
src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs View File

@@ -0,0 +1,41 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the info class of an attribute based method for command type <see cref="ApplicationCommandType.Message"/>.
/// </summary>
public class MessageCommandInfo : ContextCommandInfo
{
internal MessageCommandInfo(Builders.ContextCommandBuilder builder, ModuleInfo module, InteractionService commandService)
: base(builder, module, commandService) { }

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
if (context.Interaction is not IMessageCommandInteraction messageCommand)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation");

try
{
object[] args = new object[1] { messageCommand.Data.Message };

return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

/// <inheritdoc/>
protected override string GetLogString(IInteractionContext context)
{
if (context.Guild != null)
return $"Message Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
else
return $"Message Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}
}
}

+ 41
- 0
src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs View File

@@ -0,0 +1,41 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the info class of an attribute based method for command type <see cref="ApplicationCommandType.User"/>.
/// </summary>
public class UserCommandInfo : ContextCommandInfo
{
internal UserCommandInfo(Builders.ContextCommandBuilder builder, ModuleInfo module, InteractionService commandService)
: base(builder, module, commandService) { }

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
if (context.Interaction is not IUserCommandInteraction userCommand)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation");

try
{
object[] args = new object[1] { userCommand.Data.User };

return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

/// <inheritdoc/>
protected override string GetLogString(IInteractionContext context)
{
if (context.Guild != null)
return $"User Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
else
return $"User Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}
}
}

+ 112
- 0
src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs View File

@@ -0,0 +1,112 @@
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the info class of an attribute based method for command type <see cref="ApplicationCommandType.Slash"/>.
/// </summary>
public class SlashCommandInfo : CommandInfo<SlashCommandParameterInfo>, IApplicationCommandInfo
{
/// <summary>
/// Gets the command description that will be displayed on Discord.
/// </summary>
public string Description { get; }

/// <inheritdoc/>
public ApplicationCommandType CommandType { get; } = ApplicationCommandType.Slash;

/// <inheritdoc/>
public bool DefaultPermission { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<SlashCommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;

internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
Description = builder.Description;
DefaultPermission = builder.DefaultPermission;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
}

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync (IInteractionContext context, IServiceProvider services)
{
if(context.Interaction is not ISlashCommandInteraction slashCommand)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Slash Command Interaction");

var options = slashCommand.Data.Options;

while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup))
options = options.ElementAt(0)?.Options;

return await ExecuteAsync(context, Parameters, options?.ToList(), services);
}

private async Task<IResult> ExecuteAsync (IInteractionContext context, IEnumerable<SlashCommandParameterInfo> paramList,
List<IApplicationCommandInteractionDataOption> argList, IServiceProvider services)
{
try
{
if (paramList?.Count() < argList?.Count())
return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters");

var args = new object[paramList.Count()];

for (var i = 0; i < paramList.Count(); i++)
{
var parameter = paramList.ElementAt(i);

var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));

if (arg == default)
{
if (parameter.IsRequired)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters");
else
args[i] = parameter.DefaultValue;
}
else
{
var typeConverter = parameter.TypeConverter;

var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false);

if (!readResult.IsSuccess)
{
await InvokeModuleEvent(context, readResult).ConfigureAwait(false);
return readResult;
}

args[i] = readResult.Value;
}
}

return await RunAsync(context, args, services).ConfigureAwait(false);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
}
}

protected override Task InvokeModuleEvent (IInteractionContext context, IResult result)
=> CommandService._slashCommandExecutedEvent.InvokeAsync(this, context, result);

protected override string GetLogString (IInteractionContext context)
{
if (context.Guild != null)
return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Guild}/{context.Channel}";
else
return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Channel}";
}
}
}

+ 23
- 0
src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs View File

@@ -0,0 +1,23 @@
namespace Discord.Interactions
{
/// <summary>
/// Represents a <see cref="InteractionService"/> command that can be registered to Discord.
/// </summary>
public interface IApplicationCommandInfo
{
/// <summary>
/// Gets the name of this command.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the type of this command.
/// </summary>
ApplicationCommandType CommandType { get; }

/// <summary>
/// Gets the DefaultPermission of this command.
/// </summary>
bool DefaultPermission { get; }
}
}

+ 83
- 0
src/Discord.Net.Interactions/Info/ICommandInfo.cs View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represent a command information object that can be executed.
/// </summary>
public interface ICommandInfo
{
/// <summary>
/// Gets the name of the command.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the name of the command handler method.
/// </summary>
string MethodName { get; }

/// <summary>
/// Gets <see langword="true"/> if this command will be registered and executed as a standalone command, unaffected by the <see cref="GroupAttribute"/>s of
/// of the commands parents.
/// </summary>
bool IgnoreGroupNames { get; }

/// <summary>
/// Gets wheter this command supports wild card patterns.
/// </summary>
bool SupportsWildCards { get; }

/// <summary>
/// Gets <see langword="true"/> if this command is a top level command and none of its parents have a <see cref="GroupAttribute"/>.
/// </summary>
bool IsTopLevelCommand { get; }

/// <summary>
/// Gets the module that the method belongs to.
/// </summary>
ModuleInfo Module { get; }

/// <summary>
/// Gets the the underlying command service.
/// </summary>
InteractionService CommandService { get; }

/// <summary>
/// Get the run mode this command gets executed with.
/// </summary>
RunMode RunMode { get; }

/// <summary>
/// Gets a collection of the attributes of this command.
/// </summary>
IReadOnlyCollection<Attribute> Attributes { get; }

/// <summary>
/// Gets a collection of the preconditions of this command.
/// </summary>
IReadOnlyCollection<PreconditionAttribute> Preconditions { get; }

/// <summary>
/// Gets a collection of the parameters of this command.
/// </summary>
IReadOnlyCollection<IParameterInfo> Parameters { get; }

/// <summary>
/// Executes the command with the provided context.
/// </summary>
/// <param name="context">The execution context.</param>
/// <param name="services">Dependencies that will be used to create the module instance.</param>
/// <returns>
/// A task representing the execution process. The task result contains the execution result.
/// </returns>
Task<IResult> ExecuteAsync (IInteractionContext context, IServiceProvider services);

/// <summary>
/// Check if an execution context meets the command precondition requirements.
/// </summary>
Task<PreconditionResult> CheckPreconditionsAsync (IInteractionContext context, IServiceProvider services);
}
}

+ 57
- 0
src/Discord.Net.Interactions/Info/IParameterInfo.cs View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents a <see cref="ICommandInfo"/> parameter.
/// </summary>
public interface IParameterInfo
{
/// <summary>
/// Gets the command that this parameter belongs to.
/// </summary>
ICommandInfo Command { get; }

/// <summary>
/// Gets the name of this parameter.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the type of this parameter.
/// </summary>
Type ParameterType { get; }

/// <summary>
/// Gets whether this parameter is required.
/// </summary>
bool IsRequired { get; }

/// <summary>
/// Gets whether this parameter is marked with a <see langword="params"/> keyword.
/// </summary>
bool IsParameterArray { get; }

/// <summary>
/// Gets the default value of this parameter if the parameter is optional.
/// </summary>
object DefaultValue { get; }

/// <summary>
/// Gets a list of the attributes this parameter has.
/// </summary>
IReadOnlyCollection<Attribute> Attributes { get; }

/// <summary>
/// Gets a list of the preconditions this parameter has.
/// </summary>
IReadOnlyCollection<ParameterPreconditionAttribute> Preconditions { get; }

/// <summary>
/// Check if an execution context meets the parameter precondition requirements.
/// </summary>
Task<PreconditionResult> CheckPreconditionsAsync (IInteractionContext context, object value, IServiceProvider services);
}
}

+ 217
- 0
src/Discord.Net.Interactions/Info/ModuleInfo.cs View File

@@ -0,0 +1,217 @@
using Discord.Interactions.Builders;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Discord.Interactions
{
/// <summary>
/// Contains the information of a Interactions Module.
/// </summary>
public class ModuleInfo
{
internal ILookup<string, PreconditionAttribute> GroupedPreconditions { get; }

/// <summary>
/// Gets the underlying command service.
/// </summary>
public InteractionService CommandService { get; }

/// <summary>
/// Gets the name of this module class.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the group name of this module, if the module is marked with a <see cref="GroupAttribute"/>.
/// </summary>
public string SlashGroupName { get; }

/// <summary>
/// Gets <see langword="true"/> if this module is marked with a <see cref="GroupAttribute"/>.
/// </summary>
public bool IsSlashGroup => !string.IsNullOrEmpty(SlashGroupName);

/// <summary>
/// Gets the description of this module if <see cref="IsSlashGroup"/> is <see langword="true"/>.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the default Permission of this module.
/// </summary>
public bool DefaultPermission { get; }

/// <summary>
/// Gets the collection of Sub Modules of this module.
/// </summary>
public IReadOnlyList<ModuleInfo> SubModules { get; }

/// <summary>
/// Gets the Slash Commands that are declared in this module.
/// </summary>
public IReadOnlyList<SlashCommandInfo> SlashCommands { get; }

/// <summary>
/// Gets the Context Commands that are declared in this module.
/// </summary>
public IReadOnlyList<ContextCommandInfo> ContextCommands { get; }

/// <summary>
/// Gets the Component Commands that are declared in this module.
/// </summary>
public IReadOnlyCollection<ComponentCommandInfo> ComponentCommands { get; }

/// <summary>
/// Gets the Autocomplete Commands that are declared in this module.
/// </summary>
public IReadOnlyCollection<AutocompleteCommandInfo> AutocompleteCommands { get; }

/// <summary>
/// Gets the declaring type of this module, if <see cref="IsSubModule"/> is <see langword="true"/>.
/// </summary>
public ModuleInfo Parent { get; }

/// <summary>
/// Gets <see langword="true"/> if this module is declared by another <see cref="InteractionModuleBase{T}"/>.
/// </summary>
public bool IsSubModule => Parent != null;

/// <summary>
/// Gets a collection of the attributes of this module.
/// </summary>
public IReadOnlyCollection<Attribute> Attributes { get; }

/// <summary>
/// Gets a collection of the preconditions of this module.
/// </summary>
public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; }

/// <summary>
/// Gets <see langword="true"/> if this module has a valid <see cref="GroupAttribute"/> and has no parent with a <see cref="GroupAttribute"/>.
/// </summary>
public bool IsTopLevelGroup { get; }

/// <summary>
/// Gets <see langword="true"/> if this module will not be registered by <see cref="InteractionService.RegisterCommandsGloballyAsync(bool)"/>
/// or <see cref="InteractionService.RegisterCommandsToGuildAsync(ulong, bool)"/> methods.
/// </summary>
public bool DontAutoRegister { get; }

internal ModuleInfo (ModuleBuilder builder, InteractionService commandService, IServiceProvider services, ModuleInfo parent = null)
{
CommandService = commandService;

Name = builder.Name;
SlashGroupName = builder.SlashGroupName;
Description = builder.Description;
Parent = parent;
DefaultPermission = builder.DefaultPermission;
SlashCommands = BuildSlashCommands(builder).ToImmutableArray();
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
AutocompleteCommands = BuildAutocompleteCommands(builder).ToImmutableArray();
SubModules = BuildSubModules(builder, commandService, services).ToImmutableArray();
Attributes = BuildAttributes(builder).ToImmutableArray();
Preconditions = BuildPreconditions(builder).ToImmutableArray();
IsTopLevelGroup = CheckTopLevel(parent);
DontAutoRegister = builder.DontAutoRegister;

GroupedPreconditions = Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
}

private IEnumerable<ModuleInfo> BuildSubModules (ModuleBuilder builder, InteractionService commandService, IServiceProvider services)
{
var result = new List<ModuleInfo>();

foreach (Builders.ModuleBuilder moduleBuilder in builder.SubModules)
result.Add(moduleBuilder.Build(commandService, services, this));

return result;
}

private IEnumerable<SlashCommandInfo> BuildSlashCommands (ModuleBuilder builder)
{
var result = new List<SlashCommandInfo>();

foreach (Builders.SlashCommandBuilder commandBuilder in builder.SlashCommands)
result.Add(commandBuilder.Build(this, CommandService));

return result;
}

private IEnumerable<ContextCommandInfo> BuildContextCommands (ModuleBuilder builder)
{
var result = new List<ContextCommandInfo>();

foreach (Builders.ContextCommandBuilder commandBuilder in builder.ContextCommands)
result.Add(commandBuilder.Build(this, CommandService));

return result;
}

private IEnumerable<ComponentCommandInfo> BuildComponentCommands (ModuleBuilder builder)
{
var result = new List<ComponentCommandInfo>();

foreach (var interactionBuilder in builder.ComponentCommands)
result.Add(interactionBuilder.Build(this, CommandService));

return result;
}

private IEnumerable<AutocompleteCommandInfo> BuildAutocompleteCommands( ModuleBuilder builder)
{
var result = new List<AutocompleteCommandInfo>();

foreach (var commandBuilder in builder.AutocompleteCommands)
result.Add(commandBuilder.Build(this, CommandService));

return result;
}

private IEnumerable<Attribute> BuildAttributes (ModuleBuilder builder)
{
var result = new List<Attribute>();
var currentParent = builder;

while (currentParent != null)
{
result.AddRange(currentParent.Attributes);
currentParent = currentParent.Parent;
}

return result;
}

private static IEnumerable<PreconditionAttribute> BuildPreconditions (ModuleBuilder builder)
{
var preconditions = new List<PreconditionAttribute>();

var parent = builder;

while (parent != null)
{
preconditions.AddRange(parent.Preconditions);
parent = parent.Parent;
}

return preconditions;
}

private static bool CheckTopLevel (ModuleInfo parent)
{
var currentParent = parent;

while (currentParent != null)
{
if (currentParent.IsTopLevelGroup)
return false;

currentParent = currentParent.Parent;
}
return true;
}
}
}

+ 62
- 0
src/Discord.Net.Interactions/Info/Parameters/CommandParameterInfo.cs View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents the base parameter info class for <see cref="InteractionService"/> commands.
/// </summary>
public class CommandParameterInfo : IParameterInfo
{
/// <inheritdoc/>
public ICommandInfo Command { get; }

/// <inheritdoc/>
public string Name { get; }

/// <inheritdoc/>
public Type ParameterType { get; }

/// <inheritdoc/>
public bool IsRequired { get; }

/// <inheritdoc/>
public bool IsParameterArray { get; }

/// <inheritdoc/>
public object DefaultValue { get; }

/// <inheritdoc/>
public IReadOnlyCollection<Attribute> Attributes { get; }

/// <inheritdoc/>
public IReadOnlyCollection<ParameterPreconditionAttribute> Preconditions { get; }

internal CommandParameterInfo (Builders.IParameterBuilder builder, ICommandInfo command)
{
Command = command;
Name = builder.Name;
ParameterType = builder.ParameterType;
IsRequired = builder.IsRequired;
IsParameterArray = builder.IsParameterArray;
DefaultValue = builder.DefaultValue;
Attributes = builder.Attributes.ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray();
}

/// <inheritdoc/>
public async Task<PreconditionResult> CheckPreconditionsAsync (IInteractionContext context, object value, IServiceProvider services)
{
foreach (var precondition in Preconditions)
{
var result = await precondition.CheckRequirementsAsync(context, this, value, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}

return PreconditionResult.FromSuccess();
}
}
}

+ 72
- 0
src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs View File

@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Discord.Interactions
{
/// <summary>
/// Represents the parameter info class for <see cref="SlashCommandInfo"/> commands.
/// </summary>
public class SlashCommandParameterInfo : CommandParameterInfo
{
/// <inheritdoc/>
public new SlashCommandInfo Command => base.Command as SlashCommandInfo;

/// <summary>
/// Gets the description of the Slash Command Parameter.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the minimum value permitted for a number type parameter.
/// </summary>
public double? MinValue { get; }

/// <summary>
/// Gets the maxmimum value permitted for a number type parameter.
/// </summary>
public double? MaxValue { get; }

/// <summary>
/// Gets the <see cref="TypeConverter{T}"/> that will be used to convert the incoming <see cref="Discord.WebSocket.SocketSlashCommandDataOption"/> into
/// <see cref="CommandParameterInfo.ParameterType"/>.
/// </summary>
public TypeConverter TypeConverter { get; }

/// <summary>
/// Gets the <see cref="IAutocompleteHandler"/> thats linked to this parameter.
/// </summary>
public IAutocompleteHandler AutocompleteHandler { get; }

/// <summary>
/// Gets whether this parameter is configured for Autocomplete Interactions.
/// </summary>
public bool IsAutocomplete { get; }

/// <summary>
/// Gets the Discord option type this parameter represents.
/// </summary>
public ApplicationCommandOptionType DiscordOptionType => TypeConverter.GetDiscordType();

/// <summary>
/// Gets the parameter choices of this Slash Application Command parameter.
/// </summary>
public IReadOnlyCollection<ParameterChoice> Choices { get; }

/// <summary>
/// Gets the allowed channel types for this option.
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }

internal SlashCommandParameterInfo(Builders.SlashCommandParameterBuilder builder, SlashCommandInfo command) : base(builder, command)
{
TypeConverter = builder.TypeConverter;
AutocompleteHandler = builder.AutocompleteHandler;
Description = builder.Description;
MaxValue = builder.MaxValue;
MinValue = builder.MinValue;
IsAutocomplete = builder.Autocomplete;
Choices = builder.Choices.ToImmutableArray();
ChannelTypes = builder.ChannelTypes.ToImmutableArray();
}
}
}

+ 43
- 0
src/Discord.Net.Interactions/InteractionCommandError.cs View File

@@ -0,0 +1,43 @@
namespace Discord.Interactions
{
/// <summary>
/// Defines the type of error a command can throw.
/// </summary>
public enum InteractionCommandError
{
/// <summary>
/// Thrown when the command is unknown.
/// </summary>
UnknownCommand,

/// <summary>
/// Thrown when the Slash Command parameter fails to be converted by a TypeReader.
/// </summary>
ConvertFailed,

/// <summary>
/// Thrown when the input text has too few or too many arguments.
/// </summary>
BadArgs,

/// <summary>
/// Thrown when an exception occurs mid-command execution.
/// </summary>
Exception,

/// <summary>
/// Thrown when the command is not successfully executed on runtime.
/// </summary>
Unsuccessful,

/// <summary>
/// Thrown when the command fails to meet a <see cref="PreconditionAttribute"/>'s conditions.
/// </summary>
UnmetPrecondition,

/// <summary>
/// Thrown when the command context cannot be parsed by the <see cref="ICommandInfo"/>.
/// </summary>
ParseFailed
}
}

+ 34
- 0
src/Discord.Net.Interactions/InteractionContext.cs View File

@@ -0,0 +1,34 @@
namespace Discord.Interactions
{
/// <inheritdoc cref="IInteractionContext"/>
public class InteractionContext : IInteractionContext
{
/// <inheritdoc/>
public IDiscordClient Client { get; }
/// <inheritdoc/>
public IGuild Guild { get; }
/// <inheritdoc/>
public IMessageChannel Channel { get; }
/// <inheritdoc/>
public IUser User { get; }
/// <inheritdoc/>
public IDiscordInteraction Interaction { get; }

/// <summary>
/// Initializes a new <see cref="SocketInteractionContext{TInteraction}"/>.
/// </summary>
/// <param name="client">The underlying client.</param>
/// <param name="interaction">The underlying interaction.</param>
/// <param name="user"><see cref="IUser"/> who executed the command.</param>
/// <param name="channel"><see cref="ISocketMessageChannel"/> the command originated from.</param>
public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IUser user, IMessageChannel channel = null)
{
Client = client;
Interaction = interaction;
Channel = channel;
Guild = (interaction as IGuildUser)?.Guild;
User = user;
Interaction = interaction;
}
}
}

+ 68
- 0
src/Discord.Net.Interactions/InteractionModuleBase.cs View File

@@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Provides a base class for a command module to inherit from.
/// </summary>
/// <typeparam name="T">Type of interaction context to be injected into the module.</typeparam>
public abstract class InteractionModuleBase<T> : IInteractionModuleBase where T : class, IInteractionContext
{
/// <summary>
/// Gets the underlying context of the command.
/// </summary>
public T Context { get; private set; }

/// <inheritdoc/>
public virtual void AfterExecute (ICommandInfo command) { }

/// <inheritdoc/>
public virtual void BeforeExecute (ICommandInfo command) { }

/// <inheritdoc/>
public virtual void OnModuleBuilding (InteractionService commandService, ModuleInfo module) { }

internal void SetContext (IInteractionContext context)
{
var newValue = context as T;
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}.");
}

/// <inheritdoc cref="IDiscordInteraction.DeferAsync(bool, RequestOptions)"/>
protected virtual async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) =>
await Context.Interaction.DeferAsync(ephemeral, options).ConfigureAwait(false);

/// <inheritdoc cref="IDiscordInteraction.RespondAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/>
protected virtual async Task RespondAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) =>
await Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, component, embed, options).ConfigureAwait(false);

/// <inheritdoc cref="IDiscordInteraction.FollowupAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/>
protected virtual async Task<IUserMessage> FollowupAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false,
AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) =>
await Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, component, embed, options).ConfigureAwait(false);

/// <inheritdoc cref="IMessageChannel.SendMessageAsync(string, bool, Embed, RequestOptions, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[])"/>
protected virtual async Task<IUserMessage> ReplyAsync (string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null,
AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null) =>
await Context.Channel.SendMessageAsync(text, false, embed, options, allowedMentions, messageReference, component).ConfigureAwait(false);

/// <inheritdoc cref="IDeletable.DeleteAsync(RequestOptions)"/>
protected virtual async Task DeleteOriginalResponseAsync ( )
{
var response = await Context.Interaction.GetOriginalResponseAsync().ConfigureAwait(false);
await response.DeleteAsync().ConfigureAwait(false);
}

//IInteractionModuleBase

/// <inheritdoc/>
void IInteractionModuleBase.SetContext (IInteractionContext context) => SetContext(context);
}

/// <summary>
/// Provides a base class for a command module to inherit from.
/// </summary>
public abstract class InteractionModuleBase : InteractionModuleBase<IInteractionContext> { }
}

+ 949
- 0
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -0,0 +1,949 @@
using Discord.Interactions.Builders;
using Discord.Logging;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Provides the framework for building and registering Discord Application Commands.
/// </summary>
public class InteractionService : IDisposable
{
/// <summary>
/// Occurs when a Slash Command related information is recieved.
/// </summary>
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new ();

/// <summary>
/// Occurs when a Slash Command is executed.
/// </summary>
public event Func<SlashCommandInfo, IInteractionContext, IResult, Task> SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<SlashCommandInfo, IInteractionContext, IResult, Task>> _slashCommandExecutedEvent = new ();

/// <summary>
/// Occurs when a Context Command is executed.
/// </summary>
public event Func<ContextCommandInfo, IInteractionContext, IResult, Task> ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<ContextCommandInfo, IInteractionContext, IResult, Task>> _contextCommandExecutedEvent = new ();

/// <summary>
/// Occurs when a Message Component command is executed.
/// </summary>
public event Func<ComponentCommandInfo, IInteractionContext, IResult, Task> ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<ComponentCommandInfo, IInteractionContext, IResult, Task>> _componentCommandExecutedEvent = new ();

/// <summary>
/// Occurs when a Autocomplete command is executed.
/// </summary>
public event Func<AutocompleteCommandInfo, IInteractionContext, IResult, Task> AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<AutocompleteCommandInfo, IInteractionContext, IResult, Task>> _autocompleteCommandExecutedEvent = new();

/// <summary>
/// Occurs when a AutocompleteHandler is executed.
/// </summary>
public event Func<IAutocompleteHandler, IInteractionContext, IResult, Task> AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<IAutocompleteHandler, IInteractionContext, IResult, Task>> _autocompleteHandlerExecutedEvent = new();

private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly CommandMap<SlashCommandInfo> _slashCommandMap;
private readonly ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>> _contextCommandMaps;
private readonly CommandMap<ComponentCommandInfo> _componentCommandMap;
private readonly CommandMap<AutocompleteCommandInfo> _autocompleteCommandMap;
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly ConcurrentDictionary<Type, TypeConverter> _typeConverters;
private readonly ConcurrentDictionary<Type, Type> _genericTypeConverters;
private readonly ConcurrentDictionary<Type, IAutocompleteHandler> _autocompleteHandlers = new();
private readonly SemaphoreSlim _lock;
internal readonly Logger _cmdLogger;
internal readonly LogManager _logManager;
internal readonly Func<DiscordRestClient> _getRestClient;

internal readonly bool _throwOnError, _deleteUnkownSlashCommandAck, _useCompiledLambda, _enableAutocompleteHandlers;
internal readonly string _wildCardExp;
internal readonly RunMode _runMode;
internal readonly RestResponseCallback _restResponseCallback;

/// <summary>
/// Rest client to be used to register application commands.
/// </summary>
public DiscordRestClient RestClient { get => _getRestClient(); }

/// <summary>
/// Represents all modules loaded within <see cref="InteractionService"/>.
/// </summary>
public IReadOnlyList<ModuleInfo> Modules => _moduleDefs.ToList();

/// <summary>
/// Represents all Slash Commands loaded within <see cref="InteractionService"/>.
/// </summary>
public IReadOnlyList<SlashCommandInfo> SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList();

/// <summary>
/// Represents all Context Commands loaded within <see cref="InteractionService"/>.
/// </summary>
public IReadOnlyList<ContextCommandInfo> ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList();

/// <summary>
/// Represents all Component Commands loaded within <see cref="InteractionService"/>.
/// </summary>
public IReadOnlyCollection<ComponentCommandInfo> ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList();

/// <summary>
/// Initialize a <see cref="InteractionService"/> with provided configurations.
/// </summary>
/// <param name="discord">The discord client.</param>
/// <param name="config">The configuration class.</param>
public InteractionService (DiscordSocketClient discord, InteractionServiceConfig config = null)
: this(() => discord.Rest, config ?? new InteractionServiceConfig()) { }

/// <summary>
/// Initialize a <see cref="InteractionService"/> with provided configurations.
/// </summary>
/// <param name="discord">The discord client.</param>
/// <param name="config">The configuration class.</param>
public InteractionService (DiscordShardedClient discord, InteractionServiceConfig config = null)
: this(() => discord.Rest, config ?? new InteractionServiceConfig()) { }

/// <summary>
/// Initialize a <see cref="InteractionService"/> with provided configurations.
/// </summary>
/// <param name="discord">The discord client.</param>
/// <param name="config">The configuration class.</param>
public InteractionService (BaseSocketClient discord, InteractionServiceConfig config = null)
:this(() => discord.Rest, config ?? new InteractionServiceConfig()) { }

/// <summary>
/// Initialize a <see cref="InteractionService"/> with provided configurations.
/// </summary>
/// <param name="discord">The discord client.</param>
/// <param name="config">The configuration class.</param>
public InteractionService (DiscordRestClient discord, InteractionServiceConfig config = null)
:this(() => discord, config ?? new InteractionServiceConfig()) { }

private InteractionService (Func<DiscordRestClient> getRestClient, InteractionServiceConfig config = null)
{
config ??= new InteractionServiceConfig();

_lock = new SemaphoreSlim(1, 1);
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
_moduleDefs = new HashSet<ModuleInfo>();

_logManager = new LogManager(config.LogLevel);
_logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
_cmdLogger = _logManager.CreateLogger("App Commands");

_slashCommandMap = new CommandMap<SlashCommandInfo>(this);
_contextCommandMaps = new ConcurrentDictionary<ApplicationCommandType, CommandMap<ContextCommandInfo>>();
_componentCommandMap = new CommandMap<ComponentCommandInfo>(this, config.InteractionCustomIdDelimiters);
_autocompleteCommandMap = new CommandMap<AutocompleteCommandInfo>(this);

_getRestClient = getRestClient;

_runMode = config.DefaultRunMode;
if (_runMode == RunMode.Default)
throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}");

_throwOnError = config.ThrowOnError;
_deleteUnkownSlashCommandAck = config.DeleteUnknownSlashCommandAck;
_wildCardExp = config.WildCardExpression;
_useCompiledLambda = config.UseCompiledLambda;
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
_restResponseCallback = config.RestResponseCallback;

_genericTypeConverters = new ConcurrentDictionary<Type, Type>
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>)
};

_typeConverters = new ConcurrentDictionary<Type, TypeConverter>
{
[typeof(TimeSpan)] = new TimeSpanConverter()
};
}

/// <summary>
/// Create and loads a <see cref="ModuleInfo"/> using a builder factory.
/// </summary>
/// <param name="name">Name of the module.</param>
/// <param name="services">The <see cref="IServiceProvider"/> for your dependency injection solution if using one; otherwise, pass <c>null</c>.</param>
/// <param name="buildFunc">Module builder factory.</param>
/// <returns>
/// A task representing the operation for adding modules. The task result contains the built module instance.
/// </returns>
public async Task<ModuleInfo> CreateModuleAsync(string name, IServiceProvider services, Action<ModuleBuilder> buildFunc)
{
services ??= EmptyServiceProvider.Instance;

await _lock.WaitAsync().ConfigureAwait(false);
try
{
var builder = new ModuleBuilder(this, name);
buildFunc(builder);

var moduleInfo = builder.Build(this, services);
LoadModuleInternal(moduleInfo);

return moduleInfo;
}
finally
{
_lock.Release();
}
}

/// <summary>
/// Discover and load command modules from an <see cref="Assembly"/>.
/// </summary>
/// <param name="assembly"><see cref="Assembly"/> the command modules are defined in.</param>
/// <param name="services">The <see cref="IServiceProvider"/> for your dependency injection solution if using one; otherwise, pass <c>null</c>.</param>
/// <returns>
/// A task representing the operation for adding modules. The task result contains a collection of the modules added.
/// </returns>
public async Task<IEnumerable<ModuleInfo>> AddModulesAsync (Assembly assembly, IServiceProvider services)
{
services ??= EmptyServiceProvider.Instance;

await _lock.WaitAsync().ConfigureAwait(false);

try
{
var types = await ModuleClassBuilder.SearchAsync(assembly, this);
var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services);

foreach (var info in moduleDefs)
{
_typedModuleDefs[info.Key] = info.Value;
LoadModuleInternal(info.Value);
}
return moduleDefs.Values;
}
finally
{
_lock.Release();
}
}

/// <summary>
/// Add a command module from a <see cref="Type"/>.
/// </summary>
/// <typeparam name="T">Type of the module.</typeparam>
/// <param name="services">The <see cref="IServiceProvider" /> for your dependency injection solution if using one; otherwise, pass <c>null</c> .</param>
/// <returns>
/// A task representing the operation for adding the module. The task result contains the built module.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown if this module has already been added.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the <typeparamref name="T"/> is not a valid module definition.
/// </exception>
public Task<ModuleInfo> AddModuleAsync<T> (IServiceProvider services) where T : class =>
AddModuleAsync(typeof(T), services);

/// <summary>
/// Add a command module from a <see cref="Type"/>.
/// </summary>
/// <param name="type">Type of the module.</param>
/// <param name="services">The <see cref="IServiceProvider" /> for your dependency injection solution if using one; otherwise, pass <c>null</c> .</param>
/// <returns>
/// A task representing the operation for adding the module. The task result contains the built module.
/// </returns>
/// <exception cref="ArgumentException">
/// Thrown if this module has already been added.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the <paramref name="type"/> is not a valid module definition.
/// </exception>
public async Task<ModuleInfo> AddModuleAsync (Type type, IServiceProvider services)
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(type))
throw new ArgumentException("Type parameter must be a type of Slash Module", "T");

services ??= EmptyServiceProvider.Instance;

await _lock.WaitAsync().ConfigureAwait(false);

try
{
var typeInfo = type.GetTypeInfo();

if (_typedModuleDefs.ContainsKey(typeInfo))
throw new ArgumentException("Module definition for this type already exists.");

var moduleDef = ( await ModuleClassBuilder.BuildAsync(new List<TypeInfo> { typeInfo }, this, services).ConfigureAwait(false) ).FirstOrDefault();

if (moduleDef.Value == default)
throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?");

if (!_typedModuleDefs.TryAdd(type, moduleDef.Value))
throw new ArgumentException("Module definition for this type already exists.");

_typedModuleDefs[moduleDef.Key] = moduleDef.Value;
LoadModuleInternal(moduleDef.Value);

return moduleDef.Value;
}
finally
{
_lock.Release();
}
}

/// <summary>
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to a guild.
/// </summary>
/// <param name="guildId">Id of the target guild.</param>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> RegisterCommandsToGuildAsync (ulong guildId, bool deleteMissing = true)
{
EnsureClientReady();

var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule);
var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList();

if (!deleteMissing)
{

var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from <see cref="ContextCommands"/> and <see cref="SlashCommands"/> to Discord on in global scope.
/// </summary>
/// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active global application commands of bot.
/// </returns>
public async Task<IReadOnlyCollection<RestGlobalCommand>> RegisterCommandsGloballyAsync (bool deleteMissing = true)
{
EnsureClientReady();

var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule);
var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList();

if (!deleteMissing)
{
var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from <paramref name="commands"/> to a guild.
/// </summary>
/// <remarks>
/// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect,
/// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// </remarks>
/// <param name="guild">The target guild.</param>
/// <param name="commands">Commands to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands)
{
EnsureClientReady();

if (guild is null)
throw new ArgumentNullException(nameof(guild));

var props = new List<ApplicationCommandProperties>();

foreach (var command in commands)
{
switch (command)
{
case SlashCommandInfo slashCommand:
props.Add(slashCommand.ToApplicationCommandProps());
break;
case ContextCommandInfo contextCommand:
props.Add(contextCommand.ToApplicationCommandProps());
break;
default:
throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet");
}
}

if (!deleteMissing)
{
var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> to a guild.
/// </summary>
/// <param name="guild">The target guild.</param>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGuildCommand>> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules)
{
EnsureClientReady();

if (guild is null)
throw new ArgumentNullException(nameof(guild));

var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList();

if (!deleteMissing)
{
var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from modules provided in <paramref name="modules"/> as global commands.
/// </summary>
/// <param name="modules">Modules to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGlobalCommand>> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules)
{
EnsureClientReady();

var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList();

if (!deleteMissing)
{
var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false);
}

/// <summary>
/// Register Application Commands from <paramref name="commands"/> as global commands.
/// </summary>
/// <remarks>
/// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect,
/// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail.
/// </remarks>
/// <param name="commands">Commands to be registered to Discord.</param>
/// <returns>
/// A task representing the command registration process. The task result contains the active application commands of the target guild.
/// </returns>
public async Task<IReadOnlyCollection<RestGlobalCommand>> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands)
{
EnsureClientReady();

var props = new List<ApplicationCommandProperties>();

foreach (var command in commands)
{
switch (command)
{
case SlashCommandInfo slashCommand:
props.Add(slashCommand.ToApplicationCommandProps());
break;
case ContextCommandInfo contextCommand:
props.Add(contextCommand.ToApplicationCommandProps());
break;
default:
throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet");
}
}

if (!deleteMissing)
{
var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false);
var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name));
props.AddRange(missing.Select(x => x.ToApplicationCommandProps()));
}

return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false);
}

private void LoadModuleInternal (ModuleInfo module)
{
_moduleDefs.Add(module);

foreach (var command in module.SlashCommands)
_slashCommandMap.AddCommand(command, command.IgnoreGroupNames);

foreach (var command in module.ContextCommands)
_contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap<ContextCommandInfo>(this)).AddCommand(command, command.IgnoreGroupNames);

foreach (var interaction in module.ComponentCommands)
_componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames);

foreach (var command in module.AutocompleteCommands)
_autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command);

foreach (var subModule in module.SubModules)
LoadModuleInternal(subModule);
}

/// <summary>
/// Remove a command module.
/// </summary>
/// <typeparam name="T">The <see cref="Type"/> of the module.</typeparam>
/// <returns>
/// A task that represents the asynchronous removal operation. The task result contains a value that
/// indicates whether the module is successfully removed.
/// </returns>
public Task<bool> RemoveModuleAsync<T> ( ) =>
RemoveModuleAsync(typeof(T));

/// <summary>
/// Remove a command module.
/// </summary>
/// <param name="type">The <see cref="Type"/> of the module.</param>
/// <returns>
/// A task that represents the asynchronous removal operation. The task result contains a value that
/// indicates whether the module is successfully removed.
/// </returns>
public async Task<bool> RemoveModuleAsync (Type type)
{
await _lock.WaitAsync().ConfigureAwait(false);

try
{
if (!_typedModuleDefs.TryRemove(type, out var module))
return false;

return RemoveModuleInternal(module);
}
finally
{
_lock.Release();
}
}

/// <summary>
/// Remove a command module.
/// </summary>
/// <param name="module">The <see cref="ModuleInfo" /> to be removed from the service.</param>
/// <returns>
/// A task that represents the asynchronous removal operation. The task result contains a value that
/// indicates whether the <paramref name="module"/> is successfully removed.
/// </returns>
public async Task<bool> RemoveModuleAsync(ModuleInfo module)
{
await _lock.WaitAsync().ConfigureAwait(false);

try
{
var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module));

if (!typeModulePair.Equals(default(KeyValuePair<Type, ModuleInfo>)))
_typedModuleDefs.TryRemove(typeModulePair.Key, out var _);

return RemoveModuleInternal(module);
}
finally
{
_lock.Release();
}
}

private bool RemoveModuleInternal (ModuleInfo moduleInfo)
{
if (!_moduleDefs.Remove(moduleInfo))
return false;

foreach (var command in moduleInfo.SlashCommands)
{
_slashCommandMap.RemoveCommand(command);
}

return true;
}

/// <summary>
/// Execute a Command from a given <see cref="IInteractionContext"/>.
/// </summary>
/// <param name="context">Name context of the command.</param>
/// <param name="services">The service to be used in the command's dependency injection.</param>
/// <returns>
/// A task representing the command execution process. The task result contains the result of the execution.
/// </returns>
public async Task<IResult> ExecuteCommandAsync (IInteractionContext context, IServiceProvider services)
{
var interaction = context.Interaction;

return interaction switch
{
ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false),
IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false),
IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false),
IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false),
IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false),
_ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"),
};
}

private async Task<IResult> ExecuteSlashCommandAsync (IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services)
{
var keywords = interaction.Data.GetCommandKeywords();

var result = _slashCommandMap.GetCommand(keywords);

if (!result.IsSuccess)
{
await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})");

if (_deleteUnkownSlashCommandAck)
{
var response = await context.Interaction.GetOriginalResponseAsync().ConfigureAwait(false);
await response.DeleteAsync().ConfigureAwait(false);
}

await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

private async Task<IResult> ExecuteContextCommandAsync (IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services)
{
if (!_contextCommandMaps.TryGetValue(commandType, out var map))
return SearchResult<ContextCommandInfo>.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found.");

var result = map.GetCommand(input);

if (!result.IsSuccess)
{
await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})");

await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

private async Task<IResult> ExecuteComponentCommandAsync (IInteractionContext context, string input, IServiceProvider services)
{
var result = _componentCommandMap.GetCommand(input);

if (!result.IsSuccess)
{
await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})");

await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}

private async Task<IResult> ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services )
{
var keywords = interaction.Data.GetCommandKeywords();

if(_enableAutocompleteHandlers)
{
var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords);

if(autocompleteHandlerResult.IsSuccess)
{
var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal));

if(parameter?.AutocompleteHandler is not null)
return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false);
}
}

keywords.Add(interaction.Data.Current.Name);

var commandResult = _autocompleteCommandMap.GetCommand(keywords);

if(!commandResult.IsSuccess)
{
await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})");

await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false);
return commandResult;
}

return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null)
{
if (_typeConverters.TryGetValue(type, out var specific))
return specific;

else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type)))
{
services ??= EmptyServiceProvider.Instance;

var converterType = GetMostSpecificTypeConverter(type);
var converter = ReflectionUtils<TypeConverter>.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services);
_typeConverters[type] = converter;
return converter;
}

else if (_typeConverters.Any(x => x.Value.CanConvertTo(type)))
return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value;

throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type");
}

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <typeparam name="T">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</typeparam>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter<T> (TypeConverter converter) =>
AddTypeConverter(typeof(T), converter);

/// <summary>
/// Add a concrete type <see cref="TypeConverter"/>.
/// </summary>
/// <param name="type">Primary target <see cref="Type"/> of the <see cref="TypeConverter"/>.</param>
/// <param name="converter">The <see cref="TypeConverter"/> instance.</param>
public void AddTypeConverter (Type type, TypeConverter converter)
{
if (!converter.CanConvertTo(type))
throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}");

_typeConverters[type] = converter;
}

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
/// </summary>
/// <typeparam name="T">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</typeparam>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>

public void AddGenericTypeConverter<T> (Type converterType) =>
AddGenericTypeConverter(typeof(T), converterType);

/// <summary>
/// Add a generic type <see cref="TypeConverter{T}"/>.
/// </summary>
/// <param name="targetType">Generic Type constraint of the <see cref="Type"/> of the <see cref="TypeConverter{T}"/>.</param>
/// <param name="converterType">Type of the <see cref="TypeConverter{T}"/>.</param>
public void AddGenericTypeConverter (Type targetType, Type converterType)
{
if (!converterType.IsGenericTypeDefinition)
throw new ArgumentException($"{converterType.FullName} is not generic.");

var genericArguments = converterType.GetGenericArguments();

if (genericArguments.Count() > 1)
throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter");

var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints());

if (!constraints.Any(x => x.IsAssignableFrom(targetType)))
throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}");

_genericTypeConverters[targetType] = converterType;
}

internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null)
{
services ??= EmptyServiceProvider.Instance;

if (!_enableAutocompleteHandlers)
throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE");

if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler))
return autocompleteHandler;
else
{
autocompleteHandler = ReflectionUtils<IAutocompleteHandler>.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services);
_autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler;
return autocompleteHandler;
}
}

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="module">Module representing the top level Slash Command.</param>
/// <param name="guild">Target guild.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (!module.IsSlashGroup)
throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command");

if (!module.IsTopLevelGroup)
throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions");

if (guild is null)
throw new ArgumentNullException("guild");

var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var appCommand = commands.First(x => x.Name == module.SlashGroupName);

return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false);
}

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="command">The Slash Command.</param>
/// <param name="guild">Target guild.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions) =>
await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false);

/// <summary>
/// Modify the command permissions of the matching Discord Slash Command.
/// </summary>
/// <param name="command">The Context Command.</param>
/// <param name="guild">Target guild.</param>
/// <param name="permissions">New permission values.</param>
/// <returns>
/// The active command permissions after the modification.
/// </returns>
public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions) =>
await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false);

private async Task<GuildApplicationCommandPermission> ModifyApplicationCommandPermissionsAsync<T> (T command, IGuild guild,
params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo
{
if (!command.IsTopLevelCommand)
throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions");

if (guild is null)
throw new ArgumentNullException("guild");

var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false);
var appCommand = commands.First(x => x.Name == ( command as IApplicationCommandInfo ).Name);

return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false);
}

/// <summary>
/// Gets a <see cref="SlashCommandInfo"/>.
/// </summary>
/// <typeparam name="TModule">Declaring module type of this command, must be a type of <see cref="InteractionModuleBase{T}"/>.</typeparam>
/// <param name="methodName">Method name of the handler, use of <see langword="nameof"/> is recommended.</param>
/// <returns>
/// <see cref="SlashCommandInfo"/> instance for this command.
/// </returns>
/// <exception cref="InvalidOperationException">Module or Slash Command couldn't be found.</exception>
public SlashCommandInfo GetSlashCommandInfo<TModule> (string methodName) where TModule : class
{
var module = GetModuleInfo<TModule>();

return module.SlashCommands.First(x => x.MethodName == methodName);
}

/// <summary>
/// Gets a <see cref="ContextCommandInfo"/>.
/// </summary>
/// <typeparam name="TModule">Declaring module type of this command, must be a type of <see cref="InteractionModuleBase{T}"/>.</typeparam>
/// <param name="methodName">Method name of the handler, use of <see langword="nameof"/> is recommended.</param>
/// <returns>
/// <see cref="ContextCommandInfo"/> instance for this command.
/// </returns>
/// <exception cref="InvalidOperationException">Module or Context Command couldn't be found.</exception>
public ContextCommandInfo GetContextCommandInfo<TModule> (string methodName) where TModule : class
{
var module = GetModuleInfo<TModule>();

return module.ContextCommands.First(x => x.MethodName == methodName);
}

/// <summary>
/// Gets a <see cref="ComponentCommandInfo"/>.
/// </summary>
/// <typeparam name="TModule">Declaring module type of this command, must be a type of <see cref="InteractionModuleBase{T}"/>.</typeparam>
/// <param name="methodName">Method name of the handler, use of <see langword="nameof"/> is recommended.</param>
/// <returns>
/// <see cref="ComponentCommandInfo"/> instance for this command.
/// </returns>
/// <exception cref="InvalidOperationException">Module or Component Command couldn't be found.</exception>
public ComponentCommandInfo GetComponentCommandInfo<TModule> (string methodName) where TModule : class
{
var module = GetModuleInfo<TModule>();

return module.ComponentCommands.First(x => x.MethodName == methodName);
}

/// <summary>
/// Gets a built <see cref="ModuleInfo"/>.
/// </summary>
/// <typeparam name="TModule">Type of the module, must be a type of <see cref="InteractionModuleBase{T}"/>.</typeparam>
/// <returns>
/// <see cref="ModuleInfo"/> instance for this module.
/// </returns>
public ModuleInfo GetModuleInfo<TModule> ( ) where TModule : class
{
if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule)))
throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule");

var module = _typedModuleDefs[typeof(TModule)];

if (module is null)
throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service");

return module;
}

/// <inheritdoc/>
public void Dispose ( )
{
_lock.Dispose();
}

private Type GetMostSpecificTypeConverter (Type type)
{
if (_genericTypeConverters.TryGetValue(type, out var matching))
return matching;

var typeInterfaces = type.GetInterfaces();
var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type))
.OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key)));

return candidates.First().Value;
}

private void EnsureClientReady()
{
if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0)
throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event");
}
}
}

+ 70
- 0
src/Discord.Net.Interactions/InteractionServiceConfig.cs View File

@@ -0,0 +1,70 @@
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Represents a configuration class for <see cref="InteractionService"/>.
/// </summary>
public class InteractionServiceConfig
{
/// <summary>
/// Gets or sets the minimum log level severity that will be sent to the <see cref="InteractionService.Log"/> event.
/// </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;

/// <summary>
/// Gets or sets the default <see cref="RunMode" /> commands should have, if one is not specified on the
/// Command attribute or builder.
/// </summary>
public RunMode DefaultRunMode { get; set; } = RunMode.Async;

/// <summary>
/// Gets or sets whether <see cref="RunMode.Sync"/> commands should push exceptions up to the caller.
/// </summary>
public bool ThrowOnError { get; set; } = true;

/// <summary>
/// Gets or sets the delimiters that will be used to seperate group names and the method name when a Message Component Interaction is recieved.
/// </summary>
public char[] InteractionCustomIdDelimiters { get; set; }

/// <summary>
/// Gets or sets the string expression that will be treated as a wild card.
/// </summary>
public string WildCardExpression { get; set; }

/// <summary>
/// Gets or sets the option to delete Slash Command acknowledgements if no Slash Command handler is found in the <see cref="InteractionService"/>.
/// </summary>
public bool DeleteUnknownSlashCommandAck { get; set; } = true;

/// <summary>
/// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory.
/// </summary>
public bool UseCompiledLambda { get; set; } = false;

/// <summary>
/// Gets or sets the option allowing you to use <see cref="AutocompleteHandler"/>s.
/// </summary>
/// <remarks>
/// Since <see cref="AutocompleteHandler"/>s are prioritized over <see cref="AutocompleteCommandInfo"/>s, if <see cref="AutocompleteHandler"/>s are not used, this should be
/// disabled to decrease the lookup time.
/// </remarks>
public bool EnableAutocompleteHandlers { get; set; } = true;

/// <summary>
/// Gets or sets delegate to be used by the <see cref="InteractionService"/> when responding to a Rest based interaction.
/// </summary>
public RestResponseCallback RestResponseCallback { get; set; } = (ctx, str) => Task.CompletedTask;
}

/// <summary>
/// Represents a cached delegate for creating interaction responses to webhook based Discord Interactions.
/// </summary>
/// <param name="context">Execution context that will be injected into the module class.</param>
/// <param name="responseBody">Body of the interaction response.</param>
/// <returns>
/// A task representing the response operation.
/// </returns>
public delegate Task RestResponseCallback(IInteractionContext context, string responseBody);
}

+ 87
- 0
src/Discord.Net.Interactions/Map/CommandMap.cs View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Interactions
{
internal class CommandMap<T> where T : class, ICommandInfo
{
private readonly char[] _seperators;

private readonly CommandMapNode<T> _root;
private readonly InteractionService _commandService;

public IReadOnlyCollection<char> Seperators => _seperators;

public CommandMap(InteractionService commandService, char[] seperators = null)
{
_seperators = seperators ?? Array.Empty<char>();

_commandService = commandService;
_root = new CommandMapNode<T>(null, _commandService._wildCardExp);
}

public void AddCommand(T command, bool ignoreGroupNames = false)
{
if (ignoreGroupNames)
AddCommandToRoot(command);
else
AddCommand(command);
}

public void AddCommandToRoot(T command)
{
string[] key = new string[] { command.Name };
_root.AddCommand(key, 0, command);
}

public void AddCommand(IList<string> input, T command)
{
_root.AddCommand(input, 0, command);
}

public void RemoveCommand(T command)
{
var key = ParseCommandName(command);

_root.RemoveCommand(key, 0);
}

public SearchResult<T> GetCommand(string input)
{
if(_seperators.Any())
return GetCommand(input.Split(_seperators));
else
return GetCommand(new string[] { input });
}

public SearchResult<T> GetCommand(IList<string> input) =>
_root.GetCommand(input, 0);

private void AddCommand(T command)
{
var key = ParseCommandName(command);

_root.AddCommand(key, 0, command);
}

private IList<string> ParseCommandName(T command)
{
var keywords = new List<string>() { command.Name };

var currentParent = command.Module;

while (currentParent != null)
{
if (!string.IsNullOrEmpty(currentParent.SlashGroupName))
keywords.Add(currentParent.SlashGroupName);

currentParent = currentParent.Parent;
}

keywords.Reverse();

return keywords;
}
}
}

+ 113
- 0
src/Discord.Net.Interactions/Map/CommandMapNode.cs View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Discord.Interactions
{
internal class CommandMapNode<T> where T : class, ICommandInfo
{
private const string RegexWildCardExp = "(\\S+)?";

private readonly string _wildCardStr = "*";
private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes;
private readonly ConcurrentDictionary<string, T> _commands;
private readonly ConcurrentDictionary<Regex, T> _wildCardCommands;

public IReadOnlyDictionary<string, CommandMapNode<T>> Nodes => _nodes;
public IReadOnlyDictionary<string, T> Commands => _commands;
public IReadOnlyDictionary<Regex, T> WildCardCommands => _wildCardCommands;
public string Name { get; }

public CommandMapNode (string name, string wildCardExp = null)
{
Name = name;
_nodes = new ConcurrentDictionary<string, CommandMapNode<T>>();
_commands = new ConcurrentDictionary<string, T>();
_wildCardCommands = new ConcurrentDictionary<Regex, T>();

if (!string.IsNullOrEmpty(wildCardExp))
_wildCardStr = wildCardExp;
}

public void AddCommand (IList<string> keywords, int index, T commandInfo)
{
if (keywords.Count == index + 1)
{
if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr))
{
var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray());
var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z";
var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled);

if (!_wildCardCommands.TryAdd(regex, commandInfo))
throw new InvalidOperationException($"A {typeof(T).FullName} already exists with the same name: {string.Join(" ", keywords)}");
}
else
{
if (!_commands.TryAdd(keywords[index], commandInfo))
throw new InvalidOperationException($"A {typeof(T).FullName} already exists with the same name: {string.Join(" ", keywords)}");
}
}
else
{
var node = _nodes.GetOrAdd(keywords[index], (key) => new CommandMapNode<T>(key, _wildCardStr));
node.AddCommand(keywords, ++index, commandInfo);
}
}

public bool RemoveCommand (IList<string> keywords, int index)
{
if (keywords.Count == index + 1)
return _commands.TryRemove(keywords[index], out var _);
else
{
if (!_nodes.TryGetValue(keywords[index], out var node))
throw new InvalidOperationException($"No descendant node was found with the name {keywords[index]}");

return node.RemoveCommand(keywords, ++index);
}
}

public SearchResult<T> GetCommand (IList<string> keywords, int index)
{
string name = string.Join(" ", keywords);

if (keywords.Count == index + 1)
{
if (_commands.TryGetValue(keywords[index], out var cmd))
return SearchResult<T>.FromSuccess(name, cmd);
else
{
foreach (var cmdPair in _wildCardCommands)
{
var match = cmdPair.Key.Match(keywords[index]);
if (match.Success)
{
var args = new string[match.Groups.Count - 1];

for (var i = 1; i < match.Groups.Count; i++)
args[i - 1] = match.Groups[i].Value;

return SearchResult<T>.FromSuccess(name, cmdPair.Value, args.ToArray());
}
}
}
}
else
{
if (_nodes.TryGetValue(keywords[index], out var node))
return node.GetCommand(keywords, ++index);
}

return SearchResult<T>.FromError(name, InteractionCommandError.UnknownCommand, $"No {typeof(T).FullName} found for {name}");
}

public SearchResult<T> GetCommand (string text, int index, char[] seperators)
{
var keywords = text.Split(seperators);
return GetCommand(keywords, index);
}
}
}

+ 59
- 0
src/Discord.Net.Interactions/RestInteractionModuleBase.cs View File

@@ -0,0 +1,59 @@
using Discord.Rest;
using System;
using System.Threading.Tasks;

namespace Discord.Interactions
{
/// <summary>
/// Provides a base class for a Rest based command module to inherit from.
/// </summary>
/// <typeparam name="T">Type of interaction context to be injected into the module.</typeparam>
public abstract class RestInteractionModuleBase<T> : InteractionModuleBase<T>
where T : class, IInteractionContext
{
/// <summary>
/// Gets or sets the underlying Interaction Service.
/// </summary>
public InteractionService InteractionService { get; set; }

/// <summary>
/// Defer a Rest based Discord Interaction using the <see cref="InteractionServiceConfig.RestResponseCallback"/> delegate.
/// </summary>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="options">The request options for this response.</param>
/// <returns>
/// A Task representing the operation of creating the interaction response.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
protected override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null)
{
if (Context.Interaction is not RestInteraction restInteraction)
throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method");

await InteractionService._restResponseCallback(Context, restInteraction.Defer(ephemeral, options)).ConfigureAwait(false);
}

/// <summary>
/// Respond to a Rest based Discord Interaction using the <see cref="InteractionServiceConfig.RestResponseCallback"/> delegate.
/// </summary>
/// <param name="text">The text of the message to be sent.</param>
/// <param name="embeds">A array of embeds to send with this response. Max 10.</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/>.</param>
/// <param name="ephemeral"><see langword="true"/> if the response should be hidden to everyone besides the invoker of the command, otherwise <see langword="false"/>.</param>
/// <param name="allowedMentions">The allowed mentions for this response.</param>
/// <param name="options">The request options for this response.</param>
/// <param name="component">A <see cref="MessageComponent"/> to be sent with this response.</param>
/// <param name="embed">A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored.</param>
/// <returns>
/// A Task representing the operation of creating the interaction response.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the interaction isn't a type of <see cref="RestInteraction"/>.</exception>
protected override async Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null)
{
if (Context.Interaction is not RestInteraction restInteraction)
throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method");

await InteractionService._restResponseCallback(Context, restInteraction.Respond(text, embeds, isTTS, ephemeral, allowedMentions, component, embed, options)).ConfigureAwait(false);
}
}
}

+ 99
- 0
src/Discord.Net.Interactions/Results/AutocompletionResult.cs View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Discord.Interactions
{
/// <summary>
/// Contains the information of a Autocomplete Interaction result.
/// </summary>
public struct AutocompletionResult : IResult
{
/// <inheritdoc/>
public InteractionCommandError? Error { get; }

/// <inheritdoc/>
public string ErrorReason { get; }

/// <inheritdoc/>
public bool IsSuccess => Error is null;

/// <summary>
/// Get the collection of Autocomplete suggestions to be displayed to the user.
/// </summary>
public IReadOnlyCollection<AutocompleteResult> Suggestions { get; }

private AutocompletionResult(IEnumerable<AutocompleteResult> suggestions, InteractionCommandError? error, string reason)
{
Suggestions = suggestions?.ToImmutableArray();
Error = error;
ErrorReason = reason;
}

/// <summary>
/// Initializes a new <see cref="AutocompletionResult" /> with no error and without any <see cref="AutocompleteResult"/> indicating the command service shouldn't
/// return any suggestions.
/// </summary>
/// <returns>
/// A <see cref="AutocompletionResult" /> that does not contain any errors.
/// </returns>
public static AutocompletionResult FromSuccess() =>
new AutocompletionResult(null, null, null);

/// <summary>
/// Initializes a new <see cref="AutocompletionResult" /> with no error.
/// </summary>
/// <param name="suggestions">Autocomplete suggestions to be displayed to the user</param>
/// <returns>
/// A <see cref="AutocompletionResult" /> that does not contain any errors.
/// </returns>
public static AutocompletionResult FromSuccess(IEnumerable<AutocompleteResult> suggestions) =>
new AutocompletionResult(suggestions, null, null);

/// <summary>
/// Initializes a new <see cref="AutocompletionResult" /> with a specified result; this may or may not be an
/// successful execution depending on the <see cref="IResult.Error" /> and
/// <see cref="IResult.ErrorReason" /> specified.
/// </summary>
/// <param name="result">The result to inherit from.</param>
/// <returns>
/// A <see cref="AutocompletionResult"/> that inherits the <see cref="IResult"/> error type and reason.
/// </returns>
public static AutocompletionResult FromError(IResult result) =>
new AutocompletionResult(null, result.Error, result.ErrorReason);

/// <summary>
/// Initializes a new <see cref="AutocompletionResult" /> with a specified exception, indicating an unsuccessful
/// execution.
/// </summary>
/// <param name="exception">The exception that caused the autocomplete process to fail.</param>
/// <returns>
/// A <see cref="AutocompletionResult" /> that contains the exception that caused the unsuccessful execution, along
/// with a <see cref="InteractionCommandError" /> of type <see cref="Exception"/> as well as the exception message as the
/// reason.
/// </returns>
public static AutocompletionResult FromError(Exception exception) =>
new AutocompletionResult(null, InteractionCommandError.Exception, exception.Message);

/// <summary>
/// Initializes a new <see cref="AutocompletionResult" /> with a specified <see cref="InteractionCommandError" /> and its
/// reason, indicating an unsuccessful execution.
/// </summary>
/// <param name="error">The type of error.</param>
/// <param name="reason">The reason behind the error.</param>
/// <returns>
/// A <see cref="AutocompletionResult" /> that contains a <see cref="InteractionCommandError" /> and reason.
/// </returns>
public static AutocompletionResult FromError(InteractionCommandError error, string reason) =>
new AutocompletionResult(null, error, reason);

/// <summary>
/// Gets a string that indicates the autocompletion result.
/// </summary>
/// <returns>
/// <c>Success</c> if <see cref="IsSuccess"/> is <c>true</c>; otherwise "<see cref="Error"/>:
/// <see cref="ErrorReason"/>".
/// </returns>
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

+ 86
- 0
src/Discord.Net.Interactions/Results/ExecuteResult.cs View File

@@ -0,0 +1,86 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Contains information of the command's overall execution result.
/// </summary>
public struct ExecuteResult : IResult
{
/// <summary>
/// Gets the exception that may have occurred during the command execution.
/// </summary>
public Exception Exception { get; }

/// <inheritdoc/>
public InteractionCommandError? Error { get; }

/// <inheritdoc/>
public string ErrorReason { get; }

/// <inheritdoc/>
public bool IsSuccess => !Error.HasValue;

private ExecuteResult (Exception exception, InteractionCommandError? commandError, string errorReason)
{
Exception = exception;
Error = commandError;
ErrorReason = errorReason;
}

/// <summary>
/// Initializes a new <see cref="ExecuteResult" /> with no error, indicating a successful execution.
/// </summary>
/// <returns>
/// A <see cref="ExecuteResult" /> that does not contain any errors.
/// </returns>
public static ExecuteResult FromSuccess ( ) =>
new ExecuteResult(null, null, null);

/// <summary>
/// Initializes a new <see cref="ExecuteResult" /> with a specified <see cref="InteractionCommandError" /> and its
/// reason, indicating an unsuccessful execution.
/// </summary>
/// <param name="commandError">The type of error.</param>
/// <param name="reason">The reason behind the error.</param>
/// <returns>
/// A <see cref="ExecuteResult" /> that contains a <see cref="InteractionCommandError" /> and reason.
/// </returns>
public static ExecuteResult FromError (InteractionCommandError commandError, string reason) =>
new ExecuteResult(null, commandError, reason);

/// <summary>
/// Initializes a new <see cref="ExecuteResult" /> with a specified exception, indicating an unsuccessful
/// execution.
/// </summary>
/// <param name="exception">The exception that caused the command execution to fail.</param>
/// <returns>
/// A <see cref="ExecuteResult" /> that contains the exception that caused the unsuccessful execution, along
/// with a <see cref="InteractionCommandError" /> of type <c>Exception</c> as well as the exception message as the
/// reason.
/// </returns>
public static ExecuteResult FromError (Exception exception) =>
new ExecuteResult(exception, InteractionCommandError.Exception, exception.Message);

/// <summary>
/// Initializes a new <see cref="ExecuteResult" /> with a specified result; this may or may not be an
/// successful execution depending on the <see cref="IResult.Error" /> and
/// <see cref="IResult.ErrorReason" /> specified.
/// </summary>
/// <param name="result">The result to inherit from.</param>
/// <returns>
/// A <see cref="ExecuteResult"/> that inherits the <see cref="IResult"/> error type and reason.
/// </returns>
public static ExecuteResult FromError (IResult result) =>
new ExecuteResult(null, result.Error, result.ErrorReason);

/// <summary>
/// Gets a string that indicates the execution result.
/// </summary>
/// <returns>
/// <c>Success</c> if <see cref="IsSuccess"/> is <see langword="true"/>; otherwise "<see cref="Error"/>:
/// <see cref="ErrorReason"/>".
/// </returns>
public override string ToString ( ) => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

+ 33
- 0
src/Discord.Net.Interactions/Results/IResult.cs View File

@@ -0,0 +1,33 @@
namespace Discord.Interactions
{
/// <summary>
/// Contains information of the result related to a command.
/// </summary>
public interface IResult
{
/// <summary>
/// Gets the error type that may have occurred during the operation.
/// </summary>
/// <returns>
/// A <see cref="InteractionCommandError" /> indicating the type of error that may have occurred during the operation;
/// <see langword="null"/> if the operation was successful.
/// </returns>
InteractionCommandError? Error { get; }

/// <summary>
/// Gets the reason for the error.
/// </summary>
/// <returns>
/// A string containing the error reason.
/// </returns>
string ErrorReason { get; }

/// <summary>
/// Indicates whether the operation was successful or not.
/// </summary>
/// <returns>
/// <see langword="true"/> if the result is positive; otherwise <see langword="false"/>.
/// </returns>
bool IsSuccess { get; }
}
}

+ 51
- 0
src/Discord.Net.Interactions/Results/PreconditionGroupResult.cs View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Discord.Interactions
{
/// <summary>
/// Represents a result type for grouped command preconditions.
/// </summary>
public class PreconditionGroupResult : PreconditionResult
{
/// <summary>
/// Gets the results of the preconditions of this group.
/// </summary>
public IReadOnlyCollection<PreconditionResult> Results { get; }

private PreconditionGroupResult (InteractionCommandError? error, string reason, IEnumerable<PreconditionResult> results) : base(error, reason)
{
Results = results?.ToImmutableArray();
}

/// <summary>
/// Returns a <see cref="PreconditionGroupResult" /> with no errors.
/// </summary>
public static new PreconditionGroupResult FromSuccess ( ) =>
new PreconditionGroupResult(null, null, null);

/// <summary>
/// Returns a <see cref="PreconditionGroupResult" /> with <see cref="InteractionCommandError.Exception" /> and the <see cref="Exception.Message"/>.
/// </summary>
/// <param name="exception">The exception that caused the precondition check to fail.</param>
public static new PreconditionGroupResult FromError (Exception exception) =>
new PreconditionGroupResult(InteractionCommandError.Exception, exception.Message, null);

/// <summary>
/// Returns a <see cref="PreconditionGroupResult" /> with the specified <paramref name="result"/> type.
/// </summary>
/// <param name="result">The result of failure.</param>
public static new PreconditionGroupResult FromError (IResult result) =>
new PreconditionGroupResult(result.Error, result.ErrorReason, null);

/// <summary>
/// Returns a <see cref="PreconditionGroupResult" /> with <see cref="InteractionCommandError.UnmetPrecondition" /> and the
/// specified reason.
/// </summary>
/// <param name="reason">The reason of failure.</param>
/// <param name="results">Precondition results of this group</param>
public static PreconditionGroupResult FromError (string reason, IEnumerable<PreconditionResult> results) =>
new PreconditionGroupResult(InteractionCommandError.UnmetPrecondition, reason, results);
}
}

+ 59
- 0
src/Discord.Net.Interactions/Results/PreconditionResult.cs View File

@@ -0,0 +1,59 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Represents a result type for command preconditions.
/// </summary>
public class PreconditionResult : IResult
{
/// <inheritdoc/>
public InteractionCommandError? Error { get; }

/// <inheritdoc/>
public string ErrorReason { get; }

/// <inheritdoc/>
public bool IsSuccess => Error == null;

/// <summary>
/// Initializes a new <see cref="PreconditionResult" /> class with the command <paramref name="error"/> type
/// and reason.
/// </summary>
/// <param name="error">The type of failure.</param>
/// <param name="reason">The reason of failure.</param>
protected PreconditionResult (InteractionCommandError? error, string reason)
{
Error = error;
ErrorReason = reason;
}

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with no errors.
/// </summary>
public static PreconditionResult FromSuccess ( ) =>
new PreconditionResult(null, null);

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with <see cref="InteractionCommandError.Exception" /> and the <see cref="Exception.Message"/>.
/// </summary>
/// <param name="exception">The exception that caused the precondition check to fail.</param>
public static PreconditionResult FromError (Exception exception) =>
new PreconditionResult(InteractionCommandError.Exception, exception.Message);

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with the specified <paramref name="result"/> type.
/// </summary>
/// <param name="result">The result of failure.</param>
public static PreconditionResult FromError (IResult result) =>
new PreconditionResult(result.Error, result.ErrorReason);

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with <see cref="InteractionCommandError.UnmetPrecondition" /> and the
/// specified reason.
/// </summary>
/// <param name="reason">The reason of failure.</param>
public static PreconditionResult FromError (string reason) =>
new PreconditionResult(InteractionCommandError.UnmetPrecondition, reason);
}
}

+ 37
- 0
src/Discord.Net.Interactions/Results/RuntimeResult.cs View File

@@ -0,0 +1,37 @@
namespace Discord.Interactions
{
/// <summary>
/// Represents the base class for creating command result containers.
/// </summary>
public abstract class RuntimeResult : IResult
{
/// <inheritdoc/>
public InteractionCommandError? Error { get; }

/// <inheritdoc/>
public string ErrorReason { get; }

/// <inheritdoc/>
public bool IsSuccess => !Error.HasValue;

/// <summary>
/// Initializes a new <see cref="RuntimeResult" /> class with the type of error and reason.
/// </summary>
/// <param name="error">The type of failure, or <c>null</c> if none.</param>
/// <param name="reason">The reason of failure.</param>
protected RuntimeResult (InteractionCommandError? error, string reason)
{
Error = error;
ErrorReason = reason;
}

/// <summary>
/// Gets a string that indicates the runtime result.
/// </summary>
/// <returns>
/// <c>Success</c> if <see cref="IsSuccess"/> is <c>true</c>; otherwise "<see cref="Error"/>:
/// <see cref="ErrorReason"/>".
/// </returns>
public override string ToString ( ) => ErrorReason ?? ( IsSuccess ? "Successful" : "Unsuccessful" );
}
}

+ 37
- 0
src/Discord.Net.Interactions/Results/SearchResult.cs View File

@@ -0,0 +1,37 @@
using System;

namespace Discord.Interactions
{
internal struct SearchResult<T> : IResult where T : class, ICommandInfo
{
public string Text { get; }
public T Command { get; }
public string[] RegexCaptureGroups { get; }
public InteractionCommandError? Error { get; }

public string ErrorReason { get; }

public bool IsSuccess => !Error.HasValue;

private SearchResult (string text, T commandInfo, string[] captureGroups, InteractionCommandError? error, string reason)
{
Text = text;
Error = error;
RegexCaptureGroups = captureGroups;
Command = commandInfo;
ErrorReason = reason;
}

public static SearchResult<T> FromSuccess (string text, T commandInfo, string[] wildCardMatch = null) =>
new SearchResult<T>(text, commandInfo, wildCardMatch, null, null);

public static SearchResult<T> FromError (string text, InteractionCommandError error, string reason) =>
new SearchResult<T>(text, null, null, error, reason);
public static SearchResult<T> FromError (Exception ex) =>
new SearchResult<T>(null, null, null, InteractionCommandError.Exception, ex.Message);
public static SearchResult<T> FromError (IResult result) =>
new SearchResult<T>(null, null, null, result.Error, result.ErrorReason);

public override string ToString ( ) => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

+ 61
- 0
src/Discord.Net.Interactions/Results/TypeConverterResult.cs View File

@@ -0,0 +1,61 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Represents a result type for <see cref="TypeConverter.ReadAsync(IInteractionContext, WebSocket.SocketSlashCommandDataOption, IServiceProvider)"/>.
/// </summary>
public struct TypeConverterResult : IResult
{
/// <summary>
/// Gets the result of the convertion if the operation was successful.
/// </summary>
public object Value { get; }

/// <inheritdoc/>
public InteractionCommandError? Error { get; }

/// <inheritdoc/>
public string ErrorReason { get; }

/// <inheritdoc/>
public bool IsSuccess => !Error.HasValue;

private TypeConverterResult (object value, InteractionCommandError? error, string reason)
{
Value = value;
Error = error;
ErrorReason = reason;
}

/// <summary>
/// Returns a <see cref="TypeConverterResult" /> with no errors.
/// </summary>
public static TypeConverterResult FromSuccess (object value) =>
new TypeConverterResult(value, null, null);

/// <summary>
/// Returns a <see cref="TypeConverterResult" /> with <see cref="InteractionCommandError.Exception" /> and the <see cref="Exception.Message"/>.
/// </summary>
/// <param name="exception">The exception that caused the type convertion to fail.</param>
public static TypeConverterResult FromError (Exception exception) =>
new TypeConverterResult(null, InteractionCommandError.Exception, exception.Message);

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with the specified error and the reason.
/// </summary>
/// <param name="error">The type of error.</param>
/// <param name="reason">The reason of failure.</param>
public static TypeConverterResult FromError (InteractionCommandError error, string reason) =>
new TypeConverterResult(null, error, reason);

/// <summary>
/// Returns a <see cref="PreconditionResult" /> with the specified <paramref name="result"/> type.
/// </summary>
/// <param name="result">The result of failure.</param>
public static TypeConverterResult FromError (IResult result) =>
new TypeConverterResult(null, result.Error, result.ErrorReason);

public override string ToString ( ) => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

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

Loading…
Cancel
Save