Browse Source

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.
pull/1958/head
quin lynch 3 years ago
parent
commit
5ffedc26d9
69 changed files with 735 additions and 258 deletions
  1. +1
    -1
      docs/guides/concepts/ratelimits.md
  2. +1
    -1
      docs/guides/interactions/application-commands/01-getting-started.md
  3. +1
    -1
      docs/guides/voice/sending-voice.md
  4. +1
    -1
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  5. +90
    -46
      src/Discord.Net.Commands/CommandService.cs
  6. +39
    -19
      src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs
  7. +47
    -0
      src/Discord.Net.Commands/Results/MatchResult.cs
  8. +1
    -1
      src/Discord.Net.Core/Discord.Net.Core.csproj
  9. +86
    -0
      src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs
  10. +15
    -7
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  11. +0
    -1
      src/Discord.Net.Core/Entities/Channels/IStageChannel.cs
  12. +2
    -2
      src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
  13. +1
    -1
      src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs
  14. +4
    -0
      src/Discord.Net.Core/Entities/Emotes/Emote.cs
  15. +7
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  16. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
  17. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs
  18. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/AutocompleteResult.cs
  19. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs
  20. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs
  21. +0
    -12
      src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
  22. +4
    -4
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  23. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs
  24. +7
    -7
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuOption.cs
  25. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  26. +0
    -17
      src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs
  27. +0
    -15
      src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs
  28. +1
    -1
      src/Discord.Net.Core/Entities/Roles/IRole.cs
  29. +3
    -3
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  30. +12
    -1
      src/Discord.Net.Core/Format.cs
  31. +2
    -2
      src/Discord.Net.Core/Utils/UrlValidation.cs
  32. +1
    -1
      src/Discord.Net.Rest/API/Common/SelectMenuOption.cs
  33. +27
    -11
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  34. +1
    -1
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  35. +5
    -2
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  36. +5
    -0
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  37. +14
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  38. +0
    -1
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
  39. +1
    -0
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  40. +0
    -1
      src/Discord.Net.Rest/Entities/Roles/RestRole.cs
  41. +5
    -4
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  42. +1
    -0
      src/Discord.Net.Rest/Extensions/EntityExtensions.cs
  43. +0
    -1
      src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs
  44. +12
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  45. +30
    -2
      src/Discord.Net.WebSocket/ClientState.cs
  46. +1
    -1
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
  47. +4
    -1
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  48. +61
    -13
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  49. +5
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  50. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  51. +3
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  52. +8
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  53. +21
    -20
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  54. +1
    -1
      src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs
  55. +1
    -1
      src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs
  56. +0
    -6
      src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
  57. +8
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  58. +20
    -9
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  59. +10
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  60. +25
    -4
      src/Discord.Net.Webhook/DiscordWebhookClient.cs
  61. +40
    -13
      src/Discord.Net.Webhook/WebhookClientHelper.cs
  62. +1
    -1
      src/Discord.Net/Discord.Net.nuspec
  63. +0
    -1
      test/Discord.Net.Tests.Unit/ColorTests.cs
  64. +1
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs
  65. +3
    -1
      test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs
  66. +70
    -0
      test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs
  67. +12
    -0
      voice-natives/README.md
  68. BIN
      voice-natives/vnext_natives_win32_x64.zip
  69. BIN
      voice-natives/vnext_natives_win32_x86.zip

+ 1
- 1
docs/guides/concepts/ratelimits.md View File

@@ -1,6 +1,6 @@
# Ratelimits

Ratelimits are a core concept of the discord api, each verified library must follow the ratelimit guidelines. Labs introduces a ratelimit exposure system to help you follow these ratelimits in your own code.
Ratelimits are a core concept of any API - Discords API is no exception. each verified library must follow the ratelimit guidelines.

### Using the ratelimit callback



+ 1
- 1
docs/guides/interactions/application-commands/01-getting-started.md View File

@@ -6,7 +6,7 @@ title: Introduction to slash commands

# Getting started with application commands.

Welcome! This guide will show you how to use application commands. If you have extra questions that aren't covered here you can come to our [Discord](https://discord.com/invite/dvSfUTet3K) server and ask around there.
Welcome! This guide will show you how to use application commands.

## What is an application command?



+ 1
- 1
docs/guides/voice/sending-voice.md View File

@@ -18,7 +18,7 @@ when developing on .NET Core, this is where you execute `dotnet run`
from; typically the same directory as your csproj).

For Windows Users, precompiled binaries are available for your
convienence [here](https://discord.foxbot.me/binaries/).
convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).

For Linux Users, you will need to compile [Sodium] and [Opus] from
source, or install them from your package manager.


+ 1
- 1
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -54,7 +54,7 @@ namespace Discord.Commands.Builders
if (type.GetTypeInfo().IsValueType)
DefaultValue = Activator.CreateInstance(type);
else if (type.IsArray)
type = ParameterType.GetElementType();
DefaultValue = Array.CreateInstance(type.GetElementType(), 0);
ParameterType = type;
}



+ 90
- 46
src/Discord.Net.Commands/CommandService.cs View File

@@ -438,6 +438,13 @@ namespace Discord.Commands
_defaultTypeReaders[type] = reader;
return reader;
}
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null && underlyingType.IsEnum)
{
reader = NullableTypeReader.Create(underlyingType, EnumTypeReader.GetReader(underlyingType));
_defaultTypeReaders[type] = reader;
return reader;
}

//Is this an entity?
for (int i = 0; i < _entityTypeReaders.Count; i++)
@@ -510,19 +517,83 @@ namespace Discord.Commands
services ??= EmptyServiceProvider.Instance;

var searchResult = Search(input);
if (!searchResult.IsSuccess)

var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling);

if (validationResult is SearchResult result)
{
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, result).ConfigureAwait(false);
return result;
}

if (validationResult is MatchResult matchResult)
{
return await HandleCommandPipeline(matchResult, context, services);
}

return validationResult;
}

private async Task<IResult> HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services)
{
if (!matchResult.IsSuccess)
return matchResult;

if (matchResult.Pipeline is ParseResult parseResult)
{
var executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services);

if (!executeResult.IsSuccess && !(executeResult is RuntimeResult || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution)
await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult);
return executeResult;
}

if (matchResult.Pipeline is PreconditionResult preconditionResult)
{
await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false);
}

return matchResult;
}

// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;

if (match.Command.Parameters.Count > 0)
{
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false);
return searchResult;
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;

argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}

var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}

/// <summary>
/// Validates and gets the best <see cref="CommandMatch"/> from a specified <see cref="SearchResult"/>
/// </summary>
/// <param name="matches">The SearchResult.</param>
/// <param name="context">The context of the command.</param>
/// <param name="provider">The service provider to be used on the command's dependency injection.</param>
/// <param name="multiMatchHandling">The handling mode when multiple command matches are found.</param>
/// <returns>A task that represents the asynchronous validation operation. The task result contains the result of the
/// command validation as a <see cref="MatchResult"/> or a <see cref="SearchResult"/> if no matches were found.</returns>
public async Task<IResult> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{
if (!matches.IsSuccess)
return matches;

var commands = searchResult.Commands;
var commands = matches.Commands;
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();

foreach (var match in commands)
foreach (var command in commands)
{
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider);
}

var successfulPreconditions = preconditionResults
@@ -533,19 +604,16 @@ namespace Discord.Commands
{
//All preconditions failed, return the one from the highest priority command
var bestCandidate = preconditionResults
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);

await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false);
return bestCandidate.Value;
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value);
}

//If we get this far, at least one precondition was successful.
var parseResults = new Dictionary<CommandMatch, ParseResult>();

var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);
var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false);

if (parseResult.Error == CommandError.MultipleMatches)
{
@@ -560,51 +628,27 @@ namespace Discord.Commands
}
}

parseResultsDict[pair.Key] = parseResult;
parseResults[pair.Key] = parseResult;
}

// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;

if (match.Command.Parameters.Count > 0)
{
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var weightedParseResults = parseResults
.OrderByDescending(x => CalculateScore(x.Key, x.Value));

argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}

var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}

//Order the parse results by their score so that we choose the most likely result to execute
var parseResults = parseResultsDict
.OrderByDescending(x => CalculateScore(x.Key, x.Value));

var successfulParses = parseResults
var successfulParses = weightedParseResults
.Where(x => x.Value.IsSuccess)
.ToArray();

if (successfulParses.Length == 0)
if(successfulParses.Length == 0)
{
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults
.FirstOrDefault(x => !x.Value.IsSuccess);

await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false);
return bestMatch.Value;
return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value);
}

//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // successful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deferred execution)
await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result);
return result;

return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value);
}
#endregion



+ 39
- 19
src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs View File

@@ -6,30 +6,50 @@ namespace Discord.Commands
{
internal class TimeSpanTypeReader : TypeReader
{
private static readonly string[] Formats = {
"%d'd'%h'h'%m'm'%s's'", //4d3h2m1s
"%d'd'%h'h'%m'm'", //4d3h2m
"%d'd'%h'h'%s's'", //4d3h 1s
"%d'd'%h'h'", //4d3h
"%d'd'%m'm'%s's'", //4d 2m1s
"%d'd'%m'm'", //4d 2m
"%d'd'%s's'", //4d 1s
"%d'd'", //4d
"%h'h'%m'm'%s's'", // 3h2m1s
"%h'h'%m'm'", // 3h2m
"%h'h'%s's'", // 3h 1s
"%h'h'", // 3h
"%m'm'%s's'", // 2m1s
"%m'm'", // 2m
"%s's'", // 1s
/// <summary>
/// TimeSpan try parse formats.
/// </summary>
private static readonly string[] Formats =
{
"%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s
"%d'd'%h'h'%m'm'", // 4d3h2m
"%d'd'%h'h'%s's'", // 4d3h 1s
"%d'd'%h'h'", // 4d3h
"%d'd'%m'm'%s's'", // 4d 2m1s
"%d'd'%m'm'", // 4d 2m
"%d'd'%s's'", // 4d 1s
"%d'd'", // 4d
"%h'h'%m'm'%s's'", // 3h2m1s
"%h'h'%m'm'", // 3h2m
"%h'h'%s's'", // 3h 1s
"%h'h'", // 3h
"%m'm'%s's'", // 2m1s
"%m'm'", // 2m
"%s's'", // 1s
};

/// <inheritdoc />
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan))
? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan))
: Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
if (string.IsNullOrEmpty(input))
throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input));

var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign
if (isNegative)
{
input = input.Substring(1);
}

if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan))
{
return isNegative
? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan))
: Task.FromResult(TypeReaderResult.FromSuccess(timeSpan));
}
else
{
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
}
}
}
}

+ 47
- 0
src/Discord.Net.Commands/Results/MatchResult.cs View File

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

namespace Discord.Commands
{
public class MatchResult : IResult
{
/// <summary>
/// Gets the command that may have matched during the command execution.
/// </summary>
public CommandMatch? Match { get; }

/// <summary>
/// Gets on which pipeline stage the command may have matched or failed.
/// </summary>
public IResult? Pipeline { get; }

/// <inheritdoc />
public CommandError? Error { get; }
/// <inheritdoc />
public string ErrorReason { get; }
/// <inheritdoc />
public bool IsSuccess => !Error.HasValue;

private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason)
{
Match = match;
Error = error;
Pipeline = pipeline;
ErrorReason = errorReason;
}

public static MatchResult FromSuccess(CommandMatch match, IResult pipeline)
=> new MatchResult(match,pipeline,null, null);
public static MatchResult FromError(CommandError error, string reason)
=> new MatchResult(null,null,error, reason);
public static MatchResult FromError(Exception ex)
=> FromError(CommandError.Exception, ex.Message);
public static MatchResult FromError(IResult result)
=> new MatchResult(null, null,result.Error, result.ErrorReason);
public static MatchResult FromError(IResult pipeline, CommandError error, string reason)
=> new MatchResult(null, pipeline, error, reason);

public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";

}
}

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

@@ -26,4 +26,4 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
</Project>
</Project>

+ 86
- 0
src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs View File

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

namespace Discord
{
public enum DefaultApplications : ulong
{
/// <summary>
/// Watch youtube together.
/// </summary>
Youtube = 880218394199220334,

/// <summary>
/// Youtube development application.
/// </summary>
YoutubeDev = 880218832743055411,

/// <summary>
/// Poker!
/// </summary>
Poker = 755827207812677713,

/// <summary>
/// Betrayal: A Party Adventure. Betrayal is a social deduction game inspired by Werewolf, Town of Salem, and Among Us.
/// </summary>
Betrayal = 773336526917861400,

/// <summary>
/// Sit back, relax, and do some fishing!
/// </summary>
Fishing = 814288819477020702,

/// <summary>
/// The queens gambit.
/// </summary>
Chess = 832012774040141894,

/// <summary>
/// Development version of chess.
/// </summary>
ChessDev = 832012586023256104,

/// <summary>
/// LetterTile is a version of scrabble.
/// </summary>
LetterTile = 879863686565621790,

/// <summary>
/// Find words in a jumble of letters in coffee.
/// </summary>
WordSnack = 879863976006127627,

/// <summary>
/// It's like skribbl.io.
/// </summary>
DoodleCrew = 878067389634314250,

/// <summary>
/// It's like cards against humanity.
/// </summary>
Awkword = 879863881349087252,

/// <summary>
/// A word-search like game where you unscramble words and score points in a scrabble fashion.
/// </summary>
SpellCast = 852509694341283871,

/// <summary>
/// Classic checkers
/// </summary>
Checkers = 832013003968348200,

/// <summary>
/// The development version of poker.
/// </summary>
PokerDev = 763133495793942528,

/// <summary>
/// SketchyArtist.
/// </summary>
SketchyArtist = 879864070101172255
}
}

+ 15
- 7
src/Discord.Net.Core/Entities/Channels/INestedChannel.cs View File

@@ -60,13 +60,6 @@ namespace Discord
/// <summary>
/// Creates a new invite to this channel.
/// </summary>
/// <example>
/// <para>The following example creates a new invite to this channel; the invite lasts for 12 hours and can only
/// be used 3 times throughout its lifespan.</para>
/// <code language="cs">
/// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3);
/// </code>
/// </example>
/// <param name="applicationId">The id of the embedded application to open for this invite.</param>
/// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param>
/// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param>
@@ -79,6 +72,21 @@ namespace Discord
/// </returns>
Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);

/// <summary>
/// Creates a new invite to this channel.
/// </summary>
/// <param name="application">The application to open for this invite.</param>
/// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param>
/// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param>
/// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param>
/// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous invite creation operation. The task result contains an invite
/// metadata object containing information for the created invite.
/// </returns>
Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);

/// <summary>
/// Creates a new invite to this channel.
/// </summary>


+ 0
- 1
src/Discord.Net.Core/Entities/Channels/IStageChannel.cs View File

@@ -17,7 +17,6 @@ namespace Discord
string Topic { get; }

/// <summary>
/// The <see cref="StagePrivacyLevel"/> of the current stage.
/// Gets the <see cref="StagePrivacyLevel"/> of the current stage.
/// </summary>
/// <remarks>


+ 2
- 2
src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs View File

@@ -19,12 +19,12 @@ namespace Discord
bool HasJoined { get; }

/// <summary>
/// <see langword="true"/> if the current thread is archived, otherwise <see langword="false"/>.
/// Gets whether or not the current thread is archived.
/// </summary>
bool IsArchived { get; }

/// <summary>
/// Gets whether or not the current thread is archived.
/// Gets the duration of time before the thread is automatically archived after no activity.
/// </summary>
ThreadArchiveDuration AutoArchiveDuration { get; }



+ 1
- 1
src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs View File

@@ -1,7 +1,7 @@
namespace Discord
{
/// <summary>
/// Specifies the privacy levels of a Stage instance.
/// Represents the privacy level of a stage.
/// </summary>
public enum StagePrivacyLevel
{


+ 4
- 0
src/Discord.Net.Core/Entities/Emotes/Emote.cs View File

@@ -74,6 +74,10 @@ namespace Discord
public static bool TryParse(string text, out Emote result)
{
result = null;

if (text == null)
return false;

if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>')
{
bool animated = text[1] == 'a';


+ 7
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -313,6 +313,13 @@ namespace Discord
/// The approximate number of non-offline members in this guild.
/// </returns>
int? ApproximatePresenceCount { get; }
/// <summary>
/// Gets the max bitrate for voice channels in this guild.
/// </summary>
/// <returns>
/// A <see cref="int"/> representing the maximum bitrate value allowed by Discord in this guild.
/// </returns>
int MaxBitrate { get; }

/// <summary>
/// Gets the preferred locale of this guild in IETF BCP 47


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

@@ -11,7 +11,7 @@ namespace Discord
private object _value;

/// <summary>
/// Gets the name of this choice.
/// Gets or sets the name of this choice.
/// </summary>
public string Name
{


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

@@ -1,22 +1,22 @@
namespace Discord
{
/// <summary>
/// ApplicationCommandType is enum of current valid Application Command Types: Slash, User, Message
/// Represents the types of application commands.
/// </summary>
public enum ApplicationCommandType : byte
{
/// <summary>
/// ApplicationCommandType.Slash is Slash command type
/// A Slash command type
/// </summary>
Slash = 1,

/// <summary>
/// ApplicationCommandType.User is Context Menu User command type
/// A Context Menu User command type
/// </summary>
User = 2,

/// <summary>
/// ApplicationCommandType.Message is Context Menu Message command type
/// A Context Menu Message command type
/// </summary>
Message = 3
}


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

@@ -45,7 +45,7 @@ namespace Discord
public object Value
{
get => _value;
set
set
{
if (value is not string && !value.IsNumericType())
throw new ArgumentException($"{nameof(value)} must be a numeric type or a string!");


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

@@ -18,7 +18,7 @@ namespace Discord
string Name { get; }

/// <summary>
/// Gets the params + values from the user.
/// Gets the options that the user has provided.
/// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }
}


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

@@ -26,7 +26,7 @@ namespace Discord
ApplicationCommandOptionType Type { get; }

/// <summary>
/// Gets the options for this command.
/// Gets the nested options of this option.
/// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }
}


+ 0
- 12
src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs View File

@@ -18,18 +18,6 @@ namespace Discord
/// </summary>
Pong = 1,

/// <summary>
/// ACK a command without sending a message, eating the user's input.
/// </summary>
[Obsolete("This response type has been deprecated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)]
Acknowledge = 2,

/// <summary>
/// Respond with a message, showing the user's input.
/// </summary>
[Obsolete("This response type has been deprecated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)]
ChannelMessage = 3,

/// <summary>
/// Respond to an interaction with a message.
/// </summary>


+ 4
- 4
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

@@ -76,7 +76,7 @@ namespace Discord
AddComponent(cmp, row);
break;
case SelectMenuComponent menu:
WithSelectMenu(menu.CustomId, menu.Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row);
WithSelectMenu(menu.CustomId, menu.Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row);
break;
}
}
@@ -715,7 +715,7 @@ namespace Discord
MinValues = selectMenu.MinValues;
IsDisabled = selectMenu.IsDisabled;
Options = selectMenu.Options?
.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default))
.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault))
.ToList();
}

@@ -969,7 +969,7 @@ namespace Discord
Value = value;
Description = description;
Emote = emote;
this.IsDefault = isDefault;
IsDefault = isDefault;
}

/// <summary>
@@ -981,7 +981,7 @@ namespace Discord
Value = option.Value;
Description = option.Description;
Emote = option.Emote;
IsDefault = option.Default;
IsDefault = option.IsDefault;
}

/// <summary>


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs View File

@@ -48,7 +48,7 @@ namespace Discord
public SelectMenuBuilder ToBuilder()
=> new SelectMenuBuilder(
CustomId,
Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.Default)).ToList(),
Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(),
Placeholder,
MaxValues,
MinValues,


+ 7
- 7
src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuOption.cs View File

@@ -6,29 +6,29 @@ namespace Discord
public class SelectMenuOption
{
/// <summary>
/// The user-facing name of the option, max 25 characters.
/// Gets the user-facing name of the option.
/// </summary>
public string Label { get; }

/// <summary>
/// The dev-define value of the option, max 100 characters.
/// Gets the dev-define value of the option.
/// </summary>
public string Value { get; }

/// <summary>
/// An additional description of the option, max 50 characters.
/// Gets a description of the option.
/// </summary>
public string Description { get; }

/// <summary>
/// A <see cref="IEmote"/> that will be displayed with this menu option.
/// Gets the <see cref="IEmote"/> displayed with this menu option.
/// </summary>
public IEmote Emote { get; }

/// <summary>
/// Will render this option as selected by default.
/// Gets whether or not this option will render as selected by default.
/// </summary>
public bool? Default { get; }
public bool? IsDefault { get; }

internal SelectMenuOption(string label, string value, string description, IEmote emote, bool? defaultValue)
{
@@ -36,7 +36,7 @@ namespace Discord
Value = value;
Description = description;
Emote = emote;
Default = defaultValue;
IsDefault = defaultValue;
}
}
}

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

@@ -519,7 +519,7 @@ namespace Discord
Preconditions.AtLeast(name.Length, 1, nameof(name));
Preconditions.AtMost(name.Length, 100, nameof(name));

if (value is string str)
if(value is string str)
{
Preconditions.AtLeast(str.Length, 1, nameof(value));
Preconditions.AtMost(str.Length, 100, nameof(value));
@@ -614,7 +614,7 @@ namespace Discord
MinValue = value;
return this;
}
/// <summary>
/// Sets the current builders max value field.
/// </summary>


+ 0
- 17
src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs View File

@@ -110,12 +110,6 @@ namespace Discord
/// </summary>
ManageEmojis = 0x00_40_00_00_00,

/// <summary>
/// Allows members to use slash commands in text channels.
/// </summary>
[Obsolete("UseSlashCommands has been replaced by UseApplicationCommands", true)]
UseSlashCommands = 0x00_80_00_00_00,

/// <summary>
/// Allows members to use slash commands in text channels.
/// </summary>
@@ -131,17 +125,6 @@ namespace Discord
/// </summary>
ManageThreads = 0x04_00_00_00_00,

/// <summary>
/// Allows for creating and participating in threads
/// </summary>
[Obsolete("UsePublicThreads has been replaced by CreatePublicThreads and SendMessagesInThreads", true)]
UsePublicThreads = 0x08_00_00_00_00,

/// <summary>
/// Allows for creating and participating in private threads
/// </summary>
[Obsolete("UsePrivateThreads has been replaced by CreatePrivateThreads and SendMessagesInThreads", true)]
UsePrivateThreads = 0x10_00_00_00_00,
/// <summary>
/// Allows for creating public threads.
/// </summary>


+ 0
- 15
src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs View File

@@ -176,11 +176,6 @@ namespace Discord
/// </remarks>
ManageEmojisAndStickers = 0x40_00_00_00,
/// <summary>
/// Allows members to use slash commands in text channels.
/// </summary>
[Obsolete("UseSlashCommands has been replaced by UseApplicationCommands", true)]
UseSlashCommands = 0x80_00_00_00,
/// <summary>
/// Allows members to use application commands like slash commands and context menus in text channels.
/// </summary>
UseApplicationCommands = 0x80_00_00_00,
@@ -209,16 +204,6 @@ namespace Discord
/// </summary>
CreatePrivateThreads = 0x10_00_00_00_00,
/// <summary>
/// Allows for creating public threads.
/// </summary>
[Obsolete("UsePublicThreads has been replaced by CreatePublicThreads and SendMessagesInThreads", true)]
UsePublicThreads = 0x08_00_00_00_00,
/// <summary>
/// Allows for creating private threads.
/// </summary>
[Obsolete("UsePrivateThreads has been replaced by CreatePrivateThreads and SendMessagesInThreads", true)]
UsePrivateThreads = 0x10_00_00_00_00,
/// <summary>
/// Allows the usage of custom stickers from other servers.
/// </summary>
UseExternalStickers = 0x20_00_00_00_00,


+ 1
- 1
src/Discord.Net.Core/Entities/Roles/IRole.cs View File

@@ -58,7 +58,7 @@ namespace Discord
/// A string containing the hash of this role's icon.
/// </returns>
string Icon { get; }
/// <summary>
/// <summary>
/// Gets the unicode emoji of this role.
/// </summary>
/// <remarks>


+ 3
- 3
src/Discord.Net.Core/Entities/Users/IPresence.cs View File

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

namespace Discord
{
@@ -14,10 +14,10 @@ namespace Discord
/// <summary>
/// Gets the set of clients where this user is currently active.
/// </summary>
IImmutableSet<ClientType> ActiveClients { get; }
IReadOnlyCollection<ClientType> ActiveClients { get; }
/// <summary>
/// Gets the list of activities that this user currently has available.
/// </summary>
IImmutableList<IActivity> Activities { get; }
IReadOnlyCollection<IActivity> Activities { get; }
}
}

+ 12
- 1
src/Discord.Net.Core/Format.cs View File

@@ -7,7 +7,8 @@ namespace Discord
public static class Format
{
// Characters which need escaping
private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" };
private static readonly string[] SensitiveCharacters = {
"\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" };

/// <summary> Returns a markdown-formatted string with bold formatting. </summary>
public static string Bold(string text) => $"**{text}**";
@@ -104,5 +105,15 @@ namespace Discord
var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", "");
return newText;
}

/// <summary>
/// Formats a user's username + discriminator while maintaining bidirectional unicode
/// </summary>
/// <param name="user">The user whos username and discriminator to format</param>
/// <returns>The username + discriminator</returns>
public static string UsernameAndDiscriminator(IUser user)
{
return $"\u2066{user.Username}\u2069#{user.Discriminator}";
}
}
}

+ 2
- 2
src/Discord.Net.Core/Utils/UrlValidation.cs View File

@@ -5,7 +5,7 @@ namespace Discord.Utils
internal static class UrlValidation
{
/// <summary>
/// Not full URL validation right now. Just ensures protocol is present and that it's either http or https
/// Not full URL validation right now. Just ensures protocol is present and that it's either http or https
/// <see cref="ValidateButton(string)"/> should be used for url buttons.
/// </summary>
/// <param name="url">The URL to validate before sending to Discord.</param>
@@ -22,7 +22,7 @@ namespace Discord.Utils
}

/// <summary>
/// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord
/// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord
/// <see cref="Validate(string)"/> should be used everything other than url buttons.
/// </summary>
/// <param name="url">The URL to validate before sending to discord.</param>


+ 1
- 1
src/Discord.Net.Rest/API/Common/SelectMenuOption.cs View File

@@ -47,7 +47,7 @@ namespace Discord.API
}
}

Default = option.Default ?? Optional<bool>.Unspecified;
Default = option.IsDefault ?? Optional<bool>.Unspecified;
}
}
}

+ 27
- 11
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

@@ -11,9 +11,8 @@ namespace Discord.API.Rest
{
private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };

public Stream File { get; }
public FileAttachment[] Files { get; }

public Optional<string> Filename { get; set; }
public Optional<string> Content { get; set; }
public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; }
@@ -21,22 +20,16 @@ namespace Discord.API.Rest
public Optional<string> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; }
public Optional<AllowedMentions> AllowedMentions { get; set; }
public Optional<ActionRowComponent[]> MessageComponents { get; set; }

public bool IsSpoiler { get; set; } = false;

public UploadWebhookFileParams(Stream file)
public UploadWebhookFileParams(params FileAttachment[] files)
{
File = file;
Files = files;
}

public IReadOnlyDictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();
var filename = Filename.GetValueOrDefault("unknown.dat");
if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix))
filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix);

d["file"] = new MultipartFile(File, filename);

var payload = new Dictionary<string, object>();
if (Content.IsSpecified)
@@ -49,11 +42,34 @@ namespace Discord.API.Rest
payload["username"] = Username.Value;
if (AvatarUrl.IsSpecified)
payload["avatar_url"] = AvatarUrl.Value;
if (MessageComponents.IsSpecified)
payload["components"] = MessageComponents.Value;
if (Embeds.IsSpecified)
payload["embeds"] = Embeds.Value;
if (AllowedMentions.IsSpecified)
payload["allowed_mentions"] = AllowedMentions.Value;

List<object> attachments = new();

for (int n = 0; n != Files.Length; n++)
{
var attachment = Files[n];

var filename = attachment.FileName ?? "unknown.dat";
if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix))
filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix);
d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename);

attachments.Add(new
{
id = (ulong)n,
filename = filename,
description = attachment.Description ?? Optional<string>.Unspecified
});
}

payload["attachments"] = attachments;

var json = new StringBuilder();
using (var text = new StringWriter(json))
using (var writer = new JsonTextWriter(text))


+ 1
- 1
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -55,7 +55,7 @@ namespace Discord.API
_restClientProvider = restClientProvider;
UserAgent = userAgent;
DefaultRetryMode = defaultRetryMode;
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver(), NullValueHandling = NullValueHandling.Include };
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
UseSystemClock = useSystemClock;

RequestQueue = new RequestQueue();


+ 5
- 2
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -227,8 +227,11 @@ namespace Discord.Rest
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
public virtual Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
/// <inheritdoc />


+ 5
- 0
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -21,8 +21,10 @@ namespace Discord.Rest
public int? UserLimit { get; private set; }
/// <inheritdoc />
public ulong? CategoryId { get; private set; }

/// <inheritdoc />
public string Mention => MentionUtils.MentionChannel(Id);

internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, guild, id)
{
@@ -76,6 +78,9 @@ namespace Discord.Rest
public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 14
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -85,6 +85,20 @@ namespace Discord.Rest
public int? ApproximateMemberCount { get; private set; }
/// <inheritdoc />
public int? ApproximatePresenceCount { get; private set; }
/// <inheritdoc/>
public int MaxBitrate
{
get
{
return PremiumTier switch
{
PremiumTier.Tier1 => 128000,
PremiumTier.Tier2 => 256000,
PremiumTier.Tier3 => 384000,
_ => 96000,
};
}
}
/// <inheritdoc />
public NsfwLevel NsfwLevel { get; private set; }
/// <inheritdoc />


+ 0
- 1
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs View File

@@ -47,7 +47,6 @@ namespace Discord.Rest
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; private set; }


internal RestApplicationCommandOption() { }

internal static RestApplicationCommandOption Create(Model model)


+ 1
- 0
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -51,6 +51,7 @@ namespace Discord.Rest
AllowedMentions allowedMentions = args.AllowedMentions.Value;
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(args.Embeds.Value?.Length ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)


+ 0
- 1
src/Discord.Net.Rest/Entities/Roles/RestRole.cs View File

@@ -115,7 +115,6 @@ namespace Discord.Rest
throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
}
}

#endregion
}
}

+ 5
- 4
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -5,6 +5,7 @@ using System.Globalization;
using System.Threading.Tasks;
using Model = Discord.API.User;
using EventUserModel = Discord.API.GuildScheduledEventUser;
using System.Collections.Generic;

namespace Discord.Rest
{
@@ -41,9 +42,9 @@ namespace Discord.Rest
/// <inheritdoc />
public virtual UserStatus Status => UserStatus.Offline;
/// <inheritdoc />
public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
public virtual IReadOnlyCollection<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty;
public virtual IReadOnlyCollection<IActivity> Activities => ImmutableList<IActivity>.Empty;
/// <inheritdoc />
public virtual bool IsWebhook => false;

@@ -128,8 +129,8 @@ namespace Discord.Rest
/// <returns>
/// A string that resolves to Username#Discriminator of the user.
/// </returns>
public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";
public override string ToString() => Format.UsernameAndDiscriminator(this);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})";
#endregion

#region IUser


+ 1
- 0
src/Discord.Net.Rest/Extensions/EntityExtensions.cs View File

@@ -71,6 +71,7 @@ namespace Discord.Rest

public static API.AllowedMentions ToModel(this AllowedMentions entity)
{
if (entity == null) return null;
return new API.AllowedMentions()
{
Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(),


+ 0
- 1
src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs View File

@@ -36,7 +36,6 @@ namespace Discord.Net.Converters

return new GuildFeatures(features, experimental.ToArray());
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();


+ 12
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -502,6 +502,18 @@ namespace Discord.WebSocket
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();
#endregion

#region Presence

/// <summary> Fired when a users presence is updated. </summary>
public event Func<SocketUser, SocketPresence, SocketPresence, Task> PresenceUpdated
{
add { _presenceUpdated.Add(value); }
remove { _presenceUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>> _presenceUpdated = new AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>>();

#endregion

#region Invites
/// <summary>
/// Fired when an invite is created.


+ 30
- 2
src/Discord.Net.WebSocket/ClientState.cs View File

@@ -115,7 +115,7 @@ namespace Discord.WebSocket
if (_guilds.TryRemove(id, out SocketGuild guild))
{
guild.PurgeChannelCache(this);
guild.PurgeGuildUserCache();
guild.PurgeUserCache();
return guild;
}
return null;
@@ -140,7 +140,35 @@ namespace Discord.WebSocket
internal void PurgeUsers()
{
foreach (var guild in _guilds.Values)
guild.PurgeGuildUserCache();
guild.PurgeUserCache();
}

internal SocketApplicationCommand GetCommand(ulong id)
{
if (_commands.TryGetValue(id, out SocketApplicationCommand command))
return command;
return null;
}
internal void AddCommand(SocketApplicationCommand command)
{
_commands[command.Id] = command;
}
internal SocketApplicationCommand GetOrAddCommand(ulong id, Func<ulong, SocketApplicationCommand> commandFactory)
{
return _commands.GetOrAdd(id, commandFactory);
}
internal SocketApplicationCommand RemoveCommand(ulong id)
{
if (_commands.TryRemove(id, out SocketApplicationCommand command))
return command;
return null;
}
internal void PurgeCommands(Func<SocketApplicationCommand, bool> precondition)
{
var ids = _commands.Where(x => precondition(x.Value)).Select(x => x.Key);

foreach (var id in ids)
_commands.TryRemove(id, out var _);
}

internal SocketApplicationCommand GetCommand(ulong id)


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

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>


+ 4
- 1
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -495,9 +495,12 @@ namespace Discord.WebSocket
client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2);
client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2);
}
#endregion
#endregion

#region IDiscordClient
/// <inheritdoc />
ISelfUser IDiscordClient.CurrentUser => CurrentUser;

/// <inheritdoc />
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync().ConfigureAwait(false);


+ 61
- 13
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -76,6 +76,7 @@ namespace Discord.WebSocket
internal int? HandlerTimeout { get; private set; }
internal bool AlwaysDownloadDefaultStickers { get; private set; }
internal bool AlwaysResolveStickers { get; private set; }
internal bool LogGatewayIntentWarnings { get; private set; }
internal new DiscordSocketApiClient ApiClient => base.ApiClient;
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds;
@@ -147,6 +148,7 @@ namespace Discord.WebSocket
AlwaysDownloadUsers = config.AlwaysDownloadUsers;
AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers;
AlwaysResolveStickers = config.AlwaysResolveStickers;
LogGatewayIntentWarnings = config.LogGatewayIntentWarnings;
HandlerTimeout = config.HandlerTimeout;
State = new ClientState(0, 0);
Rest = new DiscordSocketRestClient(config, ApiClient);
@@ -238,6 +240,9 @@ namespace Discord.WebSocket

_defaultStickers = builder.ToImmutable();
}

if(LogGatewayIntentWarnings)
await LogGatewayIntentsWarning().ConfigureAwait(false);
}

/// <inheritdoc />
@@ -708,6 +713,52 @@ namespace Discord.WebSocket
game);
}

private async Task LogGatewayIntentsWarning()
{
if(_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && !_presenceUpdated.HasSubscribers)
{
await _gatewayLogger.WarningAsync("You're using the GuildPresences intent without listening to the PresenceUpdate event, consider removing the intent from your config.").ConfigureAwait(false);
}

if(!_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && _presenceUpdated.HasSubscribers)
{
await _gatewayLogger.WarningAsync("You're using the PresenceUpdate event without specifying the GuildPresences intent, consider adding the intent to your config.").ConfigureAwait(false);
}

bool hasGuildScheduledEventsSubscribers =
_guildScheduledEventCancelled.HasSubscribers ||
_guildScheduledEventUserRemove.HasSubscribers ||
_guildScheduledEventCompleted.HasSubscribers ||
_guildScheduledEventCreated.HasSubscribers ||
_guildScheduledEventStarted.HasSubscribers ||
_guildScheduledEventUpdated.HasSubscribers ||
_guildScheduledEventUserAdd.HasSubscribers;

if(_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && !hasGuildScheduledEventsSubscribers)
{
await _gatewayLogger.WarningAsync("You're using the GuildScheduledEvents gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false);
}

if(!_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && hasGuildScheduledEventsSubscribers)
{
await _gatewayLogger.WarningAsync("You're using events related to the GuildScheduledEvents gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false);
}

bool hasInviteEventSubscribers =
_inviteCreatedEvent.HasSubscribers ||
_inviteDeletedEvent.HasSubscribers;

if (_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && !hasInviteEventSubscribers)
{
await _gatewayLogger.WarningAsync("You're using the GuildInvites gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false);
}

if (!_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && hasInviteEventSubscribers)
{
await _gatewayLogger.WarningAsync("You're using events related to the GuildInvites gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false);
}
}

#region ProcessMessageAsync
private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload)
{
@@ -1858,6 +1909,8 @@ namespace Discord.WebSocket

var data = (payload as JToken).ToObject<API.Presence>(_serializer);

SocketUser user = null;

if (data.GuildId.IsSpecified)
{
var guild = State.GetGuild(data.GuildId.Value);
@@ -1872,7 +1925,7 @@ namespace Discord.WebSocket
return;
}

var user = guild.GetUser(data.User.Id);
user = guild.GetUser(data.User.Id);
if (user == null)
{
if (data.Status == UserStatus.Offline)
@@ -1890,26 +1943,21 @@ namespace Discord.WebSocket
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false);
}
}

var before = user.Clone();
user.Update(State, data, true);
var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => Task.FromResult(user));
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false);
}
else
{
var globalUser = State.GetUser(data.User.Id);
if (globalUser == null)
user = State.GetUser(data.User.Id);
if (user == null)
{
await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false);
return;
}

var before = globalUser.Clone();
globalUser.Update(State, data.User);
globalUser.Update(State, data);
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false);
}

var before = user.Presence.Clone();
user.Update(State, data.User);
user.Update(data);
await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false);
}
break;
case "TYPING_START":


+ 5
- 0
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -183,6 +183,11 @@ namespace Discord.WebSocket
/// </remarks>
public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged;

/// <summary>
/// Gets or sets whether or not to log warnings related to guild intents and events.
/// </summary>
public bool LogGatewayIntentWarnings { get; set; } = true;

/// <summary>
/// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration.
/// </summary>


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -34,12 +34,12 @@ namespace Discord.WebSocket
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();

/// <summary>
/// Returns a collection representing all of the users in the group.
/// Returns a collection representing all of the users in the group.
/// </summary>
public new IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection();

/// <summary>
/// Returns a collection representing all users in the group, not including the client.
/// Returns a collection representing all users in the group, not including the client.
/// </summary>
public IReadOnlyCollection<SocketGroupUser> Recipients
=> _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1);


+ 3
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -324,6 +324,9 @@ namespace Discord.WebSocket
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 8
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -21,6 +21,7 @@ namespace Discord.WebSocket
public int Bitrate { get; private set; }
/// <inheritdoc />
public int? UserLimit { get; private set; }

/// <inheritdoc />
public ulong? CategoryId { get; private set; }
/// <summary>
@@ -31,6 +32,10 @@ namespace Discord.WebSocket
/// </returns>
public ICategoryChannel Category
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null;

/// <inheritdoc />
public string Mention => MentionUtils.MentionChannel(Id);

/// <inheritdoc />
public string Mention => MentionUtils.MentionChannel(Id);
/// <inheritdoc />
@@ -97,6 +102,9 @@ namespace Discord.WebSocket
public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
/// <inheritdoc />
public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 21
- 20
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -185,24 +185,18 @@ namespace Discord.WebSocket
return id.HasValue ? GetVoiceChannel(id.Value) : null;
}
}
/// <summary>
/// Gets the max bitrate for voice channels in this guild.
/// </summary>
/// <returns>
/// A <see cref="int"/> representing the maximum bitrate value allowed by Discord in this guild.
/// </returns>
/// <inheritdoc/>
public int MaxBitrate
{
get
{
var maxBitrate = PremiumTier switch
return PremiumTier switch
{
PremiumTier.Tier1 => 128000,
PremiumTier.Tier2 => 256000,
PremiumTier.Tier3 => 384000,
_ => 96000,
};
return maxBitrate;
}
}
/// <summary>
@@ -1150,22 +1144,29 @@ namespace Discord.WebSocket
}
return null;
}
internal void PurgeGuildUserCache()

/// <summary>
/// Purges this guild's user cache.
/// </summary>
public void PurgeUserCache() => PurgeUserCache(_ => true);
/// <summary>
/// Purges this guild's user cache.
/// </summary>
/// <param name="predicate">The predicate used to select which users to clear.</param>
public void PurgeUserCache(Func<SocketGuildUser, bool> predicate)
{
var members = Users;
var self = CurrentUser;
_members.Clear();
if (self != null)
_members.TryAdd(self.Id, self);
var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id);
var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id);

foreach (var member in membersToPurge)
if(_members.TryRemove(member.Id, out _))
member.GlobalUser.RemoveRef(Discord);

foreach (var member in membersToKeep)
_members.TryAdd(member.Id, member);

_downloaderPromise = new TaskCompletionSource<bool>();
DownloadedMemberCount = _members.Count;

foreach (var member in members)
{
if (member.Id != self?.Id)
member.GlobalUser.RemoveRef(Discord);
}
}

/// <summary>


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs View File

@@ -7,7 +7,7 @@ using Model = Discord.API.Gateway.InviteCreateEvent;
namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based invite to a guild.
/// Represents a WebSocket-based invite to a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketInvite : SocketEntity<string>, IInviteMetadata


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs View File

@@ -57,7 +57,7 @@ namespace Discord.WebSocket
public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id);

/// <summary>
/// Returns an IEnumerable containing all <see cref="SocketGuildUser"/> that have this role.
/// Returns an IEnumerable containing all <see cref="SocketGuildUser"/> that have this role.
/// </summary>
public IEnumerable<SocketGuildUser> Members
=> Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id));


+ 0
- 6
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs View File

@@ -1,7 +1,6 @@
using System.Diagnostics;
using System.Linq;
using Model = Discord.API.User;
using PresenceModel = Discord.API.Presence;

namespace Discord.WebSocket
{
@@ -48,11 +47,6 @@ namespace Discord.WebSocket
}
}

internal void Update(ClientState state, PresenceModel model)
{
Presence = SocketPresence.Create(model);
}

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)";
internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser;
}


+ 8
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -164,8 +164,7 @@ namespace Discord.WebSocket
{
if (updatePresence)
{
Presence = SocketPresence.Create(model);
GlobalUser.Update(state, model);
Update(model);
}
if (model.Nick.IsSpecified)
Nickname = model.Nick.Value;
@@ -174,6 +173,13 @@ namespace Discord.WebSocket
if (model.PremiumSince.IsSpecified)
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks;
}

internal override void Update(PresenceModel model)
{
Presence.Update(model);
GlobalUser.Update(model);
}

private void UpdateRoles(ulong[] roleIds)
{
var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1);


+ 20
- 9
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -11,26 +11,37 @@ namespace Discord.WebSocket
/// Represents the WebSocket user's presence status. This may include their online status and their activity.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct SocketPresence : IPresence
public class SocketPresence : IPresence
{
/// <inheritdoc />
public UserStatus Status { get; }
public UserStatus Status { get; private set; }
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients { get; }
public IReadOnlyCollection<ClientType> ActiveClients { get; private set; }
/// <inheritdoc />
public IImmutableList<IActivity> Activities { get; }
public IReadOnlyCollection<IActivity> Activities { get; private set; }

internal SocketPresence() { }
internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities)
{
Status = status;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.Empty;
}

internal static SocketPresence Create(Model model)
{
var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault());
var activities = ConvertActivitiesList(model.Activities);
return new SocketPresence(model.Status, clients, activities);
var entity = new SocketPresence();
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
Status = model.Status;
ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray<ClientType>.Empty;
Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty;
}

/// <summary>
/// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types
/// where a user is active from the data supplied in the Presence update frame.
@@ -42,7 +53,7 @@ namespace Discord.WebSocket
/// <returns>
/// A collection of all <see cref="ClientType"/>s that this user is active.
/// </returns>
private static IImmutableSet<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict)
private static IReadOnlyCollection<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict)
{
if (clientTypesDict == null || clientTypesDict.Count == 0)
return ImmutableHashSet<ClientType>.Empty;
@@ -84,6 +95,6 @@ namespace Discord.WebSocket
public override string ToString() => Status.ToString();
private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}";

internal SocketPresence Clone() => this;
internal SocketPresence Clone() => MemberwiseClone() as SocketPresence;
}
}

+ 10
- 4
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading.Tasks;
using Discord.Rest;
using Model = Discord.API.User;
using PresenceModel = Discord.API.Presence;

namespace Discord.WebSocket
{
@@ -40,9 +41,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public UserStatus Status => Presence.Status;
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary>
/// Gets mutual guilds shared with this user.
/// </summary>
@@ -91,6 +92,11 @@ namespace Discord.WebSocket
return hasChanges;
}

internal virtual void Update(PresenceModel model)
{
Presence.Update(model);
}

/// <inheritdoc />
public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null)
=> await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false);
@@ -109,8 +115,8 @@ namespace Discord.WebSocket
/// <returns>
/// The full name of the user.
/// </returns>
public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";
public override string ToString() => Format.UsernameAndDiscriminator(this);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})";
internal SocketUser Clone() => MemberwiseClone() as SocketUser;
}
}

+ 25
- 4
src/Discord.Net.Webhook/DiscordWebhookClient.cs View File

@@ -123,14 +123,35 @@ namespace Discord.Webhook
/// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFileAsync(string filePath, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler);
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null,
MessageComponent components = null)
=> WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl,
allowedMentions, options, isSpoiler, components);
/// <summary> Sends a message to the channel for this webhook with an attachment. </summary>
/// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null)
=> WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler);
RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null,
MessageComponent components = null)
=> WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username,
avatarUrl, allowedMentions, options, isSpoiler, components);

/// <summary> Sends a message to the channel for this webhook with an attachment. </summary>
/// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null)
=> WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username,
avatarUrl, allowedMentions, components, options);

/// <summary> Sends a message to the channel for this webhook with an attachment. </summary>
/// <returns> Returns the ID of the created message. </returns>
public Task<ulong> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false,
IEnumerable<Embed> embeds = null, string username = null, string avatarUrl = null,
RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null)
=> WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl,
allowedMentions, components, options);


/// <summary> Modifies the properties of this webhook. </summary>
public Task ModifyWebhookAsync(Action<WebhookProperties> func, RequestOptions options = null)


+ 40
- 13
src/Discord.Net.Webhook/WebhookClientHelper.cs View File

@@ -97,24 +97,51 @@ namespace Discord.Webhook
await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false);
}
public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS,
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components)
{
string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath))
return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler).ConfigureAwait(false);
return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false);
}
public static async Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS,
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler)
public static Task<ulong> SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS,
IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler,
MessageComponent components)
=> SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options);

public static Task<ulong> SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options)
=> SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options);

public static async Task<ulong> SendFilesAsync(DiscordWebhookClient client,
IEnumerable<FileAttachment> attachments, string text, bool isTTS, IEnumerable<Embed> embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options)
{
var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler };
if (username != null)
args.Username = username;
if (avatarUrl != null)
args.AvatarUrl = avatarUrl;
if (embeds != null)
args.Embeds = embeds.Select(x => x.ToModel()).ToArray();
if(allowedMentions != null)
args.AllowedMentions = allowedMentions.ToModel();
embeds ??= Array.Empty<Embed>();
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Count(), 10, nameof(embeds), "A max of 10 embeds are allowed.");

foreach (var attachment in attachments)
{
Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null");
}

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}

if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}

var args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional<API.AllowedMentions>.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional<API.ActionRowComponent[]>.Unspecified };
var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false);
return msg.Id;
}


+ 1
- 1
src/Discord.Net/Discord.Net.nuspec View File

@@ -47,4 +47,4 @@
</group>
</dependencies>
</metadata>
</package>
</package>

+ 0
- 1
test/Discord.Net.Tests.Unit/ColorTests.cs View File

@@ -10,7 +10,6 @@ namespace Discord
/// </summary>
public class ColorTests
{
[Fact]
public void Color_New()
{
Assert.Equal(0u, new Color().RawValue);


+ 1
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs View File

@@ -214,5 +214,6 @@ namespace Discord
public Task<IUserMessage> SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException();
public Task<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException();
public Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException();
public Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException();
}
}

+ 3
- 1
test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs View File

@@ -12,6 +12,8 @@ namespace Discord

public int? UserLimit => throw new NotImplementedException();

public string Mention => throw new NotImplementedException();

public ulong? CategoryId => throw new NotImplementedException();

public int Position => throw new NotImplementedException();
@@ -25,7 +27,6 @@ namespace Discord
public string Name => throw new NotImplementedException();

public DateTimeOffset CreatedAt => throw new NotImplementedException();
public string Mention => throw new NotImplementedException();
public ulong Id => throw new NotImplementedException();

public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
@@ -49,6 +50,7 @@ namespace Discord
}
public Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
public Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException();
public Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();



+ 70
- 0
test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs View File

@@ -0,0 +1,70 @@
using Discord.Commands;
using System;
using Xunit;

namespace Discord
{
public class TimeSpanTypeReaderTests
{
[Theory]
[InlineData("4d3h2m1s", false)] // tests format "%d'd'%h'h'%m'm'%s's'"
[InlineData("4d3h2m", false)] // tests format "%d'd'%h'h'%m'm'"
[InlineData("4d3h1s", false)] // tests format "%d'd'%h'h'%s's'"
[InlineData("4d3h", false)] // tests format "%d'd'%h'h'"
[InlineData("4d2m1s", false)] // tests format "%d'd'%m'm'%s's'"
[InlineData("4d2m", false)] // tests format "%d'd'%m'm'"
[InlineData("4d1s", false)] // tests format "%d'd'%s's'"
[InlineData("4d", false)] // tests format "%d'd'"
[InlineData("3h2m1s", false)] // tests format "%h'h'%m'm'%s's'"
[InlineData("3h2m", false)] // tests format "%h'h'%m'm'"
[InlineData("3h1s", false)] // tests format "%h'h'%s's'"
[InlineData("3h", false)] // tests format "%h'h'"
[InlineData("2m1s", false)] // tests format "%m'm'%s's'"
[InlineData("2m", false)] // tests format "%m'm'"
[InlineData("1s", false)] // tests format "%s's'"
// Negatives
[InlineData("-4d3h2m1s", true)] // tests format "-%d'd'%h'h'%m'm'%s's'"
[InlineData("-4d3h2m", true)] // tests format "-%d'd'%h'h'%m'm'"
[InlineData("-4d3h1s", true)] // tests format "-%d'd'%h'h'%s's'"
[InlineData("-4d3h", true)] // tests format "-%d'd'%h'h'"
[InlineData("-4d2m1s", true)] // tests format "-%d'd'%m'm'%s's'"
[InlineData("-4d2m", true)] // tests format "-%d'd'%m'm'"
[InlineData("-4d1s", true)] // tests format "-%d'd'%s's'"
[InlineData("-4d", true)] // tests format "-%d'd'"
[InlineData("-3h2m1s", true)] // tests format "-%h'h'%m'm'%s's'"
[InlineData("-3h2m", true)] // tests format "-%h'h'%m'm'"
[InlineData("-3h1s", true)] // tests format "-%h'h'%s's'"
[InlineData("-3h", true)] // tests format "-%h'h'"
[InlineData("-2m1s", true)] // tests format "-%m'm'%s's'"
[InlineData("-2m", true)] // tests format "-%m'm'"
[InlineData("-1s", true)] // tests format "-%s's'"
public void TestTimeSpanParse(string input, bool isNegative)
{
var reader = new TimeSpanTypeReader();
var result = reader.ReadAsync(null, input, null).Result;
Assert.True(result.IsSuccess);

var actual = (TimeSpan)result.BestMatch;
Assert.True(actual != TimeSpan.Zero);

if (isNegative)
{
Assert.True(actual < TimeSpan.Zero);

Assert.True(actual.Seconds == 0 || actual.Seconds == -1);
Assert.True(actual.Minutes == 0 || actual.Minutes == -2);
Assert.True(actual.Hours == 0 || actual.Hours == -3);
Assert.True(actual.Days == 0 || actual.Days == -4);
}
else
{
Assert.True(actual > TimeSpan.Zero);

Assert.True(actual.Seconds == 0 || actual.Seconds == 1);
Assert.True(actual.Minutes == 0 || actual.Minutes == 2);
Assert.True(actual.Hours == 0 || actual.Hours == 3);
Assert.True(actual.Days == 0 || actual.Days == 4);
}
}
}
}

+ 12
- 0
voice-natives/README.md View File

@@ -0,0 +1,12 @@
# Voice binaries

These binaries were taken from the [DSharpPlus](https://dsharpplus.github.io/natives/index.html) website and are temporary until we resolve the old url for them.

**NOTE**: You need to rename libopus.dll to opus.dll before use, otherwise audio client will complain about missing libraries.

#### Licenses

| Library | License |
| :-------: | :-------------------------------------------------------- |
| Opus | https://opus-codec.org/license/ |
| libsodium | https://github.com/jedisct1/libsodium/blob/master/LICENSE |

BIN
voice-natives/vnext_natives_win32_x64.zip View File


BIN
voice-natives/vnext_natives_win32_x86.zip View File


Loading…
Cancel
Save