diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 03ec54546..a344594aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,28 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - - package-ecosystem: "nuget" # See documentation for possible values - directory: "/" # Location of package manifests + # Maintain dependencies for NuGet + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" + assignees: + - "NyuwBot" + reviewers: + - "Aiko-IT-Systems/discatsharp" + - "Lulalaby" + labels: + - "dependencies" + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" schedule: interval: "daily" + assignees: + - "NyuwBot" + reviewers: + - "Aiko-IT-Systems/discatsharp" + - "Lulalaby" + labels: + - "ci/cd" + - "dependencies" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..29290d17c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,26 @@ +version: 1 +labels: + - label: "ci/cd" + files: + - ".github/.*" + - ".github/workflows/.*" + - label: "branding" + files: + - ".*.jpg" + - ".*.png" + - ".*.svg" + - ".*.jpeg" + - ".*.guf" + - ".*.css" + - label: "documentation" + files: + - "DisCatSharp.Docs/.*" + - label: "core" + files: + - "DisCatSharp/.*" + - label: "experimental" + title: ^[Exp]:.* + - label: "dependencies" + files: + - ".*.csproj" + - ".*.sln" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 729f0837c..7b1eea54f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,57 +1,77 @@ name: "DisCatSharp Docs" on: push: branches: [ main ] workflow_dispatch: jobs: build: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v3 with: path: DisCatSharp + - uses: actions/checkout@v3 with: repository: Aiko-IT-Systems/DisCatSharp.Docs path: DisCatSharp.Docs token: ${{ secrets.NYUW_TOKEN_GH }} - - - name: Get SSH - uses: webfactory/ssh-agent@v0.5.3 + + - name: Setup .NET + uses: actions/setup-dotnet@v2 with: - ssh-private-key: ${{ secrets.AITSYS_SSH }} + dotnet-version: 6.0.202 + + - name: Restore dependencies + working-directory: ./DisCatSharp + run: dotnet restore + + - name: Build Projects + working-directory: ./DisCatSharp + run: dotnet build --no-restore - name: Build Docs working-directory: ./DisCatSharp shell: pwsh run: | ./rebuild-docs.ps1 -DocsPath "./DisCatSharp.Docs" -Output ".." -PackageName "dcs-docs" - name: Purge old docs working-directory: ./DisCatSharp.Docs + shell: pwsh run: | - shopt -s extglob - rm -rf !(.git|.gitignore) + Get-ChildItem -Exclude .git* | Remove-Item -Recurse -Force - name: Extract new docs + shell: pwsh run: | - tar -xf dcs-docs.tar.xz -C ./DisCatSharp.Docs + Expand-Archive -Path dcs-docs.zip DisCatSharp.Docs/ - name: Commit and push changes uses: EndBug/add-and-commit@main with: cwd: ./DisCatSharp.Docs default_author: user_info author_name: DisCatSharp author_email: team@aitsys.dev committer_name: NyuwBot committer_email: nyuw@aitsys.dev commit: --signoff message: 'Docs update for commit ${{ github.repository }} (${{ github.sha }})' - - name: Publish to Prod - run: | - ssh -o StrictHostKeyChecking=no -T root@80.153.182.68 -f 'cd /var/www/dcs/docs && git pull -f' + publish: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Get SSH Agent + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.AITSYS_SSH }} + + - name: Publish on server + run: | + ssh -o StrictHostKeyChecking=no -T root@80.153.182.68 -f 'cd /var/www/dcs/docs && git pull -f' diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 068fad201..c91fd07d0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,30 +1,30 @@ name: "DisCatSharp .NET" on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - dotnet: [6.0.x] + dotnet: [6.0.202] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v2 with: dotnet-version: ${{ matrix.dotnet }} - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..f885850bf --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,16 @@ +name: Label PRs + +on: + pull_request_target: + types: + - opened + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: srvaroa/labeler@v0.9 + env: + GITHUB_TOKEN: "${{ secrets.NYUW_TOKEN_GH }}" diff --git a/.gitignore b/.gitignore index 57018eb60..902ce304f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,308 +1,325 @@ # Created by https://www.gitignore.io/api/visualstudio ### VisualStudio ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates *.vcxproj.filters # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # NuGET [Nn]uget/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/ ### VisualStudio Patch ### build/ ### Built docs DisCatSharp.Docs/_site/ # Docs build artifacts docfx/ 7zip/ # Build artifacts artifacts/ ### Test files config.json ffmpeg.exe *.pcm appveyor-test.ps1 *.diff CodeMap1.dgml .vscode/launch.json *.patch dcs-docs-preview.tar.xz dcs-docs.tar.xz +dcs-docs.zip +dcs-docs-preview.zip _site/ + +#Ignore thumbnails created by Windows +Thumbs.db +#Ignore files built by Visual Studio +*.exe +*.bak +*.cache +[Bb]in +[Dd]ebug*/ +*.lib +obj/ +[Rr]elease*/ +[Tt]est[Rr]esult* +#Nuget packages folder +packages/ diff --git a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj index ea5fbec84..61ec08f77 100644 --- a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj +++ b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj @@ -1,41 +1,32 @@ DisCatSharp.ApplicationCommands DisCatSharp.ApplicationCommands DisCatSharp.ApplicationCommands ApplicationCommands for DisCatSharp discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, slash, slashcommands, contextmenu - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs index 99b05b664..45e03fe21 100644 --- a/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs +++ b/DisCatSharp.ApplicationCommands/GlobalSuppressions.cs @@ -1,39 +1,44 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.s_registeredCommands")] [assembly: SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.BuildGuildCreateList(System.UInt64,System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}")] [assembly: SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.BuildGuildDeleteList(System.UInt64,System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.Collections.Generic.List{System.UInt64}")] [assembly: SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.BuildGuildOverwriteList(System.UInt64,System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.ValueTuple{System.Collections.Generic.Dictionary{System.UInt64,DisCatSharp.Entities.DiscordApplicationCommand},System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.CheckRegistrationStartup(System.Boolean)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.RegisterCommands(System.Collections.Generic.IEnumerable{DisCatSharp.ApplicationCommands.ApplicationCommandsModuleConfiguration},System.Nullable{System.UInt64})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.UpdateAsync~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1842:Do not use 'WhenAll' with a single task", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.DefaultHelpModule.DefaultHelpAutoCompleteLevelOneProvider.Provider(DisCatSharp.ApplicationCommands.AutocompleteContext)~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{DisCatSharp.Entities.DiscordApplicationCommandAutocompleteChoice}}")] [assembly: SuppressMessage("Performance", "CA1842:Do not use 'WhenAll' with a single task", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.DefaultHelpModule.DefaultHelpAutoCompleteLevelTwoProvider.Provider(DisCatSharp.ApplicationCommands.AutocompleteContext)~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{DisCatSharp.Entities.DiscordApplicationCommandAutocompleteChoice}}")] [assembly: SuppressMessage("Performance", "CA1842:Do not use 'WhenAll' with a single task", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.DefaultHelpModule.DefaultHelpAutoCompleteProvider.Provider(DisCatSharp.ApplicationCommands.AutocompleteContext)~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{DisCatSharp.Entities.DiscordApplicationCommandAutocompleteChoice}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.ApplicationCommandEqualityChecks.IsEqualTo(DisCatSharp.Entities.DiscordApplicationCommand,DisCatSharp.Entities.DiscordApplicationCommand)~System.Boolean")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.BuildGlobalOverwriteList(System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.ValueTuple{System.Collections.Generic.Dictionary{System.UInt64,DisCatSharp.Entities.DiscordApplicationCommand},System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.BuildGuildOverwriteList(System.UInt64,System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.ValueTuple{System.Collections.Generic.Dictionary{System.UInt64,DisCatSharp.Entities.DiscordApplicationCommand},System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.RegisterGlobalCommandsAsync(System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.Threading.Tasks.Task{System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.RegistrationWorker.RegisterGuilldCommandsAsync(System.UInt64,System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand})~System.Threading.Tasks.Task{System.Collections.Generic.List{DisCatSharp.Entities.DiscordApplicationCommand}}")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.DefaultHelpModule.DefaultHelpAsync(DisCatSharp.ApplicationCommands.InteractionContext,System.String,System.String,System.String)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.RunPreexecutionChecksAsync(System.Reflection.MethodInfo,DisCatSharp.ApplicationCommands.BaseContext)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.GlobalCommands")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.GuildCommands")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension.RegisteredCommands")] diff --git a/DisCatSharp.ApplicationCommands/global.json b/DisCatSharp.ApplicationCommands/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.ApplicationCommands/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj index 712863f21..46aceab9a 100644 --- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj +++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj @@ -1,40 +1,31 @@ DisCatSharp.CommandsNext DisCatSharp.CommandsNext - DisCatSharp.CommandsNext - CommandNext extension for DisCatSharp. - discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext - - LICENSE.md + DisCatSharp.CommandsNext + CommandNext extension for DisCatSharp. + discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext - + - - - True - - - - diff --git a/DisCatSharp.CommandsNext/GlobalSuppressions.cs b/DisCatSharp.CommandsNext/GlobalSuppressions.cs index cdd81b5f2..8072fefed 100644 --- a/DisCatSharp.CommandsNext/GlobalSuppressions.cs +++ b/DisCatSharp.CommandsNext/GlobalSuppressions.cs @@ -1,25 +1,27 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Attributes.RequireUserPermissionsAttribute.ExecuteCheckAsync(DisCatSharp.CommandsNext.CommandContext,System.Boolean)~System.Threading.Tasks.Task{System.Boolean}")] +[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.Converters.DiscordMemberConverter.DisCatSharp#CommandsNext#Converters#IArgumentConverter#ConvertAsync(System.String,DisCatSharp.CommandsNext.CommandContext)~System.Threading.Tasks.Task{DisCatSharp.Entities.Optional{DisCatSharp.Entities.DiscordMember}}")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.CommandsNext.CommandsNextExtension.DefaultHelpModule.DefaultHelpAsync(DisCatSharp.CommandsNext.CommandContext,System.String[])~System.Threading.Tasks.Task")] diff --git a/DisCatSharp.CommandsNext/global.json b/DisCatSharp.CommandsNext/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.CommandsNext/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Common/DisCatSharp.Common.csproj b/DisCatSharp.Common/DisCatSharp.Common.csproj index ebb0acaef..097cfbfec 100644 --- a/DisCatSharp.Common/DisCatSharp.Common.csproj +++ b/DisCatSharp.Common/DisCatSharp.Common.csproj @@ -1,46 +1,38 @@ DisCatSharp.Common DisCatSharp.Common True True True Portable DisCatSharp.Common Assortment of various common types and utilities for DisCatSharp's projects. common utilities dotnet dotnet-core dotnetfx netfx netcore csharp - LICENSE.md enable False - + - - - True - - - - diff --git a/DisCatSharp.Common/global.json b/DisCatSharp.Common/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Common/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj index b62452f69..3345d75a3 100644 --- a/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj +++ b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj @@ -1,53 +1,53 @@ net6.0 enable 1591;NU5128;DV2001 false false - + runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all Always Always Always Always Always Always diff --git a/DisCatSharp.Configuration.Tests/global.json b/DisCatSharp.Configuration.Tests/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj index 38117056c..f4618a4d9 100644 --- a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj +++ b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj @@ -1,43 +1,34 @@ net6.0 enable DisCatSharp.Configuration DisCatSharp.Configuration True DisCatSharp.Configuration Configuration for DisCatSharp. discord, discord-api, bots, discord-bots, net-sdk, dcs, discatsharp, csharp, dotnet, vb-net, fsharp - - LICENSE.md - + - - - True - - - - diff --git a/DisCatSharp.Configuration/global.json b/DisCatSharp.Configuration/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Configuration/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Docs/articles/application_commands/translations/using.md b/DisCatSharp.Docs/articles/application_commands/translations/using.md index a1ed12190..78f4577bb 100644 --- a/DisCatSharp.Docs/articles/application_commands/translations/using.md +++ b/DisCatSharp.Docs/articles/application_commands/translations/using.md @@ -1,201 +1,197 @@ --- uid: application_commands_translations_using title: Using Translations --- # Using Translations ## Why Do We Outsource Translation In External JSON Files Pretty simple: It's common to have translations external stored. This makes it easier to modify them, while keeping the code itself clean. ## Adding Translations Translations are added the same way like permissions are added to Application Commands: ```cs const string TRANSLATION_PATH = "translations/"; -Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, perms => { - perms.AddRole(915747497668915230, true); -}, translations => +Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, translations => { string json = File.ReadAllText(TRANSLATION_PATH + "my_command.json"); translations.AddTranslation(json); }); -Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, perms => { - perms.AddRole(915747497668915230, true); -}, translations => +Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, translations => { string json = File.ReadAllText(TRANSLATION_PATH + "my_simple_command.json"); translations.AddTranslation(json); }); ``` > [!WARNING] > If you add a translation to a class, you have to supply translations for every command in this class. Otherwise it will fail. ## Creating The Translation JSON We split the translation in two categories. One for slash command groups and one for normal slash commands and context menu commands. The `name` key in the JSON will be mapped to the command / option / choice names internally. ### Translation For Slash Command Groups Imagine, your class look like the following example: ```cs public class MyCommand : ApplicationCommandsModule { [SlashCommandGroup("my_command", "This is description of the command group.")] public class MyCommandGroup : ApplicationCommandsModule { [SlashCommand("first", "This is description of the command.")] public async Task MySlashCommand(InteractionContext context) { await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() { Content = "This is first subcommand." }); } [SlashCommand("second", "This is description of the command.")] public async Task MySecondCommand(InteractionContext context, [Option("value", "Some string value.")] string value) { await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() { Content = "This is second subcommand. The value was " + value }); } } } ``` The translation json is a object of [Command Group Objects](xref:application_commands_translations_reference#command-group-object) A correct translation json for english and german would look like that: ```json [ { "name": "my_command", "name_translations": { "en-US": "my_command", "de": "mein_befehl" }, "description_translations": { "en-US": "This is description of the command group.", "de": "Das ist die description der Befehl Gruppe." }, "commands": [ { "name": "first", "type": 1, // Type 1 for slash command "name_translations": { "en-US": "first", "de": "erste" }, "description_translations": { "en-US": "This is description of the command.", "de": "Das ist die Beschreibung des Befehls." } }, { "name": "second", "type": 1, // Type 1 for slash command "name_translations": { "en-US": "second", "de": "zweite" }, "description_translations": { "en-US": "This is description of the command.", "de": "Das ist die Beschreibung des Befehls." }, "options": [ { "name": "value", "name_translations": { "en-US": "value", "de": "wert" }, "description_translations": { "en-US": "Some string value.", "de": "Ein string Wert." } } ] } ] } ] ``` ### Translation For Slash Commands & Context Menu Commands Now imagine, that your class look like this example: ```cs public class MySimpleCommands : ApplicationCommandsModule { [SlashCommand("my_command", "This is description of the command.")] public async Task MySlashCommand(InteractionContext context) { } [ContextMenu(ApplicationCommandType.User, "My Command")] public async Task MyContextMenuCommand(ContextMenuContext context) { } } ``` The slash command is a simple [Command Object](xref:application_commands_translations_reference#command-object). Same goes for the context menu command, but note that it can't have a description. Slash Commands has the [type](xref:application_commands_translations_reference#application-command-type) `1` and context menu commands the [type](xref:application_commands_translations_reference#application-command-type) `2` or `3`. We use this to determine, where the translation belongs to. A correct json for this example would look like that: ```json [ { "name":"my_command", "type": 1, // Type 1 for slash command "name_translations":{ "en-US":"my_command", "de":"mein_befehl" }, "description_translations":{ "en-US":"This is description of the command.", "de":"Das ist die Beschreibung des Befehls." } }, { "name":"My Command", "type": 2, // Type 2 for user context menu command "name_translations":{ "en-US":"My Command", "de":"Mein Befehl" } } ] ``` ## Available Locales Discord has a limited choice of locales, in particular, the ones you can select in the client. To see the available locales, visit [this](xref:application_commands_translations_reference#valid-locales) page. ## Can We Get The User And Guild Locale? Yes, you can! Discord sends the user on all [interaction types](xref:DisCatSharp.InteractionType), except `Ping`. We introduced two new properties `Locale` and `GuildLocale` on [InteractionContext](xref:DisCatSharp.ApplicationCommands.InteractionContext), [ContextMenuContext](xref:DisCatSharp.ApplicationCommands.ContextMenuContext), [AutoCompleteContext](xref:DisCatSharp.ApplicationCommands.AutocompleteContext) and [DiscordInteraction](xref:DisCatSharp.Entities.DiscordInteraction). `Locale` is the locale of the user and always represented. `GuildLocale` is only represented, when the interaction is **not** in a DM. Furthermore we cache known user locales on the [DiscordUser](xref:DisCatSharp.Entities.DiscordUser#DisCatSharp_Entities_DiscordUser_Locale) object. diff --git a/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json index beef69613..9ea96e586 100644 --- a/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json +++ b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json @@ -1,1068 +1,1068 @@ { "runtimeTarget": { "name": ".NETStandard,Version=v2.0/", "signature": "" }, "compilationOptions": {}, "targets": { ".NETStandard,Version=v2.0": {}, ".NETStandard,Version=v2.0/": { "DisCatSharp.DocFx.CustomMemberIndexer/1.0.0": { "dependencies": { "CsQuery": "1.3.5-beta5", "Microsoft.Composition": "1.0.31", - "Microsoft.DocAsCode.Common": "2.58.9", - "Microsoft.DocAsCode.Plugins": "2.58.9", + "Microsoft.DocAsCode.Common": "2.59.2", + "Microsoft.DocAsCode.Plugins": "2.59.2", "NETStandard.Library": "2.0.3" }, "runtime": { "DisCatSharp.DocFx.CustomMemberIndexer.dll": {} } }, "CsQuery/1.3.5-beta5": { "runtime": { "lib/net40/CsQuery.dll": { "assemblyVersion": "1.3.5.124", "fileVersion": "1.3.5.124" } } }, "Microsoft.Composition/1.0.31": { "dependencies": { "System.Composition": "1.0.31" } }, "Microsoft.CSharp/4.0.1": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Dynamic.Runtime": "4.0.11", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.ObjectModel": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Reflection.TypeExtensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Runtime.InteropServices": "4.1.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.3/Microsoft.CSharp.dll": { "assemblyVersion": "4.0.1.0", "fileVersion": "1.0.24212.1" } } }, - "Microsoft.DocAsCode.Common/2.58.9": { + "Microsoft.DocAsCode.Common/2.59.2": { "dependencies": { - "Microsoft.DocAsCode.Plugins": "2.58.9", - "Microsoft.DocAsCode.YamlSerialization": "2.58.9" + "Microsoft.DocAsCode.Plugins": "2.59.2", + "Microsoft.DocAsCode.YamlSerialization": "2.59.2" }, "runtime": { "lib/netstandard2.0/Microsoft.DocAsCode.Common.dll": { - "assemblyVersion": "2.58.9.0", - "fileVersion": "2.58.9.12548" + "assemblyVersion": "2.59.2.0", + "fileVersion": "2.59.2.13435" } } }, - "Microsoft.DocAsCode.Plugins/2.58.9": { + "Microsoft.DocAsCode.Plugins/2.59.2": { "dependencies": { "Newtonsoft.Json": "9.0.1", "System.Collections.Immutable": "5.0.0" }, "runtime": { "lib/netstandard2.0/Microsoft.DocAsCode.Plugins.dll": { - "assemblyVersion": "2.58.9.0", - "fileVersion": "2.58.9.12548" + "assemblyVersion": "2.59.2.0", + "fileVersion": "2.59.2.13435" } } }, - "Microsoft.DocAsCode.YamlSerialization/2.58.9": { + "Microsoft.DocAsCode.YamlSerialization/2.59.2": { "dependencies": { "System.Reflection.Emit.Lightweight": "4.3.0", "YamlDotNet.Signed": "5.1.0" }, "runtime": { "lib/netstandard2.0/Microsoft.DocAsCode.YamlSerialization.dll": { - "assemblyVersion": "2.58.9.0", - "fileVersion": "2.58.9.12548" + "assemblyVersion": "2.59.2.0", + "fileVersion": "2.59.2.13435" } } }, "Microsoft.NETCore.Platforms/1.1.0": {}, "Microsoft.NETCore.Targets/1.1.0": {}, "NETStandard.Library/2.0.3": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0" } }, "Newtonsoft.Json/9.0.1": { "dependencies": { "Microsoft.CSharp": "4.0.1", "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Dynamic.Runtime": "4.0.11", "System.Globalization": "4.3.0", "System.IO": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.ObjectModel": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Runtime.Serialization.Primitives": "4.1.1", "System.Text.Encoding": "4.3.0", "System.Text.Encoding.Extensions": "4.0.11", "System.Text.RegularExpressions": "4.1.0", "System.Threading": "4.3.0", "System.Threading.Tasks": "4.3.0", "System.Xml.ReaderWriter": "4.0.11", "System.Xml.XDocument": "4.0.11" }, "runtime": { "lib/netstandard1.0/Newtonsoft.Json.dll": { "assemblyVersion": "9.0.0.0", "fileVersion": "9.0.1.19813" } } }, "System.Buffers/4.5.1": { "runtime": { "lib/netstandard2.0/System.Buffers.dll": { "assemblyVersion": "4.0.3.0", "fileVersion": "4.6.28619.1" } } }, "System.Collections/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Collections.Immutable/5.0.0": { "dependencies": { "System.Memory": "4.5.4" }, "runtime": { "lib/netstandard2.0/System.Collections.Immutable.dll": { "assemblyVersion": "5.0.0.0", "fileVersion": "5.0.20.51904" } } }, "System.Composition/1.0.31": { "dependencies": { "System.Composition.AttributedModel": "1.0.31", "System.Composition.Convention": "1.0.31", "System.Composition.Hosting": "1.0.31", "System.Composition.Runtime": "1.0.31", "System.Composition.TypedParts": "1.0.31" } }, "System.Composition.AttributedModel/1.0.31": { "dependencies": { "System.Reflection": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Composition.AttributedModel.dll": { "assemblyVersion": "1.0.31.0", "fileVersion": "4.6.24705.1" } } }, "System.Composition.Convention/1.0.31": { "dependencies": { "System.Collections": "4.3.0", "System.Composition.AttributedModel": "1.0.31", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.Tools": "4.3.0", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Composition.Convention.dll": { "assemblyVersion": "1.0.31.0", "fileVersion": "4.6.24705.1" } } }, "System.Composition.Hosting/1.0.31": { "dependencies": { "System.Collections": "4.3.0", "System.Composition.Runtime": "1.0.31", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.Tools": "4.3.0", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.ObjectModel": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Composition.Hosting.dll": { "assemblyVersion": "1.0.31.0", "fileVersion": "4.6.24705.1" } } }, "System.Composition.Runtime/1.0.31": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.Tools": "4.3.0", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Reflection": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Composition.Runtime.dll": { "assemblyVersion": "1.0.31.0", "fileVersion": "4.6.24705.1" } } }, "System.Composition.TypedParts/1.0.31": { "dependencies": { "System.Collections": "4.3.0", "System.Composition.AttributedModel": "1.0.31", "System.Composition.Hosting": "1.0.31", "System.Composition.Runtime": "1.0.31", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.Tools": "4.3.0", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Composition.TypedParts.dll": { "assemblyVersion": "1.0.31.0", "fileVersion": "4.6.24705.1" } } }, "System.Diagnostics.Debug/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Diagnostics.Tools/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Dynamic.Runtime/4.0.11": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Globalization": "4.3.0", "System.Linq": "4.3.0", "System.Linq.Expressions": "4.3.0", "System.ObjectModel": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Emit": "4.3.0", "System.Reflection.Emit.ILGeneration": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Reflection.TypeExtensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Dynamic.Runtime.dll": { "assemblyVersion": "4.0.11.0", "fileVersion": "1.0.24212.1" } } }, "System.Globalization/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.IO/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0", "System.Text.Encoding": "4.3.0", "System.Threading.Tasks": "4.3.0" } }, "System.IO.FileSystem/4.0.1": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.IO": "4.3.0", "System.IO.FileSystem.Primitives": "4.0.1", "System.Runtime": "4.3.0", "System.Runtime.Handles": "4.0.1", "System.Text.Encoding": "4.3.0", "System.Threading.Tasks": "4.3.0" } }, "System.IO.FileSystem.Primitives/4.0.1": { "dependencies": { "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.IO.FileSystem.Primitives.dll": { "assemblyVersion": "4.0.1.0", "fileVersion": "1.0.24212.1" } } }, "System.Linq/4.3.0": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0" }, "runtime": { "lib/netstandard1.6/System.Linq.dll": { "assemblyVersion": "4.1.1.0", "fileVersion": "4.6.24705.1" } } }, "System.Linq.Expressions/4.3.0": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Globalization": "4.3.0", "System.IO": "4.3.0", "System.Linq": "4.3.0", "System.ObjectModel": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Emit": "4.3.0", "System.Reflection.Emit.ILGeneration": "4.3.0", "System.Reflection.Emit.Lightweight": "4.3.0", "System.Reflection.Extensions": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Reflection.TypeExtensions": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.6/System.Linq.Expressions.dll": { "assemblyVersion": "4.1.1.0", "fileVersion": "4.6.24705.1" } } }, "System.Memory/4.5.4": { "dependencies": { "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.4.0", "System.Runtime.CompilerServices.Unsafe": "4.5.3" }, "runtime": { "lib/netstandard2.0/System.Memory.dll": { "assemblyVersion": "4.0.1.1", "fileVersion": "4.6.28619.1" } } }, "System.Numerics.Vectors/4.4.0": { "runtime": { "lib/netstandard2.0/System.Numerics.Vectors.dll": { "assemblyVersion": "4.1.3.0", "fileVersion": "4.6.25519.3" } } }, "System.ObjectModel/4.3.0": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.ObjectModel.dll": { "assemblyVersion": "4.0.13.0", "fileVersion": "4.6.24705.1" } } }, "System.Reflection/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.IO": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Runtime": "4.3.0" } }, "System.Reflection.Emit/4.3.0": { "dependencies": { "System.IO": "4.3.0", "System.Reflection": "4.3.0", "System.Reflection.Emit.ILGeneration": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Reflection.Emit.dll": { "assemblyVersion": "4.0.2.0", "fileVersion": "4.6.24705.1" } } }, "System.Reflection.Emit.ILGeneration/4.3.0": { "dependencies": { "System.Reflection": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Reflection.Emit.ILGeneration.dll": { "assemblyVersion": "4.0.2.0", "fileVersion": "4.6.24705.1" } } }, "System.Reflection.Emit.Lightweight/4.3.0": { "dependencies": { "System.Reflection": "4.3.0", "System.Reflection.Emit.ILGeneration": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Reflection.Emit.Lightweight.dll": { "assemblyVersion": "4.0.2.0", "fileVersion": "4.6.24705.1" } } }, "System.Reflection.Extensions/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Reflection": "4.3.0", "System.Runtime": "4.3.0" } }, "System.Reflection.Primitives/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Reflection.TypeExtensions/4.3.0": { "dependencies": { "System.Reflection": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.5/System.Reflection.TypeExtensions.dll": { "assemblyVersion": "4.1.1.0", "fileVersion": "4.6.24705.1" } } }, "System.Resources.ResourceManager/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Globalization": "4.3.0", "System.Reflection": "4.3.0", "System.Runtime": "4.3.0" } }, "System.Runtime/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0" } }, "System.Runtime.CompilerServices.Unsafe/4.5.3": { "runtime": { "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll": { "assemblyVersion": "4.0.4.1", "fileVersion": "4.6.28619.1" } } }, "System.Runtime.Extensions/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Runtime.Handles/4.0.1": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Runtime.InteropServices/4.1.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Reflection": "4.3.0", "System.Reflection.Primitives": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Handles": "4.0.1" } }, "System.Runtime.Serialization.Primitives/4.1.1": { "dependencies": { "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Runtime.Serialization.Primitives.dll": { "assemblyVersion": "4.1.1.0", "fileVersion": "1.0.24212.1" } } }, "System.Text.Encoding/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Text.Encoding.Extensions/4.0.11": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0", "System.Text.Encoding": "4.3.0" } }, "System.Text.RegularExpressions/4.1.0": { "dependencies": { "System.Collections": "4.3.0", "System.Globalization": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Threading": "4.3.0" }, "runtime": { "lib/netstandard1.6/System.Text.RegularExpressions.dll": { "assemblyVersion": "4.1.0.0", "fileVersion": "1.0.24212.1" } } }, "System.Threading/4.3.0": { "dependencies": { "System.Runtime": "4.3.0", "System.Threading.Tasks": "4.3.0" }, "runtime": { "lib/netstandard1.3/System.Threading.dll": { "assemblyVersion": "4.0.12.0", "fileVersion": "4.6.24705.1" } } }, "System.Threading.Tasks/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", "Microsoft.NETCore.Targets": "1.1.0", "System.Runtime": "4.3.0" } }, "System.Threading.Tasks.Extensions/4.0.0": { "dependencies": { "System.Collections": "4.3.0", "System.Runtime": "4.3.0", "System.Threading.Tasks": "4.3.0" }, "runtime": { "lib/netstandard1.0/System.Threading.Tasks.Extensions.dll": { "assemblyVersion": "4.0.0.0", "fileVersion": "1.0.24212.1" } } }, "System.Xml.ReaderWriter/4.0.11": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Globalization": "4.3.0", "System.IO": "4.3.0", "System.IO.FileSystem": "4.0.1", "System.IO.FileSystem.Primitives": "4.0.1", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Runtime.InteropServices": "4.1.0", "System.Text.Encoding": "4.3.0", "System.Text.Encoding.Extensions": "4.0.11", "System.Text.RegularExpressions": "4.1.0", "System.Threading.Tasks": "4.3.0", "System.Threading.Tasks.Extensions": "4.0.0" }, "runtime": { "lib/netstandard1.3/System.Xml.ReaderWriter.dll": { "assemblyVersion": "4.0.11.0", "fileVersion": "1.0.24212.1" } } }, "System.Xml.XDocument/4.0.11": { "dependencies": { "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.Tools": "4.3.0", "System.Globalization": "4.3.0", "System.IO": "4.3.0", "System.Reflection": "4.3.0", "System.Resources.ResourceManager": "4.3.0", "System.Runtime": "4.3.0", "System.Runtime.Extensions": "4.3.0", "System.Text.Encoding": "4.3.0", "System.Threading": "4.3.0", "System.Xml.ReaderWriter": "4.0.11" }, "runtime": { "lib/netstandard1.3/System.Xml.XDocument.dll": { "assemblyVersion": "4.0.11.0", "fileVersion": "1.0.24212.1" } } }, "YamlDotNet.Signed/5.1.0": { "runtime": { "lib/netstandard1.3/YamlDotNet.dll": { "assemblyVersion": "5.0.0.0", "fileVersion": "5.1.0.0" } } } } }, "libraries": { "DisCatSharp.DocFx.CustomMemberIndexer/1.0.0": { "type": "project", "serviceable": false, "sha512": "" }, "CsQuery/1.3.5-beta5": { "type": "package", "serviceable": true, "sha512": "sha512-pMZvX7bHebyfGVJADRGxAC3m8dovsyhSk/lDZtvTboZiPedfwoF9kh2rJXFTjFcbC8VPhPdohwvUC4jSEuLwxg==", "path": "csquery/1.3.5-beta5", "hashPath": "csquery.1.3.5-beta5.nupkg.sha512" }, "Microsoft.Composition/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-R8V1rw4ldOoKIg0QzDY033V8uKrNR0VRKuKVuA1wzuIVeBLwYGghF0y+WbmPI245xSnjRh5eMxxBaxDX9DYZmA==", "path": "microsoft.composition/1.0.31", "hashPath": "microsoft.composition.1.0.31.nupkg.sha512" }, "Microsoft.CSharp/4.0.1": { "type": "package", "serviceable": true, "sha512": "sha512-17h8b5mXa87XYKrrVqdgZ38JefSUqLChUQpXgSnpzsM0nDOhE40FTeNWOJ/YmySGV6tG6T8+hjz6vxbknHJr6A==", "path": "microsoft.csharp/4.0.1", "hashPath": "microsoft.csharp.4.0.1.nupkg.sha512" }, - "Microsoft.DocAsCode.Common/2.58.9": { + "Microsoft.DocAsCode.Common/2.59.2": { "type": "package", "serviceable": true, - "sha512": "sha512-1C/+wx0cQtsmi7d5QHIT77HV5MTKAHKUo3ywSbIWz0L3LkDktuEAScmZ0xrxwUFPcThKdsRw702Bai6QriAPUg==", - "path": "microsoft.docascode.common/2.58.9", - "hashPath": "microsoft.docascode.common.2.58.9.nupkg.sha512" + "sha512": "sha512-ezlS+U8v152D+Bn8iDMW0PWmaYEYw6PQhRSeCUDMlLYlA0KfbI6HBPMkwAdLRxWIh7nUREIzC6EoFfjuwqv22w==", + "path": "microsoft.docascode.common/2.59.2", + "hashPath": "microsoft.docascode.common.2.59.2.nupkg.sha512" }, - "Microsoft.DocAsCode.Plugins/2.58.9": { + "Microsoft.DocAsCode.Plugins/2.59.2": { "type": "package", "serviceable": true, - "sha512": "sha512-y+7Gj2sPURzM+tv7kB/nnnwWYnPiapVnK44Jmg8e6xVVEjTWstEwoc/iY1BBJZsOKNJGamR+9x5kpr6zApV48g==", - "path": "microsoft.docascode.plugins/2.58.9", - "hashPath": "microsoft.docascode.plugins.2.58.9.nupkg.sha512" + "sha512": "sha512-u+2kcqCPZEkaMt0vnNiAJx70FeU3I9HIe0r0fLbbqqKS/mmDz7ir3Km0HJqbSnqsf1dykpNi1l/+AFvSoWbWEw==", + "path": "microsoft.docascode.plugins/2.59.2", + "hashPath": "microsoft.docascode.plugins.2.59.2.nupkg.sha512" }, - "Microsoft.DocAsCode.YamlSerialization/2.58.9": { + "Microsoft.DocAsCode.YamlSerialization/2.59.2": { "type": "package", "serviceable": true, - "sha512": "sha512-dQqzGf6S5xjDACZ5TFcBcrK1+pe2aaYvaGTgqpfNhhCtcNQX+LwByX8lZfICkqMiHv+cDXGg6+PYuxLv7d4UpA==", - "path": "microsoft.docascode.yamlserialization/2.58.9", - "hashPath": "microsoft.docascode.yamlserialization.2.58.9.nupkg.sha512" + "sha512": "sha512-dCLxa63Ey+duzopUDlPCwOYQTuhMwP3uyd/40ZZqp9x/CShV9romA5+WZqY2SxVhw1iG9s70kc0DnBk6XBii4g==", + "path": "microsoft.docascode.yamlserialization/2.59.2", + "hashPath": "microsoft.docascode.yamlserialization.2.59.2.nupkg.sha512" }, "Microsoft.NETCore.Platforms/1.1.0": { "type": "package", "serviceable": true, "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", "path": "microsoft.netcore.platforms/1.1.0", "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" }, "Microsoft.NETCore.Targets/1.1.0": { "type": "package", "serviceable": true, "sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==", "path": "microsoft.netcore.targets/1.1.0", "hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512" }, "NETStandard.Library/2.0.3": { "type": "package", "serviceable": true, "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", "path": "netstandard.library/2.0.3", "hashPath": "netstandard.library.2.0.3.nupkg.sha512" }, "Newtonsoft.Json/9.0.1": { "type": "package", "serviceable": true, "sha512": "sha512-U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==", "path": "newtonsoft.json/9.0.1", "hashPath": "newtonsoft.json.9.0.1.nupkg.sha512" }, "System.Buffers/4.5.1": { "type": "package", "serviceable": true, "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==", "path": "system.buffers/4.5.1", "hashPath": "system.buffers.4.5.1.nupkg.sha512" }, "System.Collections/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", "path": "system.collections/4.3.0", "hashPath": "system.collections.4.3.0.nupkg.sha512" }, "System.Collections.Immutable/5.0.0": { "type": "package", "serviceable": true, "sha512": "sha512-FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", "path": "system.collections.immutable/5.0.0", "hashPath": "system.collections.immutable.5.0.0.nupkg.sha512" }, "System.Composition/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", "path": "system.composition/1.0.31", "hashPath": "system.composition.1.0.31.nupkg.sha512" }, "System.Composition.AttributedModel/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==", "path": "system.composition.attributedmodel/1.0.31", "hashPath": "system.composition.attributedmodel.1.0.31.nupkg.sha512" }, "System.Composition.Convention/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", "path": "system.composition.convention/1.0.31", "hashPath": "system.composition.convention.1.0.31.nupkg.sha512" }, "System.Composition.Hosting/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", "path": "system.composition.hosting/1.0.31", "hashPath": "system.composition.hosting.1.0.31.nupkg.sha512" }, "System.Composition.Runtime/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==", "path": "system.composition.runtime/1.0.31", "hashPath": "system.composition.runtime.1.0.31.nupkg.sha512" }, "System.Composition.TypedParts/1.0.31": { "type": "package", "serviceable": true, "sha512": "sha512-0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", "path": "system.composition.typedparts/1.0.31", "hashPath": "system.composition.typedparts.1.0.31.nupkg.sha512" }, "System.Diagnostics.Debug/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", "path": "system.diagnostics.debug/4.3.0", "hashPath": "system.diagnostics.debug.4.3.0.nupkg.sha512" }, "System.Diagnostics.Tools/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", "path": "system.diagnostics.tools/4.3.0", "hashPath": "system.diagnostics.tools.4.3.0.nupkg.sha512" }, "System.Dynamic.Runtime/4.0.11": { "type": "package", "serviceable": true, "sha512": "sha512-db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", "path": "system.dynamic.runtime/4.0.11", "hashPath": "system.dynamic.runtime.4.0.11.nupkg.sha512" }, "System.Globalization/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", "path": "system.globalization/4.3.0", "hashPath": "system.globalization.4.3.0.nupkg.sha512" }, "System.IO/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", "path": "system.io/4.3.0", "hashPath": "system.io.4.3.0.nupkg.sha512" }, "System.IO.FileSystem/4.0.1": { "type": "package", "serviceable": true, "sha512": "sha512-IBErlVq5jOggAD69bg1t0pJcHaDbJbWNUZTPI96fkYWzwYbN6D9wRHMULLDd9dHsl7C2YsxXL31LMfPI1SWt8w==", "path": "system.io.filesystem/4.0.1", "hashPath": "system.io.filesystem.4.0.1.nupkg.sha512" }, "System.IO.FileSystem.Primitives/4.0.1": { "type": "package", "serviceable": true, "sha512": "sha512-kWkKD203JJKxJeE74p8aF8y4Qc9r9WQx4C0cHzHPrY3fv/L/IhWnyCHaFJ3H1QPOH6A93whlQ2vG5nHlBDvzWQ==", "path": "system.io.filesystem.primitives/4.0.1", "hashPath": "system.io.filesystem.primitives.4.0.1.nupkg.sha512" }, "System.Linq/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", "path": "system.linq/4.3.0", "hashPath": "system.linq.4.3.0.nupkg.sha512" }, "System.Linq.Expressions/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", "path": "system.linq.expressions/4.3.0", "hashPath": "system.linq.expressions.4.3.0.nupkg.sha512" }, "System.Memory/4.5.4": { "type": "package", "serviceable": true, "sha512": "sha512-1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", "path": "system.memory/4.5.4", "hashPath": "system.memory.4.5.4.nupkg.sha512" }, "System.Numerics.Vectors/4.4.0": { "type": "package", "serviceable": true, "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==", "path": "system.numerics.vectors/4.4.0", "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512" }, "System.ObjectModel/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", "path": "system.objectmodel/4.3.0", "hashPath": "system.objectmodel.4.3.0.nupkg.sha512" }, "System.Reflection/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", "path": "system.reflection/4.3.0", "hashPath": "system.reflection.4.3.0.nupkg.sha512" }, "System.Reflection.Emit/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", "path": "system.reflection.emit/4.3.0", "hashPath": "system.reflection.emit.4.3.0.nupkg.sha512" }, "System.Reflection.Emit.ILGeneration/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", "path": "system.reflection.emit.ilgeneration/4.3.0", "hashPath": "system.reflection.emit.ilgeneration.4.3.0.nupkg.sha512" }, "System.Reflection.Emit.Lightweight/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", "path": "system.reflection.emit.lightweight/4.3.0", "hashPath": "system.reflection.emit.lightweight.4.3.0.nupkg.sha512" }, "System.Reflection.Extensions/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", "path": "system.reflection.extensions/4.3.0", "hashPath": "system.reflection.extensions.4.3.0.nupkg.sha512" }, "System.Reflection.Primitives/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", "path": "system.reflection.primitives/4.3.0", "hashPath": "system.reflection.primitives.4.3.0.nupkg.sha512" }, "System.Reflection.TypeExtensions/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", "path": "system.reflection.typeextensions/4.3.0", "hashPath": "system.reflection.typeextensions.4.3.0.nupkg.sha512" }, "System.Resources.ResourceManager/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", "path": "system.resources.resourcemanager/4.3.0", "hashPath": "system.resources.resourcemanager.4.3.0.nupkg.sha512" }, "System.Runtime/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", "path": "system.runtime/4.3.0", "hashPath": "system.runtime.4.3.0.nupkg.sha512" }, "System.Runtime.CompilerServices.Unsafe/4.5.3": { "type": "package", "serviceable": true, "sha512": "sha512-3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==", "path": "system.runtime.compilerservices.unsafe/4.5.3", "hashPath": "system.runtime.compilerservices.unsafe.4.5.3.nupkg.sha512" }, "System.Runtime.Extensions/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", "path": "system.runtime.extensions/4.3.0", "hashPath": "system.runtime.extensions.4.3.0.nupkg.sha512" }, "System.Runtime.Handles/4.0.1": { "type": "package", "serviceable": true, "sha512": "sha512-nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", "path": "system.runtime.handles/4.0.1", "hashPath": "system.runtime.handles.4.0.1.nupkg.sha512" }, "System.Runtime.InteropServices/4.1.0": { "type": "package", "serviceable": true, "sha512": "sha512-16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", "path": "system.runtime.interopservices/4.1.0", "hashPath": "system.runtime.interopservices.4.1.0.nupkg.sha512" }, "System.Runtime.Serialization.Primitives/4.1.1": { "type": "package", "serviceable": true, "sha512": "sha512-HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", "path": "system.runtime.serialization.primitives/4.1.1", "hashPath": "system.runtime.serialization.primitives.4.1.1.nupkg.sha512" }, "System.Text.Encoding/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", "path": "system.text.encoding/4.3.0", "hashPath": "system.text.encoding.4.3.0.nupkg.sha512" }, "System.Text.Encoding.Extensions/4.0.11": { "type": "package", "serviceable": true, "sha512": "sha512-jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", "path": "system.text.encoding.extensions/4.0.11", "hashPath": "system.text.encoding.extensions.4.0.11.nupkg.sha512" }, "System.Text.RegularExpressions/4.1.0": { "type": "package", "serviceable": true, "sha512": "sha512-i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==", "path": "system.text.regularexpressions/4.1.0", "hashPath": "system.text.regularexpressions.4.1.0.nupkg.sha512" }, "System.Threading/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", "path": "system.threading/4.3.0", "hashPath": "system.threading.4.3.0.nupkg.sha512" }, "System.Threading.Tasks/4.3.0": { "type": "package", "serviceable": true, "sha512": "sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", "path": "system.threading.tasks/4.3.0", "hashPath": "system.threading.tasks.4.3.0.nupkg.sha512" }, "System.Threading.Tasks.Extensions/4.0.0": { "type": "package", "serviceable": true, "sha512": "sha512-pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==", "path": "system.threading.tasks.extensions/4.0.0", "hashPath": "system.threading.tasks.extensions.4.0.0.nupkg.sha512" }, "System.Xml.ReaderWriter/4.0.11": { "type": "package", "serviceable": true, "sha512": "sha512-ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==", "path": "system.xml.readerwriter/4.0.11", "hashPath": "system.xml.readerwriter.4.0.11.nupkg.sha512" }, "System.Xml.XDocument/4.0.11": { "type": "package", "serviceable": true, "sha512": "sha512-Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==", "path": "system.xml.xdocument/4.0.11", "hashPath": "system.xml.xdocument.4.0.11.nupkg.sha512" }, "YamlDotNet.Signed/5.1.0": { "type": "package", "serviceable": true, "sha512": "sha512-3iZF8/sDp/AxnA3YZ3gR+8irfd2FiBsqdR9fywzcpeBdaqUDRncqejAv3+jIwwCoHJz3No6JT7u+NzTeVE+5VA==", "path": "yamldotnet.signed/5.1.0", "hashPath": "yamldotnet.signed.5.1.0.nupkg.sha512" } } } \ No newline at end of file diff --git a/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.dll b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.dll index 45e978f4d..255d514ec 100644 Binary files a/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.dll and b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.dll differ diff --git a/DisCatSharp.Docs/dcs/src/scripts/search-worker.js b/DisCatSharp.Docs/dcs/src/scripts/search-worker.js index 60852af4c..d794bb2de 100644 --- a/DisCatSharp.Docs/dcs/src/scripts/search-worker.js +++ b/DisCatSharp.Docs/dcs/src/scripts/search-worker.js @@ -1,80 +1,80 @@ (function () { importScripts('lunr.min.js'); var lunrIndex; var stopWords = null; var searchData = {}; lunr.tokenizer.separator = /[\s\-\.\(\)]+/; var stopWordsRequest = new XMLHttpRequest(); - stopWordsRequest.open('GET', '../search-stopwords.json'); + stopWordsRequest.open('GET', '/search-stopwords.json'); stopWordsRequest.onload = function () { if (this.status != 200) { return; } stopWords = JSON.parse(this.responseText); buildIndex(); } stopWordsRequest.send(); var searchDataRequest = new XMLHttpRequest(); - searchDataRequest.open('GET', '../index.json'); + searchDataRequest.open('GET', '/index.json'); searchDataRequest.onload = function () { if (this.status != 200) { return; } searchData = JSON.parse(this.responseText); buildIndex(); postMessage({ e: 'index-ready' }); } searchDataRequest.send(); onmessage = function (oEvent) { var q = oEvent.data.q; var hits = lunrIndex.search(q); var results = []; hits.forEach(function (hit) { var item = searchData[hit.ref]; results.push({ 'href': item.href, 'title': item.title, 'keywords': item.keywords }); }); postMessage({ e: 'query-ready', q: q, d: results }); } function buildIndex() { if (stopWords !== null && !isEmpty(searchData)) { lunrIndex = lunr(function () { this.pipeline.remove(lunr.stopWordFilter); this.ref('href'); this.field('title', { boost: 50 }); this.field('keywords', { boost: 20 }); for (var prop in searchData) { if (searchData.hasOwnProperty(prop)) { this.add(searchData[prop]); } } var docfxStopWordFilter = lunr.generateStopWordFilter(stopWords); lunr.Pipeline.registerFunction(docfxStopWordFilter, 'docfxStopWordFilter'); this.pipeline.add(docfxStopWordFilter); this.searchPipeline.add(docfxStopWordFilter); }); } } function isEmpty(obj) { if(!obj) return true; for (var prop in obj) { if (obj.hasOwnProperty(prop)) return false; } return true; } })(); diff --git a/DisCatSharp.Docs/dcs/src/styles/main.css b/DisCatSharp.Docs/dcs/src/styles/main.css index 5ca71731d..dd14c104e 100644 --- a/DisCatSharp.Docs/dcs/src/styles/main.css +++ b/DisCatSharp.Docs/dcs/src/styles/main.css @@ -1,31 +1,35 @@ +:root { + color-scheme: dark; +} + #logo { max-height: 50px; } .navbar-default { background-color: #2f3136; } h1.logo-center { text-align: center; margin-top: 0; } article p > img { box-shadow: 3px 3px 6px #111; border-radius: 3px; border: 1px solid #222; } h1.delete-this { display: none; } span.xref { color: #e87408; } diff --git a/DisCatSharp.Docs/docfx.json b/DisCatSharp.Docs/docfx.json index 8c43850d7..9b85b07b6 100644 --- a/DisCatSharp.Docs/docfx.json +++ b/DisCatSharp.Docs/docfx.json @@ -1,106 +1,102 @@ { "metadata": [ { "src": [ { "src": "../", "files": [ "**.csproj" ], "exclude": [ "**/obj/**", "**/bin/**", "_site/**" ] } ], "dest": "api", "filter": "filter_config.yml" } ], "build": { "content": [ { "files": [ "api/**.yml", "api/**.md" ] }, { "files": [ "**.md", "toc.yml", "faq/**.yml", "faq/**.md", "articles/**.yml", "articles/**.md", "natives/**.yml", "natives/**.md" ], "exclude": [ "**/bin/**", "**/obj/**", "_site/**", "dcs/**" ] } ], "resource": [ { "files": [ "images/**", "natives/**.zip" ], "exclude": [ "**/bin/**", "**/obj/**", "_site/**", "images/_**" ] } ], "overwrite": [ { "files": [], "exclude": [ "**/bin/**", "**/obj/**", "_site/**" ] } ], "dest": "_site", "globalMetadata": { "_appFooter": "© 2021-2022 Aiko IT Systems", "_enableSearch": true, "_enableNewTab": true, "_appTitle": "DisCatSharp Docs", "_gitUrlPattern": "git", "_gitContribute": { "repo": "https://github.com/Aiko-IT-Systems/DisCatSharp", "branch": "main" - }, - //"_disableNavbar": false, - //"_disableBreadcrumb": false, - //"_disableAffix": true, - //"_disableToc": false, - //"_disableContribution": true + } }, "disableGitFeatures": false, "exportRawModel": true, + "rawModelOutputFolder": "_site/.bot-query/raw/", "globalMetadataFiles": [], "fileMetadataFiles": [], "template": [ "dcs" ], "postProcessors": ["ExtractSearchIndex", "CustomMemberIndexer"], "noLangKeyword": false, "keepFileLink": false, "cleanupCacheHistory": false, "sitemap": { "baseUrl": "https://docs.dcs.aitsys.dev/", "changefreq": "hourly", "priority": 1.0 } } } diff --git a/DisCatSharp.EventHandlers.Tests/global.json b/DisCatSharp.EventHandlers.Tests/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.EventHandlers.Tests/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj index cd1432f82..edbb9056a 100644 --- a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj +++ b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj @@ -1,38 +1,29 @@ net6.0 enable True DisCatSharp.Hosting.DependencyInjection Dependency Injection for DisCatSharp.Hosting. discord, discord-api, bots, discord-bots, net-sdk, dcs, discatsharp, csharp, dotnet, vb-net, fsharp - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.Hosting.DependencyInjection/global.json b/DisCatSharp.Hosting.DependencyInjection/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Hosting.DependencyInjection/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj index e02ba5b6c..8f77ba521 100644 --- a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj +++ b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj @@ -1,49 +1,49 @@ net6.0 enable 1591;NU5128;DV2001 false false - + runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all Always Always Always Always diff --git a/DisCatSharp.Hosting.Tests/global.json b/DisCatSharp.Hosting.Tests/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj index 49eea7954..1b42203af 100644 --- a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj +++ b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj @@ -1,40 +1,31 @@ net6.0 enable True DisCatSharp.Hosting Hosting for DisCatSharp. discord, discord-api, bots, discord-bots, net-sdk, dcs, discatsharp, csharp, dotnet, vb-net, fsharp - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.Hosting/global.json b/DisCatSharp.Hosting/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Hosting/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj index a55d3130e..d3a703399 100644 --- a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj +++ b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj @@ -1,40 +1,31 @@ DisCatSharp.Interactivity DisCatSharp.Interactivity DisCatSharp.Interactivity Interactivity extension for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, interactive, pagination, reactions - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs index f040e5933..658274844 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs @@ -1,265 +1,265 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The interaction pagination request. /// internal class InteractionPaginationRequest : IPaginationRequest { private int _index; private readonly List _pages = new(); private readonly TaskCompletionSource _tcs = new(); private DiscordInteraction _lastInteraction; private CancellationTokenSource _interactionCts; private readonly CancellationToken _token; private readonly DiscordUser _user; private readonly DiscordMessage _message; private readonly PaginationButtons _buttons; private readonly PaginationBehaviour _wrapBehavior; private readonly ButtonPaginationBehavior _behaviorBehavior; /// /// Initializes a new instance of the class. /// /// The interaction. /// The message. /// The user. /// The behavior. /// The behavior behavior. /// The buttons. /// The pages. /// The token. public InteractionPaginationRequest(DiscordInteraction interaction, DiscordMessage message, DiscordUser user, PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, PaginationButtons buttons, IEnumerable pages, CancellationToken token) { this._user = user; this._token = token; this._buttons = new PaginationButtons(buttons); this._message = message; this._wrapBehavior = behavior; this._behaviorBehavior = behaviorBehavior; this._pages.AddRange(pages); this.RegenerateCts(interaction); this._token.Register(() => this._tcs.TrySetResult(false)); } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Regenerates the cts. /// /// The interaction. internal void RegenerateCts(DiscordInteraction interaction) { this._interactionCts?.Dispose(); this._lastInteraction = interaction; this._interactionCts = new CancellationTokenSource(TimeSpan.FromSeconds((60 * 15) - 5)); this._interactionCts.Token.Register(() => this._tcs.TrySetResult(false)); } /// /// Gets the page. /// public Task GetPageAsync() { var page = Task.FromResult(this._pages[this._index]); if (this.PageCount is 1) { - this._buttons.ButtonArray.Select(b => b.Disable()); + _ = this._buttons.ButtonArray.Select(b => b.Disable()); this._buttons.Stop.Enable(); return page; } if (this._wrapBehavior is PaginationBehaviour.WrapAround) return page; this._buttons.SkipLeft.Disabled = this._index < 2; this._buttons.Left.Disabled = this._index < 1; this._buttons.Right.Disabled = this._index == this.PageCount - 1; this._buttons.SkipRight.Disabled = this._index >= this.PageCount - 2; return page; } /// /// Skips the left page. /// public Task SkipLeftAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index is 0 ? this._pages.Count - 1 : 0; return Task.CompletedTask; } this._index = 0; return Task.CompletedTask; } /// /// Skips the right page. /// public Task SkipRightAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index == this.PageCount - 1 ? 0 : this.PageCount - 1; return Task.CompletedTask; } this._index = this._pages.Count - 1; return Task.CompletedTask; } /// /// Gets the next page. /// /// A Task. public Task NextPageAsync() { this._index++; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index >= this.PageCount) this._index = 0; return Task.CompletedTask; } this._index = Math.Min(this._index, this.PageCount - 1); return Task.CompletedTask; } /// /// Gets the previous page. /// public Task PreviousPageAsync() { this._index--; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index is -1) this._index = this._pages.Count - 1; return Task.CompletedTask; } this._index = Math.Max(this._index, 0); return Task.CompletedTask; } /// /// Gets the emojis. /// public Task GetEmojisAsync() => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); /// /// Gets the buttons. /// public Task> GetButtonsAsync() => Task.FromResult((IEnumerable)this._buttons.ButtonArray); /// /// Gets the message. /// public Task GetMessageAsync() => Task.FromResult(this._message); /// /// Gets the user. /// public Task GetUserAsync() => Task.FromResult(this._user); /// /// Gets the task completion source. /// public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this._tcs); /// /// Cleanup. /// public async Task DoCleanupAsync() { switch (this._behaviorBehavior) { case ButtonPaginationBehavior.Disable: var buttons = this._buttons.ButtonArray .Select(b => new DiscordButtonComponent(b)) .Select(b => b.Disable()); var builder = new DiscordWebhookBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed) .AddComponents(buttons); await this._lastInteraction.EditOriginalResponseAsync(builder).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteButtons: builder = new DiscordWebhookBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed); await this._lastInteraction.EditOriginalResponseAsync(builder).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteMessage: await this._lastInteraction.DeleteOriginalResponseAsync().ConfigureAwait(false); break; case ButtonPaginationBehavior.Ignore: break; } } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Paginator.cs b/DisCatSharp.Interactivity/EventHandling/Paginator.cs index 8711e1df4..3e3da649f 100644 --- a/DisCatSharp.Interactivity/EventHandling/Paginator.cs +++ b/DisCatSharp.Interactivity/EventHandling/Paginator.cs @@ -1,314 +1,305 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading.Tasks; using ConcurrentCollections; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// The paginator. /// internal class Paginator : IPaginator { DiscordClient _client; ConcurrentHashSet _requests; /// /// Creates a new EventWaiter object. /// /// Discord client public Paginator(DiscordClient client) { this._client = client; this._requests = new ConcurrentHashSet(); this._client.MessageReactionAdded += this.HandleReactionAdd; this._client.MessageReactionRemoved += this.HandleReactionRemove; this._client.MessageReactionsCleared += this.HandleReactionClear; } /// /// Dos the pagination async. /// /// The request. public async Task DoPaginationAsync(IPaginationRequest request) { await this.ResetReactionsAsync(request).ConfigureAwait(false); this._requests.Add(request); try { var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false); await tcs.Task.ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); } finally { this._requests.TryRemove(request); try { await request.DoCleanupAsync().ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); } } } /// /// Handles the reaction add. /// /// The client. /// The event's arguments. private Task HandleReactionAdd(DiscordClient client, MessageReactionAddEventArgs eventArgs) { if (this._requests.Count == 0) return Task.CompletedTask; _ = Task.Run(async () => { foreach (var req in this._requests) { var emojis = await req.GetEmojisAsync().ConfigureAwait(false); var msg = await req.GetMessageAsync().ConfigureAwait(false); var usr = await req.GetUserAsync().ConfigureAwait(false); if (msg.Id == eventArgs.Message.Id) { if (eventArgs.User.Id == usr.Id) { if (req.PageCount > 1 && (eventArgs.Emoji == emojis.Left || eventArgs.Emoji == emojis.SkipLeft || eventArgs.Emoji == emojis.Right || eventArgs.Emoji == emojis.SkipRight || eventArgs.Emoji == emojis.Stop)) { await this.PaginateAsync(req, eventArgs.Emoji).ConfigureAwait(false); } else if (eventArgs.Emoji == emojis.Stop && req is PaginationRequest paginationRequest && paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) { await this.PaginateAsync(req, eventArgs.Emoji).ConfigureAwait(false); } else { await msg.DeleteReactionAsync(eventArgs.Emoji, eventArgs.User).ConfigureAwait(false); } } else if (eventArgs.User.Id != this._client.CurrentUser.Id) { if (eventArgs.Emoji != emojis.Left && eventArgs.Emoji != emojis.SkipLeft && eventArgs.Emoji != emojis.Right && eventArgs.Emoji != emojis.SkipRight && eventArgs.Emoji != emojis.Stop) { await msg.DeleteReactionAsync(eventArgs.Emoji, eventArgs.User).ConfigureAwait(false); } } } } }); return Task.CompletedTask; } /// /// Handles the reaction remove. /// /// The client. /// The event's arguments. private Task HandleReactionRemove(DiscordClient client, MessageReactionRemoveEventArgs eventArgs) { if (this._requests.Count == 0) return Task.CompletedTask; _ = Task.Run(async () => { foreach (var req in this._requests) { var emojis = await req.GetEmojisAsync().ConfigureAwait(false); var msg = await req.GetMessageAsync().ConfigureAwait(false); var usr = await req.GetUserAsync().ConfigureAwait(false); if (msg.Id == eventArgs.Message.Id) { if (eventArgs.User.Id == usr.Id) { if (req.PageCount > 1 && (eventArgs.Emoji == emojis.Left || eventArgs.Emoji == emojis.SkipLeft || eventArgs.Emoji == emojis.Right || eventArgs.Emoji == emojis.SkipRight || eventArgs.Emoji == emojis.Stop)) { await this.PaginateAsync(req, eventArgs.Emoji).ConfigureAwait(false); } else if (eventArgs.Emoji == emojis.Stop && req is PaginationRequest paginationRequest && paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) { await this.PaginateAsync(req, eventArgs.Emoji).ConfigureAwait(false); } } } } }); return Task.CompletedTask; } /// /// Handles the reaction clear. /// /// The client. /// The eventArgs. private Task HandleReactionClear(DiscordClient client, MessageReactionsClearEventArgs eventArgs) { if (this._requests.Count == 0) return Task.CompletedTask; _ = Task.Run(async () => { foreach (var req in this._requests) { var msg = await req.GetMessageAsync().ConfigureAwait(false); if (msg.Id == eventArgs.Message.Id) { await this.ResetReactionsAsync(req).ConfigureAwait(false); } } }); return Task.CompletedTask; } /// /// Resets the reactions async. /// /// The p. private async Task ResetReactionsAsync(IPaginationRequest p) { var msg = await p.GetMessageAsync().ConfigureAwait(false); var emojis = await p.GetEmojisAsync().ConfigureAwait(false); - // Test permissions to avoid a 403: - // https://totally-not.a-sketchy.site/3pXpRLK.png - // Yes, this is an issue - // No, we should not require people to guarantee MANAGE_MESSAGES - // Need to check following: - // - In guild? - // - If yes, check if have permission - // - If all above fail (DM || guild && no permission), skip this var chn = msg.Channel; var gld = chn?.Guild; var mbr = gld?.CurrentMember; - if (mbr != null /* == is guild and cache is valid */ && (chn.PermissionsFor(mbr) & Permissions.ManageChannels) != 0) /* == has permissions */ + if (mbr != null && (chn.PermissionsFor(mbr) & Permissions.ManageMessages) != 0) await msg.DeleteAllReactionsAsync("Pagination").ConfigureAwait(false); - // ENDOF: 403 fix if (p.PageCount > 1) { if (emojis.SkipLeft != null) await msg.CreateReactionAsync(emojis.SkipLeft).ConfigureAwait(false); if (emojis.Left != null) await msg.CreateReactionAsync(emojis.Left).ConfigureAwait(false); if (emojis.Right != null) await msg.CreateReactionAsync(emojis.Right).ConfigureAwait(false); if (emojis.SkipRight != null) await msg.CreateReactionAsync(emojis.SkipRight).ConfigureAwait(false); if (emojis.Stop != null) await msg.CreateReactionAsync(emojis.Stop).ConfigureAwait(false); } else if (emojis.Stop != null && p is PaginationRequest paginationRequest && paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) { await msg.CreateReactionAsync(emojis.Stop).ConfigureAwait(false); } } /// /// Paginates the async. /// /// The p. /// The emoji. private async Task PaginateAsync(IPaginationRequest p, DiscordEmoji emoji) { var emojis = await p.GetEmojisAsync().ConfigureAwait(false); var msg = await p.GetMessageAsync().ConfigureAwait(false); if (emoji == emojis.SkipLeft) await p.SkipLeftAsync().ConfigureAwait(false); else if (emoji == emojis.Left) await p.PreviousPageAsync().ConfigureAwait(false); else if (emoji == emojis.Right) await p.NextPageAsync().ConfigureAwait(false); else if (emoji == emojis.SkipRight) await p.SkipRightAsync().ConfigureAwait(false); else if (emoji == emojis.Stop) { var tcs = await p.GetTaskCompletionSourceAsync().ConfigureAwait(false); tcs.TrySetResult(true); return; } var page = await p.GetPageAsync().ConfigureAwait(false); var builder = new DiscordMessageBuilder() .WithContent(page.Content) .WithEmbed(page.Embed); await builder.ModifyAsync(msg).ConfigureAwait(false); } ~Paginator() { this.Dispose(); } /// /// Disposes this EventWaiter /// public void Dispose() { this._client.MessageReactionAdded -= this.HandleReactionAdd; this._client.MessageReactionRemoved -= this.HandleReactionRemove; this._client.MessageReactionsCleared -= this.HandleReactionClear; this._client = null; this._requests.Clear(); this._requests = null; } } } diff --git a/DisCatSharp.Interactivity/GlobalSuppressions.cs b/DisCatSharp.Interactivity/GlobalSuppressions.cs index b33d83feb..386c3f81b 100644 --- a/DisCatSharp.Interactivity/GlobalSuppressions.cs +++ b/DisCatSharp.Interactivity/GlobalSuppressions.cs @@ -1,28 +1,33 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.InteractivityExtension.HandleInvalidInteraction(DisCatSharp.Entities.DiscordInteraction)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Interactivity.EventHandling.PaginationRequest._timeout")] [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Interactivity.InteractivityExtension._componentInteractionWaiter")] [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Interactivity.InteractivityExtension._modalInteractionWaiter")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.EventHandling.EventWaiter`1.CollectMatchesAsync(DisCatSharp.Interactivity.EventHandling.CollectRequest{`0})~System.Threading.Tasks.Task{System.Collections.ObjectModel.ReadOnlyCollection{`0}}")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.EventHandling.EventWaiter`1.WaitForMatchAsync(DisCatSharp.Interactivity.EventHandling.MatchRequest{`0})~System.Threading.Tasks.Task{`0}")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.InteractivityExtension.SplitString(System.String,System.Int32)~System.Collections.Generic.List{System.String}")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.EventHandling.Paginator.PaginateAsync(DisCatSharp.Interactivity.EventHandling.IPaginationRequest,DisCatSharp.Entities.DiscordEmoji)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Interactivity.EventHandling.Paginator.ResetReactionsAsync(DisCatSharp.Interactivity.EventHandling.IPaginationRequest)~System.Threading.Tasks.Task")] diff --git a/DisCatSharp.Interactivity/global.json b/DisCatSharp.Interactivity/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Interactivity/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj index 780b45e7a..0b5ab864f 100644 --- a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj +++ b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj @@ -1,40 +1,31 @@ DisCatSharp.Lavalink DisCatSharp.Lavalink true DisCatSharp.Lavalink Lavalink implementation for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music, lavalink, lavaplayer - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.Lavalink/GlobalSuppressions.cs b/DisCatSharp.Lavalink/GlobalSuppressions.cs index 1e34a019f..8e5e316a6 100644 --- a/DisCatSharp.Lavalink/GlobalSuppressions.cs +++ b/DisCatSharp.Lavalink/GlobalSuppressions.cs @@ -1,28 +1,34 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkGuildConnection.#ctor(DisCatSharp.Lavalink.LavalinkNodeConnection,DisCatSharp.Entities.DiscordChannel,DisCatSharp.EventArgs.VoiceStateUpdateEventArgs)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkNodeConnection.StartAsync~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkNodeConnection.WebSocket_OnMessage(DisCatSharp.Net.WebSocket.IWebSocketClient,DisCatSharp.EventArgs.SocketMessageEventArgs)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkNodeConnection.WsSendAsync(System.String)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkNodeConnection.WebSocket_OnDisconnect(DisCatSharp.Net.WebSocket.IWebSocketClient,DisCatSharp.EventArgs.SocketCloseEventArgs)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkRestClient.InternalDecodeTrackAsync(System.Uri)~System.Threading.Tasks.Task{DisCatSharp.Lavalink.LavalinkTrack}")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkRestClient.InternalFreeAddressAsync(System.Uri,System.String)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkRestClient.InternalFreeAllAddressesAsync(System.Uri)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.Entities.LavalinkRouteStatus.GetLavalinkRoutePlannerType(System.String)~System.Nullable{DisCatSharp.Lavalink.LavalinkRoutePlannerType}")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Lavalink.LavalinkExtension.FilterByLoad(DisCatSharp.Lavalink.LavalinkNodeConnection[])~DisCatSharp.Lavalink.LavalinkNodeConnection")] diff --git a/DisCatSharp.Lavalink/global.json b/DisCatSharp.Lavalink/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Lavalink/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.Tests/global.json b/DisCatSharp.Tests/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.Tests/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj index 71ef73db1..fb247669f 100644 --- a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj +++ b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj @@ -1,45 +1,36 @@ net6.0 false win-x86;win-x64 true false symbols.nupkg DisCatSharp.VoiceNext.Natives Voice Natives for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music - - LICENSE.md true runtimes - - - True - - - - diff --git a/DisCatSharp.VoiceNext.Natives/global.json b/DisCatSharp.VoiceNext.Natives/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.VoiceNext.Natives/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj index fbaf8ae5b..e27724095 100644 --- a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj +++ b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj @@ -1,45 +1,36 @@ DisCatSharp.VoiceNext DisCatSharp.VoiceNext true DisCatSharp.VoiceNext Voice extension for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, audio, voice, radio, music - - LICENSE.md - - - True - - - - diff --git a/DisCatSharp.VoiceNext/GlobalSuppressions.cs b/DisCatSharp.VoiceNext/GlobalSuppressions.cs index c1235c608..083ba1ad0 100644 --- a/DisCatSharp.VoiceNext/GlobalSuppressions.cs +++ b/DisCatSharp.VoiceNext/GlobalSuppressions.cs @@ -1,33 +1,50 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.Dispose")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.VoiceWS_SocketMessage(DisCatSharp.Net.WebSocket.IWebSocketClient,DisCatSharp.EventArgs.SocketMessageEventArgs)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.WsSendAsync(System.String)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Interop.OpusGetLastPacketDuration(System.IntPtr,System.Int32@)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.HandleDispatch(Newtonsoft.Json.Linq.JObject)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.ProcessKeepalive(System.Byte[])")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.Stage1(DisCatSharp.VoiceNext.Entities.VoiceReadyPayload)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.Stage2(DisCatSharp.VoiceNext.Entities.VoiceSessionDescriptionPayload)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.VoiceSenderTask~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.VoiceNextConnection.VoiceWS_SocketClosed(DisCatSharp.Net.WebSocket.IWebSocketClient,DisCatSharp.EventArgs.SocketCloseEventArgs)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Opus.GetLastPacketSampleCount(DisCatSharp.VoiceNext.Codec.OpusDecoder)~System.Int32")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Opus.ProcessPacketLoss(DisCatSharp.VoiceNext.Codec.OpusDecoder,System.Int32,System.Span{System.Byte}@)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Rtp.CalculatePacketSize(System.Int32,DisCatSharp.VoiceNext.Codec.EncryptionMode)~System.Int32")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Rtp.DecodeHeader(System.ReadOnlySpan{System.Byte},System.UInt16@,System.UInt32@,System.UInt32@,System.Boolean@)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Rtp.EncodeHeader(System.UInt16,System.UInt32,System.UInt32,System.Span{System.Byte})")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Rtp.GetDataFromPacket(System.ReadOnlySpan{System.Byte},System.ReadOnlySpan{System.Byte}@,DisCatSharp.VoiceNext.Codec.EncryptionMode)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Rtp.IsRtpHeader(System.ReadOnlySpan{System.Byte})~System.Boolean")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Sodium.AppendNonce(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},DisCatSharp.VoiceNext.Codec.EncryptionMode)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Sodium.GenerateNonce(System.ReadOnlySpan{System.Byte},System.Span{System.Byte})")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Sodium.GenerateNonce(System.UInt32,System.Span{System.Byte})")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.VoiceNext.Codec.Sodium.GetNonce(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},DisCatSharp.VoiceNext.Codec.EncryptionMode)")] diff --git a/DisCatSharp.VoiceNext/VoiceNextConnection.cs b/DisCatSharp.VoiceNext/VoiceNextConnection.cs index ff2e96e1c..c0e07234c 100644 --- a/DisCatSharp.VoiceNext/VoiceNextConnection.cs +++ b/DisCatSharp.VoiceNext/VoiceNextConnection.cs @@ -1,1361 +1,1361 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using DisCatSharp.VoiceNext.Codec; using DisCatSharp.VoiceNext.Entities; using DisCatSharp.VoiceNext.EventArgs; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.VoiceNext { internal delegate Task VoiceDisconnectedEventHandler(DiscordGuild guild); /// /// VoiceNext connection to a voice channel. /// public sealed class VoiceNextConnection : IDisposable { /// /// Triggered whenever a user speaks in the connected voice channel. /// public event AsyncEventHandler UserSpeaking { add => this._userSpeaking.Register(value); remove => this._userSpeaking.Unregister(value); } private readonly AsyncEvent _userSpeaking; /// /// Triggered whenever a user joins voice in the connected guild. /// public event AsyncEventHandler UserJoined { add => this._userJoined.Register(value); remove => this._userJoined.Unregister(value); } private readonly AsyncEvent _userJoined; /// /// Triggered whenever a user leaves voice in the connected guild. /// public event AsyncEventHandler UserLeft { add => this._userLeft.Register(value); remove => this._userLeft.Unregister(value); } private readonly AsyncEvent _userLeft; /// /// Triggered whenever voice data is received from the connected voice channel. /// public event AsyncEventHandler VoiceReceived { add => this._voiceReceived.Register(value); remove => this._voiceReceived.Unregister(value); } private readonly AsyncEvent _voiceReceived; /// /// Triggered whenever voice WebSocket throws an exception. /// public event AsyncEventHandler VoiceSocketErrored { add => this._voiceSocketError.Register(value); remove => this._voiceSocketError.Unregister(value); } private readonly AsyncEvent _voiceSocketError; internal event VoiceDisconnectedEventHandler VoiceDisconnected; /// /// Gets the unix epoch. /// private static DateTimeOffset s_unixEpoch { get; } = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); /// /// Gets the discord. /// private readonly DiscordClient _discord; /// /// Gets the guild. /// private readonly DiscordGuild _guild; /// /// Gets the transmitting s s r cs. /// private readonly ConcurrentDictionary _transmittingSsrCs; /// /// Gets the udp client. /// private readonly BaseUdpClient _udpClient; /// /// Gets or sets the voice ws. /// private IWebSocketClient _voiceWs; /// /// Gets or sets the heartbeat task. /// private Task _heartbeatTask; /// /// Gets or sets the heartbeat interval. /// private int _heartbeatInterval; /// /// Gets or sets the last heartbeat. /// private DateTimeOffset _lastHeartbeat; /// /// Gets or sets the token source. /// private CancellationTokenSource _tokenSource; /// /// Gets the token. /// private CancellationToken TOKEN => this._tokenSource.Token; /// /// Gets or sets the server data. /// internal VoiceServerUpdatePayload ServerData { get; set; } /// /// Gets or sets the state data. /// internal VoiceStateUpdatePayload StateData { get; set; } /// /// Gets or sets a value indicating whether resume. /// internal bool Resume { get; set; } /// /// Gets the configuration. /// private readonly VoiceNextConfiguration _configuration; /// /// Gets or sets the opus. /// private Opus _opus; /// /// Gets or sets the sodium. /// private Sodium _sodium; /// /// Gets or sets the rtp. /// private Rtp _rtp; /// /// Gets or sets the selected encryption mode. /// private EncryptionMode _selectedEncryptionMode; /// /// Gets or sets the nonce. /// private uint _nonce; /// /// Gets or sets the sequence. /// private ushort _sequence; /// /// Gets or sets the timestamp. /// private uint _timestamp; /// /// Gets or sets the s s r c. /// private uint _ssrc; /// /// Gets or sets the key. /// private byte[] _key; /// /// Gets or sets the discovered endpoint. /// private IpEndpoint _discoveredEndpoint; /// /// Gets or sets the web socket endpoint. /// internal ConnectionEndpoint WebSocketEndpoint { get; set; } /// /// Gets or sets the udp endpoint. /// internal ConnectionEndpoint UdpEndpoint { get; set; } /// /// Gets or sets the ready wait. /// private readonly TaskCompletionSource _readyWait; /// /// Gets or sets a value indicating whether is initialized. /// private bool _isInitialized; /// /// Gets or sets a value indicating whether is disposed. /// private bool _isDisposed; /// /// Gets or sets the playing wait. /// private TaskCompletionSource _playingWait; /// /// Gets the pause event. /// private readonly AsyncManualResetEvent _pauseEvent; /// /// Gets or sets the transmit stream. /// private VoiceTransmitSink _transmitStream; /// /// Gets the transmit channel. /// private readonly Channel _transmitChannel; /// /// Gets the keepalive timestamps. /// private readonly ConcurrentDictionary _keepaliveTimestamps; private ulong _lastKeepalive; /// /// Gets or sets the sender task. /// private Task _senderTask; /// /// Gets or sets the sender token source. /// private CancellationTokenSource _senderTokenSource; /// /// Gets the sender token. /// private CancellationToken SENDER_TOKEN => this._senderTokenSource.Token; /// /// Gets or sets the receiver task. /// private Task _receiverTask; /// /// Gets or sets the receiver token source. /// private CancellationTokenSource _receiverTokenSource; /// /// Gets the receiver token. /// private CancellationToken RECEIVER_TOKEN => this._receiverTokenSource.Token; /// /// Gets or sets the keepalive task. /// private Task _keepaliveTask; /// /// Gets or sets the keepalive token source. /// private CancellationTokenSource _keepaliveTokenSource; /// /// Gets the keepalive token. /// private CancellationToken KEEPALIVE_TOKEN => this._keepaliveTokenSource.Token; private volatile bool _isSpeaking; /// /// Gets the audio format used by the Opus encoder. /// public AudioFormat AudioFormat => this._configuration.AudioFormat; /// /// Gets whether this connection is still playing audio. /// public bool IsPlaying => this._playingWait != null && !this._playingWait.Task.IsCompleted; /// /// Gets the websocket round-trip time in ms. /// public int WebSocketPing => Volatile.Read(ref this._wsPing); private int _wsPing; /// /// Gets the UDP round-trip time in ms. /// public int UdpPing => Volatile.Read(ref this._udpPing); private int _udpPing; private int _queueCount; /// /// Gets the channel this voice client is connected to. /// public DiscordChannel TargetChannel { get; internal set; } /// /// Initializes a new instance of the class. /// /// The client. /// The guild. /// The channel. /// The config. /// The server. /// The state. internal VoiceNextConnection(DiscordClient client, DiscordGuild guild, DiscordChannel channel, VoiceNextConfiguration config, VoiceServerUpdatePayload server, VoiceStateUpdatePayload state) { this._discord = client; this._guild = guild; this.TargetChannel = channel; this._transmittingSsrCs = new ConcurrentDictionary(); this._userSpeaking = new AsyncEvent("VNEXT_USER_SPEAKING", TimeSpan.Zero, this._discord.EventErrorHandler); this._userJoined = new AsyncEvent("VNEXT_USER_JOINED", TimeSpan.Zero, this._discord.EventErrorHandler); this._userLeft = new AsyncEvent("VNEXT_USER_LEFT", TimeSpan.Zero, this._discord.EventErrorHandler); this._voiceReceived = new AsyncEvent("VNEXT_VOICE_RECEIVED", TimeSpan.Zero, this._discord.EventErrorHandler); this._voiceSocketError = new AsyncEvent("VNEXT_WS_ERROR", TimeSpan.Zero, this._discord.EventErrorHandler); this._tokenSource = new CancellationTokenSource(); this._configuration = config; this._isInitialized = false; this._isDisposed = false; this._opus = new Opus(this.AudioFormat); //this.Sodium = new Sodium(); this._rtp = new Rtp(); this.ServerData = server; this.StateData = state; var eps = this.ServerData.Endpoint; var epi = eps.LastIndexOf(':'); var eph = string.Empty; var epp = 443; if (epi != -1) { eph = eps[..epi]; epp = int.Parse(eps[(epi + 1)..]); } else { eph = eps; } this.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp }; this._readyWait = new TaskCompletionSource(); this._playingWait = null; this._transmitChannel = Channel.CreateBounded(new BoundedChannelOptions(this._configuration.PacketQueueSize)); this._keepaliveTimestamps = new ConcurrentDictionary(); this._pauseEvent = new AsyncManualResetEvent(true); this._udpClient = this._discord.Configuration.UdpClientFactory(); this._voiceWs = this._discord.Configuration.WebSocketClientFactory(this._discord.Configuration.Proxy, this._discord.ServiceProvider); this._voiceWs.Disconnected += this.VoiceWS_SocketClosed; this._voiceWs.MessageReceived += this.VoiceWS_SocketMessage; this._voiceWs.Connected += this.VoiceWS_SocketOpened; this._voiceWs.ExceptionThrown += this.VoiceWs_SocketException; } ~VoiceNextConnection() { this.Dispose(); } /// /// Connects to the specified voice channel. /// /// A task representing the connection operation. internal Task ConnectAsync() { var gwuri = new UriBuilder { Scheme = "wss", Host = this.WebSocketEndpoint.Hostname, Query = "encoding=json&v=4" }; return this._voiceWs.ConnectAsync(gwuri.Uri); } /// /// Reconnects . /// /// A Task. internal Task ReconnectAsync() => this._voiceWs.DisconnectAsync(); /// /// Starts . /// /// A Task. internal async Task StartAsync() { // Let's announce our intentions to the server var vdp = new VoiceDispatch(); if (!this.Resume) { vdp.OpCode = 0; vdp.Payload = new VoiceIdentifyPayload { ServerId = this.ServerData.GuildId, UserId = this.StateData.UserId.Value, SessionId = this.StateData.SessionId, Token = this.ServerData.Token }; this.Resume = true; } else { vdp.OpCode = 7; vdp.Payload = new VoiceIdentifyPayload { ServerId = this.ServerData.GuildId, SessionId = this.StateData.SessionId, Token = this.ServerData.Token }; } var vdj = JsonConvert.SerializeObject(vdp, Formatting.None); await this.WsSendAsync(vdj).ConfigureAwait(false); } /// /// Waits the for ready async. /// /// A Task. internal Task WaitForReadyAsync() => this._readyWait.Task; /// /// Enqueues the packet async. /// /// The packet. /// The token. /// A Task. internal async Task EnqueuePacketAsync(RawVoicePacket packet, CancellationToken token = default) { await this._transmitChannel.Writer.WriteAsync(packet, token).ConfigureAwait(false); this._queueCount++; } /// /// Prepares the packet. /// /// The pcm. /// The target. /// The length. /// A bool. internal bool PreparePacket(ReadOnlySpan pcm, out byte[] target, out int length) { target = null; length = 0; if (this._isDisposed) return false; var audioFormat = this.AudioFormat; var packetArray = ArrayPool.Shared.Rent(this._rtp.CalculatePacketSize(audioFormat.SampleCountToSampleSize(audioFormat.CalculateMaximumFrameSize()), this._selectedEncryptionMode)); var packet = packetArray.AsSpan(); this._rtp.EncodeHeader(this._sequence, this._timestamp, this._ssrc, packet); var opus = packet.Slice(Rtp.HEADER_SIZE, pcm.Length); this._opus.Encode(pcm, ref opus); this._sequence++; this._timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); Span nonce = stackalloc byte[Sodium.NonceSize]; switch (this._selectedEncryptionMode) { case EncryptionMode.XSalsa20Poly1305: this._sodium.GenerateNonce(packet[..Rtp.HEADER_SIZE], nonce); break; case EncryptionMode.XSalsa20Poly1305Suffix: this._sodium.GenerateNonce(nonce); break; case EncryptionMode.XSalsa20Poly1305Lite: this._sodium.GenerateNonce(this._nonce++, nonce); break; default: ArrayPool.Shared.Return(packetArray); throw new Exception("Unsupported encryption mode."); } Span encrypted = stackalloc byte[Sodium.CalculateTargetSize(opus)]; this._sodium.Encrypt(opus, encrypted, nonce); encrypted.CopyTo(packet[Rtp.HEADER_SIZE..]); packet = packet[..this._rtp.CalculatePacketSize(encrypted.Length, this._selectedEncryptionMode)]; this._sodium.AppendNonce(nonce, packet, this._selectedEncryptionMode); target = packetArray; length = packet.Length; return true; } /// /// Voices the sender task. /// /// A Task. private async Task VoiceSenderTask() { var token = this.SENDER_TOKEN; var client = this._udpClient; var reader = this._transmitChannel.Reader; byte[] data = null; var length = 0; var synchronizerTicks = (double)Stopwatch.GetTimestamp(); var synchronizerResolution = Stopwatch.Frequency * 0.005; var tickResolution = 10_000_000.0 / Stopwatch.Frequency; this._discord.Logger.LogDebug(VoiceNextEvents.Misc, "Timer accuracy: {0}/{1} (high resolution? {2})", Stopwatch.Frequency, synchronizerResolution, Stopwatch.IsHighResolution); while (!token.IsCancellationRequested) { await this._pauseEvent.WaitAsync().ConfigureAwait(false); var hasPacket = reader.TryRead(out var rawPacket); if (hasPacket) { this._queueCount--; if (this._playingWait == null || this._playingWait.Task.IsCompleted) this._playingWait = new TaskCompletionSource(); } // Provided by Laura#0090 (214796473689178133); this is Python, but adaptable: // // delay = max(0, self.delay + ((start_time + self.delay * loops) + - time.time())) // // self.delay // sample size // start_time // time since streaming started // loops // number of samples sent // time.time() // DateTime.Now if (hasPacket) { hasPacket = this.PreparePacket(rawPacket.Bytes.Span, out data, out length); if (rawPacket.RentedBuffer != null) ArrayPool.Shared.Return(rawPacket.RentedBuffer); } var durationModifier = hasPacket ? rawPacket.Duration / 5 : 4; var cts = Math.Max(Stopwatch.GetTimestamp() - synchronizerTicks, 0); if (cts < synchronizerResolution * durationModifier) await Task.Delay(TimeSpan.FromTicks((long)(((synchronizerResolution * durationModifier) - cts) * tickResolution))).ConfigureAwait(false); synchronizerTicks += synchronizerResolution * durationModifier; if (!hasPacket) continue; await this.SendSpeakingAsync(true).ConfigureAwait(false); await client.SendAsync(data, length).ConfigureAwait(false); ArrayPool.Shared.Return(data); if (!rawPacket.Silence && this._queueCount == 0) { var nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; for (var i = 0; i < 3; i++) { var nullpacket = new byte[nullpcm.Length]; var nullpacketmem = nullpacket.AsMemory(); await this.EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)).ConfigureAwait(false); } } else if (this._queueCount == 0) { await this.SendSpeakingAsync(false).ConfigureAwait(false); this._playingWait?.SetResult(true); } } } /// /// Processes the packet. /// /// The data. /// The opus. /// The pcm. /// The pcm packets. /// The voice sender. /// The output format. /// A bool. private bool ProcessPacket(ReadOnlySpan data, ref Memory opus, ref Memory pcm, IList> pcmPackets, out AudioSender voiceSender, out AudioFormat outputFormat) { voiceSender = null; outputFormat = default; if (!this._rtp.IsRtpHeader(data)) return false; this._rtp.DecodeHeader(data, out var shortSequence, out var timestamp, out var ssrc, out var hasExtension); if (!this._transmittingSsrCs.TryGetValue(ssrc, out var vtx)) { var decoder = this._opus.CreateDecoder(); vtx = new AudioSender(ssrc, decoder) { // user isn't present as we haven't received a speaking event yet. User = null }; } voiceSender = vtx; - ulong sequence = vtx.GetTrueSequenceAfterWrapping(shortSequence); + var sequence = vtx.GetTrueSequenceAfterWrapping(shortSequence); ushort gap = 0; if (vtx.LastTrueSequence is ulong lastTrueSequence) { if (sequence <= lastTrueSequence) // out-of-order packet; discard return false; gap = (ushort)(sequence - 1 - lastTrueSequence); if (gap >= 5) this._discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving"); } Span nonce = stackalloc byte[Sodium.NonceSize]; this._sodium.GetNonce(data, nonce, this._selectedEncryptionMode); this._rtp.GetDataFromPacket(data, out var encryptedOpus, this._selectedEncryptionMode); var opusSize = Sodium.CalculateSourceSize(encryptedOpus); opus = opus[..opusSize]; var opusSpan = opus.Span; try { this._sodium.Decrypt(encryptedOpus, opusSpan, nonce); // Strip extensions, if any if (hasExtension) { // RFC 5285, 4.2 One-Byte header // http://www.rfcreader.com/#rfc5285_line186 if (opusSpan[0] == 0xBE && opusSpan[1] == 0xDE) { var headerLen = (opusSpan[2] << 8) | opusSpan[3]; var i = 4; for (; i < headerLen + 4; i++) { var @byte = opusSpan[i]; // ID is currently unused since we skip it anyway //var id = (byte)(@byte >> 4); var length = (byte)(@byte & 0x0F) + 1; i += length; } // Strip extension padding too while (opusSpan[i] == 0) i++; opusSpan = opusSpan[i..]; } // TODO: consider implementing RFC 5285, 4.3. Two-Byte Header } if (opusSpan[0] == 0x90) { // I'm not 100% sure what this header is/does, however removing the data causes no // real issues, and has the added benefit of removing a lot of noise. opusSpan = opusSpan[2..]; } if (gap == 1) { var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder); var fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; var fecpcmMem = fecpcm.AsSpan(); this._opus.Decode(vtx.Decoder, opusSpan, ref fecpcmMem, true, out _); pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); } else if (gap > 1) { var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder); for (var i = 0; i < gap; i++) { var fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; var fecpcmMem = fecpcm.AsSpan(); this._opus.ProcessPacketLoss(vtx.Decoder, lastSampleCount, ref fecpcmMem); pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); } } var pcmSpan = pcm.Span; this._opus.Decode(vtx.Decoder, opusSpan, ref pcmSpan, false, out outputFormat); pcm = pcm[..pcmSpan.Length]; } finally { vtx.LastTrueSequence = sequence; } return true; } /// /// Processes the voice packet. /// /// The data. /// A Task. private async Task ProcessVoicePacket(byte[] data) { if (data.Length < 13) // minimum packet length return; try { var pcm = new byte[this.AudioFormat.CalculateMaximumFrameSize()]; var pcmMem = pcm.AsMemory(); var opus = new byte[pcm.Length]; var opusMem = opus.AsMemory(); var pcmFillers = new List>(); if (!this.ProcessPacket(data, ref opusMem, ref pcmMem, pcmFillers, out var vtx, out var audioFormat)) return; foreach (var pcmFiller in pcmFillers) await this._voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs(this._discord.ServiceProvider) { Ssrc = vtx.Ssrc, User = vtx.User, PcmData = pcmFiller, OpusData = Array.Empty().AsMemory(), AudioFormat = audioFormat, AudioDuration = audioFormat.CalculateSampleDuration(pcmFiller.Length) }).ConfigureAwait(false); await this._voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs(this._discord.ServiceProvider) { Ssrc = vtx.Ssrc, User = vtx.User, PcmData = pcmMem, OpusData = opusMem, AudioFormat = audioFormat, AudioDuration = audioFormat.CalculateSampleDuration(pcmMem.Length) }).ConfigureAwait(false); } catch (Exception ex) { this._discord.Logger.LogError(VoiceNextEvents.VoiceReceiveFailure, ex, "Exception occurred when decoding incoming audio data"); } } /// /// Processes the keepalive. /// /// The data. private void ProcessKeepalive(byte[] data) { try { var keepalive = BinaryPrimitives.ReadUInt64LittleEndian(data); if (!this._keepaliveTimestamps.TryRemove(keepalive, out var timestamp)) return; var tdelta = (int)((Stopwatch.GetTimestamp() - timestamp) / (double)Stopwatch.Frequency * 1000); this._discord.Logger.LogDebug(VoiceNextEvents.VoiceKeepalive, "Received UDP keepalive {0} (ping {1}ms)", keepalive, tdelta); Volatile.Write(ref this._udpPing, tdelta); } catch (Exception ex) { this._discord.Logger.LogError(VoiceNextEvents.VoiceKeepalive, ex, "Exception occurred when handling keepalive"); } } /// /// Udps the receiver task. /// /// A Task. private async Task UdpReceiverTask() { var token = this.RECEIVER_TOKEN; var client = this._udpClient; while (!token.IsCancellationRequested) { var data = await client.ReceiveAsync().ConfigureAwait(false); if (data.Length == 8) this.ProcessKeepalive(data); else if (this._configuration.EnableIncoming) await this.ProcessVoicePacket(data).ConfigureAwait(false); } } /// /// Sends a speaking status to the connected voice channel. /// /// Whether the current user is speaking or not. /// A task representing the sending operation. public async Task SendSpeakingAsync(bool speaking = true) { if (!this._isInitialized) throw new InvalidOperationException("The connection is not initialized"); if (this._isSpeaking != speaking) { this._isSpeaking = speaking; var pld = new VoiceDispatch { OpCode = 5, Payload = new VoiceSpeakingPayload { Speaking = speaking, Delay = 0 } }; var plj = JsonConvert.SerializeObject(pld, Formatting.None); await this.WsSendAsync(plj).ConfigureAwait(false); } } /// /// Gets a transmit stream for this connection, optionally specifying a packet size to use with the stream. If a stream is already configured, it will return the existing one. /// /// Duration, in ms, to use for audio packets. /// Transmit stream. public VoiceTransmitSink GetTransmitSink(int sampleDuration = 20) { if (!AudioFormat.AllowedSampleDurations.Contains(sampleDuration)) throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid PCM sample duration specified."); if (this._transmitStream == null) this._transmitStream = new VoiceTransmitSink(this, sampleDuration); return this._transmitStream; } /// /// Asynchronously waits for playback to be finished. Playback is finished when speaking = false is signaled. /// /// A task representing the waiting operation. public async Task WaitForPlaybackFinishAsync() { if (this._playingWait != null) await this._playingWait.Task.ConfigureAwait(false); } /// /// Pauses playback. /// public void Pause() => this._pauseEvent.Reset(); /// /// Asynchronously resumes playback. /// /// public async Task ResumeAsync() => await this._pauseEvent.SetAsync().ConfigureAwait(false); /// /// Disconnects and disposes this voice connection. /// public void Disconnect() => this.Dispose(); /// /// Disconnects and disposes this voice connection. /// public void Dispose() { if (this._isDisposed) return; try { this._isDisposed = true; this._isInitialized = false; this._tokenSource?.Cancel(); this._senderTokenSource?.Cancel(); this._receiverTokenSource?.Cancel(); } catch (Exception ex) { this._discord.Logger.LogError(ex, ex.Message); } try { this._voiceWs.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this._udpClient.Close(); } catch { } try { this._keepaliveTokenSource?.Cancel(); this._tokenSource?.Dispose(); this._senderTokenSource?.Dispose(); this._receiverTokenSource?.Dispose(); this._keepaliveTokenSource?.Dispose(); this._opus?.Dispose(); this._opus = null; this._sodium?.Dispose(); this._sodium = null; this._rtp?.Dispose(); this._rtp = null; } catch (Exception ex) { this._discord.Logger.LogError(ex, ex.Message); } this.VoiceDisconnected?.Invoke(this._guild); GC.SuppressFinalize(this); } /// /// Heartbeats . /// /// A Task. private async Task HeartbeatAsync() { await Task.Yield(); var token = this.TOKEN; while (true) { try { token.ThrowIfCancellationRequested(); var dt = DateTime.Now; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHeartbeat, "Sent heartbeat"); var hbd = new VoiceDispatch { OpCode = 3, Payload = UnixTimestamp(dt) }; var hbj = JsonConvert.SerializeObject(hbd); await this.WsSendAsync(hbj).ConfigureAwait(false); this._lastHeartbeat = dt; await Task.Delay(this._heartbeatInterval).ConfigureAwait(false); } catch (OperationCanceledException) { return; } } } /// /// Keepalives . /// /// A Task. private async Task KeepaliveAsync() { await Task.Yield(); var token = this.KEEPALIVE_TOKEN; var client = this._udpClient; while (!token.IsCancellationRequested) { var timestamp = Stopwatch.GetTimestamp(); var keepalive = Volatile.Read(ref this._lastKeepalive); Volatile.Write(ref this._lastKeepalive, keepalive + 1); this._keepaliveTimestamps.TryAdd(keepalive, timestamp); var packet = new byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(packet, keepalive); await client.SendAsync(packet, packet.Length).ConfigureAwait(false); await Task.Delay(5000, token).ConfigureAwait(false); } } /// /// Stage1S . /// /// The voice ready. /// A Task. private async Task Stage1(VoiceReadyPayload voiceReady) { // IP Discovery this._udpClient.Setup(this.UdpEndpoint); var pck = new byte[70]; PreparePacket(pck); await this._udpClient.SendAsync(pck, pck.Length).ConfigureAwait(false); var ipd = await this._udpClient.ReceiveAsync().ConfigureAwait(false); ReadPacket(ipd, out var ip, out var port); this._discoveredEndpoint = new IpEndpoint { Address = ip, Port = port }; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Endpoint discovery finished - discovered endpoint is {0}:{1}", ip, port); void PreparePacket(byte[] packet) { var ssrc = this._ssrc; var packetSpan = packet.AsSpan(); MemoryMarshal.Write(packetSpan, ref ssrc); Helpers.ZeroFill(packetSpan); } void ReadPacket(byte[] packet, out System.Net.IPAddress decodedIp, out ushort decodedPort) { var packetSpan = packet.AsSpan(); var ipString = Utilities.UTF8.GetString(packet, 4, 64 /* 70 - 6 */).TrimEnd('\0'); decodedIp = System.Net.IPAddress.Parse(ipString); decodedPort = BinaryPrimitives.ReadUInt16LittleEndian(packetSpan[68 /* 70 - 2 */..]); } // Select voice encryption mode var selectedEncryptionMode = Sodium.SelectMode(voiceReady.Modes); this._selectedEncryptionMode = selectedEncryptionMode.Value; // Ready this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Selected encryption mode is {0}", selectedEncryptionMode.Key); var vsp = new VoiceDispatch { OpCode = 1, Payload = new VoiceSelectProtocolPayload { Protocol = "udp", Data = new VoiceSelectProtocolPayloadData { Address = this._discoveredEndpoint.Address.ToString(), Port = (ushort)this._discoveredEndpoint.Port, Mode = selectedEncryptionMode.Key } } }; var vsj = JsonConvert.SerializeObject(vsp, Formatting.None); await this.WsSendAsync(vsj).ConfigureAwait(false); this._senderTokenSource = new CancellationTokenSource(); this._senderTask = Task.Run(this.VoiceSenderTask, this.SENDER_TOKEN); this._receiverTokenSource = new CancellationTokenSource(); this._receiverTask = Task.Run(this.UdpReceiverTask, this.RECEIVER_TOKEN); } /// /// Stage2S . /// /// The voice session description. /// A Task. private async Task Stage2(VoiceSessionDescriptionPayload voiceSessionDescription) { this._selectedEncryptionMode = Sodium.SupportedModes[voiceSessionDescription.Mode.ToLowerInvariant()]; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Discord updated encryption mode - new mode is {0}", this._selectedEncryptionMode); // start keepalive this._keepaliveTokenSource = new CancellationTokenSource(); this._keepaliveTask = this.KeepaliveAsync(); // send 3 packets of silence to get things going var nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; for (var i = 0; i < 3; i++) { var nullPcm = new byte[nullpcm.Length]; var nullpacketmem = nullPcm.AsMemory(); await this.EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)).ConfigureAwait(false); } this._isInitialized = true; this._readyWait.SetResult(true); } /// /// Handles the dispatch. /// /// The jo. /// A Task. private async Task HandleDispatch(JObject jo) { var opc = (int)jo["op"]; var opp = jo["d"] as JObject; switch (opc) { case 2: // READY this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)"); var vrp = opp.ToObject(); this._ssrc = vrp.Ssrc; this.UdpEndpoint = new ConnectionEndpoint(vrp.Address, vrp.Port); // this is not the valid interval // oh, discord //this.HeartbeatInterval = vrp.HeartbeatInterval; this._heartbeatTask = Task.Run(this.HeartbeatAsync); await this.Stage1(vrp).ConfigureAwait(false); break; case 4: // SESSION_DESCRIPTION this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)"); var vsd = opp.ToObject(); this._key = vsd.SecretKey; this._sodium = new Sodium(this._key.AsMemory()); await this.Stage2(vsd).ConfigureAwait(false); break; case 5: // SPEAKING // Don't spam OP5 // No longer spam, Discord supposedly doesn't send many of these this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SPEAKING (OP5)"); var spd = opp.ToObject(); var foundUserInCache = this._discord.TryGetCachedUserInternal(spd.UserId.Value, out var resolvedUser); var spk = new UserSpeakingEventArgs(this._discord.ServiceProvider) { Speaking = spd.Speaking, Ssrc = spd.Ssrc.Value, User = resolvedUser, }; if (foundUserInCache && this._transmittingSsrCs.TryGetValue(spk.Ssrc, out var txssrc5) && txssrc5.Id == 0) { txssrc5.User = spk.User; } else { var opus = this._opus.CreateDecoder(); var vtx = new AudioSender(spk.Ssrc, opus) { User = await this._discord.GetUserAsync(spd.UserId.Value).ConfigureAwait(false) }; if (!this._transmittingSsrCs.TryAdd(spk.Ssrc, vtx)) this._opus.DestroyDecoder(opus); } await this._userSpeaking.InvokeAsync(this, spk).ConfigureAwait(false); break; case 6: // HEARTBEAT ACK var dt = DateTime.Now; var ping = (int)(dt - this._lastHeartbeat).TotalMilliseconds; Volatile.Write(ref this._wsPing, ping); this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HEARTBEAT_ACK (OP6, {0}ms)", ping); this._lastHeartbeat = dt; break; case 8: // HELLO // this sends a heartbeat interval that we need to use for heartbeating this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HELLO (OP8)"); this._heartbeatInterval = opp["heartbeat_interval"].ToObject(); break; case 9: // RESUMED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)"); this._heartbeatTask = Task.Run(this.HeartbeatAsync); break; case 12: // CLIENT_CONNECTED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)"); var ujpd = opp.ToObject(); var usrj = await this._discord.GetUserAsync(ujpd.UserId).ConfigureAwait(false); { var opus = this._opus.CreateDecoder(); var vtx = new AudioSender(ujpd.Ssrc, opus) { User = usrj }; if (!this._transmittingSsrCs.TryAdd(vtx.Ssrc, vtx)) this._opus.DestroyDecoder(opus); } await this._userJoined.InvokeAsync(this, new VoiceUserJoinEventArgs(this._discord.ServiceProvider) { User = usrj, Ssrc = ujpd.Ssrc }).ConfigureAwait(false); break; case 13: // CLIENT_DISCONNECTED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)"); var ulpd = opp.ToObject(); var txssrc = this._transmittingSsrCs.FirstOrDefault(x => x.Value.Id == ulpd.UserId); if (this._transmittingSsrCs.ContainsKey(txssrc.Key)) { this._transmittingSsrCs.TryRemove(txssrc.Key, out var txssrc13); this._opus.DestroyDecoder(txssrc13.Decoder); } var usrl = await this._discord.GetUserAsync(ulpd.UserId).ConfigureAwait(false); await this._userLeft.InvokeAsync(this, new VoiceUserLeaveEventArgs(this._discord.ServiceProvider) { User = usrl, Ssrc = txssrc.Key }).ConfigureAwait(false); break; default: this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP{0})", opc); break; } } /// /// Voices the w s_ socket closed. /// /// The client. /// The e. /// A Task. private async Task VoiceWS_SocketClosed(IWebSocketClient client, SocketCloseEventArgs e) { this._discord.Logger.LogDebug(VoiceNextEvents.VoiceConnectionClose, "Voice WebSocket closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); // generally this should not be disposed on all disconnects, only on requested ones // or something // otherwise problems happen //this.Dispose(); if (e.CloseCode == 4006 || e.CloseCode == 4009) this.Resume = false; if (!this._isDisposed) { this._tokenSource.Cancel(); this._tokenSource = new CancellationTokenSource(); this._voiceWs = this._discord.Configuration.WebSocketClientFactory(this._discord.Configuration.Proxy, this._discord.ServiceProvider); this._voiceWs.Disconnected += this.VoiceWS_SocketClosed; this._voiceWs.MessageReceived += this.VoiceWS_SocketMessage; this._voiceWs.Connected += this.VoiceWS_SocketOpened; if (this.Resume) // emzi you dipshit await this.ConnectAsync().ConfigureAwait(false); } } /// /// Voices the w s_ socket message. /// /// The client. /// The e. /// A Task. private Task VoiceWS_SocketMessage(IWebSocketClient client, SocketMessageEventArgs e) { if (e is not SocketTextMessageEventArgs et) { this._discord.Logger.LogCritical(VoiceNextEvents.VoiceGatewayError, "Discord Voice Gateway sent binary data - unable to process"); return Task.CompletedTask; } this._discord.Logger.LogTrace(VoiceNextEvents.VoiceWsRx, et.Message); return this.HandleDispatch(JObject.Parse(et.Message)); } /// /// Voices the w s_ socket opened. /// /// The client. /// The e. /// A Task. private Task VoiceWS_SocketOpened(IWebSocketClient client, SocketEventArgs e) => this.StartAsync(); /// /// Voices the ws_ socket exception. /// /// The client. /// The e. /// A Task. private Task VoiceWs_SocketException(IWebSocketClient client, SocketErrorEventArgs e) => this._voiceSocketError.InvokeAsync(this, new SocketErrorEventArgs(this._discord.ServiceProvider) { Exception = e.Exception }); /// /// Ws the send async. /// /// The payload. /// A Task. private async Task WsSendAsync(string payload) { this._discord.Logger.LogTrace(VoiceNextEvents.VoiceWsTx, payload); await this._voiceWs.SendMessageAsync(payload).ConfigureAwait(false); } /// /// Gets the unix timestamp. /// /// The datetime. private static uint UnixTimestamp(DateTime dt) { var ts = dt - s_unixEpoch; var sd = ts.TotalSeconds; var si = (uint)sd; return si; } } } diff --git a/DisCatSharp.VoiceNext/global.json b/DisCatSharp.VoiceNext/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp.VoiceNext/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/DisCatSharp.targets b/DisCatSharp.targets index b063ad919..bf2cc22bc 100644 --- a/DisCatSharp.targets +++ b/DisCatSharp.targets @@ -1,12 +1,28 @@ - - Lala Sabathil, Lunar, Auros, Geferon, J_M_Lutra, Alice, AITSYS contributors - Aiko IT Systems - False - https://github.com/Aiko-IT-Systems/DisCatSharp - https://github.com/Aiko-IT-Systems/DisCatSharp - Git - https://raw.githubusercontent.com/Aiko-IT-Systems/DisCatSharp/main/DisCatSharp.Logos/logobig.png - + + Aiko-IT-Systems, Lala Sabathil, J_M_Lutra , Xorog, Badger2-3, aiko, AITSYS Contributors + Aiko IT Systems + False + https://github.com/Aiko-IT-Systems/DisCatSharp + https://github.com/Aiko-IT-Systems/DisCatSharp + Git + logobig.png + + LICENSE.md + True + + + + + True + + + + + + True + + + diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index c0f1743b8..fa011cff3 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3412 +1,3412 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// Represents a discord client. /// public sealed partial class DiscordClient { #region Private Fields private string _sessionId; private bool _guildDownloadCompleted; private readonly Dictionary> _tempTimers = new(); /// /// Represents a timeout handler. /// internal class TimeoutHandler { /// /// Gets the member. /// internal readonly DiscordMember Member; /// /// Gets the guild. /// internal readonly DiscordGuild Guild; /// /// Gets the old timeout value. /// internal DateTime? TimeoutUntilOld; /// /// Gets the new timeout value. /// internal DateTime? TimeoutUntilNew; /// /// Constructs a new . /// /// The affected member. /// The affected guild. /// The old timeout value. /// The new timeout value. internal TimeoutHandler(DiscordMember mbr, DiscordGuild guild, DateTime? too, DateTime? ton) { this.Guild = guild; this.Member = mbr; this.TimeoutUntilOld = too; this.TimeoutUntilNew = ton; } } #endregion #region Dispatch Handler /// /// Handles the dispatch payloads. /// /// The payload. internal async Task HandleDispatchAsync(GatewayPayload payload) { if (payload.Data is not JObject dat) { this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data); return; } await this._payloadReceived.InvokeAsync(this, new PayloadReceivedEventArgs(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); #region Default objects DiscordChannel chn; ulong gid; ulong cid; ulong uid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; DiscordScheduledEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; #endregion switch (payload.EventName.ToLowerInvariant()) { #region Gateway Status case "ready": var glds = (JArray)dat["guilds"]; await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false); break; case "resumed": await this.OnResumedAsync().ConfigureAwait(false); break; #endregion #region Channel case "channel_create": chn = dat.ToObject(); await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false); break; case "channel_update": await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "channel_delete": chn = dat.ToObject(); await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false); break; case "channel_pins_update": cid = (ulong)dat["channel_id"]; var ts = (string)dat["last_pin_timestamp"]; await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false); break; #endregion #region Guild case "guild_create": await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_update": await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false); break; case "guild_delete": await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false); break; case "guild_sync": gid = (ulong)dat["id"]; await this.OnGuildSyncEventAsync(this.GuildsInternal[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_emojis_update": gid = (ulong)dat["guild_id"]; var ems = dat["emojis"].ToObject>(); await this.OnGuildEmojisUpdateEventAsync(this.GuildsInternal[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": gid = (ulong)dat["guild_id"]; var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, gid).ConfigureAwait(false); break; case "guild_integrations_update": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_join_request_create": break; case "guild_join_request_update": break; case "guild_join_request_delete": break; #endregion #region Guild Ban case "guild_ban_add": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanAddEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventCreateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_update": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventUpdateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_delete": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventDeleteEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_add": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_remove": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Integration case "integration_create": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_update": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_delete": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false); break; #endregion #region Guild Member case "guild_member_add": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberAddEventAsync(dat.ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this.GuildsInternal.ContainsKey(gid)) { // discord fires this event inconsistently if the current user leaves a guild. if (usr.Id != this.CurrentUser.Id) this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid); return; } await this.OnGuildMemberRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.GuildsInternal[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false); break; case "guild_members_chunk": await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false); break; #endregion #region Guild Role case "guild_role_create": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Invite case "invite_create": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false); break; case "invite_delete": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false); break; #endregion #region Message case "message_ack": cid = (ulong)dat["channel_id"]; var mid = (ulong)dat["message_id"]; await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false); break; case "message_create": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; case "message_update": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; // delete event does *not* include message object case "message_delete": await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_delete_bulk": await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; #endregion #region Message Reaction case "message_reaction_add": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove": await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove_all": await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_reaction_remove_emoji": await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false); break; #endregion #region Stage Instance case "stage_instance_create": stg = dat.ToObject(); await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_update": stg = dat.ToObject(); await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_delete": stg = dat.ToObject(); await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false); break; #endregion #region Thread case "thread_create": trd = dat.ToObject(); await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false); break; case "thread_update": trd = dat.ToObject(); await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false); break; case "thread_delete": trd = dat.ToObject(); await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false); break; case "thread_list_sync": gid = (ulong)dat["guild_id"]; //get guild await this.OnThreadListSyncEventAsync(this.GuildsInternal[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false); break; case "thread_member_update": trdm = dat.ToObject(); await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false); break; case "thread_members_update": gid = (ulong)dat["guild_id"]; await this.OnThreadMembersUpdateEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false); break; #endregion #region Activities case "embedded_activity_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this.GuildsInternal[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false); break; #endregion #region User/Presence Update case "presence_update": await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false); break; case "user_settings_update": await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "user_update": await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; #endregion #region Voice case "voice_state_update": await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false); break; case "voice_server_update": gid = (ulong)dat["guild_id"]; await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Interaction/Integration/Application case "interaction_create": rawMbr = dat["member"]; if (rawMbr != null) { mbr = dat["member"].ToObject(); usr = mbr.User; } else { usr = dat["user"].ToObject(); } cid = (ulong)dat["channel_id"]; await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject()).ConfigureAwait(false); break; case "application_command_create": await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_update": await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_delete": await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_counts_update": var counts = dat["application_command_counts"]; await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_index_update": // TODO: Implement. break; case "application_command_permissions_update": var aid = (ulong)dat["application_id"]; if (aid != this.CurrentApplication.Id) return; var pms = dat["permissions"].ToObject>(); gid = (ulong)dat["guild_id"]; await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false); break; #endregion #region Misc case "gift_code_update": //Not supposed to be dispatched to bots break; case "typing_start": cid = (ulong)dat["channel_id"]; rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false); break; case "webhooks_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnWebhooksUpdateAsync(this.GuildsInternal[gid].GetChannel(cid), this.GuildsInternal[gid]).ConfigureAwait(false); break; default: await this.OnUnknownEventAsync(payload).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data); break; #endregion } } #endregion #region Events #region Gateway /// /// Handles the ready event. /// /// The ready payload. /// The raw guilds. internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds) { //ready.CurrentUser.Discord = this; var rusr = ready.CurrentUser; this.CurrentUser.Username = rusr.Username; this.CurrentUser.Discriminator = rusr.Discriminator; this.CurrentUser.AvatarHash = rusr.AvatarHash; this.CurrentUser.MfaEnabled = rusr.MfaEnabled; this.CurrentUser.Verified = rusr.Verified; this.CurrentUser.IsBot = rusr.IsBot; this.CurrentUser.Flags = rusr.Flags; this.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this.GuildsInternal.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr.GuildId = guild.Id; } var rawGuild = rawGuildIndex[guild.Id]; var rawMembers = (JArray)rawGuild["members"]; if (guild.MembersInternal != null) guild.MembersInternal.Clear(); else guild.MembersInternal = new ConcurrentDictionary(); if (rawMembers != null) { foreach (var xj in rawMembers) { var xtm = xj.ToObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); foreach (var xs in guild.Stickers.Values) xs.Discord = this; if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); foreach (var xt in guild.ThreadsInternal.Values) xt.Discord = this; if (guild.StageInstancesInternal == null) guild.StageInstancesInternal = new ConcurrentDictionary(); foreach (var xsi in guild.StageInstancesInternal.Values) xsi.Discord = this; if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); foreach (var xse in guild.ScheduledEventsInternal.Values) xse.Discord = this; this.GuildsInternal[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed event. /// internal Task OnResumedAsync() { this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed"); return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)); } #endregion #region Channel /// /// Handles the channel create event. /// /// The channel. internal async Task OnChannelCreateEventAsync(DiscordChannel channel) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } this.GuildsInternal[channel.GuildId.Value].ChannelsInternal[channel.Id] = channel; /*if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); }*/ await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false); } /// /// Handles the channel update event. /// /// The channel. internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; var gld = channel.Guild; var channelNew = this.InternalGetCachedChannel(channel.Id); DiscordChannel channelOld = null; if (channelNew != null) { channelOld = new DiscordChannel { Bitrate = channelNew.Bitrate, Discord = this, GuildId = channelNew.GuildId, Id = channelNew.Id, LastMessageId = channelNew.LastMessageId, Name = channelNew.Name, PermissionOverwritesInternal = new List(channelNew.PermissionOverwritesInternal), Position = channelNew.Position, Topic = channelNew.Topic, Type = channelNew.Type, UserLimit = channelNew.UserLimit, ParentId = channelNew.ParentId, IsNsfw = channelNew.IsNsfw, PerUserRateLimit = channelNew.PerUserRateLimit, RtcRegionId = channelNew.RtcRegionId, QualityMode = channelNew.QualityMode, DefaultAutoArchiveDuration = channelNew.DefaultAutoArchiveDuration }; channelNew.Bitrate = channel.Bitrate; channelNew.Name = channel.Name; channelNew.Position = channel.Position; channelNew.Topic = channel.Topic; channelNew.UserLimit = channel.UserLimit; channelNew.ParentId = channel.ParentId; channelNew.IsNsfw = channel.IsNsfw; channelNew.PerUserRateLimit = channel.PerUserRateLimit; channelNew.Type = channel.Type; channelNew.RtcRegionId = channel.RtcRegionId; channelNew.QualityMode = channel.QualityMode; channelNew.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channelNew.PermissionOverwritesInternal.Clear(); foreach (var po in channel.PermissionOverwritesInternal) { po.Discord = this; po.ChannelId = channel.Id; } channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld.ChannelsInternal[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channelNew, Guild = gld, ChannelBefore = channelOld }).ConfigureAwait(false); } /// /// Handles the channel delete event. /// /// The channel. internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; //if (channel.IsPrivate) if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private) { var dmChannel = channel as DiscordDmChannel; await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false); } else { var gld = channel.Guild; if (gld.ChannelsInternal.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false); } } /// /// Refreshes the channels. /// /// The guild id. internal async Task RefreshChannelsAsync(ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var channels = await this.ApiClient.GetGuildChannelsAsync(guildId); guild.ChannelsInternal.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } guild.ChannelsInternal[channel.Id] = channel; } } /// /// Handles the channel pins update event. /// /// The optional guild id. /// The channel id. /// The optional last pin timestamp. internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider) { Guild = guild, Channel = channel, LastPinTimestamp = lastPinTimestamp }; await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild /// /// Handles the guild create event. /// /// The guild. /// The raw members. /// The presences. internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) { if (presences != null) { foreach (var xp in presences) { xp.Discord = this; xp.GuildId = guild.Id; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp.InternalActivities = xp.RawActivities .Select(x => new DiscordActivity(x)).ToArray(); } this.PresencesInternal[xp.InternalUser.Id] = xp; } } var exists = this.GuildsInternal.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.MembersInternal.Count); guild.IsUnavailable = eventGuild.IsUnavailable; guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; guild.PremiumTier = eventGuild.PremiumTier; guild.BannerHash = eventGuild.BannerHash; guild.VanityUrlCode = eventGuild.VanityUrlCode; guild.Description = eventGuild.Description; guild.IsNsfw = eventGuild.IsNsfw; foreach (var kvp in eventGuild.VoiceStatesInternal) guild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ChannelsInternal) guild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.RolesInternal) guild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.EmojisInternal) guild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ThreadsInternal) guild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StickersInternal) guild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StageInstancesInternal) guild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ScheduledEventsInternal) guild.ScheduledEventsInternal[kvp.Key] = kvp.Value; foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xt in guild.ThreadsInternal.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } var old = Volatile.Read(ref this._guildDownloadCompleted); var dcompl = this.GuildsInternal.Values.All(xg => !xg.IsUnavailable); Volatile.Write(ref this._guildDownloadCompleted, dcompl); if (exists) await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); else await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); if (dcompl && !old) await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the guild update event. /// /// The guild. /// The raw members. internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) { DiscordGuild oldGuild; if (!this.GuildsInternal.ContainsKey(guild.Id)) { this.GuildsInternal[guild.Id] = guild; oldGuild = null; } else { var gld = this.GuildsInternal[guild.Id]; oldGuild = new DiscordGuild { Discord = gld.Discord, Name = gld.Name, AfkChannelId = gld.AfkChannelId, AfkTimeout = gld.AfkTimeout, ApplicationId = gld.ApplicationId, DefaultMessageNotifications = gld.DefaultMessageNotifications, ExplicitContentFilter = gld.ExplicitContentFilter, RawFeatures = gld.RawFeatures, IconHash = gld.IconHash, Id = gld.Id, IsLarge = gld.IsLarge, IsSynced = gld.IsSynced, IsUnavailable = gld.IsUnavailable, JoinedAt = gld.JoinedAt, MemberCount = gld.MemberCount, MaxMembers = gld.MaxMembers, MaxPresences = gld.MaxPresences, ApproximateMemberCount = gld.ApproximateMemberCount, ApproximatePresenceCount = gld.ApproximatePresenceCount, MaxVideoChannelUsers = gld.MaxVideoChannelUsers, DiscoverySplashHash = gld.DiscoverySplashHash, PreferredLocale = gld.PreferredLocale, MfaLevel = gld.MfaLevel, OwnerId = gld.OwnerId, SplashHash = gld.SplashHash, SystemChannelId = gld.SystemChannelId, SystemChannelFlags = gld.SystemChannelFlags, Description = gld.Description, WidgetEnabled = gld.WidgetEnabled, WidgetChannelId = gld.WidgetChannelId, VerificationLevel = gld.VerificationLevel, RulesChannelId = gld.RulesChannelId, PublicUpdatesChannelId = gld.PublicUpdatesChannelId, VoiceRegionId = gld.VoiceRegionId, IsNsfw = gld.IsNsfw, PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, PremiumSubscriptionCount = gld.PremiumSubscriptionCount, PremiumTier = gld.PremiumTier, ChannelsInternal = new ConcurrentDictionary(), ThreadsInternal = new ConcurrentDictionary(), EmojisInternal = new ConcurrentDictionary(), StickersInternal = new ConcurrentDictionary(), MembersInternal = new ConcurrentDictionary(), RolesInternal = new ConcurrentDictionary(), StageInstancesInternal = new ConcurrentDictionary(), VoiceStatesInternal = new ConcurrentDictionary(), ScheduledEventsInternal = new ConcurrentDictionary() }; foreach (var kvp in gld.ChannelsInternal) oldGuild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ThreadsInternal) oldGuild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.EmojisInternal) oldGuild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StickersInternal) oldGuild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.RolesInternal) oldGuild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.VoiceStatesInternal) oldGuild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.MembersInternal) oldGuild.MembersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StageInstancesInternal) oldGuild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ScheduledEventsInternal) oldGuild.ScheduledEventsInternal[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this.GuildsInternal[eventGuild.Id]; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.StageInstancesInternal == null) guild.StageInstancesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xc in guild.ThreadsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false); } /// /// Handles the guild delete event. /// /// The guild. internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) { if (guild.IsUnavailable) { if (!this.GuildsInternal.TryGetValue(guild.Id, out var gld)) return; gld.IsUnavailable = true; await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false); } else { if (!this.GuildsInternal.TryRemove(guild.Id, out var gld)) return; await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false); } } /// /// Handles the guild sync event. /// /// The guild. /// Whether the guild is a large guild.. /// The raw members. /// The presences. internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences) { presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; }); foreach (var xp in presences) this.PresencesInternal[xp.InternalUser.Id] = xp; guild.IsSynced = true; guild.IsLarge = isLarge; this.UpdateCachedGuild(guild, rawMembers); await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild emojis update event. /// /// The guild. /// The new emojis. internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) { var oldEmojis = new ConcurrentDictionary(guild.EmojisInternal); guild.EmojisInternal.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild.EmojisInternal[emoji.Id] = emoji; } var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider) { Guild = guild, EmojisAfter = guild.Emojis, EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis) }; await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the stickers updated. /// /// The new stickers. /// The guild id. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var oldStickers = new ConcurrentDictionary(guild.StickersInternal); guild.StickersInternal.Clear(); foreach (var nst in newStickers) { if (nst.User is not null) { nst.User.Discord = this; this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new); } nst.Discord = this; guild.StickersInternal[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The transport user. /// The guild. internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild ban remove event. /// /// The transport user. /// The guild. internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Scheduled Event /// /// Handles the scheduled event create event. /// /// The created event. /// The guild. internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (old, newScheduledEvent) => newScheduledEvent); if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild }).ConfigureAwait(false); } /// /// Handles the scheduled event update event. /// /// The updated event. /// The guild. internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (guild == null) return; DiscordScheduledEvent oldEvent; if (!guild.ScheduledEventsInternal.ContainsKey(scheduledEvent.Id)) { oldEvent = null; } else { var ev = guild.ScheduledEventsInternal[scheduledEvent.Id]; oldEvent = new DiscordScheduledEvent { Id = ev.Id, ChannelId = ev.ChannelId, EntityId = ev.EntityId, EntityMetadata = ev.EntityMetadata, CreatorId = ev.CreatorId, Creator = ev.Creator, Discord = this, Description = ev.Description, EntityType = ev.EntityType, ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw, ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw, GuildId = ev.GuildId, Status = ev.Status, Name = ev.Name, UserCount = ev.UserCount, CoverImageHash = ev.CoverImageHash }; } if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } if (scheduledEvent.Status == ScheduledEventStatus.Completed) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false); } else if (scheduledEvent.Status == ScheduledEventStatus.Canceled) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); scheduledEvent.Status = ScheduledEventStatus.Canceled; await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false); } else { this.UpdateScheduledEvent(scheduledEvent, guild); await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = oldEvent, ScheduledEventAfter = scheduledEvent, Guild = guild }).ConfigureAwait(false); } } /// /// Handles the scheduled event delete event. /// /// The deleted event. /// The guild. internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; if (scheduledEvent.Status == ScheduledEventStatus.Scheduled) scheduledEvent.Status = ScheduledEventStatus.Canceled; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild, Reason = scheduledEvent.Status }).ConfigureAwait(false); guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); } /// /// Handles the scheduled event user add event. /// The event. /// The added user id. /// The guild. /// internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount++; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } /// /// Handles the scheduled event user remove event. /// The event. /// The removed user id. /// The guild. /// internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount = scheduledEvent.UserCount == 0 ? 0 : scheduledEvent.UserCount - 1; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } #endregion #region Guild Integration /// /// Handles the guild integration create event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration update event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integrations update event. /// /// The guild. internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) { var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider) { Guild = guild }; await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild integration delete event. /// /// The guild. /// The integration id. /// The optional application id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integrationId, ulong? applicationId) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integrationId, ApplicationId = applicationId }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The transport member. /// The guild. internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id }; guild.MembersInternal[mbr.Id] = mbr; guild.MemberCount++; var ea = new GuildMemberAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member remove event. /// /// The transport user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild.MembersInternal.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; guild.MemberCount--; _ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new); var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member update event. /// /// The transport member. /// The guild. /// The roles. /// The nick. /// Whether the member is pending. internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(member.User.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var old = mbr; var gAvOld = old.GuildAvatarHash; var avOld = old.AvatarHash; var nickOld = mbr.Nickname; var pendingOld = mbr.IsPending; var rolesOld = new ReadOnlyCollection(new List(mbr.Roles)); var cduOld = mbr.CommunicationDisabledUntil; mbr.MemberFlags = member.MemberFlags; mbr.AvatarHashInternal = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.IsPending = pending; mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil; mbr.RoleIdsInternal.Clear(); mbr.RoleIdsInternal.AddRange(roles); guild.MembersInternal.AddOrUpdate(member.User.Id, mbr, (id, oldMbr) => oldMbr); var timeoutUntil = member.CommunicationDisabledUntil; /*this.Logger.LogTrace($"Timeout:\nBefore - {cduOld}\nAfter - {timeoutUntil}"); if ((timeoutUntil.HasValue && cduOld.HasValue) || (timeoutUntil == null && cduOld.HasValue) || (timeoutUntil.HasValue && cduOld == null)) { // We are going to add a scheduled timer to assure that we get a auditlog entry. var id = $"tt-{mbr.Id}-{guild.Id}-{DateTime.Now.ToLongTimeString()}"; this._tempTimers.Add( id, new( new TimeoutHandler( mbr, guild, cduOld, timeoutUntil ), new Timer( this.TimeoutTimer, id, 2000, Timeout.Infinite ) ) ); this.Logger.LogTrace("Scheduling timeout event."); return; }*/ //this.Logger.LogTrace("No timeout detected. Continuing on normal operation."); var eargs = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, TimeoutAfter = mbr.CommunicationDisabledUntil, AvatarHashAfter = mbr.AvatarHash, GuildAvatarHashAfter = mbr.GuildAvatarHash, NicknameBefore = nickOld, RolesBefore = rolesOld, PendingBefore = pendingOld, TimeoutBefore = cduOld, AvatarHashBefore = avOld, GuildAvatarHashBefore = gAvOld }; await this._guildMemberUpdated.InvokeAsync(this, eargs).ConfigureAwait(false); } /// /// Handles timeout events. /// /// Internally used as uid for the timer data. private async void TimeoutTimer(object state) { var tid = (string)state; var data = this._tempTimers.First(x=> x.Key == tid).Value.Key; var timer = this._tempTimers.First(x=> x.Key == tid).Value.Value; IReadOnlyList auditlog = null; DiscordAuditLogMemberUpdateEntry filtered = null; try { auditlog = await data.Guild.GetAuditLogsAsync(10, null, AuditLogActionType.MemberUpdate); var preFiltered = auditlog.Select(x => x as DiscordAuditLogMemberUpdateEntry).Where(x => x.Target.Id == data.Member.Id); filtered = preFiltered.First(); } catch (UnauthorizedException) { } catch (Exception) { this.Logger.LogTrace("Failing timeout event."); await timer.DisposeAsync(); this._tempTimers.Remove(tid); return; } var actor = filtered?.UserResponsible as DiscordMember; this.Logger.LogTrace("Trying to execute timeout event."); if (data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue) { // A timeout was updated. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutUpdateEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, TimeoutBefore = data.TimeoutUntilOld.Value, TimeoutAfter = data.TimeoutUntilNew.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutChanged.InvokeAsync(this, ea).ConfigureAwait(false); } else if (!data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue) { // A timeout was added. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutAddEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, Timeout = data.TimeoutUntilNew.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutAdded.InvokeAsync(this, ea).ConfigureAwait(false); } else if (data.TimeoutUntilOld.HasValue && !data.TimeoutUntilNew.HasValue) { // A timeout was removed. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutRemoveEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, TimeoutBefore = data.TimeoutUntilOld.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } // Ending timer because it worked. this.Logger.LogTrace("Removing timeout event."); await timer.DisposeAsync(); this._tempTimers.Remove(tid); } /// /// Handles the guild members chunk event. /// /// The raw chunk data. internal async Task OnGuildMembersChunkEventAsync(JObject dat) { var guild = this.Guilds[(ulong)dat["guild_id"]]; var chunkIndex = (int)dat["chunk_index"]; var chunkCount = (int)dat["chunk_count"]; var nonce = (string)dat["nonce"]; var mbrs = new HashSet(); var pres = new HashSet(); var members = dat["members"].ToObject(); foreach (var member in members) { var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(member.User) { Discord = this }; guild.MembersInternal[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild.MembersInternal.Count; var ea = new GuildMembersChunkEventArgs(this.ServiceProvider) { Guild = guild, Members = new ReadOnlySet(mbrs), ChunkIndex = chunkIndex, ChunkCount = chunkCount, Nonce = nonce, }; if (dat["presences"] != null) { var presences = dat["presences"].ToObject(); var presCount = presences.Length; foreach (var presence in presences) { presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); if (presence.RawActivities != null) { presence.InternalActivities = presence.RawActivities .Select(x => new DiscordActivity(x)).ToArray(); } pres.Add(presence); } ea.Presences = new ReadOnlySet(pres); } if (dat["not_found"] != null) { var nf = dat["not_found"].ToObject>(); ea.NotFound = new ReadOnlySet(nf); } await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Role /// /// Handles the guild role create event. /// /// The role. /// The guild. internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) { role.Discord = this; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; var ea = new GuildRoleCreateEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role update event. /// /// The role. /// The guild. internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) { var newRole = guild.GetRole(role.Id); var oldRole = new DiscordRole { GuildId = guild.Id, ColorInternal = newRole.ColorInternal, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position, IconHash = newRole.IconHash, Tags = newRole.Tags ?? null, UnicodeEmojiString = newRole.UnicodeEmojiString }; newRole.GuildId = guild.Id; newRole.ColorInternal = role.ColorInternal; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; newRole.IconHash = role.IconHash; newRole.Tags = role.Tags ?? null; newRole.UnicodeEmojiString = role.UnicodeEmojiString; var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider) { Guild = guild, RoleAfter = newRole, RoleBefore = oldRole }; await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role delete event. /// /// The role id. /// The guild. internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) { if (!guild.RolesInternal.TryRemove(roleId, out var role)) this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild})."); var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Invite /// /// Handles the invite create event. /// /// The channel id. /// The guild id. /// The invite. internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); invite.Discord = this; if (invite.Inviter is not null) { invite.Inviter.Discord = this; this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new); } guild.Invites[invite.Code] = invite; var ea = new InviteCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the invite delete event. /// /// The channel id. /// The guild id. /// The raw invite. internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); if (!guild.Invites.TryRemove(dat["code"].ToString(), out var invite)) { invite = dat.ToObject(); invite.Discord = this; } invite.IsRevoked = true; var ea = new InviteDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message /// /// Handles the message acknowledge event. /// /// The channel. /// The message id. internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId) { if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = chn.Id, Discord = this, }; } await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false); } /// /// Handles the message create event. /// /// The message. /// The transport user (author). /// The transport member. /// The reference transport user (author). /// The reference transport member. internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { message.Discord = this; this.PopulateMessageReactionsAndCache(message, author, member); message.PopulateMentions(); if (message.Channel == null && message.ChannelId == default) this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!"); if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } foreach (var sticker in message.Stickers) sticker.Discord = this; var ea = new MessageCreateEventArgs(this.ServiceProvider) { Message = message, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The transport user (author). /// The transport member. /// The reference transport user (author). /// The reference transport member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var eventMessage = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == eventMessage.Id && xm.ChannelId == eventMessage.ChannelId, out message)) { message = eventMessage; this.PopulateMessageReactionsAndCache(message, author, member); guild = message.Channel?.Guild; if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } } else { oldmsg = new DiscordMessage(message); guild = message.Channel?.Guild; message.EditedTimestampRaw = eventMessage.EditedTimestampRaw; if (eventMessage.Content != null) message.Content = eventMessage.Content; message.EmbedsInternal.Clear(); message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal); message.Pinned = eventMessage.Pinned; message.IsTts = eventMessage.IsTts; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The optional guild id. internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); var ea = new MessageDeleteEventArgs(this.ServiceProvider) { Channel = channel, Message = msg, Guild = guild }; await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message bulk delete event. /// /// The message ids. /// The channel id. /// The optional guild id. internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var msgs = new List(messageIds.Length); foreach (var messageId in messageIds) { if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); msgs.Add(msg); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider) { Channel = channel, Messages = new ReadOnlyCollection(msgs), Guild = guild }; await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message Reaction /// /// Handles the message reaction add event. /// /// The user id. /// The message id. /// The channel id. /// The optional guild id. /// The transport member. /// The emoji. internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); emoji.Discord = this; var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, ReactionsInternal = new List() }; } var react = msg.ReactionsInternal.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg.ReactionsInternal.Add(react = new DiscordReaction { Count = 1, Emoji = emoji, IsMe = this.CurrentUser.Id == userId }); } else { react.Count++; react.IsMe |= this.CurrentUser.Id == userId; } var ea = new MessageReactionAddEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The emoji. internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); emoji.Discord = this; if (!this.UserCache.TryGetValue(userId, out var usr)) usr = new DiscordUser { Id = userId, Discord = this }; if (channel?.Guild != null) usr = channel.Guild.Members.TryGetValue(userId, out var member) ? member : new DiscordMember(usr) { Discord = this, GuildId = channel.GuildId.Value }; if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var react = msg.ReactionsInternal?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg.ReactionsInternal != null && react.Count <= 0) // shit happens msg.ReactionsInternal.RemoveFirst(x => x.Emoji == emoji); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// Fired when all message reactions were removed. /// /// The message id. /// The channel id. /// The optional guild id. internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } msg.ReactionsInternal?.Clear(); var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionsClearEventArgs(this.ServiceProvider) { Message = msg }; await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// Fired when a emoji got removed. /// /// The message id. /// The channel id. /// The guild id. /// The raw discord emoji. internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var partialEmoji = dat.ToObject(); if (!guild.EmojisInternal.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg.ReactionsInternal?.RemoveAll(r => r.Emoji.Equals(emoji)); var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Message = msg, Emoji = emoji }; await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Stage Instance /// /// Handles the stage instance create event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Handles the stage instance update event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Handles the stage instance delete event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Handles the thread create event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId).ThreadsInternal.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread); await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false); } /// /// Handles the thread update event. /// /// The updated thread. internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var guild = thread.Guild; var threadNew = this.InternalGetCachedThread(thread.Id); DiscordThreadChannel threadOld = null; ThreadUpdateEventArgs updateEvent; if (threadNew != null) { threadOld = new DiscordThreadChannel { Discord = this, Type = threadNew.Type, ThreadMetadata = thread.ThreadMetadata, ThreadMembersInternal = threadNew.ThreadMembersInternal, ParentId = thread.ParentId, OwnerId = thread.OwnerId, Name = thread.Name, LastMessageId = threadNew.LastMessageId, MessageCount = thread.MessageCount, MemberCount = thread.MemberCount, GuildId = thread.GuildId, LastPinTimestampRaw = threadNew.LastPinTimestampRaw, PerUserRateLimit = threadNew.PerUserRateLimit, CurrentMember = threadNew.CurrentMember }; threadNew.ThreadMetadata = thread.ThreadMetadata; threadNew.ParentId = thread.ParentId; threadNew.OwnerId = thread.OwnerId; threadNew.Name = thread.Name; threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId; threadNew.MessageCount = thread.MessageCount; threadNew.MemberCount = thread.MemberCount; threadNew.GuildId = thread.GuildId; updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, ThreadBefore = threadOld, Guild = thread.Guild, Parent = thread.Parent }; } else { updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, Guild = thread.Guild, Parent = thread.Parent }; guild.ThreadsInternal[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Handles the thread delete event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld.ThreadsInternal.TryRemove(thread.Id, out var cachedThread)) thread = cachedThread; await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false); } /// /// Handles the thread list sync event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced thread members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channelIds, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channelIds.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.Discord = this; } - threads.Select(x => x.Discord = this); + _ = threads.Select(x => x.Discord = this); await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false); } /// /// Handles the thread member update event. /// /// The updated member. internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; var thread = this.InternalGetCachedThread(member.Id); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(member.Id); thread = this.GuildsInternal[member.GuildId].ThreadsInternal.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread); } thread.CurrentMember = member; thread.Guild.ThreadsInternal.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Handles the thread members update event. /// /// The target guild. /// The thread id of the target thread this update belongs to. /// The added members. /// The ids of the removed members. /// The new member count. internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong threadId, JArray membersAdded, JArray membersRemoved, int memberCount) { var thread = this.InternalGetCachedThread(threadId); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(threadId); thread = guild.ThreadsInternal.AddOrUpdate(threadId, tempThread, (old, newThread) => newThread); } thread.Discord = this; guild.Discord = this; List addedMembers = new(); List removedMemberIds = new(); if (membersAdded != null) { foreach (var xj in membersAdded) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; xtm.GuildId = guild.Id; if (xtm != null) addedMembers.Add(xtm); if (xtm.Id == this.CurrentUser.Id) thread.CurrentMember = xtm; } } var removedMembers = new List(); if (membersRemoved != null) { foreach (var removedId in membersRemoved) { removedMembers.Add(guild.MembersInternal.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, GuildId = guild.Id, Discord = this }); } } if (removedMemberIds.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; thread.MemberCount = memberCount; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = memberCount }; await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false); } #endregion #region Activities /// /// Dispatches the event. /// /// The transport activity. /// The guild. /// The channel id. /// The users in the activity. /// The application id. internal async Task OnEmbeddedActivityUpdateAsync(JObject trActivity, DiscordGuild guild, ulong channelId, JArray jUsers, ulong appId) => await Task.Delay(20); /*{ try { var users = j_users?.ToObject>(); DiscordActivity old = null; var uid = $"{guild.Id}_{channel_id}_{app_id}"; if (this._embeddedActivities.TryGetValue(uid, out var activity)) { old = new DiscordActivity(activity); DiscordJson.PopulateObject(tr_activity, activity); } else { activity = tr_activity.ToObject(); this._embeddedActivities[uid] = activity; } var activity_users = new List(); var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id); if (users != null) { foreach (var user in users) { var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this }; activity_users.Add(activity_user); } } else activity_users = null; var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider) { Guild = guild, Users = activity_users, Channel = channel }; await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(ex, ex.Message); } }*/ #endregion #region User/Presence Update /// /// Handles the presence update event. /// /// The raw presence. /// The raw user. internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) { var uid = (ulong)rawUser["id"]; DiscordPresence old = null; if (this.PresencesInternal.TryGetValue(uid, out var presence)) { old = new DiscordPresence(presence); DiscordJson.PopulateObject(rawPresence, presence); } else { presence = rawPresence.ToObject(); presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); this.PresencesInternal[presence.InternalUser.Id] = presence; } // reuse arrays / avoid linq (this is a hot zone) if (presence.Activities == null || rawPresence["activities"] == null) { presence.InternalActivities = Array.Empty(); } else { if (presence.InternalActivities.Length != presence.RawActivities.Length) presence.InternalActivities = new DiscordActivity[presence.RawActivities.Length]; for (var i = 0; i < presence.InternalActivities.Length; i++) presence.InternalActivities[i] = new DiscordActivity(presence.RawActivities[i]); if (presence.InternalActivities.Length > 0) { presence.RawActivity = presence.RawActivities[0]; if (presence.Activity != null) presence.Activity.UpdateWith(presence.RawActivity); else presence.Activity = new DiscordActivity(presence.RawActivity); } } if (this.UserCache.TryGetValue(uid, out var usr)) { if (old != null) { old.InternalUser.Username = usr.Username; old.InternalUser.Discriminator = usr.Discriminator; old.InternalUser.AvatarHash = usr.AvatarHash; } if (rawUser["username"] is object) usr.Username = (string)rawUser["username"]; if (rawUser["discriminator"] is object) usr.Discriminator = (string)rawUser["discriminator"]; if (rawUser["avatar"] is object) usr.AvatarHash = (string)rawUser["avatar"]; presence.InternalUser.Username = usr.Username; presence.InternalUser.Discriminator = usr.Discriminator; presence.InternalUser.AvatarHash = usr.AvatarHash; } var usrafter = usr ?? new DiscordUser(presence.InternalUser); var ea = new PresenceUpdateEventArgs(this.ServiceProvider) { Status = presence.Status, Activity = presence.Activity, User = usr, PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, UserAfter = usrafter }; await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user settings update event. /// /// The transport user. internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) { var usr = new DiscordUser(user) { Discord = this }; var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider) { User = usr }; await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user update event. /// /// The transport user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usrOld = new DiscordUser { AvatarHash = this.CurrentUser.AvatarHash, Discord = this, Discriminator = this.CurrentUser.Discriminator, Email = this.CurrentUser.Email, Id = this.CurrentUser.Id, IsBot = this.CurrentUser.IsBot, MfaEnabled = this.CurrentUser.MfaEnabled, Username = this.CurrentUser.Username, Verified = this.CurrentUser.Verified }; this.CurrentUser.AvatarHash = user.AvatarHash; this.CurrentUser.Discriminator = user.Discriminator; this.CurrentUser.Email = user.Email; this.CurrentUser.Id = user.Id; this.CurrentUser.IsBot = user.IsBot; this.CurrentUser.MfaEnabled = user.MfaEnabled; this.CurrentUser.Username = user.Username; this.CurrentUser.Verified = user.Verified; var ea = new UserUpdateEventArgs(this.ServiceProvider) { UserAfter = this.CurrentUser, UserBefore = usrOld }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw voice state update object. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this.GuildsInternal[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld.VoiceStatesInternal.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld.VoiceStatesInternal[vstateNew.UserId] = vstateNew; } if (gld.MembersInternal.TryGetValue(uid, out var mbr)) { mbr.IsMuted = vstateNew.IsServerMuted; mbr.IsDeafened = vstateNew.IsServerDeafened; } else { var transportMbr = vstateNew.TransportMember; this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); } var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider) { Guild = vstateNew.Guild, Channel = vstateNew.Channel, User = vstateNew.User, SessionId = vstateNew.SessionId, Before = vstateOld, After = vstateNew }; await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the voice server update event. /// /// The new endpoint. /// The new token. /// The guild. internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) { var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider) { Endpoint = endpoint, VoiceToken = token, Guild = guild }; await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Commands /// /// Handles the application command create event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command update event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command delete event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild application command counts update event. /// /// The count. /// The count. /// The count. /// The guild id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int chatInputCommandCount, int userContextMenuCommandCount, int messageContextMenuCount, ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = chatInputCommandCount, UserContextMenuCommands = userContextMenuCommandCount, MessageContextMenuCommands = messageContextMenuCount, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update event. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong channelId, ulong guildId, ulong applicationId) { if (applicationId != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guildId); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guildId, channelId); } catch (NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(channelId); } if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = applicationId, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create event. /// /// The guild id. /// The channel id. /// The transport user. /// The transport member. /// The interaction. internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction) { var usr = new DiscordUser(user) { Discord = this }; interaction.ChannelId = channelId; interaction.GuildId = guildId; interaction.Discord = this; interaction.Data.Discord = this; if (member != null) { usr = new DiscordMember(member) { GuildId = guildId.Value, Discord = this }; this.UpdateUser(usr, guildId, interaction.Guild, member); } else { this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new); } interaction.User = usr; var resolved = interaction.Data.Resolved; if (resolved != null) { if (resolved.Users != null) { foreach (var c in resolved.Users) { c.Value.Discord = this; this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new); } } if (resolved.Members != null) { foreach (var c in resolved.Members) { c.Value.Discord = this; c.Value.Id = c.Key; c.Value.GuildId = guildId.Value; c.Value.User.Discord = this; this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new); } } if (resolved.Channels != null) { foreach (var c in resolved.Channels) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Roles != null) { foreach (var c in resolved.Roles) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Messages != null) { foreach (var m in resolved.Messages) { m.Value.Discord = this; if (guildId.HasValue) m.Value.GuildId = guildId.Value; } } if (resolved.Attachments != null) foreach (var a in resolved.Attachments) a.Value.Discord = this; } if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit) { if (interaction.Message != null) { interaction.Message.Discord = this; interaction.Message.ChannelId = interaction.ChannelId; } var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider) { Message = interaction.Message, Interaction = interaction }; await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false); } else { if (interaction.Data.Target.HasValue) // Context-Menu. // { var targetId = interaction.Data.Target.Value; DiscordUser targetUser = null; DiscordMember targetMember = null; DiscordMessage targetMessage = null; interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); var ctea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, Type = interaction.Data.Type, }; await this._contextMenuInteractionCreated.InvokeAsync(this, ctea).ConfigureAwait(false); } else { var ea = new InteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction }; await this._interactionCreated.InvokeAsync(this, ea).ConfigureAwait(false); } } } #endregion #region Misc /// /// Handles the typing start event. /// /// The user id. /// The channel id. /// The channel. /// The optional guild id. /// The time when the user started typing. /// The transport member. internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) { if (channel == null) { channel = new DiscordChannel { Discord = this, Id = channelId, GuildId = guildId ?? default, }; } var guild = this.InternalGetCachedGuild(guildId); var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); var ea = new TypingStartEventArgs(this.ServiceProvider) { Channel = channel, User = usr, Guild = guild, StartedAt = started }; await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the webhooks update. /// /// The channel. /// The guild. internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) { var ea = new WebhooksUpdateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild }; await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles all unknown events. /// /// The payload. internal async Task OnUnknownEventAsync(GatewayPayload payload) { var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #endregion } } diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj index d661d181e..922164b0e 100644 --- a/DisCatSharp/DisCatSharp.csproj +++ b/DisCatSharp/DisCatSharp.csproj @@ -1,68 +1,59 @@ DisCatSharp DisCatSharp DisCatSharp Another C# API/Framework for Discord Bots. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, webhooks - - LICENSE.md - + - - - True - - - - True True Resources.resx ResXFileCodeGenerator Resources.Designer.cs diff --git a/DisCatSharp/Entities/Guild/DiscordMember.cs b/DisCatSharp/Entities/Guild/DiscordMember.cs index 6e5278253..e1ae926ab 100644 --- a/DisCatSharp/Entities/Guild/DiscordMember.cs +++ b/DisCatSharp/Entities/Guild/DiscordMember.cs @@ -1,785 +1,785 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord guild member. /// public class DiscordMember : DiscordUser, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMember() { this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); } /// /// Initializes a new instance of the class. /// /// The user. internal DiscordMember(DiscordUser user) { this.Discord = user.Discord; this.Id = user.Id; this.RoleIdsInternal = new List(); this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); } /// /// Initializes a new instance of the class. /// /// The mbr. internal DiscordMember(TransportMember mbr) { this.Id = mbr.User.Id; this.IsDeafened = mbr.IsDeafened; this.IsMuted = mbr.IsMuted; this.JoinedAt = mbr.JoinedAt; this.Nickname = mbr.Nickname; this.PremiumSince = mbr.PremiumSince; this.IsPending = mbr.IsPending; this.GuildAvatarHash = mbr.GuildAvatarHash; this.GuildBannerHash = mbr.GuildBannerHash; this.GuildBio = mbr.GuildBio; this.CommunicationDisabledUntil = mbr.CommunicationDisabledUntil; this.AvatarHashInternal = mbr.AvatarHash; this.RoleIdsInternal = mbr.Roles ?? new List(); this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); this.MemberFlags = mbr.MemberFlags; } /// /// Gets the members avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string GuildAvatarHash { get; internal set; } /// /// Gets the members avatar URL. /// [JsonIgnore] public string GuildAvatarUrl => string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? this.User.AvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the members banner hash. /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string GuildBannerHash { get; internal set; } /// /// Gets the members banner URL. /// [JsonIgnore] public string GuildBannerUrl => string.IsNullOrWhiteSpace(this.GuildBannerHash) ? this.User.BannerUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.GuildBannerHash}.{(this.GuildBannerHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// The color of this member's banner. Mutually exclusive with . /// [JsonIgnore] public override DiscordColor? BannerColor => this.User.BannerColor; /// /// Gets this member's nickname. /// [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] public string Nickname { get; internal set; } /// /// Gets the members guild bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public string GuildBio { get; internal set; } /// /// Gets the members flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MemberFlags MemberFlags { get; internal set; } [JsonIgnore] internal string AvatarHashInternal; /// /// Gets this member's display name. /// [JsonIgnore] public string DisplayName => this.Nickname ?? this.Username; /// /// List of role ids /// [JsonIgnore] internal IReadOnlyList RoleIds => this._roleIdsLazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] internal List RoleIdsInternal; [JsonIgnore] private readonly Lazy> _roleIdsLazy; /// /// Gets the list of roles associated with this member. /// [JsonIgnore] public IEnumerable Roles => this.RoleIds.Select(id => this.Guild.GetRole(id)).Where(x => x != null); /// /// Gets the color associated with this user's top color-giving role, otherwise 0 (no color). /// [JsonIgnore] public DiscordColor Color { get { var role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Color.Value != 0); return role != null ? role.Color : new DiscordColor(); } } /// /// Date the user joined the guild /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Date the user started boosting this server /// [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset? PremiumSince { get; internal set; } /// /// Date until the can communicate again. /// [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] public DateTime? CommunicationDisabledUntil { get; internal set; } /// /// If the user is deafened /// [JsonProperty("is_deafened", NullValueHandling = NullValueHandling.Ignore)] public bool IsDeafened { get; internal set; } /// /// If the user is muted /// [JsonProperty("is_muted", NullValueHandling = NullValueHandling.Ignore)] public bool IsMuted { get; internal set; } /// /// Whether the user has not passed the guild's Membership Screening requirements yet. /// [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] public bool? IsPending { get; internal set; } /// /// Gets this member's voice state. /// [JsonIgnore] public DiscordVoiceState VoiceState => this.Discord.Guilds[this.GuildId].VoiceStates.TryGetValue(this.Id, out var voiceState) ? voiceState : null; [JsonIgnore] internal ulong GuildId = 0; /// /// Gets the guild of which this member is a part of. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds[this.GuildId]; /// /// Gets whether this member is the Guild owner. /// [JsonIgnore] public bool IsOwner => this.Id == this.Guild.OwnerId; /// /// Gets the member's position in the role hierarchy, which is the member's highest role's position. Returns for the guild's owner. /// [JsonIgnore] public int Hierarchy => this.IsOwner ? int.MaxValue : this.RoleIds.Count == 0 ? 0 : this.Roles.Max(x => x.Position); /// /// Gets the permissions for the current member. /// [JsonIgnore] public Permissions Permissions => this.GetPermissions(); #region Overridden user properties /// /// Gets the user. /// [JsonIgnore] internal DiscordUser User => this.Discord.UserCache[this.Id]; /// /// Gets this member's username. /// [JsonIgnore] public override string Username { get => this.User.Username; internal set => this.User.Username = value; } /// /// Gets the member's 4-digit discriminator. /// [JsonIgnore] public override string Discriminator { get => this.User.Discriminator; internal set => this.User.Discriminator = value; } /// /// Gets the member's avatar hash. /// [JsonIgnore] public override string AvatarHash { get => this.User.AvatarHash; internal set => this.User.AvatarHash = value; } /// /// Gets the member's banner hash. /// [JsonIgnore] public override string BannerHash { get => this.User.BannerHash; internal set => this.User.BannerHash = value; } /// /// Gets whether the member is a bot. /// [JsonIgnore] public override bool IsBot { get => this.User.IsBot; internal set => this.User.IsBot = value; } /// /// Gets the member's email address. /// This is only present in OAuth. /// [JsonIgnore] public override string Email { get => this.User.Email; internal set => this.User.Email = value; } /// /// Gets whether the member has multi-factor authentication enabled. /// [JsonIgnore] public override bool? MfaEnabled { get => this.User.MfaEnabled; internal set => this.User.MfaEnabled = value; } /// /// Gets whether the member is verified. /// This is only present in OAuth. /// [JsonIgnore] public override bool? Verified { get => this.User.Verified; internal set => this.User.Verified = value; } /// /// Gets the member's chosen language /// [JsonIgnore] public override string Locale { get => this.User.Locale; internal set => this.User.Locale = value; } /// /// Gets the user's flags. /// [JsonIgnore] public override UserFlags? OAuthFlags { get => this.User.OAuthFlags; internal set => this.User.OAuthFlags = value; } /// /// Gets the member's flags for OAuth. /// [JsonIgnore] public override UserFlags? Flags { get => this.User.Flags; internal set => this.User.Flags = value; } #endregion /// /// Creates a direct message channel to this member. /// /// Direct message channel to this member. /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateDmChannelAsync() => this.Discord.ApiClient.CreateDmAsync(this.Id); /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// The sent message. /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(embed).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// Embed to attach to the message. /// The sent message. /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content, DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content, embed).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Builder to with the message. /// The sent message. /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordMessageBuilder message) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(message).ConfigureAwait(false); } /// /// Sets this member's voice mute status. /// /// Whether the member is to be muted. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SetMuteAsync(bool mute, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, mute, default, default, reason); /// /// Sets this member's voice deaf status. /// /// Whether the member is to be deafened. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SetDeafAsync(bool deaf, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, default, deaf, default, reason); /// /// Modifies this member. /// /// Action to perform on this member. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new MemberEditModel(); action(mdl); if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != ChannelType.Voice && mdl.VoiceChannel.Value.Type != ChannelType.Stage) - throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(mdl.VoiceChannel)); + throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(action)); if (mdl.Nickname.HasValue && this.Discord.CurrentUser.Id == this.Id) { await this.Discord.ApiClient.ModifyCurrentMemberNicknameAsync(this.Guild.Id, mdl.Nickname.Value, mdl.AuditLogReason).ConfigureAwait(false); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, Optional.None, mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.Map(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } else { await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname, mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.Map(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } } /// /// Adds a timeout to a member. /// /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, until, reason); /// /// Adds a timeout to a member. /// /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(TimeSpan until, string reason = null) => this.TimeoutAsync(DateTimeOffset.UtcNow + until, reason); /// /// Adds a timeout to a member. /// /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(DateTime until, string reason = null) => this.TimeoutAsync(until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a member. /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveTimeoutAsync(string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, null, reason); /// /// Grants a role to the member. /// /// Role to grant. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GrantRoleAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Revokes a role from a member. /// /// Role to revoke. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RevokeRoleAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Sets the member's roles to ones specified. /// /// Roles to set. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ReplaceRolesAsync(IEnumerable roles, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, Optional.Some(roles.Select(xr => xr.Id)), default, default, default, reason); /// /// Bans this member from their guild. /// /// How many days to remove messages from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task BanAsync(int deleteMessageDays = 0, string reason = null) => this.Guild.BanMemberAsync(this, deleteMessageDays, reason); /// /// Unbans this member from their guild. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanAsync(string reason = null) => this.Guild.UnbanMemberAsync(this, reason); /// /// Kicks this member from their guild. /// /// Reason for audit logs. /// /// [alias="KickAsync"] /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveAsync(string reason = null) => this.Discord.ApiClient.RemoveGuildMemberAsync(this.GuildId, this.Id, reason); /// /// Moves this member to the specified voice channel /// /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task PlaceInAsync(DiscordChannel channel) => channel.PlaceMemberAsync(this); /// /// Updates the member's suppress state in a stage channel. /// /// The channel the member is currently in. /// Toggles the member's suppress state. /// Thrown when the channel in not a voice channel. public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress) { if (channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress).ConfigureAwait(false); } /// /// Makes the user a speaker. /// /// Thrown when the user is not inside an stage channel. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task MakeSpeakerAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, false).ConfigureAwait(false); } /// /// Moves the user to audience. /// /// Thrown when the user is not inside an stage channel. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task MoveToAudienceAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, true).ConfigureAwait(false); } /// /// Calculates permissions in a given channel for this member. /// /// Channel to calculate permissions for. /// Calculated permissions for this member in the channel. public Permissions PermissionsIn(DiscordChannel channel) => channel.PermissionsFor(this); /// /// Get's the current member's roles based on the sum of the permissions of their given roles. /// private Permissions GetPermissions() { if (this.Guild.OwnerId == this.Id) return PermissionMethods.FullPerms; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // assign permissions from member's roles (in order) perms |= this.Roles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Administrator grants all permissions and cannot be overridden return (perms & Permissions.Administrator) == Permissions.Administrator ? PermissionMethods.FullPerms : perms; } /// /// Returns a string representation of this member. /// /// String representation of this member. public override string ToString() => $"Member {this.Id}; {this.Username}#{this.Discriminator} ({this.DisplayName})"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordMember); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.GuildId == e.GuildId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.GuildId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First member to compare. /// Second member to compare. /// Whether the two members are equal. public static bool operator ==(DiscordMember e1, DiscordMember e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.GuildId == e2.GuildId)); } /// /// Gets whether the two objects are not equal. /// /// First member to compare. /// Second member to compare. /// Whether the two members are not equal. public static bool operator !=(DiscordMember e1, DiscordMember e2) => !(e1 == e2); } } diff --git a/DisCatSharp/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs index 5f1d9b3cb..f5ed683a3 100644 --- a/DisCatSharp/GlobalSuppressions.cs +++ b/DisCatSharp/GlobalSuppressions.cs @@ -1,47 +1,81 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Net.Abstractions.ClientProperties.OperatingSystem")] [assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone1")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone2")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone3")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone4")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._1SkinTone5")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordUnicodeEmoji._8ball")] [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnEmbeddedActivityUpdateAsync(Newtonsoft.Json.Linq.JObject,DisCatSharp.Entities.DiscordGuild,System.UInt64,Newtonsoft.Json.Linq.JArray,System.UInt64)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.UpdateCachedScheduledEvent(DisCatSharp.Entities.DiscordGuild,Newtonsoft.Json.Linq.JArray)")] [assembly: SuppressMessage("Style", "IDE0150:Prefer 'null' check over type check", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnPresenceUpdateEventAsync(Newtonsoft.Json.Linq.JObject,Newtonsoft.Json.Linq.JObject)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.s_permissionStrings")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.s_versionHeader")] [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.DiscordClient._heartbeatTask")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.DiscordApiClient.BulkOverwriteGlobalApplicationCommandsAsync(System.UInt64,System.Collections.Generic.IEnumerable{DisCatSharp.Entities.DiscordApplicationCommand})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.DiscordApiClient.BulkOverwriteGuildApplicationCommandsAsync(System.UInt64,System.UInt64,System.Collections.Generic.IEnumerable{DisCatSharp.Entities.DiscordApplicationCommand})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordApplicationCommand}}")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.EventErrorHandler``2(DisCatSharp.Common.Utilities.AsyncEvent{``0,``1},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{``0,``1},``0,``1)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.InternalConnectAsync~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnGuildRoleDeleteEventAsync(System.UInt64,DisCatSharp.Entities.DiscordGuild)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.WsSendAsync(System.String)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.BuildRequest(DisCatSharp.Net.BaseRestRequest)~System.Net.Http.HttpRequestMessage")] [assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnEmbeddedActivityUpdateAsync(Newtonsoft.Json.Linq.JObject,DisCatSharp.Entities.DiscordGuild,System.UInt64,Newtonsoft.Json.Linq.JArray,System.UInt64)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.GamePartySizeConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.Abstractions.ShardInfoConverter.ReadArrayObject(Newtonsoft.Json.JsonReader,Newtonsoft.Json.JsonSerializer)~Newtonsoft.Json.Linq.JArray")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.Handle429(DisCatSharp.Net.RestResponse,System.Threading.Tasks.Task@,System.Boolean@)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Net.Abstractions.ClientProperties.OperatingSystem")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Net.Abstractions.ClientProperties.Referrer")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Net.Abstractions.ClientProperties.ReferringDomain")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.HandleDispatchAsync(DisCatSharp.Net.Abstractions.GatewayPayload)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.HandleSocketMessageAsync(System.String)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.InternalConnectAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.OnHeartbeatAckAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.SendIdentifyAsync(DisCatSharp.Net.Abstractions.StatusUpdate)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.EventErrorHandler``2(DisCatSharp.Common.Utilities.AsyncEvent{``0,``1},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{``0,``1},``0,``1)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.Goof``2(DisCatSharp.Common.Utilities.AsyncEvent{``0,``1},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{``0,``1},``0,``1)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordClient.ConnectAsync(DisCatSharp.Entities.DiscordActivity,System.Nullable{DisCatSharp.Entities.UserStatus},System.Nullable{System.DateTimeOffset})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.CleanupBucketsAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.ExecuteRequestAsync(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.Threading.Tasks.TaskCompletionSource{System.Boolean})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.UpdateHashCaches(DisCatSharp.Net.BaseRestRequest,DisCatSharp.Net.RateLimitBucket,System.String)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Net.RestClient.WaitForInitialRateLimit(DisCatSharp.Net.RateLimitBucket)~System.Threading.Tasks.Task{System.Threading.Tasks.TaskCompletionSource{System.Boolean}}")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.GetGatewayInfoAsync~System.Threading.Tasks.Task{DisCatSharp.Net.GatewayInfo}")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DisCatSharpTeam.Get(System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILogger,DisCatSharp.Net.DiscordApiClient)~System.Threading.Tasks.Task{DisCatSharp.Entities.DisCatSharpTeam}")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.StartAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Utilities.CheckThreadAutoArchiveDurationFeature(DisCatSharp.Entities.DiscordGuild,DisCatSharp.ThreadAutoArchiveDuration)~System.Boolean")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Utilities.LogTaskFault(System.Threading.Tasks.Task,Microsoft.Extensions.Logging.ILogger,Microsoft.Extensions.Logging.LogLevel,Microsoft.Extensions.Logging.EventId,System.String)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.ConnectShardAsync(System.Int32)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.EventErrorHandler``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.Goof``1(DisCatSharp.Common.Utilities.AsyncEvent{DisCatSharp.DiscordClient,``0},System.Exception,DisCatSharp.Common.Utilities.AsyncEventHandler{DisCatSharp.DiscordClient,``0},DisCatSharp.DiscordClient,``0)")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.InternalStopAsync(System.Boolean)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.DiscordShardedClient.StartAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordChannel.GetMessagesInternalAsync(System.Int32,System.Nullable{System.UInt64},System.Nullable{System.UInt64},System.Nullable{System.UInt64})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordMessage}}")] +[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordGuild.GetAllMembersAsync~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyCollection{DisCatSharp.Entities.DiscordMember}}")] +[assembly: SuppressMessage("Usage", "CA2253:Named placeholders should not be numeric values", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordGuild.GetAuditLogsAsync(System.Nullable{System.Int32},DisCatSharp.Entities.DiscordMember,System.Nullable{DisCatSharp.Entities.AuditLogActionType})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordAuditLogEntry}}")] +[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordMessage.GetReactionsInternalAsync(DisCatSharp.Entities.DiscordEmoji,System.Int32,System.Nullable{System.UInt64})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordUser}}")] +[assembly: SuppressMessage("Performance", "CA1826:Do not use Enumerable methods on indexable collections", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Entities.DiscordThreadChannel.GetMessagesInternalAsync(System.Int32,System.Nullable{System.UInt64},System.Nullable{System.UInt64},System.Nullable{System.UInt64})~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{DisCatSharp.Entities.DiscordMessage}}")] diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index a2c0ff340..6f68fc85c 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -1,247 +1,247 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using DisCatSharp.Entities; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents a application command create payload. /// internal class RestApplicationCommandCreatePayload { /// /// Gets the type. /// [JsonProperty("type")] public ApplicationCommandType Type { get; set; } /// /// Gets the name. /// [JsonProperty("name")] public string Name { get; set; } /// /// Gets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } /// /// Gets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Options { get; set; } /// /// Whether the command is allowed for everyone. /// - [JsonProperty("default_permission")] - public bool DefaultPermission { get; set; } = true; + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Include)] + public bool? DefaultPermission { get; set; } = null; /// /// The command needed permissions. /// [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)] public Permissions? DefaultMemberPermission { get; set; } /// /// Whether the command is allowed for dms. /// [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)] public bool? DmPermission { get; set; } /// /// Whether the command is marked as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool Nsfw { get; set; } } /// /// Represents a application command edit payload. /// internal class RestApplicationCommandEditPayload { /// /// Gets the name. /// [JsonProperty("name")] public Optional Name { get; set; } /// /// Gets the name localizations. /// [JsonProperty("name_localizations")] public Optional> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description")] public Optional Description { get; set; } /// /// Gets the description localizations. /// [JsonProperty("description_localizations")] public Optional> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options")] public Optional> Options { get; set; } /// /// Gets the default permission. /// - [JsonProperty("default_permission")] - public Optional DefaultPermission { get; set; } = true; + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Include)] + public bool? DefaultPermission { get; set; } = null; /// /// The command needed permissions. /// [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)] public Optional DefaultMemberPermission { get; set; } /// /// Whether the command is allowed for dms. /// [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)] public Optional DmPermission { get; set; } /// /// Whether the command is marked as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public Optional Nsfw { get; set; } } /// /// Represents a interaction response payload. /// internal class RestInteractionResponsePayload { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandCallbackData Data { get; set; } /// /// Gets the attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } } /// /// Represents a interaction response payload. /// internal class RestInteractionModalResponsePayload { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandModalCallbackData Data { get; set; } } /// /// Represents a followup message create payload. /// internal class RestFollowupMessageCreatePayload { /// /// Gets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; set; } /// /// Get whether the message is tts. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool? IsTts { get; set; } /// /// Gets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; set; } /// /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public DiscordMentions Mentions { get; set; } /// /// Gets the flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public int? Flags { get; set; } /// /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; set; } /// /// Gets attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } } } diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index 6caf3f4a7..0f5588951 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,462 +1,470 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Various Discord-related utilities. /// public static class Utilities { /// /// Gets the version of the library /// internal static string VersionHeader { get; set; } /// /// Gets or sets the permission strings. /// internal static Dictionary PermissionStrings { get; set; } /// /// Gets the utf8 encoding /// // ReSharper disable once InconsistentNaming internal static UTF8Encoding UTF8 { get; } = new(false); /// /// Initializes a new instance of the class. /// static Utilities() { PermissionStrings = new Dictionary(); var t = typeof(Permissions); var ti = t.GetTypeInfo(); var vals = Enum.GetValues(t).Cast(); foreach (var xv in vals) { var xsv = xv.ToString(); var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); var xav = xmv.GetCustomAttribute(); PermissionStrings[xv] = xav.String; } var a = typeof(DiscordClient).GetTypeInfo().Assembly; var vs = ""; var iv = a.GetCustomAttribute(); if (iv != null) vs = iv.InformationalVersion; else { var v = a.GetName().Version; vs = v.ToString(3); } VersionHeader = $"DiscordBot (https://github.com/Aiko-IT-Systems/DisCatSharp, v{vs})"; } /// /// Gets the api base uri. /// /// The config /// A string. internal static string GetApiBaseUri(DiscordConfiguration config = null) => config == null ? Endpoints.BASE_URI + "9" : config.UseCanary ? Endpoints.CANARY_URI + config.ApiVersion : Endpoints.BASE_URI + config.ApiVersion; /// /// Gets the api uri for. /// /// The path. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the api uri for. /// /// The path. /// The query string. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, string queryString, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}{queryString}"); /// /// Gets the api uri builder for. /// /// The path. /// The config /// A QueryUriBuilder. internal static QueryUriBuilder GetApiUriBuilderFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the formatted token. /// /// The client. /// A string. internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); /// /// Gets the formatted token. /// /// The config. /// A string. internal static string GetFormattedToken(DiscordConfiguration config) => config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", - _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), + _ => throw new ArgumentException("Invalid token type specified.", nameof(config)), }; /// /// Gets the base headers. /// /// A Dictionary. internal static Dictionary GetBaseHeaders() => new(); /// /// Gets the user agent. /// /// A string. internal static string GetUserAgent() => VersionHeader; /// /// Contains the user mentions. /// /// The message. /// A bool. internal static bool ContainsUserMentions(string message) { var pattern = @"<@(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the nickname mentions. /// /// The message. /// A bool. internal static bool ContainsNicknameMentions(string message) { var pattern = @"<@!(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the channel mentions. /// /// The message. /// A bool. internal static bool ContainsChannelMentions(string message) { var pattern = @"<#(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the role mentions. /// /// The message. /// A bool. internal static bool ContainsRoleMentions(string message) { var pattern = @"<@&(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the emojis. /// /// The message. /// A bool. internal static bool ContainsEmojis(string message) { var pattern = @""; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Gets the user mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetUserMentions(DiscordMessage message) { var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + return from Match match in matches + select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the role mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetRoleMentions(DiscordMessage message) { var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + return from Match match in matches + select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the channel mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetChannelMentions(DiscordMessage message) { var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + return from Match match in matches + select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the emojis. /// /// The message. /// A list of ulong. internal static IEnumerable GetEmojis(DiscordMessage message) { var regex = new Regex(@"", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + return from Match match in matches + select ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$"); return regex.IsMatch(name); } /// /// Checks the thread auto archive duration feature. /// /// The guild. /// The taad. /// A bool. internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) => true; /// /// Checks the thread private feature. /// /// The guild. /// A bool. internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.CanCreatePrivateThreads; /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages); + + /// + /// Have the message intents. + /// + /// The intents. + /// A bool. + internal static bool HasMessageContentIntents(DiscordIntents intents) + => intents.HasIntent(DiscordIntents.MessageContent); /// /// Have the reaction intents. /// /// The intents. /// A bool. internal static bool HasReactionIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); /// /// Have the typing intents. /// /// The intents. /// A bool. internal static bool HasTypingIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula /// /// Gets a shard id from a guild id and total shard count. /// /// The guild id the shard is on. /// The total amount of shards. /// The shard id. public static int GetShardId(ulong guildId, int shardCount) => (int)(guildId >> 22) % shardCount; /// /// Helper method to create a from Unix time seconds for targets that do not support this natively. /// /// Unix time seconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeSeconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. /// /// Unix time milliseconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// /// to calculate Unix time for. /// Calculated Unix time. public static long GetUnixTime(DateTimeOffset dto) => dto.ToUnixTimeMilliseconds(); /// /// Computes a timestamp from a given snowflake. /// /// Snowflake to compute a timestamp from. /// Computed timestamp. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) => DiscordClient.DiscordEpoch.AddMilliseconds(snowflake >> 22); /// /// Converts this into human-readable format. /// /// Permissions enumeration to convert. /// Human-readable permissions. public static string ToPermissionString(this Permissions perm) { if (perm == Permissions.None) return PermissionStrings[perm]; perm &= PermissionMethods.FullPerms; var strs = PermissionStrings .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) .Select(xkvp => xkvp.Value); return string.Join(", ", strs.OrderBy(xs => xs)); } /// /// Checks whether this string contains given characters. /// /// String to check. /// Characters to check for. /// Whether the string contained these characters. public static bool Contains(this string str, params char[] characters) { foreach (var xc in str) if (characters.Contains(xc)) return true; return false; } /// /// Logs the task fault. /// /// The task. /// The logger. /// The level. /// The event id. /// The message. internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) { if (task == null) throw new ArgumentNullException(nameof(task)); if (logger == null) return; task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); } /// /// Deconstructs the. /// /// The kvp. /// The key. /// The value. internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } } } diff --git a/DisCatSharp/global.json b/DisCatSharp/global.json new file mode 100644 index 000000000..90a51a911 --- /dev/null +++ b/DisCatSharp/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.202", + "rollForward": "major" + } +} diff --git a/global.json b/global.json index 126149e6b..90a51a911 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", - "rollForward": "latestMajor" + "version": "6.0.202", + "rollForward": "major" } } diff --git a/rebuild-docs.ps1 b/rebuild-docs.ps1 index 252695e75..e5f11190a 100644 --- a/rebuild-docs.ps1 +++ b/rebuild-docs.ps1 @@ -1,492 +1,350 @@ #!/usr/bin/env pwsh # Rebuild-docs # # Rebuilds the documentation for DisCatSharp project, and places artifacts in specified directory. # # Author: Emzi0767 # Version: 2017-09-11 14:20 # # Arguments: # .\rebuild-docs.ps1 # # Run as: # .\rebuild-docs.ps1 .\path\to\docfx\project .\path\to\output project-docs param ( [parameter(Mandatory = $true)] [string] $DocsPath, [parameter(Mandatory = $true)] [string] $OutputPath, [parameter(Mandatory = $true)] [string] $PackageName ) # Backup the environment $current_path = $Env:PATH $current_location = Get-Location # Tool paths $docfx_path = Join-Path "$current_location" "docfx" -$sevenzip_path = Join-Path "$current_location" "7zip" # Restores the environment function Restore-Environment() { Write-Host "Restoring environment variables" $Env:PATH = $current_path Set-Location -path "$current_location" if (Test-Path "$docfx_path") { Remove-Item -recurse -force "$docfx_path" } - - if (Test-Path "$sevenzip_path") - { - Remove-Item -recurse -force "$sevenzip_path" - } } # Downloads and installs latest version of DocFX function Install-DocFX([string] $target_dir_path) { Write-Host "Installing DocFX" # Check if the target directory exists # If it does, remove it if (Test-Path "$target_dir_path") { Write-Host "Target directory exists, deleting" Remove-Item -recurse -force "$target_dir_path" } # Create target directory $target_dir = New-Item -type directory "$target_dir_path" $target_fn = "docfx.zip" # Form target path $target_dir = $target_dir.FullName $target_path = Join-Path "$target_dir" "$target_fn" # Download release info from Chocolatey API try { Write-Host "Getting latest DocFX release" $release_json = Invoke-WebRequest -uri "https://chocolatey.org/api/v2/package-versions/docfx" | ConvertFrom-JSON $release_json = $release_json | ForEach-Object { [System.Version]::Parse($_) } | Sort-Object -Descending } catch { Return 1 } # Set TLS version to 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Download the release # Since GH releases are unreliable, we have to try up to 3 times $tries = 0 $fail = $true while ($tries -lt 3) { # Prepare the assets $release = $release_json[$tries] # Pick the next available release $release_version = $release.ToString() # Convert to string #$release_version = "2.58.5" $release_asset = "https://github.com/dotnet/docfx/releases/download/v$release_version/docfx.zip" # increment try counter $tries = $tries + 1 try { Write-Host "Downloading DocFX $release_version to $target_path" Invoke-WebRequest -uri "$release_asset" -outfile "$target_path" # No failure, carry on Write-Host "DocFX version $release_version downloaded successfully" $fail = $false Break } catch { Write-Host "Downloading DocFX version $release_version failed, trying next ($tries / 3)" #Return 1 } } # Check if we succeeded in downloading if ($fail) { Return 1 } # Switch directory Set-Location -Path "$target_dir" # Extract the release try { Write-Host "Extracting DocFX" Expand-Archive -path "$target_path" -destinationpath "$target_dir" } catch { Return 1 } # Remove the downloaded zip Write-Host "Removing temporary files" Remove-Item "$target_path" # Add DocFX to PATH Write-Host "Adding DocFX to PATH" if ($null -eq $Env:OS) { $Env:DOCFX_PATH = "$target_dir" } else { $Env:PATH = "$target_dir;$current_path" } Set-Location -path "$current_location" Return 0 } -# Downloads and installs latest version of 7-zip CLI -function Install-7zip([string] $target_dir_path) -{ - # First, download 7-zip 9.20 CLI to extract latest CLI - # http://www.7-zip.org/a/7za920.zip - - Write-Host "Installing 7-zip" - - # Check if the target directory exists - # If it does, remove it - if (Test-Path "$target_dir_path") - { - Write-Host "Target directory exists, deleting" - Remove-Item -recurse -force "$target_dir_path" - } - - # Create target directory - $target_dir = New-Item -type directory "$target_dir_path" - $target_fn = "7za920.zip" - - # Form target path - $target_dir = $target_dir.FullName - $target_path = Join-Path "$target_dir" "v920" - $target_dir_920 = New-Item -type directory "$target_path" - - $target_dir_920 = $target_dir_920.FullName - $target_path = Join-Path "$target_dir_920" "$target_fn" - - # Download the 9.20 CLI - try - { - Write-Host "Downloading 7-zip 9.20 CLI to $target_path" - Invoke-WebRequest -uri "http://www.7-zip.org/a/7za920.zip" -outfile "$target_path" - Set-Location -Path "$target_dir_920" - } - catch - { - Return 1 - } - - # Extract the 9.20 CLI - try - { - Write-Host "Extracting 7-zip latest CLI" - Expand-Archive -path "$target_path" -destinationpath "$target_dir_920" - } - catch - { - Return 1 - } - - # Temporarily add the 9.20 CLI to PATH - Write-Host "Adding 7-zip 9.20 CLI to PATH" - $old_path = $Env:PATH - $Env:PATH = "$target_dir_920;$old_path" - - # Next, download latest CLI - # http://www.7-zip.org/a/7z1604-extra.7z - - # Form target path - $target_version = "19.00" - $target_fn = "7z1900-extra.7z" - $target_path = Join-Path "$target_dir" "$target_fn" - - # Download the latest CLI - try - { - Write-Host "Downloading 7-zip $target_version CLI to $target_path" - Invoke-WebRequest -uri "http://www.7-zip.org/a/$target_fn" -outfile "$target_path" - Set-Location -Path "$target_dir" - } - catch - { - Return 1 - } - - # Extract the latest CLI - Write-Host "Extracting 7-zip $target_version CLI" - & 7za x "$target_path" | Out-Host - if ($LastExitCode -ne 0) - { - Return $LastExitCode - } - - # Remove the 9.20 CLI from PATH - Write-Host "Removing 7-zip 9.20 CLI from PATH" - $Env:PATH = "$old_path" - - # Remove temporary files and 9.20 CLI - Write-Host "Removing temporary files" - Remove-Item -recurse -force "$target_dir_920" - Remove-Item -recurse -force "$target_path" - - # Add the latest CLI to PATH - Write-Host "Adding 7-zip $target_version CLI to PATH" - $target_dir = Join-Path "$target_dir" "x64" - $Env:PATH = "$target_dir;$old_path" - Set-Location -path "$current_location" - - Return 0 -} - # Builds the documentation using available DocFX function BuildDocs([string] $target_dir_path) { # Check if documentation source path exists if (-not (Test-Path "$target_dir_path")) { #Write-Host "Specified path does not exist" Return 65536 } # Check if documentation source path is a directory $target_path = Get-Item "$target_dir_path" if (-not ($target_path -is [System.IO.DirectoryInfo])) { #Write-Host "Specified path is not a directory" Return 65536 } # Form target path $target_path = $target_path.FullName # Form component paths $docs_site = Join-Path "$target_path" "_site" $docs_api = Join-Path "$target_path" "api" $docs_obj = Join-Path "$target_path" "obj" # Check if API documentation source path exists if (-not (Test-Path "$docs_api")) { #Write-Host "API build target directory does not exist" Return 32768 } # Check if API documentation source path is a directory $docs_api_dir = Get-Item "$docs_api" if (-not ($docs_api_dir -is [System.IO.DirectoryInfo])) { #Write-Host "API build target directory is not a directory" Return 32768 } # Purge old API documentation Write-Host "Purging old API documentation" Set-Location -path "$docs_api" Remove-Item "*.yml" Set-Location -path "$current_location" # Check if old built site exists # If it does, remove it if (Test-Path "$docs_site") { Write-Host "Purging old products" Remove-Item -recurse -force "$docs_site" } # Create target directory for the built site $docs_site = New-Item -type directory "$docs_site" $docs_site = $docs_site.FullName # Check if old object cache exists # If it does, remove it if (Test-Path "$docs_obj") { Write-Host "Purging object cache" Remove-Item -recurse -force "$docs_obj" } # Create target directory for the object cache $docs_obj = New-Item -type directory "$docs_obj" $docs_obj = $docs_obj.FullName # Enter the documentation directory Set-Location -path "$target_path" # Check OS # Null means non-Windows if ($null -eq $Env:OS) { # Generate new API documentation & mono "$Env:DOCFX_PATH/docfx.exe" docfx.json | Out-Host # Check if successful if ($LastExitCode -eq 0) { # Build new documentation site & mono "$Env:DOCFX_PATH/docfx.exe" build docfx.json | Out-Host } } else { # Generate new API documentation & docfx docfx.json | Out-Host # Check if successful if ($LastExitCode -eq 0) { # Build new documentation site & docfx build docfx.json | Out-Host } } # Exit back Set-Location -path "$current_location" # Check if building was a success if ($LastExitCode -eq 0) { Return 0 } else { Return $LastExitCode } } -# Packages the build site to a .tar.xz archive +# Packages the build site to a .zip archive function PackDocs([string] $target_dir_path, [string] $output_dir_path, [string] $pack_name) { # Form target path $target_path = Get-Item "$target_dir_path" $target_path = $target_path.FullName $target_path = Join-Path "$target_path" "_site" # Form output path $output_path_dir = Get-Item "$output_dir_path" $output_path_dir = $output_path_dir.FullName $output_path = Join-Path "$output_path_dir" "$pack_name" # Enter target path Set-Location -path "$target_path" - # Check if target .tar exists + # Check if target .zip exists # If it does, remove it - if (Test-Path "$output_path.tar") - { - Write-Host "$output_path.tar exists, deleting" - Remove-Item "$output_path.tar" - } - - # Package .tar archive - Write-Host "Packaging docs to $output_path.tar" - & 7za -r a "$output_path.tar" * | Out-Host - - # Check if prepackaging was a success - if ($LastExitCode -ne 0) + if (Test-Path "$output_path.zip") { - Return $LastExitCode + Write-Host "$output_path.zip exists, deleting" + Remove-Item "$output_path.zip" } - # Go to package's location - Set-Location -path "$output_path_dir" - - # Check if target .tar.xz exists - # If it does, remove it - if (Test-Path "$output_path.tar.xz") - { - Write-Host "$output_path.tar.xz exists, deleting" - Remove-Item "$output_path.tar.xz" - } - - # Package .tar.xz - Write-Host "Packaging docs to $output_path.tar.xz" - & 7za -sdel -mx9 a "$pack_name.tar.xz" "$pack_name.tar" | Out-Host + # Package .zip archive + Write-Host "Packing $output_path.zip" + Compress-Archive -Path "$target_path/*" -DestinationPath "$output_path.zip" -Force -CompressionLevel Fastest # Exit back Set-Location -path "$current_location" # Check if packaging was a success if ($LastExitCode -eq 0) { Return 0 } else { Return $LastExitCode } } # Install DocFX $result = Install-DocFX "$docfx_path" if ($result -ne 0) { Write-Host "Installing DocFX failed" Restore-Environment $host.SetShouldExit(1) Exit 1 } -# Install 7-zip, if Windows -if ($null -ne $Env:OS) -{ - $result = Install-7zip "$sevenzip_path" - if ($result -ne 0) - { - Write-Host "Installing 7-zip failed" - Restore-Environment - $host.SetShouldExit(1) - Exit 1 - } -} - # Build and package docs # At this point nothing should fail as everything is already set up $result = BuildDocs "$DocsPath" if ($result -eq 0) { $result = PackDocs "$DocsPath" "$OutputPath" "$PackageName" if ($result -ne 0) { Write-Host "Packaging API documentation failed" } } else { Write-Host "Building API documentation failed" } # Restore the environment Restore-Environment # All was well, exit with success if ($result -eq 0) { Write-Host "All operations completed" Exit 0 } else { $host.SetShouldExit($result) Exit $result }