diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index bf9c20a63..ee8e1430f 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -1,56 +1,53 @@ name: Docs Preview on: - pull_request: - workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: path: DisCatSharp - uses: actions/checkout@v2 with: repository: Aiko-IT-Systems/DisCatSharp.Docs.Preview path: DisCatSharp.Docs.Preview token: ${{ secrets.DOCS_TOKEN }} - name: Show Dir run: dir - name: Build Docs working-directory: ./DisCatSharp shell: pwsh run: | ./rebuild-docs.ps1 -DocsPath "./DisCatSharp.Docs" -Output ".." -PackageName "dcs-docs-preview" - name: Archive Docs uses: actions/upload-artifact@v2 with: name: preview-docs path: dcs-docs-preview.tar.xz - name: Purge old docs working-directory: ./DisCatSharp.Docs.Preview run: | shopt -s extglob rm -rf !(.git|.gitignore) - name: Extract new docs run: | tar -xf dcs-docs-preview.tar.xz -C ./DisCatSharp.Docs.Preview - name: Commit and push changes uses: EndBug/add-and-commit@master with: cwd: ./DisCatSharp.Docs.Preview default_author: github_actions author_name: DisCatSharp author_email: discatsharp@aitsys.dev message: 'Preview docs (${{ github.sha }})' - pull: 'NO-PULL' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 56e3a7d18..f4fc24738 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,54 +1,55 @@ name: "DisCatSharp Docs" on: push: - branches: [ main ] + branches: [ main ] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: path: DisCatSharp - uses: actions/checkout@v2 with: repository: Aiko-IT-Systems/DisCatSharp.Docs path: DisCatSharp.Docs token: ${{ secrets.DOCS_TOKEN }} - name: Get SSH uses: webfactory/ssh-agent@v0.5.3 with: ssh-private-key: ${{ secrets.AITSYS_SSH }} - 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 run: | shopt -s extglob rm -rf !(.git|.gitignore) - name: Extract new docs run: | tar -xf dcs-docs.tar.xz -C ./DisCatSharp.Docs - name: Commit and push changes uses: EndBug/add-and-commit@master with: cwd: ./DisCatSharp.Docs default_author: github_actions author_name: DisCatSharp author_email: discatsharp@aitsys.dev message: 'Docs update for commit ${{ github.repository }} (${{ github.sha }})' pull: 'NO-PULL' - name: Publish to Prod 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 4cd6d1040..d91360631 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,25 +1,30 @@ name: "DisCatSharp .NET" on: push: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: jobs: build: - - runs-on: ubuntu-latest - + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + dotnet: [6.0.x] + + runs-on: ${{ matrix.os }} + steps: - uses: actions/checkout@v2 - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + 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/BUILDING.md b/BUILDING.md index 970e8a32d..1571f043c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,74 +1,74 @@ # Building DisCatSharp These are detailed instructions on how to build the DisCatSharp library under various environmnets. It is recommended you have prior experience with multi-target .NET Core/Standard projects, as well as the `dotnet` CLI utility, and MSBuild. ## Requirements In order to build the library, you will first need to install some software. ### Windows On Windows, we only officially support Visual Studio 2019 16.10 or newer. Visual Studio Code and other IDEs might work, but are generally not supported or even guaranteed to work properly. * **Windows 10** - while we support running the library on Windows 7 and above, we only support building on Windows 10 and better. * [**Git for Windows**](https://git-scm.com/download/win) - required to clone the repository. * [**Visual Studio 2019**](https://www.visualstudio.com/downloads/) - community edition or better. We do not support Visual Studio 2017 and older. Note that to build the library, you need Visual Studio 2019 version 16.10 or newer. * **Workloads**: * **.NET Framework Desktop** - required to build .NETFX (4.5, 4.6, and 4.7 targets) * **.NET Core Cross-Platform Development** - required to build .NET Standard targets (1.1, 1.3, and 2.0) and the project overall. * **Individual Components**: * **.NET Framework 4.5 SDK** - required for .NETFX 4.5 target * **.NET Framework 4.6 SDK** - required for .NETFX 4.6 target * **.NET Framework 4.7 SDK** - required for .NETFX 4.7 target * [**.NET Core SDK 3.1**](https://www.microsoft.com/net/download) - required to build the project. * **Windows PowerShell** - required to run the build scripts. You need to make sure your script execution policy allows execution of unsigned scripts. ### GNU/Linux On GNU/Linux, we support building via Visual Studio Code and .NET Core SDK. Other IDEs might work, but are not supported or guaranteed to work properly. While these should apply to any modern distribution, we only test against Debian 10. Your mileage may vary. When installing the below, make sure you install all the dependencies properly. We might ship a build environmnent as a docker container in the future. * **Any modern GNU/Linux distribution** - like Debian 10. * **Git** - to clone the repository. * [**Visual Studio Code**](https://code.visualstudio.com/Download) - a recent version is required. * **C# for Visual Studio Code (powered by OmniSharp)** - required for syntax highlighting and basic Intellisense * [**.NET Core SDK 3.1**](https://www.microsoft.com/net/download) - required to build the project. * [**Mono 5.x**](http://www.mono-project.com/download/#download-lin) - required to build the .NETFX 4.5, 4.6, and 4.7 targets, as well as to build the docs. * [**PowerShell Core**](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1) - required to execute the build scripts. * **p7zip-full** - required to package docs. ## Instructions Once you install all the necessary prerequisites, you can proceed to building. These instructions assume you have already cloned the repository. ### Windows Building on Windows is relatively easy. There's 2 ways to build the project: #### Building through Visual Studio Building through Visual Studio yields just binaries you can use in your projects. 1. Open the solution in Visual Studio. 2. Set the configuration to Release. 3. Select Build > Build Solution to build the project. -4. Select Build > Publish DSharpPlus to publish the binaries. +4. Select Build > Publish DisCatSharp to publish the binaries. #### Building with the build script Building this way outputs NuGet packages, and a documentation package. Ensure you have an internet connection available, as the script will install programs necessary to build the documentation. 1. Open PowerShell and navigate to the directory which you cloned DisCatSharp to. 2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution. 3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned. ### GNU/Linux When all necessary prerequisites are installed, you can proceed to building. There are technically 2 ways to build the library, though both of them perform the same steps, they are just invoked slightly differently. #### Through Visual Studio Code 1. Open Visual Studio Code and open the folder to which you cloned DisCatSharp as your workspace. 2. Select Build > Run Task... 3. Select `buildRelease` task and wait for it to finish. 4. The artifacts will be placed in *dcs-artifacts* directory, next to whoch the repository is cloned. #### Through PowerShell 1. Open PowerShell (`pwsh`) and navigate to the directory which you cloned DisCatSharp to. 2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution. 3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned. diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs index 6ab119fdb..ad6ebe3a9 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs @@ -1,46 +1,50 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.ApplicationCommands { /// /// A configuration for a /// public class ApplicationCommandsConfiguration { /// /// Sets the service provider. /// Objects in this provider are used when instantiating application command modules. This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); + /// + /// Initializes a new instance of the class. + /// + /// The service provider. [ActivatorUtilitiesConstructor] public ApplicationCommandsConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } } } diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 6110efa55..fd7dfad8c 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1368 +1,1368 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Reflection; using System.Threading.Tasks; using System.Collections.Generic; using DisCatSharp.Entities; using System.Linq; using DisCatSharp.EventArgs; using Microsoft.Extensions.Logging; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.DependencyInjection; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Enums; using DisCatSharp.ApplicationCommands.Attributes; using System.Text.RegularExpressions; using DisCatSharp.Common; namespace DisCatSharp.ApplicationCommands { /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// private static List _commandMethods { get; set; } = new List(); /// /// List of groups. /// private static List _groupCommands { get; set; } = new List(); /// /// List of groups with subgroups. /// private static List _subGroupCommands { get; set; } = new List(); /// /// List of context menus. /// private static List _contextMenuCommands { get; set; } = new List(); /// /// Singleton modules. /// private static List _singletonModules { get; set; } = new List(); /// /// List of modules to register. /// private List> _updateList { get; set; } = new List>(); /// /// Configuration for Discord. /// private readonly ApplicationCommandsConfiguration _configuration; /// /// Set to true if anything fails when registering. /// private static bool _errored { get; set; } = false; /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => _registeredCommands; private static List>> _registeredCommands = new(); /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration) { this._configuration = configuration; } /// /// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. /// /// The client to setup on. protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); this.Client.Ready += this.Update; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Registers a command class. /// /// The command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T)))); } /// /// Registers a command class. /// - /// The of the command class to register. + /// The of the command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(Type type, ulong? guildId = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type))); } /// /// Registers a command class with permission setup. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup permissions with. public void RegisterCommands(ulong guildId, Action permissionSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); } /// /// Registers a command class with permission setup. /// - /// The of the command class to register. + /// The of the command class to register. /// The guild id to register it on. /// A callback to setup permissions with. public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); } /* /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup permissions with. public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// - /// The of the command class to register. + /// The of the command class to register. /// A callback to setup permissions with. public void RegisterCommands(Type type, Action permissionSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); } */ /// /// To be run on ready. /// /// The client. /// The ready event args. internal Task Update(DiscordClient client, ReadyEventArgs e) => this.Update(); /// /// Actual method for registering, used for RegisterCommands and on Ready. /// internal Task Update() { //Only update for shard 0 if (this.Client.ShardId == 0) { //Groups commands by guild id or global foreach (var key in this._updateList.Select(x => x.Key).Distinct()) { this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } } return Task.CompletedTask; } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private void RegisterCommands(IEnumerable types, ulong? guildid) { //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); _ = Task.Run(async () => { //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); //Add module to classes list if it's a group if (module.GetCustomAttribute() != null) { classes.Add(module); } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } //Handles groups foreach (var subclassinfo in classes) { //Gets the attribute and methods in the group var groupAttribute = subclassinfo.GetCustomAttribute(); var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null); if (subclasses.Any() && submethods.Any()) { throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); } //Initializes the command var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, default_permission: groupAttribute.DefaultPermission); commandTypeSources.Add(new KeyValuePair(type, type)); var commandmethods = new List>(); //Handles commands in the group foreach (var submethod in submethods) { var commandAttribute = submethod.GetCustomAttribute(); //Gets the paramaters and accounts for InteractionContext var parameters = submethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await this.ParseParameters(parameters, guildid); //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, options); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); commandTypeSources.Add(new KeyValuePair(subclassinfo, type)); //Adds it to the method lists commandmethods.Add(new KeyValuePair(commandAttribute.Name, submethod)); groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandmethods }); } var command = new SubGroupCommand { Name = groupAttribute.Name }; //Handles subgroups foreach (var subclass in subclasses) { var subGroupAttribute = subclass.GetCustomAttribute(); //I couldn't think of more creative naming var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var options = new List(); var currentMethods = new List>(); //Similar to the one for regular groups foreach (var subsubmethod in subsubmethods) { var suboptions = new List(); var commatt = subsubmethod.GetCustomAttribute(); var parameters = subsubmethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); suboptions = suboptions.Concat(await this.ParseParameters(parameters, guildid)).ToList(); var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, suboptions); options.Add(subsubpayload); commandmethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); } //Adds the group to the command and method lists var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options); command.SubCommands.Add(new GroupCommand { Name = subGroupAttribute.Name, Methods = currentMethods }); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); commandTypeSources.Add(new KeyValuePair(subclass, type)); //Accounts for lifespans for the sub group if (subclass.GetCustomAttribute() != null) { if (subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(subclass, this._configuration?.ServiceProvider)); } } } if (command.SubCommands.Any()) subGroupCommands.Add(command); updateList.Add(payload); //Accounts for lifespans if (subclassinfo.GetCustomAttribute() != null) { if (subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(subclassinfo, this._configuration?.ServiceProvider)); } } } //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { //Slash commands (again, similar to the one for groups) var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); foreach (var method in methods) { var commandattribute = method.GetCustomAttribute(); var parameters = method.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await this.ParseParameters(parameters, guildid); commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name }); var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, options, commandattribute.DefaultPermission); updateList.Add(payload); commandTypeSources.Add(new KeyValuePair(type, type)); } //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); foreach (var contextMethod in contextMethods) { var contextAttribute = contextMethod.GetCustomAttribute(); var command = new DiscordApplicationCommand(contextAttribute.Name, null, type: contextAttribute.Type, default_permission: contextAttribute.DefaultPermission); var parameters = contextMethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext))) throw new ArgumentException($"The first argument must be a ContextMenuContext!"); if (parameters.Length > 1) throw new ArgumentException($"A context menu cannot have parameters!"); contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name }); updateList.Add(command); commandTypeSources.Add(new KeyValuePair(type, type)); } //Accounts for lifespans if (module.GetCustomAttribute() != null) { if (module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(module, this._configuration?.ServiceProvider)); } } } } catch (Exception ex) { //This isn't really much more descriptive but I added a separate case for it anyway if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); _errored = true; } } if (!_errored) { try { async Task UpdateCommandPermission(ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType) { if (guildid == null) { //throw new NotImplementedException("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions"); } else { var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName); var conf = types.First(t => t.Type == commandRootType); conf.Setup?.Invoke(ctx); if (ctx.Permissions.Count == 0) return; await this.Client.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions); } } async Task UpdateCommandPermissionGroup(GroupCommand groupCommand) { foreach (var com in groupCommand.Methods) { var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Value.DeclaringType); await UpdateCommandPermission(groupCommand.CommandId, com.Key, source.Key, source.Value); } } var commands = guildid == null ? await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList) : (IEnumerable)await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, updateList); //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods.Any(x => x.Name == command.Name)) { var com = commandMethods.First(x => x.Name == command.Name); com.CommandId = command.Id; var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType); await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); } else if (groupCommands.Any(x => x.Name == command.Name)) { var com = groupCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; await UpdateCommandPermissionGroup(com); } else if (subGroupCommands.Any(x => x.Name == command.Name)) { var com = subGroupCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; foreach (var groupComs in com.SubCommands) await UpdateCommandPermissionGroup(groupComs); } else if (contextMenuCommands.Any(x => x.Name == command.Name)) { var com = contextMenuCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; var source = commandTypeSources.First(f => f.Key == com.Method.DeclaringType); await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); } } //Adds to the global lists finally _commandMethods.AddRange(commandMethods); _groupCommands.AddRange(groupCommands); _subGroupCommands.AddRange(subGroupCommands); _contextMenuCommands.AddRange(contextMenuCommands); _registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); _errored = true; } } }); } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = this._configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), Type = ApplicationCommandType.ChatInput }; try { if (_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); if (methods.Any()) { var method = methods.First().Method; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var method = groups.First().Methods.First(x => x.Key == command.Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it."); try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = this.Client, Services = this._configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = this._configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } /*else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var method = methods.First().Method; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused); this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue); var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value; var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = this._configuration?.Services, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.First(x => x.Name == group.Name).Options.ToList(), FocusedOption = focusedOption }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); }*/ } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = this._configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type }; try { if (_errored) throw new InvalidOperationException("Context menus failed to register properly on startup."); //Gets the method for the command var method = _contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = _singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection copied over from CommandsNext /// /// The type. /// The services. internal object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); for (var i = 0; i < parameters.Count(); i++) { var parameter = parameters.ElementAt(i); //Accounts for optional arguments without values given if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); //Checks the type and casts/references resolved and adds the value to the list //This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) args.Add((long?)option.Value); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the preexecution checks. /// /// The method info. /// The basecontext. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext CMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(CMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// private async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method == null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, _configuration.ServiceProvider); } //Gets the choices from the method var result = await (Task>)method.Invoke(instance, null); if (result.Any()) { choices.AddRange(result); } } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) { var choices = new List(); foreach (Enum enumValue in Enum.GetValues(enumParam)) { choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); } return choices; } /// /// Gets the parameter type. /// /// The type. private ApplicationCommandOptionType GetParameterType(Type type) { var parametertype = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parametertype; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private List GetChoiceAttributesFromParameter(IEnumerable choiceattributes) { return !choiceattributes.Any() ? null : choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); } /// /// Parses the parameters. /// /// The parameters. /// The guild id. /// A Task. private async Task> ParseParameters(ParameterInfo[] parameters, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionattribute = parameter.GetCustomAttribute(); if (optionattribute == null) throw new ArgumentException("Arguments must have the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); if (optionattribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); if (!optionattribute.Autocomplete && autocompleteAttribute != null) throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); //Sets the type var type = parameter.ParameterType; var parametertype = this.GetParameterType(type); //Handles choices //From attributes var choices = this.GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); //From enums if (parameter.ParameterType.IsEnum) { choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); } //From choice provider var choiceProviders = parameter.GetCustomAttributes(); if (choiceProviders.Any()) { choices = await this.GetChoiceAttributesFromProvider(choiceProviders, guildId); } var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete, minimumValue, maximumValue)); } return options; } /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Should only be run on the slash command extension linked to shard 0 if sharding. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { _commandMethods.Clear(); _groupCommands.Clear(); _subGroupCommands.Clear(); _registeredCommands.Clear(); _contextMenuCommands.Clear(); await this.Update(); } /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add { this._slashError.Register(value); } remove { this._slashError.Unregister(value); } } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add { this._slashExecuted.Register(value); } remove { this._slashExecuted.Unregister(value); } } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add { this._contextMenuErrored.Register(value); } remove { this._contextMenuErrored.Unregister(value); } } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add { this._contextMenuExecuted.Register(value); } remove { this._contextMenuExecuted.Unregister(value); } } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The permission setup. /// public Action Setup { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The permission setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action setup = null) { this.Type = type; this.Setup = setup; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new List(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } } diff --git a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs index 75363284a..c4aea5d85 100644 --- a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs @@ -1,163 +1,170 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; using DisCatSharp.Enums; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.ApplicationCommands { /// /// Respresents a base context for application command contexts. /// public class BaseContext { /// /// Gets the interaction that was created. /// public DiscordInteraction Interaction { get; internal set; } /// /// Gets the client for this interaction. /// public DiscordClient Client { get; internal set; } /// /// Gets the guild this interaction was executed in. /// public DiscordGuild Guild { get; internal set; } /// /// Gets the channel this interaction was executed in. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the user which executed this interaction. /// public DiscordUser User { get; internal set; } /// /// Gets the member which executed this interaction, or null if the command is in a DM. /// public DiscordMember Member => this.User is DiscordMember member ? member : null; /// /// Gets the application command module this interaction was created in. /// public ApplicationCommandsExtension ApplicationCommandsExtension { get; internal set; } /// /// Gets the token for this interaction. /// public string Token { get; internal set; } /// /// Gets the id for this interaction. /// public ulong InteractionId { get; internal set; } /// /// Gets the name of the command. /// public string CommandName { get; internal set; } /// /// Gets the type of this interaction. /// public ApplicationCommandType Type { get; internal set;} /// /// Gets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Creates a response to this interaction. /// You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, create a at the start, and edit the response later. /// /// The type of the response. /// The data to be sent, if any. /// public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => this.Interaction.CreateResponseAsync(type, builder); + /// + /// Creates a modal response to this interaction. + /// + /// The data to send. + public Task CreateModalResponseAsync(DiscordInteractionModalBuilder builder) => + this.Interaction.CreateInteractionModalResponseAsync(builder); + /// /// Edits the interaction response. /// /// The data to edit the response with. /// public Task EditResponseAsync(DiscordWebhookBuilder builder) => this.Interaction.EditOriginalResponseAsync(builder); /// /// Deletes the interaction response. /// /// public Task DeleteResponseAsync() => this.Interaction.DeleteOriginalResponseAsync(); /// /// Creates a follow up message to the interaction. /// /// The message to be sent, in the form of a webhook. /// The created message. public Task FollowUpAsync(DiscordFollowupMessageBuilder builder) => this.Interaction.CreateFollowupMessageAsync(builder); /// /// Edits a followup message. /// /// The id of the followup message to edit. /// The webhook builder. /// public Task EditFollowupAsync(ulong followupMessageId, DiscordWebhookBuilder builder) => this.Interaction.EditFollowupMessageAsync(followupMessageId, builder); /// /// Deletes a followup message. /// /// The id of the followup message to delete. /// public Task DeleteFollowupAsync(ulong followupMessageId) => this.Interaction.DeleteFollowupMessageAsync(followupMessageId); /// /// Gets the followup message. /// /// The followup message id. public Task GetFollowupMessageAsync(ulong followupMessageId) => this.Interaction.GetFollowupMessageAsync(followupMessageId); /// /// Gets the original interaction response. /// /// The original interaction response. public Task GetOriginalResponseAsync() => this.Interaction.GetOriginalResponseAsync(); } } diff --git a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj index ca8e6cccd..5b9feb4e3 100644 --- a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj +++ b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj @@ -1,39 +1,40 @@ DisCatSharp.ApplicationCommands DisCatSharp.ApplicationCommands Library netstandard2.0 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/ExtensionMethods.cs b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs index 6c3164b74..fa76989e0 100644 --- a/DisCatSharp.ApplicationCommands/ExtensionMethods.cs +++ b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs @@ -1,134 +1,134 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; namespace DisCatSharp.ApplicationCommands { /// /// Defines various extension methods for application commands. /// public static class ExtensionMethods { /// /// Enables application commands on this . /// /// Client to enable application commands for. /// Configuration to use. /// Created . public static ApplicationCommandsExtension UseApplicationCommands(this DiscordClient client, ApplicationCommandsConfiguration config = null) { if (client.GetExtension() != null) throw new InvalidOperationException("Application commands are already enabled for that client."); var scomm = new ApplicationCommandsExtension(config); client.AddExtension(scomm); return scomm; } /// /// Gets the application commands module for this client. /// /// Client to get application commands for. /// The module, or null if not activated. public static ApplicationCommandsExtension GetApplicationCommands(this DiscordClient client) => client.GetExtension(); /// /// Enables application commands on this . /// /// Client to enable application commands on. /// Configuration to use. /// A dictionary of created with the key being the shard id. public static async Task> UseApplicationCommandsAsync(this DiscordShardedClient client, ApplicationCommandsConfiguration config = null) { var modules = new Dictionary(); await (Task)client.GetType().GetMethod("InitializeShardsAsync", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(client, null); foreach (var shard in client.ShardClients.Values) { var scomm = shard.GetApplicationCommands(); if (scomm == null) scomm = shard.UseApplicationCommands(config); modules[shard.ShardId] = scomm; } return modules; } /// /// Registers a commands class. /// /// The command class to register. /// The modules to register it on. /// The guild id to register it on. If you want global commands, leave it null. public static void RegisterCommands(this IReadOnlyDictionary modules, ulong? guildId = null) where T : ApplicationCommandsModule { foreach (var module in modules.Values) module.RegisterCommands(guildId); } /// /// Registers a command class. /// /// The modules to register it on. - /// The of the command class to register. + /// The of the command class to register. /// The guild id to register it on. If you want global commands, leave it null. public static void RegisterCommands(this IReadOnlyDictionary modules, Type type, ulong? guildId = null) { foreach (var module in modules.Values) module.RegisterCommands(type, guildId); } /// /// Gets the name from the for this enum value. /// /// The name. public static string GetName(this T e) where T : IConvertible { if (e is Enum) { var type = e.GetType(); var values = Enum.GetValues(type); foreach (int val in values) { if (val == e.ToInt32(CultureInfo.InvariantCulture)) { var memInfo = type.GetMember(type.GetEnumName(val)); return memInfo[0] .GetCustomAttributes(typeof(ChoiceNameAttribute), false) .FirstOrDefault() is ChoiceNameAttribute nameAttribute ? nameAttribute.Name : type.GetEnumName(val); } } } return null; } } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs similarity index 91% rename from DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs rename to DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs index 6e69d25dc..59a306f45 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Threading.Tasks; namespace DisCatSharp.CommandsNext.Attributes { /// /// Defines that usage of this command is restricted to discord certified moderators. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class RequireDiscordCertifiedModeratorAttribute : CheckBaseAttribute + public sealed class RequireCertifiedModeratorAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.DiscordCertifiedModerator)) : Task.FromResult(false); + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.CertifiedModerator)) : Task.FromResult(false); } } diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs similarity index 91% rename from DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs rename to DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs index 5cffdfccb..b7bdb4b35 100644 --- a/DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Threading.Tasks; namespace DisCatSharp.CommandsNext.Attributes { /// /// Defines that usage of this command is restricted to discord employees. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class RequireDiscordEmployeeAttribute : CheckBaseAttribute + public sealed class RequireStaffAttribute : CheckBaseAttribute { /// /// Executes the a check. /// /// The command context. /// If true, help - returns true. - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.DiscordEmployee)) : Task.FromResult(false); + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.Staff)) : Task.FromResult(false); } } diff --git a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs index 1ba977af9..623c545c2 100644 --- a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs +++ b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs @@ -1,156 +1,160 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.Entities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext { /// /// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present. /// /// It is recommended that helper methods and /// be used internally for checking. Their output can be passed through. /// /// /// Message to check for prefix. /// Position of the command invocation or -1 if not present. public delegate Task PrefixResolverDelegate(DiscordMessage msg); /// /// Represents a configuration for . /// public sealed class CommandsNextConfiguration { /// /// Sets the string prefixes used for commands. /// Defaults to no value (disabled). /// public IEnumerable StringPrefixes { internal get; set; } /// /// Sets the custom prefix resolver used for commands. /// Defaults to none (disabled). /// public PrefixResolverDelegate PrefixResolver { internal get; set; } = null; /// /// Sets whether to allow mentioning the bot to be used as command prefix. /// Defaults to true. /// public bool EnableMentionPrefix { internal get; set; } = true; /// /// Sets whether strings should be matched in a case-sensitive manner. /// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion. /// Defaults to false. /// public bool CaseSensitive { internal get; set; } = false; /// /// Sets whether to enable default help command. /// Disabling this will allow you to make your own help command. /// - /// Modifying default help can be achieved via custom help formatters (see and for more details). + /// Modifying default help can be achieved via custom help formatters (see and for more details). /// It is recommended to use help formatter instead of disabling help. /// /// Defaults to true. /// public bool EnableDefaultHelp { internal get; set; } = true; /// /// Controls whether the default help will be sent via DMs or not. /// Enabling this will make the bot respond with help via direct messages. /// Defaults to false. /// public bool DmHelp { internal get; set; } = false; /// /// Sets the default pre-execution checks for the built-in help command. /// Only applicable if default help is enabled. /// Defaults to null. /// public IEnumerable DefaultHelpChecks { internal get; set; } = null; /// /// Sets whether commands sent via direct messages should be processed. /// Defaults to true. /// public bool EnableDms { internal get; set; } = true; /// /// Sets the service provider for this CommandsNext instance. /// Objects in this provider are used when instantiating command modules. This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored. /// Defaults to false. /// public bool IgnoreExtraArguments { internal get; set; } = false; /// /// Gets or sets whether to automatically enable handling commands. /// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext. /// Defaults to true. /// public bool UseDefaultCommandHandler { internal get; set; } = true; /// /// Creates a new instance of . /// public CommandsNextConfiguration() { } + /// + /// Initializes a new instance of the class. + /// + /// The service provider. [ActivatorUtilitiesConstructor] public CommandsNextConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public CommandsNextConfiguration(CommandsNextConfiguration other) { this.CaseSensitive = other.CaseSensitive; this.PrefixResolver = other.PrefixResolver; this.DefaultHelpChecks = other.DefaultHelpChecks; this.EnableDefaultHelp = other.EnableDefaultHelp; this.EnableDms = other.EnableDms; this.EnableMentionPrefix = other.EnableMentionPrefix; this.IgnoreExtraArguments = other.IgnoreExtraArguments; this.UseDefaultCommandHandler = other.UseDefaultCommandHandler; this.ServiceProvider = other.ServiceProvider; this.StringPrefixes = other.StringPrefixes?.ToArray(); this.DmHelp = other.DmHelp; } } } diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index e8079de3a..dc519b047 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1083 +1,1083 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Builders; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp.CommandsNext { /// /// This is the class which handles command registration, management, and execution. /// public class CommandsNextExtension : BaseExtension { /// /// Gets the config. /// private CommandsNextConfiguration Config { get; } /// /// Gets the help formatter. /// private HelpFormatterFactory HelpFormatter { get; } /// /// Gets the convert generic. /// private MethodInfo ConvertGeneric { get; } /// /// Gets the user friendly type names. /// private Dictionary UserFriendlyTypeNames { get; } /// /// Gets the argument converters. /// internal Dictionary ArgumentConverters { get; } /// /// Gets the service provider this CommandsNext module was configured with. /// public IServiceProvider Services => this.Config.ServiceProvider; /// /// Initializes a new instance of the class. /// /// The cfg. internal CommandsNextExtension(CommandsNextConfiguration cfg) { this.Config = new CommandsNextConfiguration(cfg); this.TopLevelCommands = new Dictionary(); this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this.TopLevelCommands)); this.HelpFormatter = new HelpFormatterFactory(); this.HelpFormatter.SetFormatterType(); this.ArgumentConverters = new Dictionary { [typeof(string)] = new StringConverter(), [typeof(bool)] = new BoolConverter(), [typeof(sbyte)] = new Int8Converter(), [typeof(byte)] = new Uint8Converter(), [typeof(short)] = new Int16Converter(), [typeof(ushort)] = new Uint16Converter(), [typeof(int)] = new Int32Converter(), [typeof(uint)] = new Uint32Converter(), [typeof(long)] = new Int64Converter(), [typeof(ulong)] = new Uint64Converter(), [typeof(float)] = new Float32Converter(), [typeof(double)] = new Float64Converter(), [typeof(decimal)] = new Float128Converter(), [typeof(DateTime)] = new DateTimeConverter(), [typeof(DateTimeOffset)] = new DateTimeOffsetConverter(), [typeof(TimeSpan)] = new TimeSpanConverter(), [typeof(Uri)] = new UriConverter(), [typeof(DiscordUser)] = new DiscordUserConverter(), [typeof(DiscordMember)] = new DiscordMemberConverter(), [typeof(DiscordRole)] = new DiscordRoleConverter(), [typeof(DiscordChannel)] = new DiscordChannelConverter(), [typeof(DiscordGuild)] = new DiscordGuildConverter(), [typeof(DiscordMessage)] = new DiscordMessageConverter(), [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), [typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), [typeof(DiscordInvite)] = new DiscordInviteConverter(), [typeof(DiscordColor)] = new DiscordColorConverter() }; this.UserFriendlyTypeNames = new Dictionary() { [typeof(string)] = "string", [typeof(bool)] = "boolean", [typeof(sbyte)] = "signed byte", [typeof(byte)] = "byte", [typeof(short)] = "short", [typeof(ushort)] = "unsigned short", [typeof(int)] = "int", [typeof(uint)] = "unsigned int", [typeof(long)] = "long", [typeof(ulong)] = "unsigned long", [typeof(float)] = "float", [typeof(double)] = "double", [typeof(decimal)] = "decimal", [typeof(DateTime)] = "date and time", [typeof(DateTimeOffset)] = "date and time", [typeof(TimeSpan)] = "time span", [typeof(Uri)] = "URL", [typeof(DiscordUser)] = "user", [typeof(DiscordMember)] = "member", [typeof(DiscordRole)] = "role", [typeof(DiscordChannel)] = "channel", [typeof(DiscordGuild)] = "guild", [typeof(DiscordMessage)] = "message", [typeof(DiscordEmoji)] = "emoji", [typeof(DiscordThreadChannel)] = "thread", [typeof(DiscordInvite)] = "invite", [typeof(DiscordColor)] = "color" }; var ncvt = typeof(NullableConverter<>); var nt = typeof(Nullable<>); var cvts = this.ArgumentConverters.Keys.ToArray(); foreach (var xt in cvts) { var xti = xt.GetTypeInfo(); if (!xti.IsValueType) continue; var xcvt = ncvt.MakeGenericType(xt); var xnt = nt.MakeGenericType(xt); if (this.ArgumentConverters.ContainsKey(xcvt)) continue; var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter; this.ArgumentConverters[xnt] = xcv; this.UserFriendlyTypeNames[xnt] = this.UserFriendlyTypeNames[xt]; } var t = typeof(CommandsNextExtension); var ms = t.GetTypeInfo().DeclaredMethods; var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgument" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); this.ConvertGeneric = m; } /// /// Sets the help formatter to use with the default help command. /// /// Type of the formatter to use. public void SetHelpFormatter() where T : BaseHelpFormatter => this.HelpFormatter.SetFormatterType(); #region DiscordClient Registration /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. - /// + /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler); this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler); if (this.Config.UseDefaultCommandHandler) this.Client.MessageCreated += this.HandleCommandsAsync; else this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message"); if (this.Config.EnableDefaultHelp) { this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds); if (this.Config.DefaultHelpChecks != null) { var checks = this.Config.DefaultHelpChecks.ToArray(); for (var i = 0; i < tcmds.Count; i++) tcmds[i].WithExecutionChecks(checks); } if (tcmds != null) foreach (var xc in tcmds) this.AddToCommandDictionary(xc.Build(null)); } } #endregion #region Command Handling /// /// Handles the commands async. /// /// The sender. /// The e. /// A Task. private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e) { if (e.Author.IsBot) // bad bot return; if (!this.Config.EnableDms && e.Channel.IsPrivate) return; var mpos = -1; if (this.Config.EnableMentionPrefix) mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); if (this.Config.StringPrefixes?.Any() == true) foreach (var pfix in this.Config.StringPrefixes) if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) mpos = e.Message.GetStringPrefixLength(pfix, this.Config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); if (mpos == -1 && this.Config.PrefixResolver != null) mpos = await this.Config.PrefixResolver(e.Message).ConfigureAwait(false); if (mpos == -1) return; var pfx = e.Message.Content.Substring(0, mpos); var cnt = e.Message.Content.Substring(mpos); var __ = 0; var fname = cnt.ExtractNextArgument(ref __); var cmd = this.FindCommand(cnt, out var args); var ctx = this.CreateContext(e.Message, pfx, cmd, args); if (cmd == null) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false); return; } _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false)); } /// /// Finds a specified command by its qualified name, then separates arguments. /// /// Qualified name of the command, optionally with arguments. /// Separated arguments. /// Found command or null if none was found. public Command FindCommand(string commandString, out string rawArguments) { rawArguments = null; var ignoreCase = !this.Config.CaseSensitive; var pos = 0; var next = commandString.ExtractNextArgument(ref pos); if (next == null) return null; if (!this.RegisteredCommands.TryGetValue(next, out var cmd)) { if (!ignoreCase) return null; next = next.ToLowerInvariant(); var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next); if (cmdKvp.Value == null) return null; cmd = cmdKvp.Value; } - if (!(cmd is CommandGroup)) + if (cmd is not CommandGroup) { rawArguments = commandString.Substring(pos).Trim(); return cmd; } while (cmd is CommandGroup) { var cm2 = cmd as CommandGroup; var oldPos = pos; next = commandString.ExtractNextArgument(ref pos); if (next == null) break; if (ignoreCase) { next = next.ToLowerInvariant(); cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true); } else { cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true); } if (cmd == null) { cmd = cm2; pos = oldPos; break; } } rawArguments = commandString.Substring(pos).Trim(); return cmd; } /// /// Creates a command execution context from specified arguments. /// /// Message to use for context. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created command execution context. public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) { var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this.Config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } /// /// Executes specified command from given context. /// /// Context to execute command from. /// public async Task ExecuteCommandAsync(CommandContext ctx) { try { var cmd = ctx.Command; await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false); var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false); if (res.IsSuccessful) await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false); else await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false); } catch (Exception ex) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false); } finally { if (ctx.ServiceScopeContext.IsInitialized) ctx.ServiceScopeContext.Dispose(); } } /// /// Runs the all checks async. /// /// The cmd. /// The ctx. /// A Task. private async Task RunAllChecksAsync(Command cmd, CommandContext ctx) { if (cmd.Parent != null) await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false); var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false); if (fchecks.Any()) throw new ChecksFailedException(cmd, ctx, fchecks); } #endregion #region Command Registration /// /// Gets a dictionary of registered top-level commands. /// public IReadOnlyDictionary RegisteredCommands => this._registeredCommandsLazy.Value; /// /// Gets or sets the top level commands. /// private Dictionary TopLevelCommands { get; set; } private readonly Lazy> _registeredCommandsLazy; /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. public void RegisterCommands(Assembly assembly) { var types = assembly.ExportedTypes.Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterCommands(xt); } /// /// Registers all commands from a given command class. /// /// Class which holds commands to register. public void RegisterCommands() where T : BaseCommandModule { var t = typeof(T); this.RegisterCommands(t); } /// /// Registers all commands from a given command class. /// /// Type of the class which holds commands to register. public void RegisterCommands(Type t) { if (t == null) throw new ArgumentNullException(nameof(t), "Type cannot be null."); if (!t.IsModuleCandidateType()) throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static."); this.RegisterCommands(t, null, null, out var tempCommands); if (tempCommands != null) foreach (var command in tempCommands) this.AddToCommandDictionary(command.Build(null)); } /// /// Registers the commands. /// /// The type. /// The current parent. /// The inherited checks. /// The found commands. private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands) { var ti = t.GetTypeInfo(); var lifespan = ti.GetCustomAttribute(); var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; var module = new CommandModuleBuilder() .WithType(t) .WithLifespan(moduleLifespan) .Build(this.Services); // restrict parent lifespan to more or equally restrictive if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient) throw new InvalidOperationException("In a transient module, child modules can only be transient."); // check if we are anything var groupBuilder = new CommandGroupBuilder(module); var isModule = false; var moduleAttributes = ti.GetCustomAttributes(); var moduleHidden = false; var moduleChecks = new List(); foreach (var xa in moduleAttributes) { switch (xa) { case GroupAttribute g: isModule = true; var moduleName = g.Name; if (moduleName == null) { moduleName = ti.Name; if (moduleName.EndsWith("Group") && moduleName != "Group") moduleName = moduleName.Substring(0, moduleName.Length - 5); else if (moduleName.EndsWith("Module") && moduleName != "Module") moduleName = moduleName.Substring(0, moduleName.Length - 6); else if (moduleName.EndsWith("Commands") && moduleName != "Commands") moduleName = moduleName.Substring(0, moduleName.Length - 8); } if (!this.Config.CaseSensitive) moduleName = moduleName.ToLowerInvariant(); groupBuilder.WithName(moduleName); if (inheritedChecks != null) foreach (var chk in inheritedChecks) groupBuilder.WithExecutionCheck(chk); foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); break; case AliasesAttribute a: foreach (var xalias in a.Aliases) groupBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case HiddenAttribute h: groupBuilder.WithHiddenStatus(true); moduleHidden = true; break; case DescriptionAttribute d: groupBuilder.WithDescription(d.Description); break; case CheckBaseAttribute c: moduleChecks.Add(c); groupBuilder.WithExecutionCheck(c); break; default: groupBuilder.WithCustomAttribute(xa); break; } } if (!isModule) { groupBuilder = null; if (inheritedChecks != null) moduleChecks.AddRange(inheritedChecks); } // candidate methods var methods = ti.DeclaredMethods; var commands = new List(); var commandBuilders = new Dictionary(); foreach (var m in methods) { if (!m.IsCommandCandidate(out _)) continue; var attrs = m.GetCustomAttributes(); if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) continue; var commandName = cattr.Name; if (commandName == null) { commandName = m.Name; if (commandName.EndsWith("Async") && commandName != "Async") commandName = commandName.Substring(0, commandName.Length - 5); } if (!this.Config.CaseSensitive) commandName = commandName.ToLowerInvariant(); if (!commandBuilders.TryGetValue(commandName, out var commandBuilder)) { commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName)); if (!isModule) if (currentParent != null) currentParent.WithChild(commandBuilder); else commands.Add(commandBuilder); else groupBuilder.WithChild(commandBuilder); } commandBuilder.WithOverload(new CommandOverloadBuilder(m)); if (!isModule && moduleChecks.Any()) foreach (var chk in moduleChecks) commandBuilder.WithExecutionCheck(chk); foreach (var xa in attrs) { switch (xa) { case AliasesAttribute a: foreach (var xalias in a.Aliases) commandBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case CheckBaseAttribute p: commandBuilder.WithExecutionCheck(p); break; case DescriptionAttribute d: commandBuilder.WithDescription(d.Description); break; case HiddenAttribute h: commandBuilder.WithHiddenStatus(true); break; default: commandBuilder.WithCustomAttribute(xa); break; } } if (!isModule && moduleHidden) commandBuilder.WithHiddenStatus(true); } // candidate types var types = ti.DeclaredNestedTypes .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); foreach (var type in types) { this.RegisterCommands(type.AsType(), groupBuilder, !isModule ? moduleChecks : null, out var tempCommands); if (isModule) foreach (var chk in moduleChecks) groupBuilder.WithExecutionCheck(chk); if (isModule && tempCommands != null) foreach (var xtcmd in tempCommands) groupBuilder.WithChild(xtcmd); else if (tempCommands != null) commands.AddRange(tempCommands); } if (isModule && currentParent == null) commands.Add(groupBuilder); else if (isModule) currentParent.WithChild(groupBuilder); foundCommands = commands; } /// /// Builds and registers all supplied commands. /// /// Commands to build and register. public void RegisterCommands(params CommandBuilder[] cmds) { foreach (var cmd in cmds) this.AddToCommandDictionary(cmd.Build(null)); } /// /// Unregisters specified commands from CommandsNext. /// /// Commands to unregister. public void UnregisterCommands(params Command[] cmds) { if (cmds.Any(x => x.Parent != null)) throw new InvalidOperationException("Cannot unregister nested commands."); var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); foreach (var key in keys) this.TopLevelCommands.Remove(key); } /// /// Adds the to command dictionary. /// /// The cmd. private void AddToCommandDictionary(Command cmd) { if (cmd.Parent != null) return; if (this.TopLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this.TopLevelCommands.ContainsKey(xs)))) throw new DuplicateCommandException(cmd.QualifiedName); this.TopLevelCommands[cmd.Name] = cmd; if (cmd.Aliases != null) foreach (var xs in cmd.Aliases) this.TopLevelCommands[xs] = cmd; } #endregion #region Default Help /// /// Represents the default help module. /// [ModuleLifespan(ModuleLifespan.Transient)] public class DefaultHelpModule : BaseCommandModule { /// /// Defaults the help async. /// /// The ctx. /// The command. /// A Task. [Command("help"), Description("Displays command help.")] public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) { var topLevel = ctx.CommandsNext.TopLevelCommands.Values.Distinct(); var helpBuilder = ctx.CommandsNext.HelpFormatter.Create(ctx); if (command != null && command.Any()) { Command cmd = null; var searchIn = topLevel; foreach (var c in command) { if (searchIn == null) { cmd = null; break; } cmd = ctx.Config.CaseSensitive ? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c))) : searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant()))); if (cmd == null) break; var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false); if (failedChecks.Any()) throw new ChecksFailedException(cmd, ctx, failedChecks); searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null; } if (cmd == null) throw new CommandNotFoundException(string.Join(" ", command)); helpBuilder.WithCommand(cmd); if (cmd is CommandGroup group) { var commandsToSearch = group.Children.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var candidateCommand in commandsToSearch) { if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) { eligibleCommands.Add(candidateCommand); continue; } var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(candidateCommand); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } } else { var commandsToSearch = topLevel.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var sc in commandsToSearch) { if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) { eligibleCommands.Add(sc); continue; } var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(sc); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } var helpMessage = helpBuilder.Build(); var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed); if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null) await ctx.RespondAsync(builder).ConfigureAwait(false); else await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false); } } #endregion #region Sudo /// /// Creates a fake command context to execute commands with. /// /// The user or member to use as message author. /// The channel the message is supposed to appear from. /// Contents of the message. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created fake context. public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null) { var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); var now = DateTimeOffset.UtcNow; var timeSpan = (ulong)(now - epoch).TotalMilliseconds; // create fake message var msg = new DiscordMessage { Discord = this.Client, Author = actor, ChannelId = channel.Id, Content = messageContents, Id = timeSpan << 22, Pinned = false, MentionEveryone = messageContents.Contains("@everyone"), IsTTS = false, _attachments = new List(), _embeds = new List(), TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"), _reactions = new List() }; var mentionedUsers = new List(); var mentionedRoles = msg.Channel.Guild != null ? new List() : null; var mentionedChannels = msg.Channel.Guild != null ? new List() : null; if (!string.IsNullOrWhiteSpace(msg.Content)) { if (msg.Channel.Guild != null) { mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild._members.TryGetValue(xid, out var member) ? member : null).Cast().ToList(); mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList(); mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList(); } else { mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList(); } } msg._mentionedUsers = mentionedUsers; msg._mentionedRoles = mentionedRoles; msg._mentionedChannels = mentionedChannels; var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this.Config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } #endregion #region Type Conversion /// /// Converts a string to specified type. /// /// Type to convert to. /// Value to convert. /// Context in which to convert to. /// Converted object. #pragma warning disable IDE1006 // Naming Styles public async Task ConvertArgument(string value, CommandContext ctx) #pragma warning restore IDE1006 // Naming Styles { var t = typeof(T); if (!this.ArgumentConverters.ContainsKey(t)) throw new ArgumentException("There is no converter specified for given type.", nameof(T)); if (this.ArgumentConverters[t] is not IArgumentConverter cv) throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false); return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value; } /// /// Converts a string to specified type. /// /// Value to convert. /// Context in which to convert to. /// Type to convert to. /// Converted object. #pragma warning disable IDE1006 // Naming Styles public async Task ConvertArgument(string value, CommandContext ctx, Type type) #pragma warning restore IDE1006 // Naming Styles { var m = this.ConvertGeneric.MakeGenericMethod(type); try { return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false); } catch (TargetInvocationException ex) { throw ex.InnerException; } } /// /// Registers an argument converter for specified type. /// /// Type for which to register the converter. /// Converter to register. public void RegisterConverter(IArgumentConverter converter) { if (converter == null) throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); var t = typeof(T); var ti = t.GetTypeInfo(); this.ArgumentConverters[t] = converter; if (!ti.IsValueType) return; var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); var nullableType = typeof(Nullable<>).MakeGenericType(t); if (this.ArgumentConverters.ContainsKey(nullableType)) return; var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; this.ArgumentConverters[nullableType] = nullableConverter; } /// /// Unregisters an argument converter for specified type. /// /// Type for which to unregister the converter. public void UnregisterConverter() { var t = typeof(T); var ti = t.GetTypeInfo(); if (this.ArgumentConverters.ContainsKey(t)) this.ArgumentConverters.Remove(t); if (this.UserFriendlyTypeNames.ContainsKey(t)) this.UserFriendlyTypeNames.Remove(t); if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); if (!this.ArgumentConverters.ContainsKey(nullableType)) return; this.ArgumentConverters.Remove(nullableType); this.UserFriendlyTypeNames.Remove(nullableType); } /// /// Registers a user-friendly type name. /// /// Type to register the name for. /// Name to register. public void RegisterUserFriendlyTypeName(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); var t = typeof(T); var ti = t.GetTypeInfo(); if (!this.ArgumentConverters.ContainsKey(t)) throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); this.UserFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); this.UserFriendlyTypeNames[nullableType] = value; } /// /// Converts a type into user-friendly type name. /// /// Type to convert. /// User-friendly type name. public string GetUserFriendlyTypeName(Type t) { if (this.UserFriendlyTypeNames.ContainsKey(t)) return this.UserFriendlyTypeNames[t]; var ti = t.GetTypeInfo(); if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { var tn = ti.GenericTypeArguments[0]; return this.UserFriendlyTypeNames.ContainsKey(tn) ? this.UserFriendlyTypeNames[tn] : tn.Name; } return t.Name; } #endregion #region Helpers /// /// Gets the configuration-specific string comparer. This returns or , /// depending on whether is set to or . /// /// A string comparer. internal IEqualityComparer GetStringComparer() => this.Config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; #endregion #region Events /// /// Triggered whenever a command executes successfully. /// public event AsyncEventHandler CommandExecuted { add { this._executed.Register(value); } remove { this._executed.Unregister(value); } } private AsyncEvent _executed; /// /// Triggered whenever a command throws an exception during execution. /// public event AsyncEventHandler CommandErrored { add { this._error.Register(value); } remove { this._error.Unregister(value); } } private AsyncEvent _error; /// /// Ons the command executed. /// /// The e. /// A Task. private Task OnCommandExecuted(CommandExecutionEventArgs e) => this._executed.InvokeAsync(this, e); /// /// Ons the command errored. /// /// The e. /// A Task. private Task OnCommandErrored(CommandErrorEventArgs e) => this._error.InvokeAsync(this, e); #endregion } } diff --git a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj index 0e0eae41e..cc520bb74 100644 --- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj +++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj @@ -1,39 +1,43 @@ DisCatSharp.CommandsNext DisCatSharp.CommandsNext Library netstandard2.0 DisCatSharp.CommandsNext CommandNext extension for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext LICENSE.md - + True + + + + diff --git a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs index 9c8cc3159..b3f04c0c4 100644 --- a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs @@ -1,133 +1,133 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; namespace DisCatSharp.Common.Serialization { /// - /// Defines the format for string-serialized and objects. + /// Defines the format for string-serialized and objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class DateTimeFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// public const string FormatISO8601 = "yyyy-MM-ddTHH:mm:ss.fffzzz"; /// /// Gets the RFC 1123 format string of "R". /// public const string FormatRFC1123 = "R"; /// /// Gets the general long format. /// public const string FormatLong = "G"; /// /// Gets the general short format. /// public const string FormatShort = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public DateTimeFormatKind Kind { get; } /// /// Specifies a predefined format to use. /// /// Predefined format kind to use. public DateTimeFormatAttribute(DateTimeFormatKind kind) { if (kind < 0 || kind > DateTimeFormatKind.InvariantLocaleShort) throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported."); this.Kind = kind; this.Format = null; } /// /// Specifies a custom format to use. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings for more details. /// /// Custom format string to use. public DateTimeFormatAttribute(string format) { if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty."); this.Kind = DateTimeFormatKind.Custom; this.Format = format; } } /// - /// Defines which built-in format to use for for and serialization. + /// Defines which built-in format to use for for and serialization. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for more details. /// public enum DateTimeFormatKind : int { /// /// Specifies ISO 8601 format, which is equivalent to .NET format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// ISO8601 = 0, /// /// Specifies RFC 1123 format, which is equivalent to .NET format string of "R". /// RFC1123 = 1, /// - /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// CurrentLocaleLong = 2, /// - /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// CurrentLocaleShort = 3, /// - /// Specifies a format defined by , with a format string of "G". + /// Specifies a format defined by , with a format string of "G". /// InvariantLocaleLong = 4, /// - /// Specifies a format defined by , with a format string of "g". + /// Specifies a format defined by , with a format string of "g". /// InvariantLocaleShort = 5, /// /// Specifies a custom format. This value is not usable directly. /// Custom = int.MaxValue } } diff --git a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs index cacfe8d67..c97653667 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp.Common.Serialization { /// - /// Specifies that this will be serialized as a number of whole seconds. + /// Specifies that this will be serialized as a number of whole seconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class TimeSpanSecondsAttribute : SerializationAttribute { } /// - /// Specifies that this will be serialized as a number of whole milliseconds. + /// Specifies that this will be serialized as a number of whole milliseconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class TimeSpanMillisecondsAttribute : SerializationAttribute { } } diff --git a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs index e0df68803..78eeb6fc5 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs @@ -1,133 +1,133 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; namespace DisCatSharp.Common.Serialization { /// - /// Defines the format for string-serialized objects. + /// Defines the format for string-serialized objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class TimeSpanFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of @"ddThh\:mm\:ss\.fff". /// public const string FormatISO8601 = @"ddThh\:mm\:ss\.fff"; /// /// Gets the constant format. /// public const string FormatConstant = "c"; /// /// Gets the general long format. /// public const string FormatLong = "G"; /// /// Gets the general short format. /// public const string FormatShort = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public TimeSpanFormatKind Kind { get; } /// /// Specifies a predefined format to use. /// /// Predefined format kind to use. public TimeSpanFormatAttribute(TimeSpanFormatKind kind) { if (kind < 0 || kind > TimeSpanFormatKind.InvariantLocaleShort) throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported."); this.Kind = kind; this.Format = null; } /// /// Specifies a custom format to use. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details. /// /// Custom format string to use. public TimeSpanFormatAttribute(string format) { if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty."); this.Kind = TimeSpanFormatKind.Custom; this.Format = format; } } /// - /// Defines which built-in format to use for serialization. + /// Defines which built-in format to use for serialization. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details. /// public enum TimeSpanFormatKind : int { /// /// Specifies ISO 8601-like time format, which is equivalent to .NET format string of @"ddThh\:mm\:ss\.fff". /// ISO8601 = 0, /// - /// Specifies a format defined by , with a format string of "c". + /// Specifies a format defined by , with a format string of "c". /// InvariantConstant = 1, /// - /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// CurrentLocaleLong = 2, /// - /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// CurrentLocaleShort = 3, /// - /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// InvariantLocaleLong = 4, /// - /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. + /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// InvariantLocaleShort = 5, /// /// Specifies a custom format. This value is not usable directly. /// Custom = int.MaxValue } } diff --git a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs index 758145715..aadb6bff6 100644 --- a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs +++ b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs @@ -1,42 +1,42 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp.Common.Serialization { /// - /// Specifies that this or will be serialized as Unix timestamp seconds. + /// Specifies that this or will be serialized as Unix timestamp seconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class UnixSecondsAttribute : SerializationAttribute { } /// - /// Specifies that this or will be serialized as Unix timestamp milliseconds. + /// Specifies that this or will be serialized as Unix timestamp milliseconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class UnixMillisecondsAttribute : SerializationAttribute { } } diff --git a/DisCatSharp.Common/DisCatSharp.Common.csproj b/DisCatSharp.Common/DisCatSharp.Common.csproj index 7e175c5e1..04fdce4bb 100644 --- a/DisCatSharp.Common/DisCatSharp.Common.csproj +++ b/DisCatSharp.Common/DisCatSharp.Common.csproj @@ -1,45 +1,54 @@ DisCatSharp.Common DisCatSharp.Common 9.0 True True True Portable Library netstandard2.0 DisCatSharp.Common Assortment of various common types and utilities for DisCatSharp's projects. common utilities dotnet dotnet-core dotnetfx netfx netcore csharp LICENSE.MD False + + 1701;1702;;DV2001 + + + + 1701;1702;;DV2001 + + - + + - + True diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs index a38c211d8..40cc56b0a 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs @@ -1,830 +1,830 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; using System.Collections.Generic; using System.Collections.Immutable; namespace DisCatSharp.Common { /// - /// Represents collection of string keys and values, allowing the use of for dictionary operations. + /// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupDictionary : IDictionary, IReadOnlyDictionary, IDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// public int Count { get; private set; } = 0; /// /// Gets whether this dictionary is read-only. /// public bool IsReadOnly => false; /// /// Gets whether this dictionary has a fixed size. /// public bool IsFixedSize => false; /// /// Gets whether this dictionary is considered thread-safe. /// public bool IsSynchronized => false; /// /// Gets the object which allows synchronizing access to this dictionary. /// public object SyncRoot { get; } = new object(); /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } set { if (key == null) throw new ArgumentNullException(nameof(key)); this.TryInsertInternal(key, value, true); } } /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } #if NETCOREAPP set => this.TryInsertInternal(new string(key), value, true); #else set { unsafe { fixed (char* chars = &key.GetPinnableReference()) this.TryInsertInternal(new string(chars, 0, key.Length), value, true); } } #endif } object IDictionary.this[object key] { get { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!this.TryRetrieveInternal(tkey.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{tkey}' was not present in the dictionary."); return value; } set { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.TryInsertInternal(tkey, tvalue, true); } } /// /// Gets the internal buckets. /// private Dictionary InternalBuckets { get; } /// /// Creates a new, empty with string keys and items of type . /// public CharSpanLookupDictionary() { this.InternalBuckets = new Dictionary(); } /// /// Creates a new, empty with string keys and items of type and sets its initial capacity to specified value. /// /// Initial capacity of the dictionary. public CharSpanLookupDictionary(int initialCapacity) { this.InternalBuckets = new Dictionary(initialCapacity); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IReadOnlyDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IEnumerable> values) : this() { foreach (var (k, v) in values) this.Add(k, v); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(string key, TValue value) { if (!this.TryInsertInternal(key, value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(ReadOnlySpan key, TValue value) #if NETCOREAPP { if (!this.TryInsertInternal(new string(key), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } #else { unsafe { fixed (char* chars = &key.GetPinnableReference()) if (!this.TryInsertInternal(new string(chars, 0, key.Length), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } } #endif /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(string key, TValue value) => this.TryInsertInternal(key, value, false); /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(ReadOnlySpan key, TValue value) #if NETCOREAPP => this.TryInsertInternal(new string(key), value, false); #else { unsafe { fixed (char* chars = &key.GetPinnableReference()) return this.TryInsertInternal(new string(chars, 0, key.Length), value, false); } } #endif /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRemoveInternal(key.AsSpan(), out value); } /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(ReadOnlySpan key, out TValue value) => this.TryRemoveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Removes all items from this dictionary. /// public void Clear() { this.InternalBuckets.Clear(); this.Count = 0; } /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Removes the. /// /// The key. /// A bool. bool IDictionary.Remove(string key) => this.TryRemove(key.AsSpan(), out _); /// /// Adds the. /// /// The key. /// The value. void IDictionary.Add(object key, object value) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.Add(tkey, tvalue); } /// /// Removes the. /// /// The key. void IDictionary.Remove(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); this.TryRemove(tkey, out _); } /// /// Contains the. /// /// The key. /// A bool. bool IDictionary.Contains(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); return this.ContainsKey(tkey); } /// /// Gets the enumerator. /// /// An IDictionaryEnumerator. IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this); /// /// Adds the. /// /// The item. void ICollection>.Add(KeyValuePair item) => this.Add(item.Key, item.Value); /// /// Removes the. /// /// The item. /// A bool. bool ICollection>.Remove(KeyValuePair item) => this.TryRemove(item.Key, out _); /// /// Contains the. /// /// The item. /// A bool. bool ICollection>.Contains(KeyValuePair item) => this.TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); /// /// Copies the to. /// /// The array. /// The array index. void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { if (array.Length - arrayIndex < this.Count) throw new ArgumentException("Target array is too small.", nameof(array)); var i = arrayIndex; foreach (var (k, v) in this.InternalBuckets) { var kdv = v; while (kdv != null) { array[i++] = new KeyValuePair(kdv.Key, kdv.Value); kdv = kdv.Next; } } } /// /// Copies the to. /// /// The array. /// The array index. void ICollection.CopyTo(Array array, int arrayIndex) { if (array is KeyValuePair[] tarray) { (this as ICollection>).CopyTo(tarray, arrayIndex); return; } - if (!(array is object[])) + if (array is not object[]) throw new ArgumentException($"Array needs to be an instance of {typeof(TValue[])} or object[]."); var i = arrayIndex; foreach (var (k, v) in this.InternalBuckets) { var kdv = v; while (kdv != null) { array.SetValue(new KeyValuePair(kdv.Key, kdv.Value), i++); kdv = kdv.Next; } } } /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the insert internal. /// /// The key. /// The value. /// If true, replace. /// A bool. private bool TryInsertInternal(string key, TValue value, bool replace) { if (key == null) throw new ArgumentNullException(nameof(key), "Key cannot be null."); var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.ContainsKey(hash)) { this.InternalBuckets.Add(hash, new KeyedValue(key, hash, value)); this.Count++; return true; } var kdv = this.InternalBuckets[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == key) { if (!replace) return false; kdv.Value = value; return true; } kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(key, hash, value); this.Count++; return true; } /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Tries the remove internal. /// /// The key. /// The value. /// A bool. private bool TryRemoveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.TryGetValue(hash, out var kdv)) return false; if (kdv.Next == null && key.SequenceEqual(kdv.Key.AsSpan())) { // Only bucket under this hash and key matches, pop the entire bucket value = kdv.Value; this.InternalBuckets.Remove(hash); this.Count--; return true; } else if (kdv.Next == null) { // Only bucket under this hash and key does not match, cannot remove return false; } else if (key.SequenceEqual(kdv.Key.AsSpan())) { // First key in the bucket matches, pop it and set its child as current bucket value = kdv.Value; this.InternalBuckets[hash] = kdv.Next; this.Count--; return true; } var kdvLast = kdv; kdv = kdv.Next; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { // Key matched, remove this bucket from the chain value = kdv.Value; kdvLast.Next = kdv.Next; this.Count--; return true; } kdvLast = kdv; kdv = kdv.Next; } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this.InternalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this.InternalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator>, IDictionaryEnumerator { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the key. /// object IDictionaryEnumerator.Key => this.Current.Key; /// /// Gets the value. /// object IDictionaryEnumerator.Value => this.Current.Value; /// /// Gets the entry. /// DictionaryEntry IDictionaryEnumerator.Entry => new DictionaryEntry(this.Current.Key, this.Current.Value); /// /// Gets the internal dictionary. /// private CharSpanLookupDictionary InternalDictionary { get; } /// /// Gets the internal enumerator. /// private IEnumerator> InternalEnumerator { get; } /// /// Gets or sets the current value. /// private KeyedValue CurrentValue { get; set; } = null; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupDictionary spDict) { this.InternalDictionary = spDict; this.InternalEnumerator = this.InternalDictionary.InternalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this.CurrentValue; if (kdv == null) { if (!this.InternalEnumerator.MoveNext()) return false; kdv = this.InternalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this.CurrentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this.CurrentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this.InternalEnumerator.Reset(); this.Current = default; this.CurrentValue = null; } /// /// Disposes the. /// public void Dispose() { this.Reset(); } } } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs index 775141380..8f955eb5d 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs @@ -1,417 +1,417 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; namespace DisCatSharp.Common { /// - /// Represents collection of string keys and values, allowing the use of for dictionary operations. + /// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupReadOnlyDictionary : IReadOnlyDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// public int Count { get; } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } } /// /// Gets the internal buckets. /// private IReadOnlyDictionary InternalBuckets { get; } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IReadOnlyDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IEnumerable> values) { this.InternalBuckets = PrepareItems(values, out var count); this.Count = count; } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this.InternalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this.InternalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this.InternalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Prepares the items. /// /// The items. /// The count. /// An IReadOnlyDictionary. private static IReadOnlyDictionary PrepareItems(IEnumerable> items, out int count) { count = 0; var dict = new Dictionary(); foreach (var (k, v) in items) { if (k == null) throw new ArgumentException("Keys cannot be null.", nameof(items)); var hash = k.CalculateKnuthHash(); if (!dict.ContainsKey(hash)) { dict.Add(hash, new KeyedValue(k, hash, v)); count++; continue; } var kdv = dict[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == k) throw new ArgumentException("Given key is already present in the dictionary.", nameof(items)); kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(k, hash, v); count++; } return new ReadOnlyDictionary(dict); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator> { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the internal dictionary. /// private CharSpanLookupReadOnlyDictionary InternalDictionary { get; } /// /// Gets the internal enumerator. /// private IEnumerator> InternalEnumerator { get; } /// /// Gets or sets the current value. /// private KeyedValue CurrentValue { get; set; } = null; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupReadOnlyDictionary spDict) { this.InternalDictionary = spDict; this.InternalEnumerator = this.InternalDictionary.InternalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this.CurrentValue; if (kdv == null) { if (!this.InternalEnumerator.MoveNext()) return false; kdv = this.InternalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this.CurrentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this.CurrentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this.InternalEnumerator.Reset(); this.Current = default; this.CurrentValue = null; } /// /// Disposes the. /// public void Dispose() { this.Reset(); } } } } diff --git a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs index e1736cd76..53a9cde41 100644 --- a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs +++ b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs @@ -1,274 +1,274 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types { /// /// Provides a resizable memory buffer analogous to , using a single continuous memory region instead. /// /// Type of item to hold in the buffer. public sealed class ContinuousMemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => (ulong)this._buff.Length; /// public ulong Length => (ulong)this._pos; /// public ulong Count => (ulong)(this._pos / this._itemSize); private readonly MemoryPool _pool; private IMemoryOwner _buffOwner; private Memory _buff; private readonly bool _clear; private int _pos; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Initial size of the buffer in bytes. Defaults to 64KiB. - /// Memory pool to use for renting buffers. Defaults to . + /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public ContinuousMemoryBuffer(int initialSize = 65536, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); this._pool = memPool ?? MemoryPool.Shared; this._clear = clearOnDispose; this._buffOwner = this._pool.Rent(initialSize); this._buff = this._buffOwner.Memory; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = MemoryMarshal.AsBytes(data); this.EnsureSize(this._pos + bytes.Length); bytes.CopyTo(this._buff.Slice(this._pos).Span); this._pos += bytes.Length; } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { if (stream.Length > int.MaxValue) throw new ArgumentException("Stream is too long.", nameof(stream)); this.EnsureSize(this._pos + (int)stream.Length); #if HAS_SPAN_STREAM_OVERLOADS stream.Read(this._buff.Slice(this._pos).Span); #else var memo = ArrayPool.Shared.Rent((int)stream.Length); try { var br = stream.Read(memo, 0, memo.Length); memo.AsSpan(0, br).CopyTo(this._buff.Slice(this._pos).Span); } finally { ArrayPool.Shared.Return(memo); } #endif this._pos += (int)stream.Length; } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { #if HAS_SPAN_STREAM_OVERLOADS var br = 0; do { this.EnsureSize(this._pos + 4096); br = stream.Read(this._buff.Slice(this._pos).Span); this._pos += br; } while (br != 0); #else var memo = ArrayPool.Shared.Rent(4096); try { var br = 0; while ((br = stream.Read(memo, 0, memo.Length)) != 0) { this.EnsureSize(this._pos + br); memo.AsSpan(0, br).CopyTo(this._buff.Slice(this._pos).Span); this._pos += br; } } finally { ArrayPool.Shared.Return(memo); } #endif } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); var start = (int)source; var sbuff = this._buff.Slice(start, this._pos - start).Span; var dbuff = MemoryMarshal.AsBytes(destination); if (sbuff.Length > dbuff.Length) sbuff = sbuff.Slice(0, dbuff.Length); itemsWritten = sbuff.Length / this._itemSize; sbuff.CopyTo(dbuff); return (this.Length - source) != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); return MemoryMarshal.Cast(this._buff.Slice(0, this._pos).Span).ToArray(); } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); #if HAS_SPAN_STREAM_OVERLOADS destination.Write(this._buff.Slice(0, this._pos).Span); #else var buff = this._buff.Slice(0, this._pos).ToArray(); destination.Write(buff, 0, buff.Length); #endif } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._pos = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buff = default; } /// /// Ensures the size. /// /// The new capacity. private void EnsureSize(int newCapacity) { var cap = this._buff.Length; if (cap >= newCapacity) return; var factor = newCapacity / cap; if (newCapacity % cap != 0) ++factor; var newActualCapacity = cap * factor; var newBuffOwner = this._pool.Rent(newActualCapacity); var newBuff = newBuffOwner.Memory; this._buff.Span.CopyTo(newBuff.Span); if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buffOwner = newBuffOwner; this._buff = newBuff; } } } diff --git a/DisCatSharp.Common/Types/MemoryBuffer.cs b/DisCatSharp.Common/Types/MemoryBuffer.cs index 935f7eefa..696c86215 100644 --- a/DisCatSharp.Common/Types/MemoryBuffer.cs +++ b/DisCatSharp.Common/Types/MemoryBuffer.cs @@ -1,356 +1,356 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types { /// /// Provides a resizable memory buffer, which can be read from and written to. It will automatically resize whenever required. /// /// Type of item to hold in the buffer. public sealed class MemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => this._segments.Aggregate(0UL, (a, x) => a + (ulong)x.Memory.Length); // .Sum() does only int /// public ulong Length { get; private set; } /// public ulong Count => this.Length / (ulong)this._itemSize; private readonly MemoryPool _pool; private readonly int _segmentSize; private int _lastSegmentLength; private int _segNo; private readonly bool _clear; private readonly List> _segments; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Byte size of an individual segment. Defaults to 64KiB. /// Number of segments to allocate. Defaults to 0. - /// Memory pool to use for renting buffers. Defaults to . + /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public MemoryBuffer(int segmentSize = 65536, int initialSegmentCount = 0, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); if (segmentSize % this._itemSize != 0) throw new ArgumentException("Segment size must match size of individual item."); this._pool = memPool ?? MemoryPool.Shared; this._segmentSize = segmentSize; this._segNo = 0; this._lastSegmentLength = 0; this._clear = clearOnDispose; this._segments = new List>(initialSegmentCount + 1); for (var i = 0; i < initialSegmentCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); this.Length = 0; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var src = MemoryMarshal.AsBytes(data); this.Grow(src.Length); while (this._segNo < this._segments.Count && src.Length > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > src.Length ? src.Length : avs; var dmem = mem.Slice(this._lastSegmentLength); src.Slice(0, avs).CopyTo(dmem.Span); src = src.Slice(avs); this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { var len = (int)(stream.Length - stream.Position); this.Grow(len); #if !HAS_SPAN_STREAM_OVERLOADS var buff = new byte[this._segmentSize]; #endif while (this._segNo < this._segments.Count && len > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > len ? len : avs; var dmem = mem.Slice(this._lastSegmentLength); #if HAS_SPAN_STREAM_OVERLOADS stream.Read(dmem.Span); #else var lsl = this._lastSegmentLength; var slen = dmem.Span.Length - lsl; stream.Read(buff, 0, slen); buff.AsSpan(0, slen).CopyTo(dmem.Span); #endif len -= dmem.Span.Length; this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { var read = 0; #if HAS_SPAN_STREAM_OVERLOADS Span buffs = stackalloc byte[this._segmentSize]; while ((read = stream.Read(buffs)) != 0) #else var buff = new byte[this._segmentSize]; var buffs = buff.AsSpan(); while ((read = stream.Read(buff, 0, buff.Length - this._lastSegmentLength)) != 0) #endif this.Write(MemoryMarshal.Cast(buffs.Slice(0, read))); } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); // Find where to begin var i = 0; for (; i < this._segments.Count; i++) { var seg = this._segments[i]; var mem = seg.Memory; if ((ulong)mem.Length > source) break; source -= (ulong)mem.Length; } // Do actual copy var dl = (int)(this.Length - source); var sri = (int)source; var dst = MemoryMarshal.AsBytes(destination); for (; i < this._segments.Count && dst.Length > 0; i++) { var seg = this._segments[i]; var mem = seg.Memory; var src = mem.Span; if (sri != 0) { src = src.Slice(sri); sri = 0; } if (itemsWritten + src.Length > dl) src = src.Slice(0, dl - itemsWritten); if (src.Length > dst.Length) src = src.Slice(0, dst.Length); src.CopyTo(dst); dst = dst.Slice(src.Length); itemsWritten += src.Length; } itemsWritten /= this._itemSize; return (this.Length - source) != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = new T[this.Count]; this.Read(bytes, 0, out _); return bytes; } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); #if HAS_SPAN_STREAM_OVERLOADS foreach (var seg in this._segments) destination.Write(seg.Memory.Span); #else var longest = this._segments.Max(x => x.Memory.Length); var buff = new byte[longest]; foreach (var seg in this._segments) { var mem = seg.Memory.Span; var spn = buff.AsSpan(0, mem.Length); mem.CopyTo(spn); destination.Write(buff, 0, spn.Length); } #endif } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._segNo = 0; this._lastSegmentLength = 0; this.Length = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; foreach (var segment in this._segments) { if (this._clear) segment.Memory.Span.Clear(); segment.Dispose(); } } /// /// Grows the. /// /// The min amount. private void Grow(int minAmount) { var capacity = this.Capacity; var length = this.Length; var totalAmt = (length + (ulong)minAmount); if (capacity >= totalAmt) return; // we're good var amt = (int)(totalAmt - capacity); var segCount = amt / this._segmentSize; if (amt % this._segmentSize != 0) segCount++; // Basically List.EnsureCapacity // Default grow behaviour is minimum current*2 var segCap = this._segments.Count + segCount; if (segCap > this._segments.Capacity) this._segments.Capacity = segCap < this._segments.Capacity * 2 ? this._segments.Capacity * 2 : segCap; for (var i = 0; i < segCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); } } } diff --git a/DisCatSharp.Common/Types/SecureRandom.cs b/DisCatSharp.Common/Types/SecureRandom.cs index 19bd691eb..e42a11075 100644 --- a/DisCatSharp.Common/Types/SecureRandom.cs +++ b/DisCatSharp.Common/Types/SecureRandom.cs @@ -1,348 +1,348 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography; namespace DisCatSharp.Common { /// - /// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with . + /// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with . /// public sealed class SecureRandom : Random, IDisposable { /// /// Gets the r n g. /// private RandomNumberGenerator RNG { get; } = RandomNumberGenerator.Create(); private volatile bool _isDisposed = false; /// /// Creates a new instance of . /// public SecureRandom() { } /// /// Finalizes this instance by disposing it. /// ~SecureRandom() { this.Dispose(); } /// /// Fills a supplied buffer with random bytes. /// /// Buffer to fill with random bytes. public void GetBytes(byte[] buffer) { this.RNG.GetBytes(buffer); } /// /// Fills a supplied buffer with random nonzero bytes. /// /// Buffer to fill with random nonzero bytes. public void GetNonZeroBytes(byte[] buffer) { this.RNG.GetNonZeroBytes(buffer); } /// /// Fills a supplied memory region with random bytes. /// /// Memmory region to fill with random bytes. public void GetBytes(Span buffer) { #if NETCOREAPP this.RNG.GetBytes(buffer); #else var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this.RNG.GetBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } #endif } /// /// Fills a supplied memory region with random nonzero bytes. /// /// Memmory region to fill with random nonzero bytes. public void GetNonZeroBytes(Span buffer) { #if NETCOREAPP this.RNG.GetNonZeroBytes(buffer); #else var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this.RNG.GetNonZeroBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } #endif } /// /// Generates a signed 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public sbyte GetInt8(sbyte min = 0, sbyte max = sbyte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (sbyte)(min < 0 ? -min : 0); min += offset; max += offset; return (sbyte)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public byte GetUInt8(byte min = 0, byte max = byte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (byte)(this.Generate() % (max - min) + min); } /// /// Generates a signed 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public short GetInt16(short min = 0, short max = short.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (short)(min < 0 ? -min : 0); min += offset; max += offset; return (short)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ushort GetUInt16(ushort min = 0, ushort max = ushort.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (ushort)(this.Generate() % (max - min) + min); } /// /// Generates a signed 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public int GetInt32(int min = 0, int max = int.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public uint GetUInt32(uint min = 0, uint max = uint.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a signed 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public long GetInt64(long min = 0, long max = long.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ulong GetUInt64(ulong min = 0, ulong max = ulong.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a 32-bit floating-point number between 0.0 and 1.0. /// /// Generated 32-bit floating-point number. public float GetSingle() { var (i1, i2) = ((float)this.GetInt32(), (float)this.GetInt32()); return i1 / i2 % 1.0F; } /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. /// /// Generated 64-bit floating-point number. public double GetDouble() { var (i1, i2) = ((double)this.GetInt64(), (double)this.GetInt64()); return i1 / i2 % 1.0; } /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Generated 32-bit integer. public override int Next() => this.GetInt32(); /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int maxValue) => this.GetInt32(0, maxValue); /// /// Generates a 32-bit integer between and . Upper end exclusive. /// /// Minimum value of the generate integer. /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int minValue, int maxValue) => this.GetInt32(minValue, maxValue); /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. public override double NextDouble() => this.GetDouble(); /// /// Fills specified buffer with random bytes. /// /// Buffer to fill with bytes. public override void NextBytes(byte[] buffer) => this.GetBytes(buffer); /// /// Fills specified memory region with random bytes. /// /// Memory region to fill with bytes. #if NETCOREAPP override #endif public void NextBytes(Span buffer) => this.GetBytes(buffer); /// /// Disposes this instance and its resources. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; this.RNG.Dispose(); } /// /// Generates a random 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. protected override double Sample() => this.GetDouble(); /// /// Generates the. /// /// A T. private T Generate() where T : struct { var size = Unsafe.SizeOf(); Span buff = stackalloc byte[size]; this.GetBytes(buff); return MemoryMarshal.Read(buff); } } } diff --git a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs index d3331898d..b9e79deec 100644 --- a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs +++ b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs @@ -1,116 +1,116 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Numerics; namespace DisCatSharp.Common.Serialization { /// - /// Decomposes numbers into tuples (arrays of 2). + /// Decomposes numbers into tuples (arrays of 2). /// public sealed class ComplexDecomposer : IDecomposer { /// /// Gets the t complex. /// private static Type TComplex { get; } = typeof(Complex); /// /// Gets the t double array. /// private static Type TDoubleArray { get; } = typeof(double[]); /// /// Gets the t double enumerable. /// private static Type TDoubleEnumerable { get; } = typeof(IEnumerable); /// /// Gets the t object array. /// private static Type TObjectArray { get; } = typeof(object[]); /// /// Gets the t object enumerable. /// private static Type TObjectEnumerable { get; } = typeof(IEnumerable); /// public bool CanDecompose(Type t) => t == TComplex; /// public bool CanRecompose(Type t) => t == TDoubleArray || t == TObjectArray || TDoubleEnumerable.IsAssignableFrom(t) || TObjectEnumerable.IsAssignableFrom(t); /// public bool TryDecompose(object obj, Type tobj, out object decomposed, out Type tdecomposed) { decomposed = null; tdecomposed = TDoubleArray; if (tobj != TComplex || obj is not Complex c) return false; decomposed = new[] { c.Real, c.Imaginary }; return true; } /// public bool TryRecompose(object obj, Type tobj, Type trecomposed, out object recomposed) { recomposed = null; if (trecomposed != TComplex) return false; // ie if (TDoubleEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ied) { if (ied.Count() < 2) return false; var (real, imag) = ied.FirstTwoOrDefault(); recomposed = new Complex(real, imag); return true; } // ie if (TObjectEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ieo) { if (ieo.Count() < 2) return false; var (real, imag) = ieo.FirstTwoOrDefault(); if (real is not double dreal || imag is not double dimag) return false; recomposed = new Complex(dreal, dimag); return true; } return false; } } } diff --git a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs index 94bf639fb..755d9fa04 100644 --- a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs @@ -1,204 +1,204 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Immutable; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities { /// /// ABC for , allowing for using instances thereof without knowing the underlying instance's type parameters. /// public abstract class AsyncEvent { /// /// Gets the name of this event. /// public string Name { get; } /// /// Prevents a default instance of the class from being created. /// /// The name. private protected AsyncEvent(string name) { this.Name = name; } } /// /// Implementation of asynchronous event. The handlers of such events are executed asynchronously, but sequentially. /// /// Type of the object that dispatches this event. /// Type of event argument object passed to this event's handlers. public sealed class AsyncEvent : AsyncEvent where TArgs : AsyncEventArgs { /// /// Gets the maximum alloted execution time for all handlers. Any event which causes the handler to time out /// will raise a non-fatal . /// public TimeSpan MaximumExecutionTime { get; } private readonly object _lock = new object(); private ImmutableArray> _handlers; private readonly AsyncEventExceptionHandler _exceptionHandler; /// /// Creates a new asynchronous event with specified name and exception handler. /// /// Name of this event. - /// Maximum handler execution time. A value of means infinite. + /// Maximum handler execution time. A value of means infinite. /// Delegate which handles exceptions caused by this event. public AsyncEvent(string name, TimeSpan maxExecutionTime, AsyncEventExceptionHandler exceptionHandler) : base(name) { this._handlers = ImmutableArray>.Empty; this._exceptionHandler = exceptionHandler; this.MaximumExecutionTime = maxExecutionTime; } /// /// Registers a new handler for this event. /// /// Handler to register for this event. public void Register(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Add(handler); } /// /// Unregisters an existing handler from this event. /// /// Handler to unregister from the event. public void Unregister(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Remove(handler); } /// /// Unregisters all existing handlers from this event. /// public void UnregisterAll() { this._handlers = ImmutableArray>.Empty; } /// /// Raises this event by invoking all of its registered handlers, in order of registration. /// All exceptions throw during invocation will be handled by the event's registered exception handler. /// /// Object which raised this event. /// Arguments for this event. /// Defines what to do with exceptions caught from handlers. /// public async Task InvokeAsync(TSender sender, TArgs e, AsyncEventExceptionMode exceptionMode = AsyncEventExceptionMode.Default) { var handlers = this._handlers; if (handlers.Length == 0) return; // Collect exceptions List exceptions = null; if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0) exceptions = new List(handlers.Length * 2 /* timeout + regular */); // If we have a timeout configured, start the timeout task var timeout = this.MaximumExecutionTime > TimeSpan.Zero ? Task.Delay(this.MaximumExecutionTime) : null; for (var i = 0; i < handlers.Length; i++) { var handler = handlers[i]; try { // Start the handler execution var handlerTask = handler(sender, e); if (handlerTask != null && timeout != null) { // If timeout is configured, wait for any task to finish // If the timeout task finishes first, the handler is causing a timeout var result = await Task.WhenAny(timeout, handlerTask).ConfigureAwait(false); if (result == timeout) { timeout = null; var timeoutEx = new AsyncEventTimeoutException(this, handler); // Notify about the timeout and complete execution if ((exceptionMode & AsyncEventExceptionMode.HandleNonFatal) == AsyncEventExceptionMode.HandleNonFatal) this.HandleException(timeoutEx, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowNonFatal) == AsyncEventExceptionMode.ThrowNonFatal) exceptions.Add(timeoutEx); await handlerTask.ConfigureAwait(false); } } else if (handlerTask != null) { // No timeout is configured, or timeout already expired, proceed as usual await handlerTask.ConfigureAwait(false); } if (e.Handled) break; } catch (Exception ex) { e.Handled = false; if ((exceptionMode & AsyncEventExceptionMode.HandleFatal) == AsyncEventExceptionMode.HandleFatal) this.HandleException(ex, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowFatal) == AsyncEventExceptionMode.ThrowFatal) exceptions.Add(ex); } } if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0 && exceptions.Count > 0) throw new AggregateException("Exceptions were thrown during execution of the event's handlers.", exceptions); } /// /// Handles the exception. /// /// The ex. /// The handler. /// The sender. /// The args. private void HandleException(Exception ex, AsyncEventHandler handler, TSender sender, TArgs args) { if (this._exceptionHandler != null) this._exceptionHandler(this, ex, handler, sender, args); } } } diff --git a/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs b/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs index 665c2c837..3ea58eb38 100644 --- a/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs @@ -1,81 +1,81 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities { /// - /// Represents a thread synchronization event that, when signaled, must be reset manually. Unlike , this event is asynchronous. + /// Represents a thread synchronization event that, when signaled, must be reset manually. Unlike , this event is asynchronous. /// public sealed class AsyncManualResetEvent { /// /// Gets whether this event has been signaled. /// public bool IsSet => this._resetTcs?.Task?.IsCompleted == true; private volatile TaskCompletionSource _resetTcs; /// /// Creates a new asynchronous synchronization event with initial state. /// /// Initial state of this event. public AsyncManualResetEvent(bool initialState) { this._resetTcs = new TaskCompletionSource(); if (initialState) this._resetTcs.TrySetResult(initialState); } // Spawn a threadpool thread instead of making a task // Maybe overkill, but I am less unsure of this than awaits and // potentially cross-scheduler interactions /// /// Asynchronously signal this event. /// /// public Task SetAsync() => Task.Run(() => this._resetTcs.TrySetResult(true)); /// /// Asynchronously wait for this event to be signaled. /// /// public Task WaitAsync() => this._resetTcs.Task; /// /// Reset this event's signal state to unsignaled. /// public void Reset() { while (true) { var tcs = this._resetTcs; if (!tcs.Task.IsCompleted || Interlocked.CompareExchange(ref this._resetTcs, new TaskCompletionSource(), tcs) == tcs) return; } } } } diff --git a/DisCatSharp.Common/Utilities/Extensions.cs b/DisCatSharp.Common/Utilities/Extensions.cs index 13b5a06f9..31b54a8fb 100644 --- a/DisCatSharp.Common/Utilities/Extensions.cs +++ b/DisCatSharp.Common/Utilities/Extensions.cs @@ -1,490 +1,490 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Runtime.CompilerServices; namespace DisCatSharp.Common { /// /// Assortment of various extension and utility methods, designed to make working with various types a little easier. /// public static class Extensions { /// - /// Deconstructs a key-value pair item () into 2 separate variables. - /// This allows for enumerating over dictionaries in foreach blocks by using a (k, v) tuple as the enumerator variable, instead of having to use a directly. + /// Deconstructs a key-value pair item () into 2 separate variables. + /// This allows for enumerating over dictionaries in foreach blocks by using a (k, v) tuple as the enumerator variable, instead of having to use a directly. /// /// Type of dictionary item key. /// Type of dictionary item value. /// Key-value pair to deconstruct. /// Deconstructed key. /// Deconstructed value. public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this sbyte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == sbyte.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this byte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this short num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == short.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ushort num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this int num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == int.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this uint num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this long num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == long.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ulong num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this sbyte num, sbyte min, sbyte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this byte num, byte min, byte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this short num, short min, short max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ushort num, ushort min, ushort max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this int num, int min, int max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this uint num, uint min, uint max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this long num, long min, long max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ulong num, ulong min, ulong max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this float num, float min, float max, bool inclusive = true) { if (min > max) return false; return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this double num, double min, double max, bool inclusive = true) { if (min > max) return false; return inclusive ? (num >= min && num <= max) : (num > min && num < max); } /// /// Returns whether supplied character is in any of the following ranges: a-z, A-Z, 0-9. /// /// Character to test. /// Whether the character is in basic alphanumeric character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicAlphanumeric(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); /// /// Returns whether supplied character is in the 0-9 range. /// /// Character to test. /// Whether the character is in basic numeric digit character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicDigit(this char c) => c >= '0' && c <= '9'; /// /// Returns whether supplied character is in the a-z or A-Z range. /// /// Character to test. /// Whether the character is in basic letter character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicLetter(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); /// /// Tests whether given string ends with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string ends with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWithCharacter(this string s, char c) => s.Length >= 1 && s[s.Length - 1] == c; /// /// Tests whether given string starts with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string starts with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool StartsWithCharacter(this string s, char c) => s.Length >= 1 && s[0] == c; // https://stackoverflow.com/questions/9545619/a-fast-hash-function-for-string-in-c-sharp // Calls are inlined to call the underlying method directly /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlySpan chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Span chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlyMemory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Memory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ArraySegment chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Firsts the two or default. /// /// The enumerable. /// A (T first, T second) . internal static (T first, T second) FirstTwoOrDefault(this IEnumerable enumerable) { using var enumerator = enumerable.GetEnumerator(); if (!enumerator.MoveNext()) return (default, default); var first = enumerator.Current; if (!enumerator.MoveNext()) return (first, default); return (first, enumerator.Current); } /// /// Knuths the. /// /// The chars. /// An ulong. private static ulong Knuth(ReadOnlySpan chars) { var hash = 3074457345618258791ul; for (var i = 0; i < chars.Length; i++) hash = (hash + chars[i]) * 3074457345618258799ul; return hash; } } } diff --git a/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj index 1cb09cd71..8d476d34e 100644 --- a/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj +++ b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj @@ -1,52 +1,53 @@ - net5.0 + 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/DisCatSharp.Configuration.csproj b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj index 0f63f27b8..38117056c 100644 --- a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj +++ b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj @@ -1,42 +1,43 @@ - net5.0 + 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.Docs/articles/basics/first_bot.md b/DisCatSharp.Docs/articles/basics/first_bot.md index 46429c2cb..d232238d1 100644 --- a/DisCatSharp.Docs/articles/basics/first_bot.md +++ b/DisCatSharp.Docs/articles/basics/first_bot.md @@ -1,226 +1,226 @@ --- uid: basics_first_bot title: Your First Bot --- # Your First Bot >[!NOTE] > This article assumes the following: > * You have [created a bot account](xref:basics_bot_account "Creating a Bot Account") and have a bot token. > * You have [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) installed on your computer. ## Create a Project Open up Visual Studio and click on `Create a new project` towards the bottom right. ![Visual Studio Start Screen](/images/basics_first_bot_01.png)
Select `Console App (.NET Core)` then click on the `Next` button. ![New Project Screen](/images/basics_first_bot_02.png)
Next, you'll give your project a name. For this example, we'll name it `MyFirstBot`.
If you'd like, you can also change the directory that your project will be created in. Enter your desired project name, then click on the `Create` button. ![Name Project Screen](/images/basics_first_bot_03.png)
Voilà! Your project has been created! ![Visual Studio IDE](/images/basics_first_bot_04.png) ## Install Package Now that you have a project created, you'll want to get DisCatSharp installed. Locate the *solution explorer* on the right side, then right click on `Dependencies` and select `Manage NuGet Packages` from the context menu. ![Dependencies Context Menu](/images/basics_first_bot_05.png)
You'll then be greeted by the NuGet package manager. Select the `Browse` tab towards the top left, then type `DisCatSharp` into the search text box with the Pre-release checkbox checked **ON**. ![NuGet Package Search](/images/basics_first_bot_06.png)
The first results should be the six DisCatSharp packages. ![Search Results](/images/basics_first_bot_07.png) Package|Description :---: |:---: `DisCatSharp`|Main package; Discord API client. `DisCatSharp.CommandsNext`|Add-on which provides a command framework. `DisCatSharp.Common`|Common tools & converters `DisCatSharp.Interactivity`|Add-on which allows for interactive commands. `DisCatSharp.Lavalink`|Client implementation for [Lavalink](xref:audio_lavalink_setup). Useful for music bots. `DisCatSharp.SlashCommands`|Add-on which makes dealing with slash commands easyer. `DisCatSharp.VoiceNext`|Add-on which enables connectivity to Discord voice channels. `DisCatSharp.VoiceNext.Natives`|Voice next natives.
We'll only need the `DisCatSharp` package for the basic bot we'll be writing in this article.
Select it from the list then click the `Install` button to the right (after verifing that you will be installing the **latest 4.0 version**). -![Install DSharpPlus](/images/basics_first_bot_08.png) +![Install DisCatSharp](/images/basics_first_bot_08.png) You're now ready to write some code! ## First Lines of Code DisCatSharp implements [Task-based Asynchronous Pattern](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern). Because of this, the majority of DisCatSharp methods must be executed in a method marked as `async` so they can be properly `await`ed. Due to the way the compiler generates the underlying [IL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) code, marking our `Main` method as `async` has the potential to cause problems. As a result, we must pass the program execution to an `async` method. Head back to your *Program.cs* tab and empty the `Main` method by deleting line 9. ![Code Editor](/images/basics_first_bot_09.png) Now, create a new `static` method named `MainAsync` beneath your `Main` method. Have it return type `Task` and mark it as `async`. After that, add `MainAsync().GetAwaiter().GetResult();` to your `Main` method. ```cs static void Main(string[] args) { MainAsync().GetAwaiter().GetResult(); } static async Task MainAsync() { } ``` If you typed this in by hand, Intellisense should have generated the required `using` directive for you.
However, if you copy-pasted the snippet above, VS will complain about being unable to find the `Task` type. Hover over `Task` with your mouse and click on `Show potential fixes` from the tooltip. ![Error Tooltip](/images/basics_first_bot_10.png) Then apply the recommended solution. ![Solution Menu](/images/basics_first_bot_11.png)
We'll now create a new `DiscordClient` instance in our brand new asynchronous method. Create a new variable in `MainAsync` and assign it a new `DiscordClient` instance, then pass an instance of `DiscordConfiguration` to its constructor. Create an object initializer for `DiscordConfiguration` and populate the `Token` property with your bot token then set the `TokenType` property to `TokenType.Bot`. Next add the `Intents` Property and Populated it with the @DisCatSharp.DiscordIntents.AllUnprivileged value. These Intents are required for certain Events to be fired. Please visit this [article](xref:beyond_basics_intents) for more information. ```cs var discord = new DiscordClient(new DiscordConfiguration() { Token = "My First Token", TokenType = TokenType.Bot, Intents = DiscordIntents.AllUnprivileged }); ``` >[!WARNING] > We hard-code the token in the above snippet to keep things simple and easy to understand. > > Hard-coding your token is *not* a smart idea, especially if you plan on distributing your source code. > Instead you should store your token in an external medium, such as a configuration file or environment variable, and read that into your program to be used with DisCatSharp. Follow that up with `await discord.ConnectAsync();` to connect and login to Discord, and `await Task.Delay(-1);` at the end of the method to prevent the console window from closing prematurely. ```cs var discord = new DiscordClient(); await discord.ConnectAsync(); await Task.Delay(-1); ``` As before, Intellisense will have auto generated the needed `using` directive for you if you typed this in by hand.
If you've copied the snippet, be sure to apply the recommended suggestion to insert the required directive. If you hit `F5` on your keyboard to compile and run your program, you'll be greeted by a happy little console with a single log message from DisCatSharp. Woo hoo! ![Program Console](/images/basics_first_bot_12.png) ## Spicing Up Your Bot Right now our bot doesn't do a whole lot. Let's bring it to life by having it respond to a message! Hook the `MessageCreated` event fired by `DiscordClient` with a [lambda](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions).
Mark it as `async` and give it two parameters: `s` and `e`. ```cs discord.MessageCreated += async (s, e) => { }; ``` Then, add an `if` statement into the body of your event lambda that will check if `e.Message.Content` starts with your desired trigger word and respond with a message using `e.Message.RespondAsync` if it does. For this example, we'll have the bot to respond with *pong!* for each message that starts with *ping*. ```cs discord.MessageCreated += async (s, e) => { if (e.Message.Content.ToLower().StartsWith("ping")) await e.Message.RespondAsync("pong!"); }; ``` ## The Finished Product Your entire program should now look like this: ```cs using System; using System.Threading.Tasks; using DisCatSharp; namespace MyFirstBot { class Program { static void Main(string[] args) { MainAsync().GetAwaiter().GetResult(); } static async Task MainAsync() { var discord = new DiscordClient(new DiscordConfiguration() { Token = "My First Token", TokenType = TokenType.Bot }); discord.MessageCreated += async (s, e) => { if (e.Message.Content.ToLower().StartsWith("ping")) await e.Message.RespondAsync("pong!"); }; await discord.ConnectAsync(); await Task.Delay(-1); } } } ``` Hit `F5` to run your bot, then send *ping* in any channel your bot account has access to.
Your bot should respond with *pong!* for each *ping* you send. Congrats, your bot now does something! ![Bot Response](/images/basics_first_bot_13.png) ## Further Reading Now that you have a basic bot up and running, you should take a look at the following: * [Events](xref:beyond_basics_events) * [CommandsNext](xref:commands_intro) diff --git a/DisCatSharp.Docs/articles/basics/templates.md b/DisCatSharp.Docs/articles/basics/templates.md new file mode 100644 index 000000000..9b769762a --- /dev/null +++ b/DisCatSharp.Docs/articles/basics/templates.md @@ -0,0 +1,60 @@ +--- +uid: basics_templates +title: Project Templates +--- + +# Prerequisites +Install the following packages: + - DisCatSharp.ProjectTemplates + +To Install the latest: +```powershell +dotnet new --install DisCatSharp.ProjectTemplates +``` + +To install a specific version (example uses 9.8.4.1): +```powershell +dotnet new --install DisCatSharp.ProjectTemplates::9.8.4.1 +``` + +# Steps + +![Install Setup](/images/pt_nuget_install.png) +----- + +If you're using Visual Studio, the templates will show up when creating a new project/solution +![Install Setup](/images/pt_project_new.png) + +To easily find the DCS templates, you can search for either `Bot` or `Discord`. These tags are generic so if anyone else creates their own +discord or bot template our DCS templates will still be discoverable. We shall be using the solution template for our example. +![Classification](/images/pt_project_new_classification.png) + +For example sake, the project name is DCSTest +![Project Name](/images/pt_project_new_name.png) + +Input your Discord Token which can be retrieved via Discord's Developer Portal. The checkboxes represent the various modules in the DCS library. Checking it, +will include it in your project. If it's an extension, it automatically gets configured/included. +![Parameters](/images/pt_project_new_options.png) + +You should see something similar to the following image. It's worth noting that you need to set the Web project as the `Startup` project. Due to the web being +list last, the `Bot` project is considered the startup. You would think that a class-library which doesn't have an exe could be considered a startup project.... +![Project Structure](/images/pt_scaffolded.png) + +At this point in time the template is ready to run! + +----- + +# Templates + +## Bot Template +This is a class library in which you place bot related code. It contains its own json file where you can +configure your bot accordingly! + +An extension class provides easy to call methods for adding the Bot's services/configuration into the dependency injection (DI) pipeline. + +## Web Template +This is a very minimal project. By itself it only has a default endpoint which displays "Hello World". + +## Solution Template +Combines the bot and web templates. Includes the appropriate references/calls to get your bot up and running with minimal +effort. diff --git a/DisCatSharp.Docs/articles/important_changes/9_8_4.md b/DisCatSharp.Docs/articles/important_changes/9_8_4.md new file mode 100644 index 000000000..1d0b6192f --- /dev/null +++ b/DisCatSharp.Docs/articles/important_changes/9_8_4.md @@ -0,0 +1,136 @@ +--- +uid: important_changes_9_8_4 +title: Version 9.8.4 +--- + +# Upgrade from **9.8.3** to **9.8.4** + + +## What is new in #discatsharp? + +- Components +- Scheduled Events +- New Application Fields +- A ExecuteRawRequest Method +- Threads +- Hosting Packages +- New Templates +- Better Guild Channel Operations +- New Permissions +- New Application Command Permission (Not v2 yet) +- A new Logo :3 :heart: + +## What changed? + +### Threads +We have fixed many bugs in threads over time, and they work completely now. +- [DisCatSharp.Net.Models.ThreadEditModel](https://docs.dcs.aitsys.dev/api/DisCatSharp.Net.Models.ThreadEditModel.html) has a new field called [PerUserRateLimit](https://docs.dcs.aitsys.dev/api/DisCatSharp.Net.Models.ThreadEditModel.html#DisCatSharp_Net_Models_ThreadEditModel_PerUserRateLimit). +- [DisCatSharp.Entities.DiscordThreadChannel](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordThreadChannel.html) now supports [DisCatSharp.Entities.DiscordThreadChannel#GetMemberAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordThreadChannel.html?q=DiscordThreadChannel#DisCatSharp_Entities_DiscordThreadChannel_GetMemberAsync_System_UInt64_) + +### Components +We support every Component available on the API. + + +### Discord Application Object +[DiscordApplication](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordApplication.html) now includes the property fields: +- [CustomInstallUrl](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordApplication.html#DisCatSharp_Entities_DiscordApplication_CustomInstallUrl) +- [InstallParams](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordApplication.html#DisCatSharp_Entities_DiscordApplication_InstallParams) +- [Tags](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordApplication.html#DisCatSharp_Entities_DiscordApplication_Tags) + +You can find the settings for these fields in the [Discord Developer Portal](https://discord.com/developers/applications). + + +### Scheduled Events +The new scheduled events are already fully supported by us. + +**Entities:** +- [DisCatSharp.Entities.DiscordScheduledEvent](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordScheduledEvent.html) +- [DisCatSharp.Entities.DiscordScheduledEventEntityMetadata](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordScheduledEventEntityMetadata.html) +- [DisCatSharp.Entities.DiscordScheduledEventUser](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordScheduledEventUser.html) + +**Emums:** +- [DisCatSharp.ScheduledEventEntityType](https://docs.dcs.aitsys.dev/api/DisCatSharp.ScheduledEventEntityType.html) +- [DisCatSharp.ScheduledEventStatus](https://docs.dcs.aitsys.dev/api/DisCatSharp.ScheduledEventStatus.html) + +**Events:** +- [DisCatSharp.EventArgs.GuildScheduledEventCreateEventArgs](https://docs.dcs.aitsys.dev/api/DisCatSharp.EventArgs.GuildScheduledEventCreateEventArgs.html) +- [DisCatSharp.EventArgs.GuildScheduledEventUpdateEventArgs](https://docs.dcs.aitsys.dev/api/DisCatSharp.EventArgs.GuildScheduledEventUpdateEventArgs.html) +- [DisCatSharp.EventArgs.GuildScheduledEventDeleteEventArgs](https://docs.dcs.aitsys.dev/api/DisCatSharp.EventArgs.GuildScheduledEventDeleteEventArgs.html) +- [DisCatSharp.EventArgs.GuildScheduledEventUserAddEventArgs](https://docs.dcs.aitsys.dev/api/DisCatSharp.EventArgs.GuildScheduledEventUserAddEventArgs.html) +- [DisCatSharp.EventArgs.GuildScheduledEventUserRemoveEventArgs](https://docs.dcs.aitsys.dev/api/DisCatSharp.EventArgs.GuildScheduledEventUserRemoveEventArgs.html) + +**Models:** +- [DisCatSharp.Net.Models.ScheduledEventEditModel](https://docs.dcs.aitsys.dev/events/api/DisCatSharp.Net.Models.ScheduledEventEditModel.html) + +**Extra Properties:** +- [DisCatSharp.Entities.DiscordGuild#ScheduledEvents](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_ScheduledEvents) + +**Extra Methods:** +- [DisCatSharp.Entities.DiscordGuild#CreateScheduledEventAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_CreateScheduledEventAsync_System_String_DateTimeOffset_System_Nullable_DateTimeOffset__DisCatSharp_Entities_DiscordChannel_DisCatSharp_Entities_DiscordScheduledEventEntityMetadata_System_String_DisCatSharp_ScheduledEventEntityType_System_String_) +- [DisCatSharp.Entities.DiscordGuild#CreateExternalScheduledEventAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_CreateExternalScheduledEventAsync_System_String_DateTimeOffset_DateTimeOffset_System_String_System_String_System_String_) +- [DisCatSharp.Entities.DiscordGuild#GetScheduledEventAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_GetScheduledEventAsync_DisCatSharp_Entities_DiscordScheduledEvent_System_Nullable_System_Boolean__) +- [DisCatSharp.Entities.DiscordGuild#GetScheduledEventsAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_GetScheduledEventsAsync_System_Nullable_System_Boolean__) +- [DisCatSharp.Entities.DiscordChannel#CreateScheduledEventAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_CreateScheduledEventAsync_System_String_DateTimeOffset_System_String_System_String_) + + +### Audit Log +- [DisCatSharp.Entities.DiscordAuditLogGuildEntry](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordAuditLogGuildEntry.html) has the new field [PremiumProgressBarChange](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordAuditLogGuildEntry.html#DisCatSharp_Entities_DiscordAuditLogGuildEntry_PremiumProgressBarChange) +- Added [DisCatSharp.Entities.DiscordAuditLogScheduledEventEntry](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordAuditLogScheduledEventEntry.html) + + +### Enums +We've updated some enums to reflect the [API Documentation](https://discord.com/developers/docs/intro) of Discord: +- [DisCatSharp.PremiumTier](https://docs.dcs.aitsys.dev/api/DisCatSharp.PremiumTier.html) +- [DisCatSharp.StagePrivacyLevel](https://docs.dcs.aitsys.dev/api/DisCatSharp.StagePrivacyLevel.html) +- [DisCatSharp.UserFlags](https://docs.dcs.aitsys.dev/api/DisCatSharp.UserFlags.html) + +### DisCatSharp.CommandsNext +- `RequireDiscordEmployeeAttribute` is now `RequireStaffAttribute` +- `RequireDiscordCertifiedModeratorAttribute` is now `RequireCertifiedModeratorAttribute` + + +### ExecuteRawRequest +We added the [ExecuteRawRequestAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.DiscordClient.html#DisCatSharp_DiscordClient_ExecuteRawRequestAsync_RestRequestMethod_System_String_System_Object_System_String_Dictionary_System_String_System_String__System_Nullable_System_Double__) method. + + +### Better Guild Channel Operations +We did some engineering in the Discord API to create a method which gets an [ordered channel dictionary](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_OrderedChannels). + +Furthermore, we added a few new functions to [DisCatSharp.Entities.DiscordGuild](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.): +- [DisCatSharp.Entities.DiscordGuild#GetOrderedChannels](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_GetOrderedChannels) +- [DisCatSharp.Entities.DiscordGuild#GetOrderedChannelsAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordGuild.html#DisCatSharp_Entities_DiscordGuild_GetOrderedChannelsAsync) + +And a couple more methods to [DisCatSharp.Entities.DiscordChannel](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel): +- [DisCatSharp.Entities.DiscordChannel#GetMinPosition](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_GetMinPosition) +- [DisCatSharp.Entities.DiscordChannel#GetMaxPosition](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_GetMaxPosition) +- [DisCatSharp.Entities.DiscordChannel#ModifyPositionAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_ModifyPositionAsync_System_Int32_System_String_) +- [DisCatSharp.Entities.DiscordChannel#RefreshPositionsAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_RefreshPositionsAsync) +- [DisCatSharp.Entities.DiscordChannel#ModifyPositionInCategorySmartAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_ModifyPositionInCategoryAsync_System_Int32_System_String_) +- [DisCatSharp.Entities.DiscordChannel#ModifyParentAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_ModifyParentAsync_DisCatSharp_Entities_DiscordChannel_System_Nullable_System_Boolean__System_String_) +- [DisCatSharp.Entities.DiscordChannel#RemoveParentAsync](https://docs.dcs.aitsys.dev/api/DisCatSharp.Entities.DiscordChannel.html#DisCatSharp_Entities_DiscordChannel_RemoveParentAsync_System_String_) + + +### New Permissions +- SendMessagesInThreads +- StartEmbeddedActivities +- ManageEvents + + +### New Application Command Permission (Not v2 yet) +- [DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext#AddUser](https://docs.dcs.aitsys.dev/api/DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext.html#DisCatSharp_ApplicationCommands_ApplicationCommandsPermissionContext_AddUser_System_UInt64_System_Boolean_) +- [DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext#AddRole](https://docs.dcs.aitsys.dev/api/DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext.html#DisCatSharp_ApplicationCommands_ApplicationCommandsPermissionContext_AddRole_System_UInt64_System_Boolean_) +- [DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext#AddChannel](https://docs.dcs.aitsys.dev/api/DisCatSharp.ApplicationCommands.ApplicationCommandsPermissionContext.html#DisCatSharp_ApplicationCommands_ApplicationCommandsPermissionContext_AddChannel_System_UInt64_System_Boolean_) + + +### New Templates +We added a few templates for you: +- [DisCatSharp.ProjectTemplates](https://github.com/Aiko-IT-Systems/DisCatSharp.ProjectTemplates) +- [DisCatSharp.TemplateBot](https://github.com/Aiko-IT-Systems/DisCatSharp.TemplateBot) + + +### Hosting Packages +You can run your bot as a hosted service now! + +If you're interested, check out this [article](https://docs.dcs.aitsys.dev/articles/basics/web_app.html). + +You can also check out our new [templates](https://docs.dcs.aitsys.dev/articles/basics/templates.html)! diff --git a/DisCatSharp.Docs/articles/misc/reporting_issues.md b/DisCatSharp.Docs/articles/misc/reporting_issues.md index a95dd7152..9dfbf43c9 100644 --- a/DisCatSharp.Docs/articles/misc/reporting_issues.md +++ b/DisCatSharp.Docs/articles/misc/reporting_issues.md @@ -1,32 +1,32 @@ --- uid: misc_reporting_issues title: Reporting Issues --- # I broke something, and I need it fixed! -We always try to fix bugs, and make sure that when we release the next version of DSharpPlus-NextGen, everything is polished and +We always try to fix bugs, and make sure that when we release the next version of DisCatSharp, everything is polished and working. However, DisCatSharp is a large codebase, and we can't always catch all the bugs, or notice all the regressions that happen while we fix bugs or implement new issues. ## Issue tracker If you find a bug, come up with a new idea, or just want to report something, you can open an ticket on our support [server](https://discord.gg/discatsharp). [Issue Tracker](https://aitsys.dev/project/view/1/ "DisCatSharp issues"). When reporting an issue, make sure to include as much detail as possible. If at all possible, please include: * Steps to reproduce the issue * What were you trying to achieve * Expected/actual result * Stack traces, exception types, messages * Attempted solutions ## Contributing Lastly, while we understand that not everyone is an expert programmer, we would appreciate it if you could fix any issues you find and submit a Pull Request on GitHub. This would reduce the amount of work we would have to do. When contributing, ensure your code matches the style of the rest of the library, and that you test the changes you make, and catch any possible regressions. diff --git a/DisCatSharp.Docs/articles/preamble.md b/DisCatSharp.Docs/articles/preamble.md index 79fff3f02..a00f5532b 100644 --- a/DisCatSharp.Docs/articles/preamble.md +++ b/DisCatSharp.Docs/articles/preamble.md @@ -1,39 +1,39 @@ --- uid: preamble title: Article Preamble --- ## Knowledge Prerequisites Before attempting to write a Discord bot, you should be familiar with the concepts of [Object Oriented Programing](https://en.wikipedia.org/wiki/Object-oriented_programming), [the C# programming language](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/), and [Task-based Asynchronous Pattern](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap). If you're brand new to C#, or programming in general, this library may prove difficult for you to use.
Fortunately, there are resources that can help you get started with the language! An excellent tutorial series to go through would be [C# Fundamentals for Absolute Beginners](https://channel9.msdn.com/Series/CSharp-Fundamentals-for-Absolute-Beginners) by Bob Tabor. His videos go through all the basics, from setting up your development environment up to some of the more advanced concepts. If you're not sure what to do first, Bob's tutorial series should be your starting point! ## Supported .NET Implementations DisCatSharp targets .NET Standard 2.0 means that there are many implementations that *may* function with DisCatSharp. However, the following versions we will *explicitly* provide support for. Implementation|Support|Notes :---: |:---:|:--- -[.NET Core](https://en.wikipedia.org/wiki/.NET_Core)|✔️|LTS versions 2.1 & 3.1 and the current 5.0 are supported. +[.NET Core](https://en.wikipedia.org/wiki/.NET_Core)|✔️|LTS versions 2.1 & 3.1 & 5.0 and the current 6.0 are supported. [.NET Framework](https://en.wikipedia.org/wiki/.NET_Framework)|⚠️|Versions 4.6.1 through 4.8 *should* work fine.
But we recommend that you use the latest LTS version of .NET Core. [Mono](https://en.wikipedia.org/wiki/Mono_(software))|❌️|If you need a cross platform runtime, use .NET Core. [Unity](https://en.wikipedia.org/wiki/Unity_(game_engine))|❌️|Consider using the official [Discord GameSDK](https://discord.com/developers/docs/game-sdk/sdk-starter-guide) instead. If you use an unsupported implementation and encounter issues, you may ask for support but mostly you're on your own. ## Getting Started If you're writing a Discord bot for the first time, you'll want to start with *[creating a bot account](xref:basics_bot_account)*.
Otherwise, if you have a bot account already, start off with the *[writing your first bot](xref:basics_first_bot)* article.
Once you're up and running, feel free to browse through the [API Documentation](/api/index.html)! ## Support and Questions You can get in contact with us on Discord through the following guild: **DisCatSharp Guild**:
[![DisCatSharp](https://discordapp.com/api/guilds/858089281214087179/embed.png?style=banner2)](https://discord.gg/discatsharp) diff --git a/DisCatSharp.Docs/articles/toc.yml b/DisCatSharp.Docs/articles/toc.yml index 7a013829b..514263e09 100644 --- a/DisCatSharp.Docs/articles/toc.yml +++ b/DisCatSharp.Docs/articles/toc.yml @@ -1,95 +1,99 @@ - name: Preamble href: preamble.md - name: Important Changes items: - name: Version 9.9.0 href: important_changes/9_9_0.md + - name: Version 9.8.4 + href: important_changes/9_8_4.md - name: Version 9.8.3 href: important_changes/9_8_3.md - name: Version 9.8.2 href: important_changes/9_8_2.md - name: The Basics items: - name: Creating a Bot Account href: basics/bot_account.md - name: Writing Your First Bot href: basics/first_bot.md - name: Bot as Hosted Service href: basics/web_app.md + - name: Project Templates + href: basics/templates.md - name: Beyond Basics items: - name: Events href: beyond_basics/events.md - name: Logging href: beyond_basics/logging/default.md items: - name: The Default Logger href: beyond_basics/logging/default.md - name: Third Party Loggers href: beyond_basics/logging/third_party.md - name: Intents href: beyond_basics/intents.md - name: Sharding href: beyond_basics/sharding.md - name: Message Builder href: beyond_basics/messagebuilder.md - name: Components items: - name: Buttons href: beyond_basics/components/buttons.md - name: Select Menu href: beyond_basics/components/select_menus.md - name: Workarounds href: beyond_basics/workarounds.md - name: Application Commands items: - name: Introduction href: application_commands/intro.md - name: Options href: application_commands/options.md - name: Events href: application_commands/events.md - name: Commands items: - name: Introduction href: commands/intro.md - name: Command Attributes href: commands/command_attributes.md - name: Dependency Injection href: commands/dependency_injection.md - name: Customization items: - name: Help Formatter href: commands/help_formatter.md - name: Argument Converters href: commands/argument_converters.md - name: Command Handler href: commands/command_handler.md - name: Audio items: - name: Lavalink items: - name: Setup href: audio/lavalink/setup.md - name: Configuration href: audio/lavalink/configuration.md - name: Music Commands href: audio/lavalink/music_commands.md - name: VoiceNext items: - name: Prerequisites href: audio/voicenext/prerequisites.md - name: Transmitting href: audio/voicenext/transmit.md - name: Receiving href: audio/voicenext/receive.md - name: Interactivity href: interactivity.md - name: Hosting href: hosting.md - name: Miscellaneous items: - name: Nightly Builds href: misc/nightly_builds.md - name: Reporting Issues href: misc/reporting_issues.md diff --git a/DisCatSharp.Docs/dcs/android-chrome-192x192.png b/DisCatSharp.Docs/dcs/android-chrome-192x192.png new file mode 100644 index 000000000..e1b6613b5 Binary files /dev/null and b/DisCatSharp.Docs/dcs/android-chrome-192x192.png differ diff --git a/DisCatSharp.Docs/dcs/android-chrome-512x512.png b/DisCatSharp.Docs/dcs/android-chrome-512x512.png new file mode 100644 index 000000000..82477be43 Binary files /dev/null and b/DisCatSharp.Docs/dcs/android-chrome-512x512.png differ diff --git a/DisCatSharp.Docs/dcs/apple-touch-icon.png b/DisCatSharp.Docs/dcs/apple-touch-icon.png new file mode 100644 index 000000000..ce26fc205 Binary files /dev/null and b/DisCatSharp.Docs/dcs/apple-touch-icon.png differ diff --git a/DisCatSharp.Docs/dcs/favicon-16x16.png b/DisCatSharp.Docs/dcs/favicon-16x16.png new file mode 100644 index 000000000..89a142e05 Binary files /dev/null and b/DisCatSharp.Docs/dcs/favicon-16x16.png differ diff --git a/DisCatSharp.Docs/dcs/favicon-32x32.png b/DisCatSharp.Docs/dcs/favicon-32x32.png new file mode 100644 index 000000000..1e1587b12 Binary files /dev/null and b/DisCatSharp.Docs/dcs/favicon-32x32.png differ diff --git a/DisCatSharp.Docs/dcs/favicon.ico b/DisCatSharp.Docs/dcs/favicon.ico index bade74c35..53a35037a 100644 Binary files a/DisCatSharp.Docs/dcs/favicon.ico and b/DisCatSharp.Docs/dcs/favicon.ico differ diff --git a/DisCatSharp.Docs/dcs/logo.png b/DisCatSharp.Docs/dcs/logo.png index 863ae18d3..fd7bceeb0 100644 Binary files a/DisCatSharp.Docs/dcs/logo.png and b/DisCatSharp.Docs/dcs/logo.png differ diff --git a/DisCatSharp.Docs/dcs/logobig.png b/DisCatSharp.Docs/dcs/logobig.png index d3456e443..3c47e76d7 100644 Binary files a/DisCatSharp.Docs/dcs/logobig.png and b/DisCatSharp.Docs/dcs/logobig.png differ diff --git a/DisCatSharp.Docs/dcs/partials/_footer.liquid b/DisCatSharp.Docs/dcs/partials/_footer.liquid index 9c6629d58..1218db5dc 100644 --- a/DisCatSharp.Docs/dcs/partials/_footer.liquid +++ b/DisCatSharp.Docs/dcs/partials/_footer.liquid @@ -1,16 +1,16 @@ {% comment -%}Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.{% endcomment -%}
diff --git a/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json new file mode 100644 index 000000000..beef69613 --- /dev/null +++ b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.deps.json @@ -0,0 +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", + "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": { + "dependencies": { + "Microsoft.DocAsCode.Plugins": "2.58.9", + "Microsoft.DocAsCode.YamlSerialization": "2.58.9" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.DocAsCode.Common.dll": { + "assemblyVersion": "2.58.9.0", + "fileVersion": "2.58.9.12548" + } + } + }, + "Microsoft.DocAsCode.Plugins/2.58.9": { + "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" + } + } + }, + "Microsoft.DocAsCode.YamlSerialization/2.58.9": { + "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" + } + } + }, + "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": { + "type": "package", + "serviceable": true, + "sha512": "sha512-1C/+wx0cQtsmi7d5QHIT77HV5MTKAHKUo3ywSbIWz0L3LkDktuEAScmZ0xrxwUFPcThKdsRw702Bai6QriAPUg==", + "path": "microsoft.docascode.common/2.58.9", + "hashPath": "microsoft.docascode.common.2.58.9.nupkg.sha512" + }, + "Microsoft.DocAsCode.Plugins/2.58.9": { + "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" + }, + "Microsoft.DocAsCode.YamlSerialization/2.58.9": { + "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" + }, + "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 new file mode 100644 index 000000000..45e978f4d Binary files /dev/null and b/DisCatSharp.Docs/dcs/plugins/DisCatSharp.DocFx.CustomMemberIndexer.dll differ diff --git a/DisCatSharp.Docs/dcs/plugins/HSNXT.DocFx.CustomMemberIndexer.dll b/DisCatSharp.Docs/dcs/plugins/HSNXT.DocFx.CustomMemberIndexer.dll deleted file mode 100644 index 5c73e6f01..000000000 Binary files a/DisCatSharp.Docs/dcs/plugins/HSNXT.DocFx.CustomMemberIndexer.dll and /dev/null differ diff --git a/DisCatSharp.Docs/dcs/site.webmanifest b/DisCatSharp.Docs/dcs/site.webmanifest new file mode 100644 index 000000000..45dc8a206 --- /dev/null +++ b/DisCatSharp.Docs/dcs/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/DisCatSharp.Docs/docfx.json b/DisCatSharp.Docs/docfx.json index 301ec4fee..d558afbe7 100644 --- a/DisCatSharp.Docs/docfx.json +++ b/DisCatSharp.Docs/docfx.json @@ -1,90 +1,90 @@ { "metadata": [ { "src": [ { "src": "..", "files": [ "**.cs" ], "exclude": [ "**/obj/**", "**/bin/**", "_site/**" ] } ], "dest": "api", "filter": "filter_config.yml" } ], "build": { "content": [ { "files": [ "api/**.yml", "api/index.md" ] }, { "files": [ "articles/**.md", "articles/**/toc.yml", "natives/**.md", "faq/**.md", "toc.yml", "*.md" ], "exclude": [ "**/bin/**", "**/obj/**", "_site/**", "_site_pdf/**" ] } ], "resource": [ { "files": [ "images/**", "natives/**.zip" ], "exclude": [ "**/bin/**", "**/obj/**", "_site/**", "_site_pdf/**" ] } ], "overwrite": [ { "files": [ ], "exclude": [ "**/bin/**", "**/obj/**", "_site/**", "_site_pdf/**" ] } ], "dest": "_site", "globalMetadata": { - "_appFooter": "© 2021 Aiko IT Systems", + "_appFooter": "© 2021-2022 Aiko IT Systems", "_enableSearch": true, "_enableNewTab": true, "_appTitle": "DisCatSharp Docs" }, "globalMetadataFiles": [], "fileMetadataFiles": [], "template": [ "dcs" ], "postProcessors": ["ExtractSearchIndex", "CustomMemberIndexer"], "noLangKeyword": false, "keepFileLink": false, "cleanupCacheHistory": false, "disableGitFeatures": false } } diff --git a/DisCatSharp.Docs/filter_config.yml b/DisCatSharp.Docs/filter_config.yml index 0dee89b81..2956c615e 100644 --- a/DisCatSharp.Docs/filter_config.yml +++ b/DisCatSharp.Docs/filter_config.yml @@ -1,83 +1,125 @@ apiRules: - exclude: uidRegex: ^System\.Collections\.Immutable$ - exclude: uidRegex: ^System\.Runtime\.CompilerServices\.Unsafe$ - exclude: uidRegex: ^System\.Runtime\.CompilerServices$ - exclude: uidRegex: ^System\..*$ - exclude: uidRegex: ^DisCatSharp\.Hosting\.Tests$ - exclude: uidRegex: ^DisCatSharp\.Configuration\.Tests$ attributeRules: - exclude: uidRegex: ^System\.Collections\.Immutable$ type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Logging$ + type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Logging\.LogLevel$ + type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Hosting\.BackgroundService$ + type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Hosting$ + type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions$ + type: Namespace +- exclude: + uidRegex: ^Newtonsoft\.Json$ + type: Namespace +- exclude: + uidRegex: ^Newtonsoft\.Json\.JsonConverter$ + type: Namespace - exclude: uidRegex: ^System\.Runtime\.CompilerServices\.Unsafe$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.CompilerServices$ type: Namespace - exclude: uidRegex: ^System\.ComponentModel\.Design$ type: Namespace - exclude: uidRegex: ^System\.ComponentModel\.Design\.Serialization$ type: Namespace - exclude: uidRegex: ^System\.Xml\.Serialization$ type: Namespace - exclude: uidRegex: ^System\.Web\.Compilation$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.Versioning$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.ConstrainedExecution$ type: Namespace - exclude: uidRegex: ^System\.EnterpriseServices$ type: Namespace +- exclude: + uidRegex: ^System\.Numerics$ + type: Namespace +- exclude: + uidRegex: ^System\.Numerics\.Complex$ + type: Namespace - exclude: uidRegex: ^System\.Diagnostics\.CodeAnalysis$ type: Namespace - include: uidRegex: ^System\.Diagnostics\.(ConditionalAttribute|EventLogPermissionAttribute|PerformanceCounterPermissionAttribute)$ type: Type - exclude: uidRegex: '^System\.Diagnostics\.[^.]+$' type: Type - include: uidRegex: ^System\.ComponentModel\.(BindableAttribute|BrowsableAttribute|ComplexBindingPropertiesAttribute|DataObjectAttribute|DefaultBindingPropertyAttribute|ListBindableAttribute|LookupBindingPropertiesAttribute|SettingsBindableAttribute|TypeConverterAttribute)$ type: Type - exclude: uidRegex: '^System\.ComponentModel\.[^.]+$' type: Type - exclude: uidRegex: ^System\.Reflection\.DefaultMemberAttribute$ type: Type - exclude: uidRegex: ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ type: Type +- exclude: + uidRegex: ^Microsoft\.Extensions\.Hosting\.BackgroundService$ + type: Type +- exclude: + uidRegex: ^Microsoft\.Extensions\.Logging\.LogLevel\.Information$ + type: Type +- exclude: + uidRegex: ^Microsoft\.Extensions\.Logging\.ILoggerFactory$ + type: Type +- exclude: + uidRegex: ^Newtonsoft\.Json\.JsonConverter$ + type: Type +- exclude: + uidRegex: ^System\.Numerics\.Complex$ + type: Type - exclude: uidRegex: '^System\.Runtime\.CompilerServices\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Runtime\.InteropServices\.[^.]+$' type: Type - include: uidRegex: ^System\.Security\.(SecurityCriticalAttribute|SecurityTreatAsSafeAttribute|AllowPartiallyTrustedCallersAttribute)$ type: Type - exclude: uidRegex: '^System\.Security\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Web\.UI\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Windows\.Markup\.[^.]+$' type: Type diff --git a/DisCatSharp.Docs/images/pt_nuget_install.png b/DisCatSharp.Docs/images/pt_nuget_install.png new file mode 100644 index 000000000..62bf16475 Binary files /dev/null and b/DisCatSharp.Docs/images/pt_nuget_install.png differ diff --git a/DisCatSharp.Docs/images/pt_project_new.png b/DisCatSharp.Docs/images/pt_project_new.png new file mode 100644 index 000000000..b146057a6 Binary files /dev/null and b/DisCatSharp.Docs/images/pt_project_new.png differ diff --git a/DisCatSharp.Docs/images/pt_project_new_classification.png b/DisCatSharp.Docs/images/pt_project_new_classification.png new file mode 100644 index 000000000..a6a9f8bf0 Binary files /dev/null and b/DisCatSharp.Docs/images/pt_project_new_classification.png differ diff --git a/DisCatSharp.Docs/images/pt_project_new_name.png b/DisCatSharp.Docs/images/pt_project_new_name.png new file mode 100644 index 000000000..88b85ab32 Binary files /dev/null and b/DisCatSharp.Docs/images/pt_project_new_name.png differ diff --git a/DisCatSharp.Docs/images/pt_project_new_options.png b/DisCatSharp.Docs/images/pt_project_new_options.png new file mode 100644 index 000000000..eb299e6b5 Binary files /dev/null and b/DisCatSharp.Docs/images/pt_project_new_options.png differ diff --git a/DisCatSharp.Docs/images/pt_scaffolded.png b/DisCatSharp.Docs/images/pt_scaffolded.png new file mode 100644 index 000000000..f699dabdb Binary files /dev/null and b/DisCatSharp.Docs/images/pt_scaffolded.png differ diff --git a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj index 17f4eb101..a22e7c6fc 100644 --- a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj +++ b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj @@ -1,37 +1,38 @@ - net5.0 + 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.Tests/DisCatSharp.Hosting.Tests.csproj b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj index 991c14c24..dffcfbdc4 100644 --- a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj +++ b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj @@ -1,48 +1,49 @@ - net5.0 + 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/GlobalSuppressions.cs b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs index 472c007d3..e12732b44 100644 --- a/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs +++ b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs @@ -1,29 +1,25 @@ // 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("DocumentationHeader", "ClassDocumentationHeader:The class must have a documentation header.", Justification = "")] [assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "")] [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._discordConfig")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._discordConfig")] [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._interactivityConfig")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._interactivityConfig")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._lavalinkConfig")] [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Hosting.Tests.HostExtensionTests._lavalinkConfig")] -[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.Bot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.DiscordHostedService},System.IServiceProvider)")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostExtensionTests.DefaultDiscord~System.Collections.Generic.Dictionary{System.String,System.String}")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostExtensionTests.DiscordInteractivityAndLavaLinkConfiguration~Microsoft.Extensions.Configuration.IConfiguration")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create(System.Collections.Generic.Dictionary{System.String,System.String})~Microsoft.Extensions.Hosting.IHostBuilder")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.DefaultDiscord~System.Collections.Generic.Dictionary{System.String,System.String}")] -[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.Bot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.Bot},System.IServiceProvider)")] -[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.BotTwoService.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.BotTwoService},System.IServiceProvider)")] -[assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.MyCustomBot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.MyCustomBot},System.IServiceProvider)")] [assembly: SuppressMessage("DocumentationHeader", "InterfaceDocumentationHeader:The interface must have a documentation header.", Justification = "", Scope = "type", Target = "~T:DisCatSharp.Hosting.Tests.IBotTwoService")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create(System.String)~Microsoft.Extensions.Hosting.IHostBuilder")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.HostTests.Create``2(System.String)~Microsoft.Extensions.Hosting.IHostBuilder")] [assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.Bot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.Bot},System.IServiceProvider,Microsoft.Extensions.Hosting.IHostApplicationLifetime)")] [assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.BotTwoService.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.BotTwoService},System.IServiceProvider,Microsoft.Extensions.Hosting.IHostApplicationLifetime)")] [assembly: SuppressMessage("DocumentationHeader", "ConstructorDocumentationHeader:The constructor must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.Tests.MyCustomBot.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{DisCatSharp.Hosting.Tests.MyCustomBot},System.IServiceProvider,Microsoft.Extensions.Hosting.IHostApplicationLifetime)")] diff --git a/DisCatSharp.Hosting/BaseHostedService.cs b/DisCatSharp.Hosting/BaseHostedService.cs index 3ec212d7b..bf2109417 100644 --- a/DisCatSharp.Hosting/BaseHostedService.cs +++ b/DisCatSharp.Hosting/BaseHostedService.cs @@ -1,196 +1,204 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// /// Contains the common logic between having a or /// as a Hosted Service /// public abstract class BaseHostedService : BackgroundService { protected readonly ILogger Logger; protected readonly IHostApplicationLifetime ApplicationLifetime; protected readonly IConfiguration Configuration; protected readonly IServiceProvider ServiceProvider; protected readonly string BotSection; + /// + /// Initializes a new instance of the class. + /// + /// The config. + /// The logger. + /// The service provider. + /// The application lifetime. + /// The config bot section. internal BaseHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) { this.Configuration = config; this.Logger = logger; this.ApplicationLifetime = applicationLifetime; this.ServiceProvider = serviceProvider; this.BotSection = configBotSection; } /// /// When the bot(s) fail to start, this method will be invoked. (Default behavior is to shutdown) /// /// The exception/reason for not starting protected virtual void OnInitializationError(Exception ex) => this.ApplicationLifetime.StopApplication(); /// /// Connect your client(s) to Discord /// /// Task protected abstract Task ConnectAsync(); /// /// Dynamically load extensions by using and /// /// /// Client to add extension method(s) to /// Task protected Task InitializeExtensions(DiscordClient client) { var typeMap = this.Configuration.FindImplementedExtensions(this.BotSection); this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); foreach (var typePair in typeMap) try { /* If section is null --> utilize the default constructor This means the extension was explicitly added in the 'Using' array, but user did not wish to override any value(s) in the extension's config */ var configInstance = typePair.Value.Section.HasValue ? typePair.Value.Section.Value.ExtractConfig(() => ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType)) : ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType); /* Explanation for bindings Internal Constructors --> NonPublic Public Constructors --> Public Constructors --> Instance */ var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; var ctors = typePair.Value.ImplementationType.GetConstructors(flags); var instance = ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType) ? Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, new[] { configInstance }, null) : Activator.CreateInstance(typePair.Value.ImplementationType, true); /* Certain extensions do not require a configuration argument Those who do -- pass config instance in, Those who don't -- simply instantiate ActivatorUtilities requires a public constructor, anything with internal breaks */ if (instance == null) { this.Logger.LogError($"Unable to instantiate '{typePair.Value.ImplementationType.Name}'"); continue; } // Add an easy reference to our extensions for later use client.AddExtension((BaseExtension)instance); } catch (Exception ex) { this.Logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}"); this.OnInitializationError(ex); } return Task.CompletedTask; } /// /// Configure / Initialize the or /// /// /// protected abstract Task ConfigureAsync(); /// /// Configure the extensions for your or /// /// /// protected abstract Task ConfigureExtensionsAsync(); /// /// Runs just prior to . /// /// protected virtual Task PreConnectAsync() => Task.CompletedTask; /// /// Runs immediately after . /// /// Task protected virtual Task PostConnectAsync() => Task.CompletedTask; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { await this.ConfigureAsync(); await this.PreConnectAsync(); await this.ConnectAsync(); await this.ConfigureExtensionsAsync(); await this.PostConnectAsync(); } catch (Exception ex) { /* * Anything before DOTNET 6 will * fail silently despite throwing an exception in this method * So to overcome this obstacle we need to log what happens and * manually exit */ - this.Logger.LogError(($"Was unable to start {this.GetType().Name} Bot as a Hosted Service")); + this.Logger.LogError($"Was unable to start {this.GetType().Name} Bot as a Hosted Service"); // Power given to developer for handling exception this.OnInitializationError(ex); } // Wait indefinitely -- but use stopping token so we can properly cancel if needed await Task.Delay(-1, stoppingToken); } } } diff --git a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj index fb56ac52c..a1ca8c990 100644 --- a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj +++ b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj @@ -1,39 +1,40 @@ - net5.0 + 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/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index 16b5ff5f2..8394c9401 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,79 +1,82 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// - /// Simple implementation for to work as a + /// Simple implementation for to work as a /// public abstract class DiscordHostedService : BaseHostedService, IDiscordHostedService { /// public DiscordClient Client { get; protected set; } #pragma warning disable 8618 + /// + /// Initializes a new instance of the class. + /// /// IConfiguration provided via Dependency Injection. Aggregate method to access configuration files /// An ILogger to work with, provided via Dependency Injection /// ServiceProvider reference which contains all items currently registered for Dependency Injection /// Contains the appropriate methods for disposing / stopping BackgroundServices during runtime /// The name of the JSON/Config Key which contains the configuration for this Discord Service protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) : base(config, logger, serviceProvider, applicationLifetime, configBotSection) { } #pragma warning restore 8618 protected override Task ConfigureAsync() { try { this.Client = this.Configuration.BuildClient(this.ServiceProvider, this.BotSection); } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } return Task.CompletedTask; } protected sealed override async Task ConnectAsync() => await this.Client.ConnectAsync(); protected override Task ConfigureExtensionsAsync() { this.InitializeExtensions(this.Client); return Task.CompletedTask; } } } diff --git a/DisCatSharp.Hosting/DiscordSharedHostedService.cs b/DisCatSharp.Hosting/DiscordSharedHostedService.cs index 7c45c4460..e75295869 100644 --- a/DisCatSharp.Hosting/DiscordSharedHostedService.cs +++ b/DisCatSharp.Hosting/DiscordSharedHostedService.cs @@ -1,79 +1,87 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// - /// Simple Implementation for to work as a + /// Simple Implementation for to work as a /// public abstract class DiscordShardedHostedService : BaseHostedService, IDiscordHostedShardService { public DiscordShardedClient ShardedClient { get; protected set; } -#pragma warning disable 8618 + #pragma warning disable 8618 + /// + /// Initializes a new instance of the class. + /// + /// The config. + /// The logger. + /// The service provider. + /// The application lifetime. + /// The config bot section. protected DiscordShardedHostedService(IConfiguration config, ILogger logger, IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) : base(config, logger, serviceProvider, applicationLifetime, configBotSection) { } -#pragma warning restore 8618 + #pragma warning restore 8618 protected override Task ConfigureAsync() { try { var config = this.Configuration.ExtractConfig(this.ServiceProvider, "Discord", this.BotSection); this.ShardedClient = new DiscordShardedClient(config); } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordShardedClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } return Task.CompletedTask; } protected sealed override async Task ConnectAsync() => await this.ShardedClient.StartAsync(); protected override Task ConfigureExtensionsAsync() { foreach (var client in this.ShardedClient.ShardClients.Values) { this.InitializeExtensions(client); } return Task.CompletedTask; } } } diff --git a/DisCatSharp.Hosting/GlobalSuppressions.cs b/DisCatSharp.Hosting/GlobalSuppressions.cs new file mode 100644 index 000000000..0cdd27c5b --- /dev/null +++ b/DisCatSharp.Hosting/GlobalSuppressions.cs @@ -0,0 +1,20 @@ +// 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("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.BaseHostedService.ExecuteAsync(System.Threading.CancellationToken)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordHostedService.ConfigureAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordHostedService.ConfigureExtensionsAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordHostedService.ConnectAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordShardedHostedService.ConfigureAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordShardedHostedService.ConfigureExtensionsAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "MethodDocumentationHeader:The method must have a documentation header.", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordShardedHostedService.ConnectAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("DocumentationHeader", "PropertyDocumentationHeader:The property must have a documentation header.", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Hosting.DiscordShardedHostedService.ShardedClient")] +[assembly: SuppressMessage("DocumentationHeader", "PropertyDocumentationHeader:The property must have a documentation header.", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Hosting.IDiscordHostedShardService.ShardedClient")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.BaseHostedService.ExecuteAsync(System.Threading.CancellationToken)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.BaseHostedService.InitializeExtensions(DisCatSharp.DiscordClient)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordHostedService.ConfigureAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:DisCatSharp.Hosting.DiscordShardedHostedService.ConfigureAsync~System.Threading.Tasks.Task")] diff --git a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj index 76c1d36d7..336772429 100644 --- a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj +++ b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj @@ -1,39 +1,40 @@ DisCatSharp.Interactivity DisCatSharp.Interactivity Library netstandard2.0 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/Requests/PaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs index 22f112ff9..005ea44e3 100644 --- a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs @@ -1,319 +1,319 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The pagination request. /// internal class PaginationRequest : IPaginationRequest { private TaskCompletionSource _tcs; private readonly CancellationTokenSource _ct; private TimeSpan _timeout; private readonly List _pages; private readonly PaginationBehaviour _behaviour; private readonly DiscordMessage _message; private readonly PaginationEmojis _emojis; private readonly DiscordUser _user; private int _index = 0; /// /// Creates a new Pagination request /// /// Message to paginate /// User to allow control for /// Behaviour during pagination /// Behavior on pagination end /// Emojis for this pagination object /// Timeout time /// Pagination pages internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion, PaginationEmojis emojis, TimeSpan timeout, params Page[] pages) { this._tcs = new(); this._ct = new(timeout); this._ct.Token.Register(() => this._tcs.TrySetResult(true)); this._timeout = timeout; this._message = message; this._user = user; this.PaginationDeletion = deletion; this._behaviour = behaviour; this._emojis = emojis; this._pages = new List(); foreach (var p in pages) { this._pages.Add(p); } } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Gets the pagination deletion. /// public PaginationDeletion PaginationDeletion { get; } /// /// Gets the page async. /// /// A Task. public async Task GetPageAsync() { await Task.Yield(); return this._pages[this._index]; } /// /// Skips the left async. /// /// A Task. public async Task SkipLeftAsync() { await Task.Yield(); this._index = 0; } /// /// Skips the right async. /// /// A Task. public async Task SkipRightAsync() { await Task.Yield(); this._index = this._pages.Count - 1; } /// /// Nexts the page async. /// /// A Task. public async Task NextPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == this._pages.Count - 1) break; else this._index++; break; case PaginationBehaviour.WrapAround: if (this._index == this._pages.Count - 1) this._index = 0; else this._index++; break; } } /// /// Previous the page async. /// /// A Task. public async Task PreviousPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == 0) break; else this._index--; break; case PaginationBehaviour.WrapAround: if (this._index == 0) this._index = this._pages.Count - 1; else this._index--; break; } } /// /// Gets the buttons async. /// - /// + /// #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task> GetButtonsAsync() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously => throw new NotSupportedException("This request does not support buttons."); /// /// Gets the emojis async. /// /// A Task. public async Task GetEmojisAsync() { await Task.Yield(); return this._emojis; } /// /// Gets the message async. /// /// A Task. public async Task GetMessageAsync() { await Task.Yield(); return this._message; } /// /// Gets the user async. /// /// A Task. public async Task GetUserAsync() { await Task.Yield(); return this._user; } /// /// Dos the cleanup async. /// /// A Task. public async Task DoCleanupAsync() { switch (this.PaginationDeletion) { case PaginationDeletion.DeleteEmojis: await this._message.DeleteAllReactionsAsync().ConfigureAwait(false); break; case PaginationDeletion.DeleteMessage: await this._message.DeleteAsync().ConfigureAwait(false); break; case PaginationDeletion.KeepEmojis: break; } } /// /// Gets the task completion source async. /// /// A Task. public async Task> GetTaskCompletionSourceAsync() { await Task.Yield(); return this._tcs; } ~PaginationRequest() { this.Dispose(); } /// /// Disposes this PaginationRequest. /// public void Dispose() { this._ct.Dispose(); this._tcs = null; } } } namespace DisCatSharp.Interactivity { /// /// The pagination emojis. /// public class PaginationEmojis { public DiscordEmoji SkipLeft; public DiscordEmoji SkipRight; public DiscordEmoji Left; public DiscordEmoji Right; public DiscordEmoji Stop; /// /// Initializes a new instance of the class. /// public PaginationEmojis() { this.Left = DiscordEmoji.FromUnicode("◀"); this.Right = DiscordEmoji.FromUnicode("▶"); this.SkipLeft = DiscordEmoji.FromUnicode("⏮"); this.SkipRight = DiscordEmoji.FromUnicode("⏭"); this.Stop = DiscordEmoji.FromUnicode("⏹"); } } /// /// The page. /// public class Page { /// /// Gets or sets the content. /// public string Content { get; set; } /// /// Gets or sets the embed. /// public DiscordEmbed Embed { get; set; } /// /// Initializes a new instance of the class. /// /// The content. /// The embed. public Page(string content = "", DiscordEmbedBuilder embed = null) { this.Content = content; this.Embed = embed?.Build(); } } } diff --git a/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs index 3c0f39ff3..871d321b6 100644 --- a/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs @@ -1,150 +1,150 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity.Extensions { /// - /// Interactivity extension methods for . + /// Interactivity extension methods for . /// public static class ChannelExtensions { /// /// Waits for the next message sent in this channel that satisfies the predicate. /// /// The channel to monitor. /// A predicate that should return if a message matches. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(channel).WaitForMessageAsync(msg => msg.ChannelId == channel.Id && predicate(msg), timeoutOverride); /// /// Waits for the next message sent in this channel. /// /// The channel to monitor. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, TimeSpan? timeoutOverride = null) => channel.GetNextMessageAsync(msg => true, timeoutOverride); /// /// Waits for the next message sent in this channel from a specific user. /// /// The channel to monitor. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) => channel.GetNextMessageAsync(msg => msg.Author.Id == user.Id, timeoutOverride); /// /// Waits for a specific user to start typing in this channel. /// /// The target channel. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> WaitForUserTypingAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(channel).WaitForUserTypingAsync(user, channel, timeoutOverride); /// /// Sends a new paginated message. /// /// Target channel. /// The user that'll be able to control the pages. /// A collection of to display. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, emojis, behaviour, deletion, timeoutoverride); /// /// Sends a new paginated message with buttons. /// /// Target channel. /// The user that'll be able to control the pages. /// A collection of to display. /// Pagination buttons (leave null to default to ones on configuration). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, token); /// public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => channel.SendPaginatedMessageAsync(user, pages, default, behaviour, deletion, token); /// /// Sends a new paginated message with buttons. /// /// Target channel. /// The user that'll be able to control the pages. /// A collection of to display. /// Pagination buttons (leave null to default to ones on configuration). /// Pagination behaviour. /// Deletion behaviour /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, timeoutoverride, behaviour, deletion); /// /// Sends the paginated message async. /// /// The channel. /// The user. /// The pages. /// The timeoutoverride. /// The behaviour. /// The deletion. /// A Task. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => channel.SendPaginatedMessageAsync(user, pages, default, timeoutoverride, behaviour, deletion); /// /// Retrieves an interactivity instance from a channel instance. /// private static InteractivityExtension GetInteractivity(DiscordChannel channel) { var client = (DiscordClient)channel.Discord; var interactivity = client.GetInteractivity(); return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client._isShard ? "shard" : "client")}."); } } } diff --git a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs index 58ed4dec6..1c18425d6 100644 --- a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs @@ -1,100 +1,100 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Threading.Tasks; namespace DisCatSharp.Interactivity.Extensions { /// /// Interactivity extension methods for and . /// public static class ClientExtensions { /// /// Enables interactivity for this instance. /// /// The client to enable interactivity for. /// A configuration instance. Default configuration values will be used if none is provided. /// A brand new instance. - /// Thrown if interactivity has already been enabled for the client instance. + /// Thrown if interactivity has already been enabled for the client instance. public static InteractivityExtension UseInteractivity(this DiscordClient client, InteractivityConfiguration configuration = null) { if (client.GetExtension() != null) throw new InvalidOperationException($"Interactivity is already enabled for this {(client._isShard ? "shard" : "client")}."); configuration ??= new InteractivityConfiguration(); var extension = new InteractivityExtension(configuration); client.AddExtension(extension); return extension; } /// /// Enables interactivity for each shard. /// /// The shard client to enable interactivity for. /// Configuration to use for all shards. If one isn't provided, default configuration values will be used. /// A dictionary containing new instances for each shard. public static async Task> UseInteractivityAsync(this DiscordShardedClient client, InteractivityConfiguration configuration = null) { var extensions = new Dictionary(); await client.InitializeShardsAsync().ConfigureAwait(false); foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value)) { var extension = shard.GetExtension() ?? shard.UseInteractivity(configuration); extensions.Add(shard.ShardId, extension); } return new ReadOnlyDictionary(extensions); } /// /// Retrieves the registered instance for this client. /// /// The client to retrieve an instance from. /// An existing instance, or if interactivity is not enabled for the instance. public static InteractivityExtension GetInteractivity(this DiscordClient client) => client.GetExtension(); /// /// Retrieves a instance for each shard. /// /// The shard client to retrieve interactivity instances from. /// A dictionary containing instances for each shard. public static async Task> GetInteractivityAsync(this DiscordShardedClient client) { await client.InitializeShardsAsync().ConfigureAwait(false); var extensions = new Dictionary(); foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value)) { extensions.Add(shard.ShardId, shard.GetExtension()); } return new ReadOnlyDictionary(extensions); } } } diff --git a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs index 4b2093aa2..e66096ef9 100644 --- a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs @@ -1,243 +1,243 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity.Extensions { /// - /// Interactivity extension methods for . + /// Interactivity extension methods for . /// public static class MessageExtensions { /// /// Waits for the next message that has the same author and channel as this message. /// /// Original message. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(message.Author, timeoutOverride); /// /// Waits for the next message with the same author and channel as this message, which also satisfies a predicate. /// /// Original message. /// A predicate that should return if a message matches. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. public static Task> WaitForButtonAsync(this DiscordMessage message) => GetInteractivity(message).WaitForButtonAsync(message); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, token); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, id, token); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, user, token); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, predicate, token); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . - /// Thrown when the message doesn't contain any dropdowns + /// Thrown when the message doesn't contain any dropdowns public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the message doesn't contain any dropdowns + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the message doesn't contain any dropdowns public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, predicate, token); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait for. /// Overrides the timeout set in public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, id, token); /// /// Waits for a dropdown to be interacted with by the specified user. /// /// The message to wait on. /// The user to wait for. /// The Id of the dropdown to wait for. /// public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride); /// /// Waits for a dropdown to be interacted with by the specified user. /// /// The message to wait on. /// The user to wait for. /// The Id of the dropdown to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, user, id, token); /// /// Waits for a reaction on this message from a specific user. /// /// Target message. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride); /// /// Waits for a specific reaction on this message from the specified user. /// /// Target message. /// The target user. /// The target emoji. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride); /// /// Collects all reactions on this message within the timeout duration. /// /// The message to collect reactions from. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride); /// /// Begins a poll using this message. /// /// Target message. /// Options for this poll. /// Overrides the action set in /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null) => GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride); /// /// Retrieves an interactivity instance from a message instance. /// internal static InteractivityExtension GetInteractivity(DiscordMessage message) { var client = (DiscordClient)message.Discord; var interactivity = client.GetInteractivity(); return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client._isShard ? "shard" : "client")}."); } } } diff --git a/DisCatSharp.Interactivity/InteractivityConfiguration.cs b/DisCatSharp.Interactivity/InteractivityConfiguration.cs index 86a64b83c..4ed1903aa 100644 --- a/DisCatSharp.Interactivity/InteractivityConfiguration.cs +++ b/DisCatSharp.Interactivity/InteractivityConfiguration.cs @@ -1,115 +1,115 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Interactivity { /// /// Configuration class for your Interactivity extension /// public sealed class InteractivityConfiguration { /// /// Sets the default interactivity action timeout. /// Defaults to 1 minute. /// public TimeSpan Timeout { internal get; set; } = TimeSpan.FromMinutes(1); /// /// What to do after the poll ends /// public PollBehaviour PollBehaviour { internal get; set; } = PollBehaviour.DeleteEmojis; /// /// Emojis to use for pagination /// public PaginationEmojis PaginationEmojis { internal get; set; } = new(); /// /// Buttons to use for pagination. /// public PaginationButtons PaginationButtons { internal get; set; } = new(); /// /// Whether interactivity should ACK buttons that are pushed. Setting this to will also prevent subsequent event handlers from running. /// public bool AckPaginationButtons { internal get; set; } /// /// How to handle buttons after pagination ends. /// public ButtonPaginationBehavior ButtonBehavior { internal get; set; } = new(); /// /// How to handle pagination. Defaults to WrapAround. /// public PaginationBehaviour PaginationBehaviour { internal get; set; } = PaginationBehaviour.WrapAround; /// /// How to handle pagination deletion. Defaults to DeleteEmojis. /// public PaginationDeletion PaginationDeletion { internal get; set; } = PaginationDeletion.DeleteEmojis; /// /// How to handle invalid interactions. Defaults to Ignore. /// public InteractionResponseBehavior ResponseBehavior { internal get; set; } = InteractionResponseBehavior.Ignore; /// - /// The message to send to the user when processing invalid interactions. Ignored if is not set to . + /// The message to send to the user when processing invalid interactions. Ignored if is not set to . /// public string ResponseMessage { internal get; set; } /// /// Creates a new instance of . /// [ActivatorUtilitiesConstructor] public InteractivityConfiguration() { } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public InteractivityConfiguration(InteractivityConfiguration other) { this.AckPaginationButtons = other.AckPaginationButtons; this.PaginationButtons = other.PaginationButtons; this.ButtonBehavior = other.ButtonBehavior; this.PaginationBehaviour = other.PaginationBehaviour; this.PaginationDeletion = other.PaginationDeletion; this.ResponseBehavior = other.ResponseBehavior; this.PaginationEmojis = other.PaginationEmojis; this.ResponseMessage = other.ResponseMessage; this.PollBehaviour = other.PollBehaviour; this.Timeout = other.Timeout; if (this.ResponseBehavior is InteractionResponseBehavior.Respond && string.IsNullOrWhiteSpace(this.ResponseMessage)) throw new ArgumentException($"{nameof(this.ResponseMessage)} cannot be null, empty, or whitespace when {nameof(this.ResponseBehavior)} is set to respond."); } } } diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs index ad961fef8..fc93fcfaf 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -1,930 +1,930 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; using DisCatSharp.Common.Utilities; using DisCatSharp.Enums; using System.Threading; using System.Globalization; namespace DisCatSharp.Interactivity { /// - /// Extension class for DSharpPlus.Interactivity + /// Extension class for DisCatSharp.Interactivity /// public class InteractivityExtension : BaseExtension { #pragma warning disable IDE1006 // Naming Styles /// /// Gets the config. /// internal InteractivityConfiguration Config { get; } private EventWaiter MessageCreatedWaiter; private EventWaiter MessageReactionAddWaiter; private EventWaiter TypingStartWaiter; private EventWaiter ComponentInteractionWaiter; private ComponentEventWaiter ComponentEventWaiter; private ReactionCollector ReactionCollector; private Poller Poller; private Paginator Paginator; private ComponentPaginator _compPaginator; #pragma warning restore IDE1006 // Naming Styles /// /// Initializes a new instance of the class. /// /// The configuration. internal InteractivityExtension(InteractivityConfiguration cfg) { this.Config = new InteractivityConfiguration(cfg); } /// /// Setups the Interactivity Extension. /// /// Discord client. protected internal override void Setup(DiscordClient client) { this.Client = client; this.MessageCreatedWaiter = new EventWaiter(this.Client); this.MessageReactionAddWaiter = new EventWaiter(this.Client); this.ComponentInteractionWaiter = new EventWaiter(this.Client); this.TypingStartWaiter = new EventWaiter(this.Client); this.Poller = new Poller(this.Client); this.ReactionCollector = new ReactionCollector(this.Client); this.Paginator = new Paginator(this.Client); this._compPaginator = new(this.Client, this.Config); this.ComponentEventWaiter = new(this.Client, this.Config); } /// /// Makes a poll and returns poll results. /// /// Message to create poll on. /// Emojis to use for this poll. /// What to do when the poll ends. /// override timeout period. /// public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); if (!emojis.Any()) throw new ArgumentException("You need to provide at least one emoji for a poll!"); foreach (var em in emojis) await m.CreateReactionAsync(em).ConfigureAwait(false); var res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false); var pollbehaviour = behaviour ?? this.Config.PollBehaviour; var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false); if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages)) await m.DeleteAllReactionsAsync().ConfigureAwait(false); return new ReadOnlyCollection(res.ToList()); } /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// Override the timeout period in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!buttons.Any()) throw new ArgumentException("You must specify at least one button to listen for."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Provided Message does not contain any button components."); var res = await this.ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false); return new(res is null, res); } /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) throw new ArgumentException($"Message does not contain button with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . - /// Thrown when the Provided message does not contain any dropdowns + /// Thrown when the Provided message does not contain any dropdowns public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the Provided message does not contain any dropdowns + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the Provided message does not contain any dropdowns public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with. /// /// This is here for backwards-compatibility and will internally create a cancellation token. /// The message to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select component with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a specific message. /// /// Predicate to match. /// override timeout period. public async Task> WaitForMessageAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No message intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns?.Message); } /// /// Wait for a specific reaction. /// /// Predicate to match. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Wait for a specific reaction. /// For this Event you need the intent specified in /// /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// Predicate to match. /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// predicate to match. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a user to start typing. /// /// User that starts typing. /// Channel the user is typing in. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for a user to start typing. /// /// User that starts typing. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for any user to start typing. /// /// Channel to type in. /// Override timeout period. public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Collects reactions on a specific message. /// /// Message to collect reactions on. /// Override timeout period. public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false); return collection; } /// /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. /// /// /// The predicate. /// Override timeout period. public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(res == null, res); } /// /// Collects the event arguments. /// /// The predicate. /// Override timeout period. public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false); return res; } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages.ToArray(), token == default ? this.GetCancellationToken() : token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// Override timeout period. public Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutoverride)); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The behaviour. /// The deletion. /// The token. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The timeoutoverride. /// The behaviour. /// The deletion. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, timeoutoverride, behaviour, deletion); /// /// Sends a paginated message. /// For this Event you need the intent specified in /// /// Channel to send paginated message in. /// User to give control. /// Pages. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutoverride ?? this.Config.Timeout; var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.PaginationDeletion; var ems = emojis ?? this.Config.PaginationEmojis; var prequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages.ToArray()); await this.Paginator.DoPaginationAsync(prequest).ConfigureAwait(false); } /// /// Sends a paginated message in response to an interaction. /// /// Pass the interaction directly. Interactivity will ACK it. /// /// /// The interaction to create a response to. /// Whether the response should be ephemeral. /// The user to listen for button presses from. /// The pages to paginate. /// Optional: custom buttons /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordInteractionResponseBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AsEphemeral(ephemeral) .AddComponents(bts.ButtonArray); await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false); var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false); var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Waits for a custom pagination request to finish. /// This does NOT handle removing emojis after finishing for you. /// /// /// public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Waits for custom button-based pagination request to finish. ///
- /// This does not invoke . + /// This does not invoke . ///
/// The request to wait for. public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Generates pages from a string, and puts them in message content. /// /// Input string. /// How to split input string. /// public IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += subsplit[i]; if (i >= 15 && i % 15 == 0) { split.Add(s); s = ""; } } if (split.All(x => x != s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page($"Page {page}:\n{s}")); page++; } return result; } /// /// Generates pages from a string, and puts them in message embeds. /// /// Input string. /// How to split input string. /// Base embed for output embeds. /// public IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var embed = embedbase ?? new DiscordEmbedBuilder(); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += $"{subsplit[i]}\n"; if (i % 15 == 0 && i != 0) { split.Add(s); s = ""; } } if (!split.Any(x => x == s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); page++; } return result; } /// /// Splits the string. /// /// The string. /// The chunk size. private List SplitString(string str, int chunkSize) { var res = new List(); var len = str.Length; var i = 0; while (i < len) { var size = Math.Min(len - i, chunkSize); res.Add(str.Substring(i, size)); i += size; } return res; } /// /// Gets the cancellation token. /// /// The timeout. private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; /// /// Handles an invalid interaction. /// /// The interaction. private async Task HandleInvalidInteraction(DiscordInteraction interaction) { var at = this.Config.ResponseBehavior switch { InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate), InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() { Content = this.Config.ResponseMessage, IsEphemeral = true}), InteractionResponseBehavior.Ignore => Task.CompletedTask, _ => throw new ArgumentException("Unknown enum value.") }; await at; } } } diff --git a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj index 55d50877e..e300baed6 100644 --- a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj +++ b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj @@ -1,36 +1,40 @@ DisCatSharp.Lavalink DisCatSharp.Lavalink Library netstandard2.0 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/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs index ecb059e53..4ebea4ce5 100644 --- a/DisCatSharp.Lavalink/LavalinkExtension.cs +++ b/DisCatSharp.Lavalink/LavalinkExtension.cs @@ -1,215 +1,215 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Lavalink.EventArgs; using DisCatSharp.Net; using DisCatSharp.Common.Utilities; namespace DisCatSharp.Lavalink { /// /// The lavalink extension. /// public sealed class LavalinkExtension : BaseExtension { /// /// Triggered whenever a node disconnects. /// public event AsyncEventHandler NodeDisconnected { add { this._nodeDisconnected.Register(value); } remove { this._nodeDisconnected.Unregister(value); } } private AsyncEvent _nodeDisconnected; /// /// Gets a dictionary of connected Lavalink nodes for the extension. /// public IReadOnlyDictionary ConnectedNodes { get; } private readonly ConcurrentDictionary _connectedNodes = new(); /// /// Creates a new instance of this Lavalink extension. /// internal LavalinkExtension() { this.ConnectedNodes = new ReadOnlyConcurrentDictionary(this._connectedNodes); } /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. - /// + /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._nodeDisconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Client.EventErrorHandler); } /// /// Connect to a Lavalink node. /// /// Lavalink client configuration. /// The established Lavalink connection. public async Task ConnectAsync(LavalinkConfiguration config) { if (this._connectedNodes.ContainsKey(config.SocketEndpoint)) return this._connectedNodes[config.SocketEndpoint]; var con = new LavalinkNodeConnection(this.Client, this, config); con.NodeDisconnected += this.Con_NodeDisconnected; con.Disconnected += this.Con_Disconnected; this._connectedNodes[con.NodeEndpoint] = con; try { await con.StartAsync().ConfigureAwait(false); } catch { this.Con_NodeDisconnected(con); throw; } return con; } /// /// Gets the Lavalink node connection for the specified endpoint. /// /// Endpoint at which the node resides. /// Lavalink node connection. public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint) => this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null; /// /// Gets a Lavalink node connection based on load balancing and an optional voice region. /// /// The region to compare with the node's , if any. /// The least load affected node connection, or null if no nodes are present. public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null) { if (this._connectedNodes.Count <= 1) return this._connectedNodes.Values.FirstOrDefault(); var nodes = this._connectedNodes.Values.ToArray(); if (region != null) { var regionPredicate = new Func(x => x.Region == region); if (nodes.Any(regionPredicate)) nodes = nodes.Where(regionPredicate).ToArray(); if (nodes.Count() <= 1) return nodes.FirstOrDefault(); } return this.FilterByLoad(nodes); } /// - /// Gets a Lavalink guild connection from a . + /// Gets a Lavalink guild connection from a . /// /// The guild the connection is on. /// The found guild connection, or null if one could not be found. public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild) { var nodes = this._connectedNodes.Values; var node = nodes.FirstOrDefault(x => x._connectedGuilds.ContainsKey(guild.Id)); return node?.GetGuildConnection(guild); } /// /// Filters the by load. /// /// The nodes. private LavalinkNodeConnection FilterByLoad(LavalinkNodeConnection[] nodes) { Array.Sort(nodes, (a, b) => { if (!a.Statistics._updated || !b.Statistics._updated) return 0; //https://github.com/FredBoat/Lavalink-Client/blob/48bc27784f57be5b95d2ff2eff6665451b9366f5/src/main/java/lavalink/client/io/LavalinkLoadBalancer.java#L122 //https://github.com/briantanner/eris-lavalink/blob/master/src/PlayerManager.js#L329 //player count var aPenaltyCount = a.Statistics.ActivePlayers; var bPenaltyCount = b.Statistics.ActivePlayers; //cpu load aPenaltyCount += (int)Math.Pow(1.05d, (100 * (a.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); bPenaltyCount += (int)Math.Pow(1.05d, (100 * (b.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); //frame load if (a.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } //frame load if (b.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } return aPenaltyCount - bPenaltyCount; }); return nodes[0]; } /// /// Removes a node. /// /// The node to be removed. private void Con_NodeDisconnected(LavalinkNodeConnection node) => this._connectedNodes.TryRemove(node.NodeEndpoint, out _); /// /// Disconnects a node. /// /// The affected node. /// The node disconnected event args. private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEventArgs e) => this._nodeDisconnected.InvokeAsync(node, e); } } diff --git a/DisCatSharp.Lavalink/LavalinkRestClient.cs b/DisCatSharp.Lavalink/LavalinkRestClient.cs index 5a6a013a7..8f6c45e52 100644 --- a/DisCatSharp.Lavalink/LavalinkRestClient.cs +++ b/DisCatSharp.Lavalink/LavalinkRestClient.cs @@ -1,423 +1,424 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Net; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Net; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Lavalink { /// /// Represents a class for Lavalink REST calls. /// public sealed class LavalinkRestClient { /// /// Gets the REST connection endpoint for this client. /// public ConnectionEndpoint RestEndpoint { get; private set; } private HttpClient _http; private readonly ILogger _logger; - private readonly Lazy _dsharpplusVersionString = new(() => + private readonly Lazy _dcsVersionString = new(() => { var a = typeof(DiscordClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); /// /// Creates a new Lavalink REST client. /// /// The REST server endpoint to connect to. /// The password for the remote server. public LavalinkRestClient(ConnectionEndpoint restEndpoint, string password) { this.RestEndpoint = restEndpoint; this.ConfigureHttpHandling(password); } /// /// Initializes a new instance of the class. /// /// The config. /// The client. internal LavalinkRestClient(LavalinkConfiguration config, BaseDiscordClient client) { this.RestEndpoint = config.RestEndpoint; this._logger = client.Logger; this.ConfigureHttpHandling(config.Password, client); } /// /// Gets the version of the Lavalink server. /// /// public Task GetVersionAsync() { var versionUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.VERSION}"); return this.InternalGetVersionAsync(versionUri); } #region Track_Loading /// /// Searches for specified terms. /// /// What to search for. /// What platform will search for. /// A collection of tracks matching the criteria. public Task GetTracksAsync(string searchQuery, LavalinkSearchType type = LavalinkSearchType.Youtube) { var prefix = type switch { LavalinkSearchType.Youtube => "ytsearch:", LavalinkSearchType.SoundCloud => "scsearch:", LavalinkSearchType.Plain => "", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; var str = WebUtility.UrlEncode(prefix + searchQuery); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Loads tracks from specified URL. /// /// URL to load tracks from. /// A collection of tracks from the URL. public Task GetTracksAsync(Uri uri) { var str = WebUtility.UrlEncode(uri.ToString()); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Loads tracks from a local file. /// /// File to load tracks from. /// A collection of tracks from the file. public Task GetTracksAsync(FileInfo file) { var str = WebUtility.UrlEncode(file.FullName); var tracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.LOAD_TRACKS}?identifier={str}"); return this.InternalResolveTracksAsync(tracksUri); } /// /// Decodes a base64 track string into a Lavalink track object. /// /// The base64 track string. /// public Task DecodeTrackAsync(string trackString) { var str = WebUtility.UrlEncode(trackString); var decodeTrackUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACK}?track={str}"); return this.InternalDecodeTrackAsync(decodeTrackUri); } /// /// Decodes an array of base64 track strings into Lavalink track objects. /// /// The array of base64 track strings. /// public Task> DecodeTracksAsync(string[] trackStrings) { var decodeTracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACKS}"); return this.InternalDecodeTracksAsync(decodeTracksUri, trackStrings); } /// /// Decodes a list of base64 track strings into Lavalink track objects. /// /// The list of base64 track strings. /// public Task> DecodeTracksAsync(List trackStrings) { var decodeTracksUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.DECODE_TRACKS}"); return this.InternalDecodeTracksAsync(decodeTracksUri, trackStrings.ToArray()); } #endregion #region Route_Planner /// /// Retrieves statistics from the route planner. /// - /// The status () details. + /// The status () details. public Task GetRoutePlannerStatusAsync() { var routeStatusUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.STATUS}"); return this.InternalGetRoutePlannerStatusAsync(routeStatusUri); } /// /// Unmarks a failed route planner IP Address. /// /// The IP address name to unmark. /// public Task FreeAddressAsync(string address) { var routeFreeAddressUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.FREE_ADDRESS}"); return this.InternalFreeAddressAsync(routeFreeAddressUri, address); } /// /// Unmarks all failed route planner IP Addresses. /// /// public Task FreeAllAddressesAsync() { var routeFreeAllAddressesUri = new Uri($"{this.RestEndpoint.ToHttpString()}{Endpoints.ROUTE_PLANNER}{Endpoints.FREE_ALL}"); return this.InternalFreeAllAddressesAsync(routeFreeAllAddressesUri); } #endregion /// /// get version async. /// /// The uri. /// A Task. internal async Task InternalGetVersionAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); return json; } #region Internal_Track_Loading /// /// resolve tracks async. /// /// The uri. /// A Task. internal async Task InternalResolveTracksAsync(Uri uri) { // this function returns a Lavalink 3-like dataset regardless of input data version var json = "[]"; using (var req = await this._http.GetAsync(uri).ConfigureAwait(false)) using (var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var sr = new StreamReader(res, Utilities.UTF8)) json = await sr.ReadToEndAsync().ConfigureAwait(false); var jdata = JToken.Parse(json); if (jdata is JArray jarr) { // Lavalink 2.x var tracks = new List(jarr.Count); foreach (var jt in jarr) { var track = jt["info"].ToObject(); track.TrackString = jt["track"].ToString(); tracks.Add(track); } return new LavalinkLoadResult { PlaylistInfo = default, LoadResultType = tracks.Count == 0 ? LavalinkLoadResultType.LoadFailed : LavalinkLoadResultType.TrackLoaded, Tracks = tracks }; } else if (jdata is JObject jo) { // Lavalink 3.x jarr = jo["tracks"] as JArray; var loadInfo = jo.ToObject(); var tracks = new List(jarr.Count); foreach (var jt in jarr) { var track = jt["info"].ToObject(); track.TrackString = jt["track"].ToString(); tracks.Add(track); } loadInfo.Tracks = new ReadOnlyCollection(tracks); return loadInfo; } else return null; } /// /// decode track async. /// /// The uri. /// A Task. internal async Task InternalDecodeTrackAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); if (!req.IsSuccessStatusCode) { var jsonError = JObject.Parse(json); this._logger?.LogError(LavalinkEvents.LavalinkDecodeError, "Unable to decode track strings: {0}", jsonError["message"]); return null; } var track = JsonConvert.DeserializeObject(json); return track; } /// /// decode tracks async. /// /// The uri. /// The ids. /// A Task. internal async Task> InternalDecodeTracksAsync(Uri uri, string[] ids) { var jsonOut = JsonConvert.SerializeObject(ids); var content = new StringContent(jsonOut, Utilities.UTF8, "application/json"); using var req = await this._http.PostAsync(uri, content).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var jsonIn = await sr.ReadToEndAsync().ConfigureAwait(false); if (!req.IsSuccessStatusCode) { var jsonError = JObject.Parse(jsonIn); this._logger?.LogError(LavalinkEvents.LavalinkDecodeError, "Unable to decode track strings", jsonError["message"]); return null; } var jarr = JToken.Parse(jsonIn) as JArray; var decodedTracks = new LavalinkTrack[jarr.Count]; for (var i = 0; i < decodedTracks.Length; i++) { decodedTracks[i] = JsonConvert.DeserializeObject(jarr[i]["info"].ToString()); decodedTracks[i].TrackString = jarr[i]["track"].ToString(); } var decodedTrackList = new ReadOnlyCollection(decodedTracks); return decodedTrackList; } #endregion #region Internal_Route_Planner /// /// get route planner status async. /// /// The uri. /// A Task. internal async Task InternalGetRoutePlannerStatusAsync(Uri uri) { using var req = await this._http.GetAsync(uri).ConfigureAwait(false); using var res = await req.Content.ReadAsStreamAsync().ConfigureAwait(false); using var sr = new StreamReader(res, Utilities.UTF8); var json = await sr.ReadToEndAsync().ConfigureAwait(false); var status = JsonConvert.DeserializeObject(json); return status; } /// /// free address async. /// /// The uri. /// The address. /// A Task. internal async Task InternalFreeAddressAsync(Uri uri, string address) { var payload = new StringContent(address, Utilities.UTF8, "application/json"); using var req = await this._http.PostAsync(uri, payload).ConfigureAwait(false); if (req.StatusCode == HttpStatusCode.InternalServerError) this._logger?.LogWarning(LavalinkEvents.LavalinkRestError, "Request to {0} returned an internal server error - your server route planner configuration is likely incorrect", uri); } /// /// free all addresses async. /// /// The uri. /// A Task. internal async Task InternalFreeAllAddressesAsync(Uri uri) { var httpReq = new HttpRequestMessage(HttpMethod.Post, uri); using var req = await this._http.SendAsync(httpReq).ConfigureAwait(false); if (req.StatusCode == HttpStatusCode.InternalServerError) this._logger?.LogWarning(LavalinkEvents.LavalinkRestError, "Request to {0} returned an internal server error - your server route planner configuration is likely incorrect", uri); } #endregion /// /// Configures the http handling. /// /// The password. /// The client. private void ConfigureHttpHandling(string password, BaseDiscordClient client = null) { var httphandler = new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseProxy = client != null && client.Configuration.Proxy != null }; if (httphandler.UseProxy) // because mono doesn't implement this properly httphandler.Proxy = client.Configuration.Proxy; this._http = new HttpClient(httphandler); - this._http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", $"DisCatSharp.LavaLink/{this._dsharpplusVersionString}"); + this._http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", $"DisCatSharp.LavaLink/{this._dcsVersionString}"); + this._http.DefaultRequestHeaders.TryAddWithoutValidation("Client-Name", $"DisCatSharp"); this._http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", password); } } } diff --git a/DisCatSharp.Logos/android-chrome-192x192.png b/DisCatSharp.Logos/android-chrome-192x192.png new file mode 100644 index 000000000..e1b6613b5 Binary files /dev/null and b/DisCatSharp.Logos/android-chrome-192x192.png differ diff --git a/DisCatSharp.Logos/android-chrome-512x512.png b/DisCatSharp.Logos/android-chrome-512x512.png new file mode 100644 index 000000000..82477be43 Binary files /dev/null and b/DisCatSharp.Logos/android-chrome-512x512.png differ diff --git a/DisCatSharp.Logos/apple-touch-icon.png b/DisCatSharp.Logos/apple-touch-icon.png new file mode 100644 index 000000000..ce26fc205 Binary files /dev/null and b/DisCatSharp.Logos/apple-touch-icon.png differ diff --git a/DisCatSharp.Logos/favicon-16x16.png b/DisCatSharp.Logos/favicon-16x16.png new file mode 100644 index 000000000..89a142e05 Binary files /dev/null and b/DisCatSharp.Logos/favicon-16x16.png differ diff --git a/DisCatSharp.Logos/favicon-32x32.png b/DisCatSharp.Logos/favicon-32x32.png new file mode 100644 index 000000000..1e1587b12 Binary files /dev/null and b/DisCatSharp.Logos/favicon-32x32.png differ diff --git a/DisCatSharp.Logos/favicon.ico b/DisCatSharp.Logos/favicon.ico new file mode 100644 index 000000000..53a35037a Binary files /dev/null and b/DisCatSharp.Logos/favicon.ico differ diff --git a/DisCatSharp.Logos/logo.png b/DisCatSharp.Logos/logo.png new file mode 100644 index 000000000..fd7bceeb0 Binary files /dev/null and b/DisCatSharp.Logos/logo.png differ diff --git a/DisCatSharp.Logos/logobig.png b/DisCatSharp.Logos/logobig.png new file mode 100644 index 000000000..3c47e76d7 Binary files /dev/null and b/DisCatSharp.Logos/logobig.png differ diff --git a/DisCatSharp.Logos/site.webmanifest b/DisCatSharp.Logos/site.webmanifest new file mode 100644 index 000000000..45dc8a206 --- /dev/null +++ b/DisCatSharp.Logos/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj index 566731ac7..e9f3d49ec 100644 --- a/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj +++ b/DisCatSharp.VoiceNext.Natives/DisCatSharp.VoiceNext.Natives.csproj @@ -1,44 +1,45 @@ netstandard2.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 LICENSES.MD true runtimes True + diff --git a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj index 6ba1b2e26..ef4888876 100644 --- a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj +++ b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj @@ -1,44 +1,45 @@ DisCatSharp.VoiceNext DisCatSharp.VoiceNext Library netstandard2.0 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/StreamExtensions.cs b/DisCatSharp.VoiceNext/StreamExtensions.cs index 8a5244d46..827e7694f 100644 --- a/DisCatSharp.VoiceNext/StreamExtensions.cs +++ b/DisCatSharp.VoiceNext/StreamExtensions.cs @@ -1,74 +1,74 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.VoiceNext { /// /// The stream extensions. /// public static class StreamExtensions { /// /// Asynchronously reads the bytes from the current stream and writes them to the specified . /// - /// The source + /// The source /// The target /// The size, in bytes, of the buffer. This value must be greater than zero. If , defaults to the packet size specified by . /// The token to monitor for cancellation requests. /// public static async Task CopyToAsync(this Stream source, VoiceTransmitSink destination, int? bufferSize = null, CancellationToken cancellationToken = default) { // adapted from CoreFX // https://source.dot.net/#System.Private.CoreLib/Stream.cs,8048a9680abdd13b if (source is null) throw new ArgumentNullException(nameof(source)); if (destination is null) throw new ArgumentNullException(nameof(destination)); if (bufferSize != null && bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "bufferSize cannot be less than or equal to zero"); var bufferLength = bufferSize ?? destination.SampleLength; var buffer = ArrayPool.Shared.Rent(bufferLength); try { int bytesRead; while ((bytesRead = await source.ReadAsync(buffer, 0, bufferLength, cancellationToken).ConfigureAwait(false)) != 0) { await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); } } finally { ArrayPool.Shared.Return(buffer); } } } } diff --git a/DisCatSharp.VoiceNext/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs index cb94f4006..7d5e1ce7d 100644 --- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs +++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs @@ -1,261 +1,261 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using DisCatSharp.VoiceNext.Entities; using Newtonsoft.Json; namespace DisCatSharp.VoiceNext { /// /// Represents VoiceNext extension, which acts as Discord voice client. /// public sealed class VoiceNextExtension : BaseExtension { /// /// Gets or sets the configuration. /// private VoiceNextConfiguration Configuration { get; set; } /// /// Gets or sets the active connections. /// private ConcurrentDictionary ActiveConnections { get; set; } /// /// Gets or sets the voice state updates. /// private ConcurrentDictionary> VoiceStateUpdates { get; set; } /// /// Gets or sets the voice server updates. /// private ConcurrentDictionary> VoiceServerUpdates { get; set; } /// /// Gets whether this connection has incoming voice enabled. /// public bool IsIncomingEnabled { get; } /// /// Initializes a new instance of the class. /// /// The config. internal VoiceNextExtension(VoiceNextConfiguration config) { this.Configuration = new VoiceNextConfiguration(config); this.IsIncomingEnabled = config.EnableIncoming; this.ActiveConnections = new ConcurrentDictionary(); this.VoiceStateUpdates = new ConcurrentDictionary>(); this.VoiceServerUpdates = new ConcurrentDictionary>(); } /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. - /// + /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this.Client.VoiceStateUpdated += this.Client_VoiceStateUpdate; this.Client.VoiceServerUpdated += this.Client_VoiceServerUpdate; } /// /// Create a VoiceNext connection for the specified channel. /// /// Channel to connect to. /// VoiceNext connection for this channel. public async Task ConnectAsync(DiscordChannel channel) { if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage) throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be voice or stage channel"); if (channel.Guild == null) throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be guild channel"); if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasPermission(Permissions.AccessChannels | Permissions.UseVoice)) throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel"); var gld = channel.Guild; if (this.ActiveConnections.ContainsKey(gld.Id)) throw new InvalidOperationException("This guild already has a voice connection"); var vstut = new TaskCompletionSource(); var vsrut = new TaskCompletionSource(); this.VoiceStateUpdates[gld.Id] = vstut; this.VoiceServerUpdates[gld.Id] = vsrut; var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = gld.Id, ChannelId = channel.Id, Deafened = false, Muted = false } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); var vstu = await vstut.Task.ConfigureAwait(false); var vstup = new VoiceStateUpdatePayload { SessionId = vstu.SessionId, UserId = vstu.User.Id }; var vsru = await vsrut.Task.ConfigureAwait(false); var vsrup = new VoiceServerUpdatePayload { Endpoint = vsru.Endpoint, GuildId = vsru.Guild.Id, Token = vsru.VoiceToken }; var vnc = new VoiceNextConnection(this.Client, gld, channel, this.Configuration, vsrup, vstup); vnc.VoiceDisconnected += this.Vnc_VoiceDisconnected; await vnc.ConnectAsync().ConfigureAwait(false); await vnc.WaitForReadyAsync().ConfigureAwait(false); this.ActiveConnections[gld.Id] = vnc; return vnc; } /// /// Gets a VoiceNext connection for specified guild. /// /// Guild to get VoiceNext connection for. /// VoiceNext connection for the specified guild. public VoiceNextConnection GetConnection(DiscordGuild guild) => this.ActiveConnections.ContainsKey(guild.Id) ? this.ActiveConnections[guild.Id] : null; /// /// Vnc_S the voice disconnected. /// /// The guild. /// A Task. private async Task Vnc_VoiceDisconnected(DiscordGuild guild) { VoiceNextConnection vnc = null; if (this.ActiveConnections.ContainsKey(guild.Id)) this.ActiveConnections.TryRemove(guild.Id, out vnc); var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = guild.Id, ChannelId = null } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); } /// /// Client_S the voice state update. /// /// The client. /// The e. /// A Task. private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (e.User == null) return Task.CompletedTask; if (e.User.Id == this.Client.CurrentUser.Id) { if (e.After.Channel == null && this.ActiveConnections.TryRemove(gld.Id, out var ac)) ac.Disconnect(); if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc)) vnc.TargetChannel = e.Channel; if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this.VoiceStateUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); } return Task.CompletedTask; } /// /// Client_S the voice server update. /// /// The client. /// The e. /// A Task. private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return; if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc)) { vnc.ServerData = new VoiceServerUpdatePayload { Endpoint = e.Endpoint, GuildId = e.Guild.Id, Token = e.VoiceToken }; var eps = e.Endpoint; var epi = eps.LastIndexOf(':'); var eph = string.Empty; var epp = 443; if (epi != -1) { eph = eps.Substring(0, epi); epp = int.Parse(eps.Substring(epi + 1)); } else { eph = eps; } vnc.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp }; vnc.Resume = false; await vnc.ReconnectAsync().ConfigureAwait(false); } if (this.VoiceServerUpdates.ContainsKey(gld.Id)) { this.VoiceServerUpdates.TryRemove(gld.Id, out var xe); xe.SetResult(e); } } } } diff --git a/DisCatSharp.sln b/DisCatSharp.sln index e65ed3ffe..9eb3bc6ad 100644 --- a/DisCatSharp.sln +++ b/DisCatSharp.sln @@ -1,149 +1,153 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31911.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp", "DisCatSharp\DisCatSharp.csproj", "{EB3D8310-DFAD-4295-97F9-82E253647583}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext", "DisCatSharp.VoiceNext\DisCatSharp.VoiceNext.csproj", "{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4255B64D-92EC-46B3-BC3B-ED2C3A8073EE}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore BUILDING.md = BUILDING.md CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md LICENSE.md = LICENSE.md README.md = README.md SECURITY.md = SECURITY.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.CommandsNext", "DisCatSharp.CommandsNext\DisCatSharp.CommandsNext.csproj", "{C8ED55FB-E028-468D-955F-1534C20274EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Interactivity", "DisCatSharp.Interactivity\DisCatSharp.Interactivity.csproj", "{DD32BEC3-0189-479F-86DC-CCF95E5634A9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F953F5D0-F0C9-41E6-ADBF-60A76D295899}" ProjectSection(SolutionItems) = preProject .nuget\NuGet.config = .nuget\NuGet.config EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Items", "Build Items", "{84464D70-687B-40A8-836D-C4F737698969}" ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml .github\dependabot.yml = .github\dependabot.yml DisCatSharp.targets = DisCatSharp.targets docs-oneclick-rebuild.ps1 = docs-oneclick-rebuild.ps1 .github\workflows\docs.yml = .github\workflows\docs.yml .github\workflows\dotnet.yml = .github\workflows\dotnet.yml NuGet.targets = NuGet.targets oneclick-rebuild.ps1 = oneclick-rebuild.ps1 Package.targets = Package.targets rebuild-all.ps1 = rebuild-all.ps1 rebuild-docs.ps1 = rebuild-docs.ps1 rebuild-lib.ps1 = rebuild-lib.ps1 Version.targets = Version.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{430C28D8-5F85-4D6E-AA68-211549435245}" ProjectSection(SolutionItems) = preProject .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md .github\CODEOWNERS = .github\CODEOWNERS + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\docs-preview.yml = .github\workflows\docs-preview.yml + .github\workflows\docs.yml = .github\workflows\docs.yml + .github\workflows\dotnet.yml = .github\workflows\dotnet.yml .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md .github\FUNDING.yml = .github\FUNDING.yml .github\pull_request_template.md = .github\pull_request_template.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext.Natives", "DisCatSharp.VoiceNext.Natives\DisCatSharp.VoiceNext.Natives.csproj", "{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Common", "DisCatSharp.Common\DisCatSharp.Common.csproj", "{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.ApplicationCommands", "DisCatSharp.ApplicationCommands\DisCatSharp.ApplicationCommands.csproj", "{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Configuration", "DisCatSharp.Configuration\DisCatSharp.Configuration.csproj", "{603287D3-1EF2-47F1-A611-C7F25869DE14}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Configuration.Tests", "DisCatSharp.Configuration.Tests\DisCatSharp.Configuration.Tests.csproj", "{E15E88B4-63AD-42DE-B685-D31697C62194}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting", "DisCatSharp.Hosting\DisCatSharp.Hosting.csproj", "{72CCE5D5-926B-432A-876A-065FA2BC9B7B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting.Tests", "DisCatSharp.Hosting.Tests\DisCatSharp.Hosting.Tests.csproj", "{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Hosting.DependencyInjection", "DisCatSharp.Hosting.DependencyInjection\DisCatSharp.Hosting.DependencyInjection.csproj", "{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B15E40E0-03FD-4852-B19B-2C50BCC67704}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.Build.0 = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.Build.0 = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.Build.0 = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.Build.0 = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.Build.0 = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.Build.0 = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.Build.0 = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.Build.0 = Release|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.Build.0 = Debug|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.ActiveCfg = Release|Any CPU {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.Build.0 = Release|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.Build.0 = Debug|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.ActiveCfg = Release|Any CPU {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.Build.0 = Release|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.Build.0 = Release|Any CPU {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.Build.0 = Debug|Any CPU {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.ActiveCfg = Release|Any CPU {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.Build.0 = Release|Any CPU {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {430C28D8-5F85-4D6E-AA68-211549435245} = {4255B64D-92EC-46B3-BC3B-ED2C3A8073EE} {E15E88B4-63AD-42DE-B685-D31697C62194} = {B15E40E0-03FD-4852-B19B-2C50BCC67704} {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94} = {B15E40E0-03FD-4852-B19B-2C50BCC67704} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23F3A981-51B8-4285-A38C-3267F1D25FE7} EndGlobalSection EndGlobal diff --git a/DisCatSharp.targets b/DisCatSharp.targets index 49fb0c0cb..b063ad919 100644 --- a/DisCatSharp.targets +++ b/DisCatSharp.targets @@ -1,11 +1,12 @@ 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 diff --git a/DisCatSharp/BaseExtension.cs b/DisCatSharp/BaseExtension.cs index 17ef12fdd..0d8da0f32 100644 --- a/DisCatSharp/BaseExtension.cs +++ b/DisCatSharp/BaseExtension.cs @@ -1,41 +1,41 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// - /// Represents base for all DSharpPlus extensions. To implement your own extension, extend this class, and implement its abstract members. + /// Represents base for all DisCatSharp extensions. To implement your own extension, extend this class, and implement its abstract members. /// public abstract class BaseExtension { /// /// Gets the instance of this extension is attached to. /// public DiscordClient Client { get; protected set; } /// /// Initializes this extension for given instance. /// /// Discord client to initialize for. protected internal abstract void Setup(DiscordClient client); } } diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs index 6c56bd801..98c9fa309 100644 --- a/DisCatSharp/Clients/BaseDiscordClient.cs +++ b/DisCatSharp/Clients/BaseDiscordClient.cs @@ -1,301 +1,304 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a common base for various Discord client implementations. /// public abstract class BaseDiscordClient : IDisposable { /// /// Gets the api client. /// internal protected DiscordApiClient ApiClient { get; } /// /// Gets the configuration. /// internal protected DiscordConfiguration Configuration { get; } /// /// Gets the instance of the logger for this client. /// public ILogger Logger { get; } /// /// Gets the string representing the version of bot lib. /// public string VersionString { get; } /// /// Gets the bot library name. /// public string BotLibrary { get; } /// /// Gets the library team. /// public DisCatSharpTeam LibraryDeveloperTeam => this.ApiClient.GetDisCatSharpTeamAsync().Result; /// /// Gets the current user. /// public DiscordUser CurrentUser { get; internal set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; internal set; } /// /// Gets the cached guilds for this client. /// public abstract IReadOnlyDictionary Guilds { get; } /// /// Gets the cached users for this client. /// protected internal ConcurrentDictionary UserCache { get; } /// /// Gets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// internal IServiceProvider ServiceProvider { get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voice_regions_lazy.Value; /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// protected internal ConcurrentDictionary InternalVoiceRegions { get; set; } internal Lazy> _voice_regions_lazy; /// /// Initializes this Discord API client. /// /// Configuration for this client. protected BaseDiscordClient(DiscordConfiguration config) { this.Configuration = new DiscordConfiguration(config); if (this.Configuration.LoggerFactory == null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this)); } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); this.ApiClient = new DiscordApiClient(this); this.UserCache = new ConcurrentDictionary(); this.InternalVoiceRegions = new ConcurrentDictionary(); this._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions)); var a = typeof(DiscordClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) { this.VersionString = iv.InformationalVersion; } else { var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) this.VersionString = $"{vs}, CI build {v.Revision}"; } this.BotLibrary = "DisCatSharp"; this.ServiceProvider = config.ServiceProvider; } /// /// Gets the current API application. /// /// Current API application. public async Task GetCurrentApplicationAsync() { var tapp = await this.ApiClient.GetCurrentApplicationInfoAsync().ConfigureAwait(false); var app = new DiscordApplication { Discord = this, Id = tapp.Id, Name = tapp.Name, Description = tapp.Description, Summary = tapp.Summary, IconHash = tapp.IconHash, RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null, Flags = tapp.Flags, RequiresCodeGrant = tapp.BotRequiresCodeGrant, IsPublic = tapp.IsPublicBot, PrivacyPolicyUrl = tapp.PrivacyPolicyUrl, - TermsOfServiceUrl = tapp.TermsOfServiceUrl + TermsOfServiceUrl = tapp.TermsOfServiceUrl, + CustomInstallUrl = tapp.CustomInstallUrl, + InstallParams = tapp.InstallParams, + Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray() }; // do team and owners // tbh fuck doing this properly if (tapp.Team == null) { // singular owner app.Owners = new ReadOnlyCollection(new[] { new DiscordUser(tapp.Owner) }); app.Team = null; app.TeamName = null; } else { // team owner app.Team = new DiscordTeam(tapp.Team); var members = tapp.Team.Members .Select(x => new DiscordTeamMember(x) { Team = app.Team, User = new DiscordUser(x.User) }) .ToArray(); var owners = members .Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted) .Select(x => x.User) .ToArray(); app.Owners = new ReadOnlyCollection(owners); app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId); app.Team.Members = new ReadOnlyCollection(members); app.TeamName = app.Team.Name; } app.GuildId = tapp.GuildId.HasValue ? tapp.GuildId.Value : null; app.Slug = tapp.Slug.HasValue ? tapp.Slug.Value : null; app.PrimarySkuId = tapp.PrimarySkuId.HasValue ? tapp.PrimarySkuId.Value : null; app.VerifyKey = tapp.VerifyKey.HasValue ? tapp.VerifyKey.Value : null; app.CoverImageHash = tapp.CoverImageHash.HasValue ? tapp.CoverImageHash.Value : null; return app; } /// /// Gets a list of regions /// /// /// Thrown when Discord is unable to process the request. public Task> ListVoiceRegionsAsync() => this.ApiClient.ListVoiceRegionsAsync(); /// /// Initializes this client. This method fetches information about current user, application, and voice regions. /// /// public virtual async Task InitializeAsync() { if (this.CurrentUser == null) { this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false); this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser); } if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null) this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false); if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.Count == 0) { var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false); foreach (var xvr in vrs) this.InternalVoiceRegions.TryAdd(xvr.Id, xvr); } } /// /// Gets the current gateway info for the provided token. /// If no value is provided, the configuration value will be used instead. /// /// A gateway info object. public async Task GetGatewayInfoAsync(string token = null) { if (this.Configuration.TokenType != TokenType.Bot) throw new InvalidOperationException("Only bot tokens can access this info."); if (string.IsNullOrEmpty(this.Configuration.Token)) { if (string.IsNullOrEmpty(token)) throw new InvalidOperationException("Could not locate a valid token."); this.Configuration.Token = token; var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false); this.Configuration.Token = null; return res; } return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false); } /// /// Gets a cached user. /// /// The user_id. internal DiscordUser GetCachedOrEmptyUserInternal(ulong user_id) { this.TryGetCachedUserInternal(user_id, out var user); return user; } /// /// Tries the get a cached user. /// /// The user_id. /// The user. internal bool TryGetCachedUserInternal(ulong user_id, out DiscordUser user) { if (this.UserCache.TryGetValue(user_id, out user)) return true; user = new DiscordUser { Id = user_id, Discord = this }; return false; } /// /// Disposes this client. /// public abstract void Dispose(); } } diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index df11ed7cf..96c632af8 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3054 +1,3226 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Utilities; 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 = false; #endregion #region Dispatch Handler /// /// Handles the dispatch. /// /// 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(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); DiscordChannel chn; ulong gid; ulong cid; + ulong uid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; - DiscordEvent gse = default; + DiscordScheduledEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; 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._guilds[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._guilds[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, dat).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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this._guilds[gid]).ConfigureAwait(false); break; /* Ok soooo.. this isn't documented yet It seems to be part of the next version of membership screening (https://discord.com/channels/641574644578648068/689591708962652289/845836910991507486) - Previews: https://github.com/DSharpPlus/DSharpPlus/pull/890#issuecomment-846464105 - advaith said the following (https://discord.com/channels/641574644578648068/689591708962652289/845838160047112202): > iirc it happens when a user leaves a server where they havent completed screening yet We have to wait till it's documented, but the fields are: { "user_id": "snowflake_user", "guild_id": "snowflake_guild" } We could handle it rn, but due to the fact that it isn't documented, it's not an good idea. */ 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._guilds[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": - gse = dat.ToObject(); - await this.OnGuildScheduledEventCreateEventAsync(gse).ConfigureAwait(false); + gse = dat.ToObject(); + gid = (ulong)dat["guild_id"]; + await this.OnGuildScheduledEventCreateEventAsync(gse, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_update": - gse = dat.ToObject(); - await this.OnGuildScheduledEventUpdateEventAsync(gse).ConfigureAwait(false); + gse = dat.ToObject(); + gid = (ulong)dat["guild_id"]; + await this.OnGuildScheduledEventUpdateEventAsync(gse, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_delete": - gse = dat.ToObject(); - await this.OnGuildScheduledEventDeleteEventAsync(gse).ConfigureAwait(false); + gse = dat.ToObject(); + gid = (ulong)dat["guild_id"]; + await this.OnGuildScheduledEventDeleteEventAsync(gse, this._guilds[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._guilds[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._guilds[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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this._guilds[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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this._guilds[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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this._guilds[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._guilds[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this._guilds.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._guilds[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this._guilds[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._guilds[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this._guilds[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this._guilds[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._guilds[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._guilds[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._guilds[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._guilds[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 "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._guilds[gid].GetChannel(cid), this._guilds[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. /// 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.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var raw_guild_index = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this._guilds.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } if (guild._roles == null) guild._roles = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } var raw_guild = raw_guild_index[guild.Id]; var raw_members = (JArray)raw_guild["members"]; if (guild._members != null) guild._members.Clear(); else guild._members = new ConcurrentDictionary(); if (raw_members != null) { foreach (var xj in raw_members) { 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._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; } } if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; this._guilds[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed. /// 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._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } this._guilds[channel.GuildId.Value]._channels[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 channel_new = this.InternalGetCachedChannel(channel.Id); DiscordChannel channel_old = null; if (channel_new != null) { channel_old = new DiscordChannel { Bitrate = channel_new.Bitrate, Discord = this, GuildId = channel_new.GuildId, Id = channel_new.Id, //IsPrivate = channel_new.IsPrivate, LastMessageId = channel_new.LastMessageId, Name = channel_new.Name, _permissionOverwrites = new List(channel_new._permissionOverwrites), Position = channel_new.Position, Topic = channel_new.Topic, Type = channel_new.Type, UserLimit = channel_new.UserLimit, ParentId = channel_new.ParentId, IsNSFW = channel_new.IsNSFW, PerUserRateLimit = channel_new.PerUserRateLimit, RtcRegionId = channel_new.RtcRegionId, QualityMode = channel_new.QualityMode, DefaultAutoArchiveDuration = channel_new.DefaultAutoArchiveDuration }; channel_new.Bitrate = channel.Bitrate; channel_new.Name = channel.Name; channel_new.Position = channel.Position; channel_new.Topic = channel.Topic; channel_new.UserLimit = channel.UserLimit; channel_new.ParentId = channel.ParentId; channel_new.IsNSFW = channel.IsNSFW; channel_new.PerUserRateLimit = channel.PerUserRateLimit; channel_new.Type = channel.Type; channel_new.RtcRegionId = channel.RtcRegionId; channel_new.QualityMode = channel.QualityMode; channel_new.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channel_new._permissionOverwrites.Clear(); foreach (var po in channel._permissionOverwrites) { po.Discord = this; po._channel_id = channel.Id; } channel_new._permissionOverwrites.AddRange(channel._permissionOverwrites); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld._channels[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channel_new, Guild = gld, ChannelBefore = channel_old }).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._channels.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._channels.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } guild._channels[channel.Id] = channel; } } /// /// Handles the channel pins update. /// /// The guild id. /// The channel id. /// The 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 = new DiscordActivity[xp.RawActivities.Length]; for (var i = 0; i < xp.RawActivities.Length; i++) xp._internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); } this._presences[xp.InternalUser.Id] = xp; } } var exists = this._guilds.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if(guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) - guild._scheduledEvents = new ConcurrentDictionary(); + guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild._members.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._voiceStates) guild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._channels) guild._channels[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._roles) guild._roles[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._emojis) guild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._threads) guild._threads[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stickers) guild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stageInstances) guild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._scheduledEvents) guild._scheduledEvents[kvp.Key] = kvp.Value; foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach(var xt in guild._threads.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xse in guild._scheduledEvents.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._guilds.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._guilds.ContainsKey(guild.Id)) { this._guilds[guild.Id] = guild; oldGuild = null; } else { var gld = this._guilds[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, _channels = new ConcurrentDictionary(), _threads = new ConcurrentDictionary(), _emojis = new ConcurrentDictionary(), _stickers = new ConcurrentDictionary(), _members = new ConcurrentDictionary(), _roles = new ConcurrentDictionary(), _stageInstances = new ConcurrentDictionary(), _voiceStates = new ConcurrentDictionary(), - _scheduledEvents = new ConcurrentDictionary() + _scheduledEvents = new ConcurrentDictionary() }; foreach (var kvp in gld._channels) oldGuild._channels[kvp.Key] = kvp.Value; foreach (var kvp in gld._threads) oldGuild._threads[kvp.Key] = kvp.Value; foreach (var kvp in gld._emojis) oldGuild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in gld._stickers) oldGuild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in gld._roles) oldGuild._roles[kvp.Key] = kvp.Value; foreach (var kvp in gld._voiceStates) oldGuild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in gld._members) oldGuild._members[kvp.Key] = kvp.Value; foreach (var kvp in gld._stageInstances) oldGuild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in gld._scheduledEvents) oldGuild._scheduledEvents[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this._guilds[eventGuild.Id]; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._stageInstances == null) guild._stageInstances = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) - guild._scheduledEvents = new ConcurrentDictionary(); + guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach (var xc in guild._threads.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild._scheduledEvents.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._guilds.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._guilds.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. /// If true, is large. /// 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._presences[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._emojis); guild._emojis.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild._emojis[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 raw. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) { var guild = this.InternalGetCachedGuild((ulong)raw["guild_id"]); var oldStickers = new ConcurrentDictionary(guild._stickers); guild._stickers.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._stickers[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).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); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The 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, _guild_id = 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 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, _guild_id = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion - #region Guild Event + #region Guild Scheduled Event /// /// Dispatches the event. /// /// The created event. - internal async Task OnGuildScheduledEventCreateEventAsync(DiscordEvent scheduled_event) + /// The target guild. + internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) { scheduled_event.Discord = this; - var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); - guild._scheduledEvents[scheduled_event.Id] = scheduled_event; + guild._scheduledEvents.AddOrUpdate(scheduled_event.Id, scheduled_event, (old, newScheduledEvent) => newScheduledEvent); + + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this; + this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated event. - internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordEvent scheduled_event) + /// The target guild. + internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) { - scheduled_event.Discord = this; - var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); - guild._scheduledEvents[scheduled_event.Id] = scheduled_event; + if (guild == null) + return; + + DiscordScheduledEvent old_event; + if (!guild._scheduledEvents.ContainsKey(scheduled_event.Id)) + { + old_event = null; + } else { + var ev = guild._scheduledEvents[scheduled_event.Id]; + old_event = 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 + }; + + } + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this; + this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } - await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); + if (scheduled_event.Status == ScheduledEventStatus.Completed) + { + guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); + await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false); + } + else if (scheduled_event.Status == ScheduledEventStatus.Canceled) + { + guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); + scheduled_event.Status = ScheduledEventStatus.Canceled; + await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false); + } + else + { + this.UpdateScheduledEvent(scheduled_event, guild); + await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = old_event, ScheduledEventAfter = scheduled_event, Guild = guild }).ConfigureAwait(false); + } } /// /// Dispatches the event. /// /// The deleted event. - internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordEvent scheduled_event) + /// The target guild. + internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) + { + scheduled_event.Discord = this; + + if (scheduled_event.Status == ScheduledEventStatus.Scheduled) + scheduled_event.Status = ScheduledEventStatus.Canceled; + + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this; + this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } + + await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild, Reason = scheduled_event.Status }).ConfigureAwait(false); + guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); + } + + /// + /// Dispatches the event. + /// The target event. + /// The added user id. + /// The target guild. + /// + internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guild_scheduled_event_id, ulong user_id, DiscordGuild guild) { + var scheduled_event = this.InternalGetCachedScheduledEvent(guild_scheduled_event_id) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { + Id = guild_scheduled_event_id, + GuildId = guild.Id, + Discord = this, + UserCount = 0 + }, guild); + + scheduled_event.UserCount++; + scheduled_event.Discord = this; + guild.Discord = this; + + var user = this.GetUserAsync(user_id, true).Result; + user.Discord = this; + var member = guild.Members.TryGetValue(user_id, out var mem) ? mem : guild.GetMemberAsync(user_id).Result; + member.Discord = this; + + await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, User = user, Member = member }).ConfigureAwait(false); + } + + /// + /// Dispatches the event. + /// The target event. + /// The removed user id. + /// The target guild. + /// + internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guild_scheduled_event_id, ulong user_id, DiscordGuild guild) + { + var scheduled_event = this.InternalGetCachedScheduledEvent(guild_scheduled_event_id) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent + { + Id = guild_scheduled_event_id, + GuildId = guild.Id, + Discord = this, + UserCount = 0 + }, guild); + + scheduled_event.UserCount = scheduled_event.UserCount == 0 ? 0 : scheduled_event.UserCount - 1; scheduled_event.Discord = this; - var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); - guild._scheduledEvents[scheduled_event.Id] = scheduled_event; + guild.Discord = this; + + var user = this.GetUserAsync(user_id, true).Result; + user.Discord = this; + var member = guild.Members.TryGetValue(user_id, out var mem) ? mem : guild.GetMemberAsync(user_id).Result; + member.Discord = this; - await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); + await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, 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 integration delete event. /// /// The guild. /// The integration_id. /// The application_id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integration_id, ulong? application_id) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integration_id, ApplicationId = application_id }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The 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, _guild_id = guild.Id }; guild._members[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 user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild._members.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = 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 member. /// The guild. /// The roles. /// The nick. /// If true, 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, _guild_id = guild.Id }; var nick_old = mbr.Nickname; var pending_old = mbr.IsPending; var roles_old = new ReadOnlyCollection(new List(mbr.Roles)); var cdu_old = mbr.CommunicationDisabledUntil; mbr._avatarHash = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.IsPending = pending; mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil; mbr._role_ids.Clear(); mbr._role_ids.AddRange(roles); var ea = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, TimeoutAfter = mbr.CommunicationDisabledUntil, NicknameBefore = nick_old, RolesBefore = roles_old, PendingBefore = pending_old, TimeoutBefore = cdu_old }; await this._guildMemberUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild members chunk event. /// /// The dat. 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(); var memCount = members.Count(); for (var i = 0; i < memCount; i++) { var mbr = new DiscordMember(members[i]) { Discord = this, _guild_id = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; guild._members[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild._members.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.Count(); for (var i = 0; i < presCount; i++) { var xp = presences[i]; xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp._internalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var j = 0; j < xp.RawActivities.Length; j++) xp._internalActivities[j] = new DiscordActivity(xp.RawActivities[j]); } pres.Add(xp); } 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._guild_id = guild.Id; guild._roles[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 { _guild_id = guild.Id, _color = newRole._color, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position }; newRole._guild_id = guild.Id; newRole._color = role._color; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; 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._roles.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 dat. 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 ack event. /// /// The chn. /// 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 author. /// The member. /// The reference author. /// The reference 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._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var event_message = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == event_message.Id && xm.ChannelId == event_message.ChannelId, out message)) { message = event_message; 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 = event_message.EditedTimestampRaw; if (event_message.Content != null) message.Content = event_message.Content; message._embeds.Clear(); message._embeds.AddRange(event_message._embeds); message.Pinned = event_message.Pinned; message.IsTTS = event_message.IsTTS; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The 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 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. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The mbr. /// 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, _reactions = new List() }; } var react = msg._reactions.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg._reactions.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. /// /// 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, _guild_id = 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._reactions?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg._reactions != null && react.Count <= 0) // shit happens for (var i = 0; i < msg._reactions.Count; i++) if (msg._reactions[i].Emoji == emoji) { msg._reactions.RemoveAt(i); break; } } 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 all. /// /// The message id. /// The channel id. /// The 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._reactions?.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 emoji. /// /// The message id. /// The channel id. /// The guild id. /// The dat. 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._emojis.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg._reactions?.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 /// /// Dispatches the event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Dispatches the event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId)._threads.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); } /// /// Dispatches the 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, _threadMembers = threadNew._threadMembers, 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._threads[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld._threads.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); } /// /// Dispatches the event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channel_ids.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.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); } /// /// Dispatches the 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._guilds[member._guild_id]._threads.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread); + } + thread.CurrentMember = member; thread.Guild._threads.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Dispatches the 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 thread_id, JArray added_members, JArray removed_members, int member_count) { var thread = this.InternalGetCachedThread(thread_id); + if (thread == null) + { + var tempThread = await this.ApiClient.GetThreadAsync(thread_id); + thread = guild._threads.AddOrUpdate(thread_id, tempThread, (old, newThread) => newThread); + } + thread.Discord = this; guild.Discord = this; - List addedMembers = null; - List removed_member_ids = null; + List addedMembers = new(); + List removed_member_ids = new(); if (added_members != null) { foreach (var xj in added_members) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; xtm._guild_id = guild.Id; - xtm.Member = guild._members.TryGetValue(xtm.Id, out var member) ? member : new DiscordMember { Id = xtm.Id, _guild_id = guild.Id, Discord = this }; - addedMembers.Add(xtm); + if(xtm != null) + addedMembers.Add(xtm); if (xtm.Id == this.CurrentUser.Id) thread.CurrentMember = xtm; } } var removedMembers = new List(); if (removed_members != null) { foreach (var removedId in removed_members) { removedMembers.Add(guild._members.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, _guild_id = guild.Id, Discord = this }); } } if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; thread.MemberCount = member_count; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = member_count }; 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. /// A Task. internal async Task OnEmbeddedActivityUpdateAsync(JObject tr_activity, DiscordGuild guild, ulong channel_id, JArray j_users, ulong app_id) - { - /*try + => 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); - }*/ - await Task.Delay(20); - } + } + }*/ + #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._presences.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._presences[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 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 user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usr_old = 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 = usr_old }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this._guilds[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld._voiceStates.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld._voiceStates[vstateNew.UserId] = vstateNew; } if (gld._members.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 endpoint. /// The 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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The count. /// The count. /// The count. /// The guild_id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int sc, int ucmc, int mcmc, ulong guild_id) { var guild = this.InternalGetCachedGuild(guild_id); if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = sc, UserContextMenuCommands = ucmc, MessageContextMenuCommands = mcmc, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong c_id, ulong guild_id, ulong a_id) { if (a_id != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guild_id); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guild_id, c_id); } catch(NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(c_id); } if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = a_id, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create. /// /// The guild id. /// The channel id. /// The user. /// The 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) { _guild_id = 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._guild_id = 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._guild_id = 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 (interaction.Type is InteractionType.Component) { 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 guild id. /// The started. /// The mbr. 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 the unknown event. /// /// 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/Clients/DiscordClient.Events.cs b/DisCatSharp/Clients/DiscordClient.Events.cs index 7ab64d25f..2931b39d4 100644 --- a/DisCatSharp/Clients/DiscordClient.Events.cs +++ b/DisCatSharp/Clients/DiscordClient.Events.cs @@ -1,955 +1,977 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.EventArgs; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a discord client. /// public sealed partial class DiscordClient { /// /// Gets the event execution limit. /// internal static TimeSpan EventExecutionLimit { get; } = TimeSpan.FromSeconds(1); #region WebSocket /// /// Fired whenever a WebSocket error occurs within the client. /// public event AsyncEventHandler SocketErrored { add => this._socketErrored.Register(value); remove => this._socketErrored.Unregister(value); } private AsyncEvent _socketErrored; /// /// Fired whenever WebSocket connection is established. /// public event AsyncEventHandler SocketOpened { add => this._socketOpened.Register(value); remove => this._socketOpened.Unregister(value); } private AsyncEvent _socketOpened; /// /// Fired whenever WebSocket connection is terminated. /// public event AsyncEventHandler SocketClosed { add => this._socketClosed.Register(value); remove => this._socketClosed.Unregister(value); } private AsyncEvent _socketClosed; /// /// Fired when the client enters ready state. /// public event AsyncEventHandler Ready { add => this._ready.Register(value); remove => this._ready.Unregister(value); } private AsyncEvent _ready; /// /// Fired whenever a session is resumed. /// public event AsyncEventHandler Resumed { add => this._resumed.Register(value); remove => this._resumed.Unregister(value); } private AsyncEvent _resumed; /// /// Fired on received heartbeat ACK. /// public event AsyncEventHandler Heartbeated { add => this._heartbeated.Register(value); remove => this._heartbeated.Unregister(value); } private AsyncEvent _heartbeated; #endregion #region Channel /// /// Fired when a new channel is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelCreated { add => this._channelCreated.Register(value); remove => this._channelCreated.Unregister(value); } private AsyncEvent _channelCreated; /// /// Fired when a channel is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelUpdated { add => this._channelUpdated.Register(value); remove => this._channelUpdated.Unregister(value); } private AsyncEvent _channelUpdated; /// /// Fired when a channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelDeleted { add => this._channelDeleted.Register(value); remove => this._channelDeleted.Unregister(value); } private AsyncEvent _channelDeleted; /// /// Fired when a dm channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler DmChannelDeleted { add => this._dmChannelDeleted.Register(value); remove => this._dmChannelDeleted.Unregister(value); } private AsyncEvent _dmChannelDeleted; /// /// Fired whenever a channel's pinned message list is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelPinsUpdated { add => this._channelPinsUpdated.Register(value); remove => this._channelPinsUpdated.Unregister(value); } private AsyncEvent _channelPinsUpdated; #endregion #region Guild /// /// Fired when the user joins a new guild. /// For this Event you need the intent specified in /// /// [alias="GuildJoined"][alias="JoinedGuild"] public event AsyncEventHandler GuildCreated { add => this._guildCreated.Register(value); remove => this._guildCreated.Unregister(value); } private AsyncEvent _guildCreated; /// /// Fired when a guild is becoming available. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildAvailable { add => this._guildAvailable.Register(value); remove => this._guildAvailable.Unregister(value); } private AsyncEvent _guildAvailable; /// /// Fired when a guild is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildUpdated { add => this._guildUpdated.Register(value); remove => this._guildUpdated.Unregister(value); } private AsyncEvent _guildUpdated; /// /// Fired when the user leaves or is removed from a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildDeleted { add => this._guildDeleted.Register(value); remove => this._guildDeleted.Unregister(value); } private AsyncEvent _guildDeleted; /// /// Fired when a guild becomes unavailable. /// public event AsyncEventHandler GuildUnavailable { add => this._guildUnavailable.Register(value); remove => this._guildUnavailable.Unregister(value); } private AsyncEvent _guildUnavailable; /// /// Fired when all guilds finish streaming from Discord. /// public event AsyncEventHandler GuildDownloadCompleted { add => this._guildDownloadCompletedEv.Register(value); remove => this._guildDownloadCompletedEv.Unregister(value); } private AsyncEvent _guildDownloadCompletedEv; /// /// Fired when a guilds emojis get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildEmojisUpdated { add => this._guildEmojisUpdated.Register(value); remove => this._guildEmojisUpdated.Unregister(value); } private AsyncEvent _guildEmojisUpdated; /// /// Fired when a guilds stickers get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildStickersUpdated { add => this._guildStickersUpdated.Register(value); remove => this._guildStickersUpdated.Unregister(value); } private AsyncEvent _guildStickersUpdated; /// /// Fired when a guild integration is updated. /// public event AsyncEventHandler GuildIntegrationsUpdated { add => this._guildIntegrationsUpdated.Register(value); remove => this._guildIntegrationsUpdated.Unregister(value); } private AsyncEvent _guildIntegrationsUpdated; #endregion #region Guild Ban /// /// Fired when a guild ban gets added /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanAdded { add => this._guildBanAdded.Register(value); remove => this._guildBanAdded.Unregister(value); } private AsyncEvent _guildBanAdded; /// /// Fired when a guild ban gets removed /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanRemoved { add => this._guildBanRemoved.Register(value); remove => this._guildBanRemoved.Unregister(value); } private AsyncEvent _guildBanRemoved; #endregion - #region Guild Event + #region Guild Scheduled Event /// /// Fired when a scheduled Event is created. - /// For this Event you need the intent specified in + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventCreated { add => this._guildScheduledEventCreated.Register(value); remove => this._guildScheduledEventCreated.Unregister(value); } private AsyncEvent _guildScheduledEventCreated; /// /// Fired when a scheduled Event is updated. - /// For this Event you need the intent specified in + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventUpdated { add => this._guildScheduledEventUpdated.Register(value); remove => this._guildScheduledEventUpdated.Unregister(value); } private AsyncEvent _guildScheduledEventUpdated; /// /// Fired when a scheduled Event is deleted. - /// For this Event you need the intent specified in + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventDeleted { add => this._guildScheduledEventDeleted.Register(value); remove => this._guildScheduledEventDeleted.Unregister(value); } private AsyncEvent _guildScheduledEventDeleted; + /// + /// Fired when a user subscribes to a scheduled event. + /// For this Event you need the intent specified in + /// + public event AsyncEventHandler GuildScheduledEventUserAdded + { + add => this._guildScheduledEventUserAdded.Register(value); + remove => this._guildScheduledEventUserAdded.Unregister(value); + } + private AsyncEvent _guildScheduledEventUserAdded; + + /// + /// Fired when a user unsubscribes from a scheduled event. + /// For this Event you need the intent specified in + /// + public event AsyncEventHandler GuildScheduledEventUserRemoved + { + add => this._guildScheduledEventUserRemoved.Register(value); + remove => this._guildScheduledEventUserRemoved.Unregister(value); + } + private AsyncEvent _guildScheduledEventUserRemoved; + #endregion #region Guild Integration /// /// Fired when a guild integration is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationCreated { add => this._guildIntegrationCreated.Register(value); remove => this._guildIntegrationCreated.Unregister(value); } private AsyncEvent _guildIntegrationCreated; /// /// Fired when a guild integration is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationUpdated { add => this._guildIntegrationUpdated.Register(value); remove => this._guildIntegrationUpdated.Unregister(value); } private AsyncEvent _guildIntegrationUpdated; /// /// Fired when a guild integration is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationDeleted { add => this._guildIntegrationDeleted.Register(value); remove => this._guildIntegrationDeleted.Unregister(value); } private AsyncEvent _guildIntegrationDeleted; #endregion #region Guild Member /// /// Fired when a new user joins a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberAdded { add => this._guildMemberAdded.Register(value); remove => this._guildMemberAdded.Unregister(value); } private AsyncEvent _guildMemberAdded; /// /// Fired when a user is removed from a guild (leave/kick/ban). /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberRemoved { add => this._guildMemberRemoved.Register(value); remove => this._guildMemberRemoved.Unregister(value); } private AsyncEvent _guildMemberRemoved; /// /// Fired when a guild member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberUpdated { add => this._guildMemberUpdated.Register(value); remove => this._guildMemberUpdated.Unregister(value); } private AsyncEvent _guildMemberUpdated; /// /// Fired in response to Gateway Request Guild Members. /// public event AsyncEventHandler GuildMembersChunked { add => this._guildMembersChunked.Register(value); remove => this._guildMembersChunked.Unregister(value); } private AsyncEvent _guildMembersChunked; #endregion #region Guild Role /// /// Fired when a guild role is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleCreated { add => this._guildRoleCreated.Register(value); remove => this._guildRoleCreated.Unregister(value); } private AsyncEvent _guildRoleCreated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleUpdated { add => this._guildRoleUpdated.Register(value); remove => this._guildRoleUpdated.Unregister(value); } private AsyncEvent _guildRoleUpdated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleDeleted { add => this._guildRoleDeleted.Register(value); remove => this._guildRoleDeleted.Unregister(value); } private AsyncEvent _guildRoleDeleted; #endregion #region Invite /// /// Fired when an invite is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteCreated { add => this._inviteCreated.Register(value); remove => this._inviteCreated.Unregister(value); } private AsyncEvent _inviteCreated; /// /// Fired when an invite is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteDeleted { add => this._inviteDeleted.Register(value); remove => this._inviteDeleted.Unregister(value); } private AsyncEvent _inviteDeleted; #endregion #region Message /// /// Fired when a message is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageCreated { add => this._messageCreated.Register(value); remove => this._messageCreated.Unregister(value); } private AsyncEvent _messageCreated; /// /// Fired when message is acknowledged by the user. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageAcknowledged { add => this._messageAcknowledged.Register(value); remove => this._messageAcknowledged.Unregister(value); } private AsyncEvent _messageAcknowledged; /// /// Fired when a message is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageUpdated { add => this._messageUpdated.Register(value); remove => this._messageUpdated.Unregister(value); } private AsyncEvent _messageUpdated; /// /// Fired when a message is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageDeleted { add => this._messageDeleted.Register(value); remove => this._messageDeleted.Unregister(value); } private AsyncEvent _messageDeleted; /// /// Fired when multiple messages are deleted at once. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessagesBulkDeleted { add => this._messagesBulkDeleted.Register(value); remove => this._messagesBulkDeleted.Unregister(value); } private AsyncEvent _messagesBulkDeleted; #endregion #region Message Reaction /// /// Fired when a reaction gets added to a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionAdded { add => this._messageReactionAdded.Register(value); remove => this._messageReactionAdded.Unregister(value); } private AsyncEvent _messageReactionAdded; /// /// Fired when a reaction gets removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemoved { add => this._messageReactionRemoved.Register(value); remove => this._messageReactionRemoved.Unregister(value); } private AsyncEvent _messageReactionRemoved; /// /// Fired when all reactions get removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionsCleared { add => this._messageReactionsCleared.Register(value); remove => this._messageReactionsCleared.Unregister(value); } private AsyncEvent _messageReactionsCleared; /// /// Fired when all reactions of a specific reaction are removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemovedEmoji { add => this._messageReactionRemovedEmoji.Register(value); remove => this._messageReactionRemovedEmoji.Unregister(value); } private AsyncEvent _messageReactionRemovedEmoji; #endregion #region Activities /// /// Fired when a embedded activity has been updated. /// public event AsyncEventHandler EmbeddedActivityUpdated { add => this._embeddedActivityUpdated.Register(value); remove => this._embeddedActivityUpdated.Unregister(value); } private AsyncEvent _embeddedActivityUpdated; #endregion #region Presence/User Update /// /// Fired when a presence has been updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler PresenceUpdated { add => this._presenceUpdated.Register(value); remove => this._presenceUpdated.Unregister(value); } private AsyncEvent _presenceUpdated; /// /// Fired when the current user updates their settings. /// For this Event you need the intent specified in /// public event AsyncEventHandler UserSettingsUpdated { add => this._userSettingsUpdated.Register(value); remove => this._userSettingsUpdated.Unregister(value); } private AsyncEvent _userSettingsUpdated; /// /// Fired when properties about the current user change. /// /// /// NB: This event only applies for changes to the current user, the client that is connected to Discord. /// For this Event you need the intent specified in /// public event AsyncEventHandler UserUpdated { add => this._userUpdated.Register(value); remove => this._userUpdated.Unregister(value); } private AsyncEvent _userUpdated; #endregion #region Stage Instance /// /// Fired when a Stage Instance is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceCreated { add => this._stageInstanceCreated.Register(value); remove => this._stageInstanceCreated.Unregister(value); } private AsyncEvent _stageInstanceCreated; /// /// Fired when a Stage Instance is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceUpdated { add => this._stageInstanceUpdated.Register(value); remove => this._stageInstanceUpdated.Unregister(value); } private AsyncEvent _stageInstanceUpdated; /// /// Fired when a Stage Instance is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceDeleted { add => this._stageInstanceDeleted.Register(value); remove => this._stageInstanceDeleted.Unregister(value); } private AsyncEvent _stageInstanceDeleted; #endregion #region Thread /// /// Fired when a thread is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadCreated { add => this._threadCreated.Register(value); remove => this._threadCreated.Unregister(value); } private AsyncEvent _threadCreated; /// /// Fired when a thread is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadUpdated { add => this._threadUpdated.Register(value); remove => this._threadUpdated.Unregister(value); } private AsyncEvent _threadUpdated; /// /// Fired when a thread is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadDeleted { add => this._threadDeleted.Register(value); remove => this._threadDeleted.Unregister(value); } private AsyncEvent _threadDeleted; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadListSynced { add => this._threadListSynced.Register(value); remove => this._threadListSynced.Unregister(value); } private AsyncEvent _threadListSynced; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadMemberUpdated { add => this._threadMemberUpdated.Register(value); remove => this._threadMemberUpdated.Unregister(value); } private AsyncEvent _threadMemberUpdated; /// /// Fired when the thread members are updated. /// For this Event you need the or intent specified in /// public event AsyncEventHandler ThreadMembersUpdated { add => this._threadMembersUpdated.Register(value); remove => this._threadMembersUpdated.Unregister(value); } private AsyncEvent _threadMembersUpdated; #endregion #region Voice /// /// Fired when someone joins/leaves/moves voice channels. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceStateUpdated { add => this._voiceStateUpdated.Register(value); remove => this._voiceStateUpdated.Unregister(value); } private AsyncEvent _voiceStateUpdated; /// /// Fired when a guild's voice server is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceServerUpdated { add => this._voiceServerUpdated.Register(value); remove => this._voiceServerUpdated.Unregister(value); } private AsyncEvent _voiceServerUpdated; #endregion #region Application /// /// Fired when a new application command is registered. /// public event AsyncEventHandler ApplicationCommandCreated { add => this._applicationCommandCreated.Register(value); remove => this._applicationCommandCreated.Unregister(value); } private AsyncEvent _applicationCommandCreated; /// /// Fired when an application command is updated. /// public event AsyncEventHandler ApplicationCommandUpdated { add => this._applicationCommandUpdated.Register(value); remove => this._applicationCommandUpdated.Unregister(value); } private AsyncEvent _applicationCommandUpdated; /// /// Fired when an application command is deleted. /// public event AsyncEventHandler ApplicationCommandDeleted { add => this._applicationCommandDeleted.Register(value); remove => this._applicationCommandDeleted.Unregister(value); } private AsyncEvent _applicationCommandDeleted; /// /// Fired when a new application command is registered. /// public event AsyncEventHandler GuildApplicationCommandCountUpdated { add => this._guildApplicationCommandCountUpdated.Register(value); remove => this._guildApplicationCommandCountUpdated.Unregister(value); } private AsyncEvent _guildApplicationCommandCountUpdated; /// /// Fired when a user uses a context menu. /// public event AsyncEventHandler ContextMenuInteractionCreated { add => this._contextMenuInteractionCreated.Register(value); remove => this._contextMenuInteractionCreated.Unregister(value); } private AsyncEvent _contextMenuInteractionCreated; /// /// Fired when application command permissions gets updated. /// public event AsyncEventHandler ApplicationCommandPermissionsUpdated { add => this._applicationCommandPermissionsUpdated.Register(value); remove => this._applicationCommandPermissionsUpdated.Unregister(value); } private AsyncEvent _applicationCommandPermissionsUpdated; #endregion #region Misc /// /// Fired when an interaction is invoked. /// public event AsyncEventHandler InteractionCreated { add => this._interactionCreated.Register(value); remove => this._interactionCreated.Unregister(value); } private AsyncEvent _interactionCreated; /// /// Fired when a component is invoked. /// public event AsyncEventHandler ComponentInteractionCreated { add => this._componentInteractionCreated.Register(value); remove => this._componentInteractionCreated.Unregister(value); } private AsyncEvent _componentInteractionCreated; /// /// Fired when a user starts typing in a channel. /// public event AsyncEventHandler TypingStarted { add => this._typingStarted.Register(value); remove => this._typingStarted.Unregister(value); } private AsyncEvent _typingStarted; /// /// Fired when an unknown event gets received. /// public event AsyncEventHandler UnknownEvent { add => this._unknownEvent.Register(value); remove => this._unknownEvent.Unregister(value); } private AsyncEvent _unknownEvent; /// /// Fired whenever webhooks update. /// public event AsyncEventHandler WebhooksUpdated { add => this._webhooksUpdated.Register(value); remove => this._webhooksUpdated.Unregister(value); } private AsyncEvent _webhooksUpdated; /// /// Fired whenever an error occurs within an event handler. /// public event AsyncEventHandler ClientErrored { add => this._clientErrored.Register(value); remove => this._clientErrored.Unregister(value); } private AsyncEvent _clientErrored; #endregion #region Error Handling /// /// Events the error handler. /// /// The async event. /// The ex. /// The handler. /// The sender. /// The event args. internal void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, TSender sender, TArgs eventArgs) where TArgs : AsyncEventArgs { if (ex is AsyncEventTimeoutException) { this.Logger.LogWarning(LoggerEvents.EventHandlerException, $"An event handler for {asyncEvent.Name} took too long to execute. Defined as \"{handler.Method.ToString().Replace(handler.Method.ReturnType.ToString(), "").TrimStart()}\" located in \"{handler.Method.DeclaringType}\"."); return; } this.Logger.LogError(LoggerEvents.EventHandlerException, ex, "Event handler exception for event {0} thrown from {1} (defined in {2})", asyncEvent.Name, handler.Method, handler.Method.DeclaringType); this._clientErrored.InvokeAsync(this, new ClientErrorEventArgs(this.ServiceProvider) { EventName = asyncEvent.Name, Exception = ex }).ConfigureAwait(false).GetAwaiter().GetResult(); } /// /// Fired on heartbeat attempt cancellation due to too many failed heartbeats. /// public event AsyncEventHandler Zombied { add => this._zombied.Register(value); remove => this._zombied.Unregister(value); } private AsyncEvent _zombied; /// /// Fired when a gateway /// public event AsyncEventHandler PayloadReceived { add => this._payloadReceived.Register(value); remove => this._payloadReceived.Unregister(value); } private AsyncEvent _payloadReceived; /// /// Goofing. /// /// The async event. /// The ex. /// The handler. /// The sender. /// The event args. private void Goof(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, TSender sender, TArgs eventArgs) where TArgs : AsyncEventArgs => this.Logger.LogCritical(LoggerEvents.EventHandlerException, ex, "Exception event handler {0} (defined in {1}) threw an exception", handler.Method, handler.Method.DeclaringType); #endregion } } diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index 70b9f06ee..d1d482964 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1237 +1,1314 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using DisCatSharp.Enums; using System.Globalization; namespace DisCatSharp { /// /// A Discord API wrapper. /// public sealed partial class DiscordClient : BaseDiscordClient { #region Internal Fields/Properties internal bool _isShard = false; /// /// Gets the message cache. /// internal RingBuffer MessageCache { get; } private List _extensions = new(); private StatusUpdate _status = null; /// /// Gets the connection lock. /// private ManualResetEventSlim ConnectionLock { get; } = new ManualResetEventSlim(true); #endregion #region Public Fields/Properties /// /// Gets the gateway protocol version. /// public int GatewayVersion { get; internal set; } /// /// Gets the gateway session information for this client. /// public GatewayInfo GatewayInfo { get; internal set; } /// /// Gets the gateway URL. /// public Uri GatewayUri { get; internal set; } /// /// Gets the total number of shards the bot is connected to. /// public int ShardCount => this.GatewayInfo != null ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; /// /// Gets the currently connected shard ID. /// public int ShardId => this.Configuration.ShardId; /// /// Gets the intents configured for this client. /// public DiscordIntents Intents => this.Configuration.Intents; /// /// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the /// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the /// or events haven't been fired yet) /// public override IReadOnlyDictionary Guilds { get; } internal ConcurrentDictionary _guilds = new(); /// /// Gets the WS latency for this client. /// public int Ping => Volatile.Read(ref this._ping); private int _ping; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary Presences => this._presencesLazy.Value; internal Dictionary _presences = new(); private Lazy> _presencesLazy; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary EmbeddedActivities => this._embeddedActivitiesLazy.Value; internal Dictionary _embeddedActivities = new(); private Lazy> _embeddedActivitiesLazy; #endregion #region Constructor/Internal Setup /// /// Initializes a new instance of . /// /// Specifies configuration parameters. public DiscordClient(DiscordConfiguration config) : base(config) { if (this.Configuration.MessageCacheSize > 0) { var intents = this.Configuration.Intents; this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages) ? new RingBuffer(this.Configuration.MessageCacheSize) : null; } this.InternalSetup(); this.Guilds = new ReadOnlyConcurrentDictionary(this._guilds); } /// /// Internal setup of the Client. /// internal void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", EventExecutionLimit, this.EventErrorHandler); + this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", EventExecutionLimit, this.EventErrorHandler); + this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guilds.Clear(); this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this._presences)); this._embeddedActivitiesLazy = new Lazy>(() => new ReadOnlyDictionary(this._embeddedActivities)); } #endregion #region Client Extension Methods /// /// Registers an extension with this client. /// /// Extension to register. public void AddExtension(BaseExtension ext) { ext.Setup(this); this._extensions.Add(ext); } /// /// Retrieves a previously-registered extension from this client. /// /// Type of extension to retrieve. /// The requested extension. public T GetExtension() where T : BaseExtension => this._extensions.FirstOrDefault(x => x.GetType() == typeof(T)) as T; #endregion #region Public Connection Methods /// /// Connects to the gateway. /// - /// Thrown when an invalid token was provided. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid token was provided. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? status = null, DateTimeOffset? idlesince = null) { // Check if connection lock is already set, and set it if it isn't if (!this.ConnectionLock.Wait(0)) throw new InvalidOperationException("This client is already connected."); this.ConnectionLock.Set(); var w = 7500; var i = 5; var s = false; Exception cex = null; if (activity == null && status == null && idlesince == null) this._status = null; else { var since_unix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; this._status = new StatusUpdate() { Activity = new TransportActivity(activity), Status = status ?? UserStatus.Online, IdleSince = since_unix, IsAFK = idlesince != null, _activity = activity }; } if (!this._isShard) { if (this.Configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this.BotLibrary, this.VersionString); } while (i-- > 0 || this.Configuration.ReconnectIndefinitely) { try { await this.InternalConnectAsync().ConfigureAwait(false); s = true; break; } catch (UnauthorizedException e) { FailConnection(this.ConnectionLock); throw new Exception("Authentication failed. Check your token and try again.", e); } catch (PlatformNotSupportedException) { FailConnection(this.ConnectionLock); throw; } catch (NotImplementedException) { FailConnection(this.ConnectionLock); throw; } catch (Exception ex) { FailConnection(null); cex = ex; if (i <= 0 && !this.Configuration.ReconnectIndefinitely) break; this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {0}s", w / 1000); await Task.Delay(w).ConfigureAwait(false); if (i > 0) w *= 2; } } if (!s && cex != null) { this.ConnectionLock.Set(); throw new Exception("Could not connect to Discord.", cex); } // non-closure, hence args static void FailConnection(ManualResetEventSlim cl) => // unlock this (if applicable) so we can let others attempt to connect cl?.Set(); } /// /// Reconnects to the gateway. /// /// If true, start new session. public Task ReconnectAsync(bool startNewSession = false) => this.InternalReconnectAsync(startNewSession, code: startNewSession ? 1000 : 4002); /// /// Disconnects from the gateway. /// /// public async Task DisconnectAsync() { this.Configuration.AutoReconnect = false; if (this._webSocketClient != null) await this._webSocketClient.DisconnectAsync().ConfigureAwait(false); } #endregion #region Public REST Methods /// /// Gets a user. /// /// Id of the user /// Whether to fetch the user again (Defaults to false). /// The requested user. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetUserAsync(ulong userId, bool fetch = false) { - if (!fetch && this.TryGetCachedUserInternal(userId, out var usr)) + if (this.TryGetCachedUserInternal(userId, out var usr)) return usr; + else if (!fetch) + return new DiscordUser { Id = userId, Discord = this }; usr = await this.ApiClient.GetUserAsync(userId).ConfigureAwait(false); usr = this.UserCache.AddOrUpdate(userId, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old._bannerColor = usr._bannerColor; return old; }); return usr; } /// /// Gets a channel. /// /// The id of the channel to get. /// The requested channel. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetChannelAsync(ulong id) => this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false); /// /// Gets a thread. /// /// The id of the thread to get. /// The requested thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetThreadAsync(ulong id) => this.InternalGetCachedThread(id) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false); /// /// Sends a normal message. /// /// Channel to send to. /// Message content to send. /// The message that was sent. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content) => this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with an embed. /// /// Channel to send to. /// Embed to attach to the message. /// The message that was sent. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with content and an embed. /// /// Channel to send to. /// Message content to send. /// Embed to attach to the message. /// The message that was sent. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// - /// Sends a message with the . + /// Sends a message with the . /// /// Channel to send the message to. /// The message builder. /// The message that was sent. - /// Thrown when the client does not have the permission if TTS is false and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission if TTS is false and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) => this.ApiClient.CreateMessageAsync(channel.Id, builder); /// - /// Sends a message with an . + /// Sends a message with an . /// /// Channel to send the message to. /// The message builder. /// The message that was sent. - /// Thrown when the client does not have the permission if TTS is false and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission if TTS is false and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.ApiClient.CreateMessageAsync(channel.Id, builder); } /// /// Creates a guild. This requires the bot to be in less than 10 guilds total. /// /// Name of the guild. /// Voice region of the guild. /// Stream containing the icon for the guild. /// Verification level for the guild. /// Default message notification settings for the guild. /// System channel flags fopr the guild. /// The created guild. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null) { var iconb64 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) using (var imgtool = new ImageTool(icon.Value)) iconb64 = imgtool.GetBase64(); else if (icon.HasValue) iconb64 = null; return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); } /// /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. /// /// The template code. /// Name of the guild. /// Stream containing the icon for the guild. /// The created guild. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) { var iconb64 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) using (var imgtool = new ImageTool(icon.Value)) iconb64 = imgtool.GetBase64(); else if (icon.HasValue) iconb64 = null; return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } /// /// Executes a raw request. /// /// /// /// var request = await Client.ExecuteRawRequestAsync(RestRequestMethod.GET, $"{Endpoints.CHANNELS}/243184972190742178964/{Endpoints.INVITES}"); /// List<DiscordInvite> invites = DiscordJson.ToDiscordObject<List<DiscordInvite>>(request.Response); /// /// /// The method. /// The route. + /// The route parameters. /// The json body. /// The addditional headers. - /// Thrown when the ressource does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// The ratelimit wait override. + /// Thrown when the ressource does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. /// A awaitable RestResponse - public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, string jsonBody = null, Dictionary additionalHeaders = null) + public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, object routeParams, string jsonBody = null, Dictionary additionalHeaders = null, double? ratelimitWaitOverride = null) { - var bucket = this.ApiClient.Rest.GetBucket(method, route, null, out var path); + var bucket = this.ApiClient.Rest.GetBucket(method, route, routeParams, out var path); var url = Utilities.GetApiUriFor(path, this.Configuration); - var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody)); + var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody), ratelimitWaitOverride); return res; } /// /// Gets a guild. /// Setting to true will make a REST request. /// /// The guild ID to search for. /// Whether to include approximate presence and member counts in the returned guild. /// The requested Guild. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetGuildAsync(ulong id, bool? withCounts = null) { if (this._guilds.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value)) return guild; guild = await this.ApiClient.GetGuildAsync(id, withCounts).ConfigureAwait(false); var channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); foreach (var channel in channels) guild._channels[channel.Id] = channel; return guild; } /// /// Gets a guild preview. /// /// The guild ID. /// - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetGuildPreviewAsync(ulong id) => this.ApiClient.GetGuildPreviewAsync(id); /// /// Gets an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested Invite. - /// Thrown when the invite does not exists. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the invite does not exists. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId); /// /// Gets a list of connections. /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetConnectionsAsync() => this.ApiClient.GetUsersConnectionsAsync(); /// /// Gets a sticker. /// /// The requested sticker. /// The id of the sticker. - /// Thrown when the sticker does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the sticker does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetStickerAsync(ulong id) => this.ApiClient.GetStickerAsync(id); /// /// Gets all nitro sticker packs. /// /// List of sticker packs. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetStickerPacksAsync() => this.ApiClient.GetStickerPacksAsync(); /// /// Gets the In-App OAuth Url. /// - /// Defaults to . + /// Defaults to . /// Redirect Uri. /// Defaults to . /// The OAuth Url public Uri GetInAppOAuth(Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null) { permissions &= PermissionMethods.FULL_PERMS; // hey look, it's not all annoying and blue :P return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.CurrentApplication.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", OAuth.ResolveScopes(scopes)) .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .AddParameter("state", "") .AddParameter("redirect_uri", redir ?? "") .ToString()); } /// /// Gets a webhook. /// /// The target webhook id. /// The requested webhook. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetWebhookAsync(ulong id) => this.ApiClient.GetWebhookAsync(id); /// /// Gets a webhook. /// /// The target webhook id. /// The target webhook token. /// The requested webhook. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetWebhookWithTokenAsync(ulong id, string token) => this.ApiClient.GetWebhookWithTokenAsync(id, token); /// /// Updates current user's activity and status. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) => this.InternalUpdateStatusAsync(activity, userStatus, idleSince); /// /// Edits current user. /// /// New username. /// New avatar. /// The modified user. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task UpdateCurrentUserAsync(string username = null, Optional avatar = default) { var av64 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) using (var imgtool = new ImageTool(avatar.Value)) av64 = imgtool.GetBase64(); else if (avatar.HasValue) av64 = null; var usr = await this.ApiClient.ModifyCurrentUserAsync(username, av64).ConfigureAwait(false); this.CurrentUser.Username = usr.Username; this.CurrentUser.Discriminator = usr.Discriminator; this.CurrentUser.AvatarHash = usr.AvatarHash; return this.CurrentUser; } /// /// Gets a guild template by the code. /// /// The code of the template. /// The guild template for the code. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetTemplateAsync(string code) => this.ApiClient.GetTemplateAsync(code); /// /// Gets all the global application commands for this application. /// /// A list of global application commands. public Task> GetGlobalApplicationCommandsAsync() => this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); /// /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. /// /// The list of commands to overwrite with. /// The list of global commands. public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// /// Creates or overwrites a global application command. /// /// The command to create. /// The created command. public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// /// Gets a global application command by its id. /// /// The id of the command to get. /// The command with the id. public Task GetGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Edits a global application command. /// /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission).ConfigureAwait(false); } /// /// Deletes a global application command. /// /// The id of the command to delete. public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets all the application commands for a guild. /// /// The id of the guild to get application commands for. /// A list of application commands in the guild. public Task> GetGuildApplicationCommandsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); /// /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. /// /// The id of the guild. /// The list of commands to overwrite with. /// The list of guild commands. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// /// Creates or overwrites a guild application command. /// /// The id of the guild to create the application command in. /// The command to create. /// The created command. public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// /// Gets a application command in a guild by its id. /// /// The id of the guild the application command is in. /// The id of the command to get. /// The command with the id. public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. /// /// The id of the guild the application command is in. /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission).ConfigureAwait(false); } /// /// Deletes a application command in a guild. /// /// The id of the guild to delete the application command in. /// The id of the command. public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Gets all command permissions for a guild. /// /// The target guild. public Task> GetGuildApplicationCommandPermissionsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); /// /// Gets the permissions for a guild command. /// /// The target guild. /// The target command id. public Task GetApplicationCommandPermissionAsync(ulong guildId, ulong commandId) => this.ApiClient.GetApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Overwrites the existing permissions for a application command in a guild. New permissions are automatically created and missing permissions are deleted. /// A command takes up to 10 permission overwrites. /// /// The id of the guild. /// The id of the command. /// List of permissions. public Task OverwriteGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) => this.ApiClient.OverwriteGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); /// /// Overwrites the existing application command permissions in a guild. New permissions are automatically created and missing permissions are deleted. /// Each command takes up to 10 permission overwrites. /// /// The id of the guild. /// The list of permissions to overwrite with. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable permissionsOverwrites) => this.ApiClient.BulkOverwriteApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissionsOverwrites); #endregion #region Internal Caching Methods /// /// Gets the internal chached threads. /// /// The target thread id. /// The requested thread. internal DiscordThreadChannel InternalGetCachedThread(ulong threadId) { foreach (var guild in this.Guilds.Values) if (guild.Threads.TryGetValue(threadId, out var foundThread)) return foundThread; return null; } + + /// + /// Gets the internal chached scheduled event. + /// + /// The target scheduled event id. + /// The requested scheduled event. + internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId) + { + foreach (var guild in this.Guilds.Values) + if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent)) + return foundScheduledEvent; + + return null; + } + /// /// Gets the internal chached channel. /// /// The target channel id. /// The requested channel. internal DiscordChannel InternalGetCachedChannel(ulong channelId) { foreach (var guild in this.Guilds.Values) if (guild.Channels.TryGetValue(channelId, out var foundChannel)) return foundChannel; return null; } /// /// Gets the internal chached guild. /// /// The target guild id. /// The requested guild. internal DiscordGuild InternalGetCachedGuild(ulong? guildId) { if (this._guilds != null && guildId.HasValue) { if (this._guilds.TryGetValue(guildId.Value, out var guild)) return guild; } return null; } /// /// Updates a message. /// /// The message to update. /// The author to update. /// The guild to update. /// The member to update. private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) { if (author != null) { var usr = new DiscordUser(author) { Discord = this }; if (member != null) member.User = author; message.Author = this.UpdateUser(usr, guild?.Id, guild, member); } var channel = this.InternalGetCachedChannel(message.ChannelId); if (channel != null) return; channel = !message.GuildId.HasValue ? new DiscordDmChannel { Id = message.ChannelId, Discord = this, Type = ChannelType.Private } : new DiscordChannel { Id = message.ChannelId, Discord = this }; message.Channel = channel; } + /// + /// Updates a scheduled event. + /// + /// The scheduled event to update. + /// The guild to update. + /// The updated scheduled event. + private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) + { + if (scheduledEvent != null) + { + _ = guild._scheduledEvents.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) => + { + old.Discord = this; + old.Description = scheduledEvent.Description; + old.ChannelId = scheduledEvent.ChannelId; + old.EntityId = scheduledEvent.EntityId; + old.EntityType = scheduledEvent.EntityType; + old.EntityMetadata = scheduledEvent.EntityMetadata; + old.PrivacyLevel = scheduledEvent.PrivacyLevel; + old.Name = scheduledEvent.Name; + old.Status = scheduledEvent.Status; + old.UserCount = scheduledEvent.UserCount; + old.ScheduledStartTimeRaw = scheduledEvent.ScheduledStartTimeRaw; + old.ScheduledEndTimeRaw = scheduledEvent.ScheduledEndTimeRaw; + return old; + }); + } + + return scheduledEvent; + } + /// /// Updates a user. /// /// The user to update. /// The guild id to update. /// The guild to update. /// The member to update. /// The updated user. private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr) { if (mbr != null) { if (mbr.User != null) { usr = new DiscordUser(mbr.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); usr = new DiscordMember(mbr) { Discord = this, _guild_id = guildId.Value }; } var intents = this.Configuration.Intents; DiscordMember member = default; if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large. { if (guild?._members.TryGetValue(usr.Id, out member) == false) { if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it { guild._members.TryAdd(usr.Id, (DiscordMember)usr); } } else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. { if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events { _ = guild._members.TryUpdate(usr.Id, (DiscordMember)usr, member); } } } } else if (usr.Username != null) // check if not a skeleton user { _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } return usr; } + /// + /// Updates the cached events in a guild. + /// + /// The guild. + /// The raw events. + private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray rawEvents) + { + if (this._disposed) + return; + + if (rawEvents != null) + { + guild._scheduledEvents.Clear(); + + foreach (var xj in rawEvents) + { + var xtm = xj.ToDiscordObject(); + + xtm.Discord = this; + + guild._scheduledEvents[xtm.Id] = xtm; + } + } + } + /// /// Updates the cached guild. /// /// The new guild. /// The raw members. private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) { if (this._disposed) return; if (!this._guilds.ContainsKey(newGuild.Id)) this._guilds[newGuild.Id] = newGuild; var guild = this._guilds[newGuild.Id]; if (newGuild._channels != null && newGuild._channels.Count > 0) { foreach (var channel in newGuild._channels.Values) { if (guild._channels.TryGetValue(channel.Id, out _)) continue; foreach (var overwrite in channel._permissionOverwrites) { overwrite.Discord = this; overwrite._channel_id = channel.Id; } guild._channels[channel.Id] = channel; } } if (newGuild._threads != null && newGuild._threads.Count > 0) { foreach (var thread in newGuild._threads.Values) { if (guild._threads.TryGetValue(thread.Id, out _)) continue; guild._threads[thread.Id] = thread; } } if (newGuild._scheduledEvents != null && newGuild._scheduledEvents.Count > 0) { foreach (var s_event in newGuild._scheduledEvents.Values) { if (guild._scheduledEvents.TryGetValue(s_event.Id, out _)) continue; guild._scheduledEvents[s_event.Id] = s_event; } } foreach (var newEmoji in newGuild._emojis.Values) _ = guild._emojis.GetOrAdd(newEmoji.Id, _ => newEmoji); foreach (var newSticker in newGuild._stickers.Values) _ = guild._stickers.GetOrAdd(newSticker.Id, _ => newSticker); foreach (var newStageInstance in newGuild._stageInstances.Values) _ = guild._stageInstances.GetOrAdd(newStageInstance.Id, _ => newStageInstance); if (rawMembers != null) { guild._members.Clear(); foreach (var xj in rawMembers) { var xtm = xj.ToDiscordObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; old.PremiumType = xu.PremiumType; return old; }); guild._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; } } foreach (var role in newGuild._roles.Values) { if (guild._roles.TryGetValue(role.Id, out _)) continue; role._guild_id = guild.Id; guild._roles[role.Id] = role; } guild.Name = newGuild.Name; guild.AfkChannelId = newGuild.AfkChannelId; guild.AfkTimeout = newGuild.AfkTimeout; guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; guild.RawFeatures = newGuild.RawFeatures; guild.IconHash = newGuild.IconHash; guild.MfaLevel = newGuild.MfaLevel; guild.OwnerId = newGuild.OwnerId; guild.VoiceRegionId = newGuild.VoiceRegionId; guild.SplashHash = newGuild.SplashHash; guild.VerificationLevel = newGuild.VerificationLevel; guild.WidgetEnabled = newGuild.WidgetEnabled; guild.WidgetChannelId = newGuild.WidgetChannelId; guild.ExplicitContentFilter = newGuild.ExplicitContentFilter; guild.PremiumTier = newGuild.PremiumTier; guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; guild.BannerHash = newGuild.BannerHash; guild.Description = newGuild.Description; guild.VanityUrlCode = newGuild.VanityUrlCode; guild.SystemChannelId = newGuild.SystemChannelId; guild.SystemChannelFlags = newGuild.SystemChannelFlags; guild.DiscoverySplashHash = newGuild.DiscoverySplashHash; guild.MaxMembers = newGuild.MaxMembers; guild.MaxPresences = newGuild.MaxPresences; guild.ApproximateMemberCount = newGuild.ApproximateMemberCount; guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; guild.PreferredLocale = newGuild.PreferredLocale; guild.RulesChannelId = newGuild.RulesChannelId; guild.PublicUpdatesChannelId = newGuild.PublicUpdatesChannelId; guild.ApplicationId = newGuild.ApplicationId; // fields not sent for update: // - guild.Channels // - voice states // - guild.JoinedAt = new_guild.JoinedAt; // - guild.Large = new_guild.Large; // - guild.MemberCount = Math.Max(new_guild.MemberCount, guild._members.Count); // - guild.Unavailable = new_guild.Unavailable; } /// /// Populates the message reactions and cache. /// /// The message. /// The author. /// The member. private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) { var guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message.GuildId); this.UpdateMessage(message, author, guild, member); if (message._reactions == null) message._reactions = new List(); foreach (var xr in message._reactions) xr.Emoji.Discord = this; if (this.Configuration.MessageCacheSize > 0 && message.Channel != null) this.MessageCache?.Add(message); } #endregion #region Disposal ~DiscordClient() { this.Dispose(); } private bool _disposed; /// /// Disposes the client. /// public override void Dispose() { if (this._disposed) return; this._disposed = true; GC.SuppressFinalize(this); this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this.ApiClient.Rest.Dispose(); this.CurrentUser = null; var extensions = this._extensions; // prevent _extensions being modified during dispose this._extensions = null; foreach (var extension in extensions) if (extension is IDisposable disposable) disposable.Dispose(); try { this._cancelTokenSource?.Cancel(); this._cancelTokenSource?.Dispose(); } catch { } this._guilds = null; this._heartbeatTask = null; } #endregion } } diff --git a/DisCatSharp/Clients/DiscordShardedClient.Events.cs b/DisCatSharp/Clients/DiscordShardedClient.Events.cs index 8ba77f261..bb55fac9a 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.Events.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.Events.cs @@ -1,1610 +1,1650 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.EventArgs; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a discord sharded client. /// public sealed partial class DiscordShardedClient { #region WebSocket /// /// Fired whenever a WebSocket error occurs within the client. /// public event AsyncEventHandler SocketErrored { add => this._socketErrored.Register(value); remove => this._socketErrored.Unregister(value); } private AsyncEvent _socketErrored; /// /// Fired whenever WebSocket connection is established. /// public event AsyncEventHandler SocketOpened { add => this._socketOpened.Register(value); remove => this._socketOpened.Unregister(value); } private AsyncEvent _socketOpened; /// /// Fired whenever WebSocket connection is terminated. /// public event AsyncEventHandler SocketClosed { add => this._socketClosed.Register(value); remove => this._socketClosed.Unregister(value); } private AsyncEvent _socketClosed; /// /// Fired when the client enters ready state. /// public event AsyncEventHandler Ready { add => this._ready.Register(value); remove => this._ready.Unregister(value); } private AsyncEvent _ready; /// /// Fired whenever a session is resumed. /// public event AsyncEventHandler Resumed { add => this._resumed.Register(value); remove => this._resumed.Unregister(value); } private AsyncEvent _resumed; /// /// Fired on received heartbeat ACK. /// public event AsyncEventHandler Heartbeated { add => this._heartbeated.Register(value); remove => this._heartbeated.Unregister(value); } private AsyncEvent _heartbeated; #endregion #region Channel /// /// Fired when a new channel is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelCreated { add => this._channelCreated.Register(value); remove => this._channelCreated.Unregister(value); } private AsyncEvent _channelCreated; /// /// Fired when a channel is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelUpdated { add => this._channelUpdated.Register(value); remove => this._channelUpdated.Unregister(value); } private AsyncEvent _channelUpdated; /// /// Fired when a channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelDeleted { add => this._channelDeleted.Register(value); remove => this._channelDeleted.Unregister(value); } private AsyncEvent _channelDeleted; /// /// Fired when a dm channel is deleted /// For this Event you need the intent specified in /// public event AsyncEventHandler DmChannelDeleted { add => this._dmChannelDeleted.Register(value); remove => this._dmChannelDeleted.Unregister(value); } private AsyncEvent _dmChannelDeleted; /// /// Fired whenever a channel's pinned message list is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ChannelPinsUpdated { add => this._channelPinsUpdated.Register(value); remove => this._channelPinsUpdated.Unregister(value); } private AsyncEvent _channelPinsUpdated; #endregion #region Guild /// /// Fired when the user joins a new guild. /// For this Event you need the intent specified in /// /// [alias="GuildJoined"][alias="JoinedGuild"] public event AsyncEventHandler GuildCreated { add => this._guildCreated.Register(value); remove => this._guildCreated.Unregister(value); } private AsyncEvent _guildCreated; /// /// Fired when a guild is becoming available. /// public event AsyncEventHandler GuildAvailable { add => this._guildAvailable.Register(value); remove => this._guildAvailable.Unregister(value); } private AsyncEvent _guildAvailable; /// /// Fired when a guild is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildUpdated { add => this._guildUpdated.Register(value); remove => this._guildUpdated.Unregister(value); } private AsyncEvent _guildUpdated; /// /// Fired when the user leaves or is removed from a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildDeleted { add => this._guildDeleted.Register(value); remove => this._guildDeleted.Unregister(value); } private AsyncEvent _guildDeleted; /// /// Fired when a guild becomes unavailable. /// public event AsyncEventHandler GuildUnavailable { add => this._guildUnavailable.Register(value); remove => this._guildUnavailable.Unregister(value); } private AsyncEvent _guildUnavailable; /// /// Fired when all guilds finish streaming from Discord. /// public event AsyncEventHandler GuildDownloadCompleted { add => this._guildDownloadCompleted.Register(value); remove => this._guildDownloadCompleted.Unregister(value); } private AsyncEvent _guildDownloadCompleted; /// /// Fired when a guilds emojis get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildEmojisUpdated { add => this._guildEmojisUpdated.Register(value); remove => this._guildEmojisUpdated.Unregister(value); } private AsyncEvent _guildEmojisUpdated; /// /// Fired when a guilds stickers get updated /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildStickersUpdated { add => this._guildStickersUpdated.Register(value); remove => this._guildStickersUpdated.Unregister(value); } private AsyncEvent _guildStickersUpdated; /// /// Fired when a guild integration is updated. /// public event AsyncEventHandler GuildIntegrationsUpdated { add => this._guildIntegrationsUpdated.Register(value); remove => this._guildIntegrationsUpdated.Unregister(value); } private AsyncEvent _guildIntegrationsUpdated; #endregion #region Guild Ban /// /// Fired when a guild ban gets added /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanAdded { add => this._guildBanAdded.Register(value); remove => this._guildBanAdded.Unregister(value); } private AsyncEvent _guildBanAdded; /// /// Fired when a guild ban gets removed /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildBanRemoved { add => this._guildBanRemoved.Register(value); remove => this._guildBanRemoved.Unregister(value); } private AsyncEvent _guildBanRemoved; #endregion #region Guild Event /// - /// Fired when a scheduled Event is created. - /// For this Event you need the intent specified in + /// Fired when a scheduled event is created. + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventCreated { add => this._guildScheduledEventCreated.Register(value); remove => this._guildScheduledEventCreated.Unregister(value); } private AsyncEvent _guildScheduledEventCreated; /// - /// Fired when a scheduled Event is updated. - /// For this Event you need the intent specified in + /// Fired when a scheduled event is updated. + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventUpdated { add => this._guildScheduledEventUpdated.Register(value); remove => this._guildScheduledEventUpdated.Unregister(value); } private AsyncEvent _guildScheduledEventUpdated; /// - /// Fired when a scheduled Event is deleted. - /// For this Event you need the intent specified in + /// Fired when a scheduled event is deleted. + /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildScheduledEventDeleted { add => this._guildScheduledEventDeleted.Register(value); remove => this._guildScheduledEventDeleted.Unregister(value); } private AsyncEvent _guildScheduledEventDeleted; + /// + /// Fired when a user subscribes to a scheduled event. + /// For this Event you need the intent specified in + /// + public event AsyncEventHandler GuildScheduledEventUserAdded + { + add => this._guildScheduledEventUserAdded.Register(value); + remove => this._guildScheduledEventUserAdded.Unregister(value); + } + private AsyncEvent _guildScheduledEventUserAdded; + + /// + /// Fired when a user unsubscribes from a scheduled event. + /// For this Event you need the intent specified in + /// + public event AsyncEventHandler GuildScheduledEventUserRemoved + { + add => this._guildScheduledEventUserRemoved.Register(value); + remove => this._guildScheduledEventUserRemoved.Unregister(value); + } + private AsyncEvent _guildScheduledEventUserRemoved; + #endregion #region Guild Integration /// /// Fired when a guild integration is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationCreated { add => this._guildIntegrationCreated.Register(value); remove => this._guildIntegrationCreated.Unregister(value); } private AsyncEvent _guildIntegrationCreated; /// /// Fired when a guild integration is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationUpdated { add => this._guildIntegrationUpdated.Register(value); remove => this._guildIntegrationUpdated.Unregister(value); } private AsyncEvent _guildIntegrationUpdated; /// /// Fired when a guild integration is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildIntegrationDeleted { add => this._guildIntegrationDeleted.Register(value); remove => this._guildIntegrationDeleted.Unregister(value); } private AsyncEvent _guildIntegrationDeleted; #endregion #region Guild Member /// /// Fired when a new user joins a guild. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberAdded { add => this._guildMemberAdded.Register(value); remove => this._guildMemberAdded.Unregister(value); } private AsyncEvent _guildMemberAdded; /// /// Fired when a user is removed from a guild (leave/kick/ban). /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberRemoved { add => this._guildMemberRemoved.Register(value); remove => this._guildMemberRemoved.Unregister(value); } private AsyncEvent _guildMemberRemoved; /// /// Fired when a guild member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildMemberUpdated { add => this._guildMemberUpdated.Register(value); remove => this._guildMemberUpdated.Unregister(value); } private AsyncEvent _guildMemberUpdated; /// /// Fired in response to Gateway Request Guild Members. /// public event AsyncEventHandler GuildMembersChunked { add => this._guildMembersChunk.Register(value); remove => this._guildMembersChunk.Unregister(value); } private AsyncEvent _guildMembersChunk; #endregion #region Guild Role /// /// Fired when a guild role is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleCreated { add => this._guildRoleCreated.Register(value); remove => this._guildRoleCreated.Unregister(value); } private AsyncEvent _guildRoleCreated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleUpdated { add => this._guildRoleUpdated.Register(value); remove => this._guildRoleUpdated.Unregister(value); } private AsyncEvent _guildRoleUpdated; /// /// Fired when a guild role is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler GuildRoleDeleted { add => this._guildRoleDeleted.Register(value); remove => this._guildRoleDeleted.Unregister(value); } private AsyncEvent _guildRoleDeleted; #endregion #region Invite /// /// Fired when an invite is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteCreated { add => this._inviteCreated.Register(value); remove => this._inviteCreated.Unregister(value); } private AsyncEvent _inviteCreated; /// /// Fired when an invite is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler InviteDeleted { add => this._inviteDeleted.Register(value); remove => this._inviteDeleted.Unregister(value); } private AsyncEvent _inviteDeleted; #endregion #region Message /// /// Fired when a message is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageCreated { add => this._messageCreated.Register(value); remove => this._messageCreated.Unregister(value); } private AsyncEvent _messageCreated; /// /// Fired when a message is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageUpdated { add => this._messageUpdated.Register(value); remove => this._messageUpdated.Unregister(value); } private AsyncEvent _messageUpdated; /// /// Fired when a message is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageDeleted { add => this._messageDeleted.Register(value); remove => this._messageDeleted.Unregister(value); } private AsyncEvent _messageDeleted; /// /// Fired when multiple messages are deleted at once. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessagesBulkDeleted { add => this._messageBulkDeleted.Register(value); remove => this._messageBulkDeleted.Unregister(value); } private AsyncEvent _messageBulkDeleted; #endregion #region Message Reaction /// /// Fired when a reaction gets added to a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionAdded { add => this._messageReactionAdded.Register(value); remove => this._messageReactionAdded.Unregister(value); } private AsyncEvent _messageReactionAdded; /// /// Fired when a reaction gets removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemoved { add => this._messageReactionRemoved.Register(value); remove => this._messageReactionRemoved.Unregister(value); } private AsyncEvent _messageReactionRemoved; /// /// Fired when all reactions get removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionsCleared { add => this._messageReactionsCleared.Register(value); remove => this._messageReactionsCleared.Unregister(value); } private AsyncEvent _messageReactionsCleared; /// /// Fired when all reactions of a specific reaction are removed from a message. /// For this Event you need the intent specified in /// public event AsyncEventHandler MessageReactionRemovedEmoji { add => this._messageReactionRemovedEmoji.Register(value); remove => this._messageReactionRemovedEmoji.Unregister(value); } private AsyncEvent _messageReactionRemovedEmoji; #endregion #region Stage Instance /// /// Fired when a Stage Instance is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceCreated { add => this._stageInstanceCreated.Register(value); remove => this._stageInstanceCreated.Unregister(value); } private AsyncEvent _stageInstanceCreated; /// /// Fired when a Stage Instance is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceUpdated { add => this._stageInstanceUpdated.Register(value); remove => this._stageInstanceUpdated.Unregister(value); } private AsyncEvent _stageInstanceUpdated; /// /// Fired when a Stage Instance is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler StageInstanceDeleted { add => this._stageInstanceDeleted.Register(value); remove => this._stageInstanceDeleted.Unregister(value); } private AsyncEvent _stageInstanceDeleted; #endregion #region Thread /// /// Fired when a thread is created. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadCreated { add => this._threadCreated.Register(value); remove => this._threadCreated.Unregister(value); } private AsyncEvent _threadCreated; /// /// Fired when a thread is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadUpdated { add => this._threadUpdated.Register(value); remove => this._threadUpdated.Unregister(value); } private AsyncEvent _threadUpdated; /// /// Fired when a thread is deleted. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadDeleted { add => this._threadDeleted.Register(value); remove => this._threadDeleted.Unregister(value); } private AsyncEvent _threadDeleted; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadListSynced { add => this._threadListSynced.Register(value); remove => this._threadListSynced.Unregister(value); } private AsyncEvent _threadListSynced; /// /// Fired when a thread member is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler ThreadMemberUpdated { add => this._threadMemberUpdated.Register(value); remove => this._threadMemberUpdated.Unregister(value); } private AsyncEvent _threadMemberUpdated; /// /// Fired when the thread members are updated. /// For this Event you need the or intent specified in /// public event AsyncEventHandler ThreadMembersUpdated { add => this._threadMembersUpdated.Register(value); remove => this._threadMembersUpdated.Unregister(value); } private AsyncEvent _threadMembersUpdated; #endregion #region Activities /// /// Fired when a embedded activity has been updated. /// public event AsyncEventHandler EmbeddedActivityUpdated { add => this._embeddedActivityUpdated.Register(value); remove => this._embeddedActivityUpdated.Unregister(value); } private AsyncEvent _embeddedActivityUpdated; #endregion #region User/Presence Update /// /// Fired when a presence has been updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler PresenceUpdated { add => this._presenceUpdated.Register(value); remove => this._presenceUpdated.Unregister(value); } private AsyncEvent _presenceUpdated; /// /// Fired when the current user updates their settings. /// For this Event you need the intent specified in /// public event AsyncEventHandler UserSettingsUpdated { add => this._userSettingsUpdated.Register(value); remove => this._userSettingsUpdated.Unregister(value); } private AsyncEvent _userSettingsUpdated; /// /// Fired when properties about the current user change. /// For this Event you need the intent specified in /// /// /// NB: This event only applies for changes to the current user, the client that is connected to Discord. /// public event AsyncEventHandler UserUpdated { add => this._userUpdated.Register(value); remove => this._userUpdated.Unregister(value); } private AsyncEvent _userUpdated; #endregion #region Voice /// /// Fired when someone joins/leaves/moves voice channels. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceStateUpdated { add => this._voiceStateUpdated.Register(value); remove => this._voiceStateUpdated.Unregister(value); } private AsyncEvent _voiceStateUpdated; /// /// Fired when a guild's voice server is updated. /// For this Event you need the intent specified in /// public event AsyncEventHandler VoiceServerUpdated { add => this._voiceServerUpdated.Register(value); remove => this._voiceServerUpdated.Unregister(value); } private AsyncEvent _voiceServerUpdated; #endregion #region Application /// /// Fired when a new application command is registered. /// public event AsyncEventHandler ApplicationCommandCreated { add => this._applicationCommandCreated.Register(value); remove => this._applicationCommandCreated.Unregister(value); } private AsyncEvent _applicationCommandCreated; /// /// Fired when an application command is updated. /// public event AsyncEventHandler ApplicationCommandUpdated { add => this._applicationCommandUpdated.Register(value); remove => this._applicationCommandUpdated.Unregister(value); } private AsyncEvent _applicationCommandUpdated; /// /// Fired when an application command is deleted. /// public event AsyncEventHandler ApplicationCommandDeleted { add => this._applicationCommandDeleted.Register(value); remove => this._applicationCommandDeleted.Unregister(value); } private AsyncEvent _applicationCommandDeleted; /// /// Fired when a new application command is registered. /// public event AsyncEventHandler GuildApplicationCommandCountUpdated { add => this._guildApplicationCommandCountUpdated.Register(value); remove => this._guildApplicationCommandCountUpdated.Unregister(value); } private AsyncEvent _guildApplicationCommandCountUpdated; /// /// Fired when a user uses a context menu. /// public event AsyncEventHandler ContextMenuInteractionCreated { add => this._contextMenuInteractionCreated.Register(value); remove => this._contextMenuInteractionCreated.Unregister(value); } private AsyncEvent _contextMenuInteractionCreated; /// /// Fired when application command permissions gets updated. /// public event AsyncEventHandler ApplicationCommandPermissionsUpdated { add => this._applicationCommandPermissionsUpdated.Register(value); remove => this._applicationCommandPermissionsUpdated.Unregister(value); } private AsyncEvent _applicationCommandPermissionsUpdated; #endregion #region Misc /// /// Fired when an interaction is invoked. /// public event AsyncEventHandler InteractionCreated { add => this._interactionCreated.Register(value); remove => this._interactionCreated.Unregister(value); } private AsyncEvent _interactionCreated; /// /// Fired when a component is invoked. /// public event AsyncEventHandler ComponentInteractionCreated { add => this._componentInteractionCreated.Register(value); remove => this._componentInteractionCreated.Unregister(value); } private AsyncEvent _componentInteractionCreated; /// /// Fired when a user starts typing in a channel. /// public event AsyncEventHandler TypingStarted { add => this._typingStarted.Register(value); remove => this._typingStarted.Unregister(value); } private AsyncEvent _typingStarted; /// /// Fired when an unknown event gets received. /// public event AsyncEventHandler UnknownEvent { add => this._unknownEvent.Register(value); remove => this._unknownEvent.Unregister(value); } private AsyncEvent _unknownEvent; /// /// Fired whenever webhooks update. /// public event AsyncEventHandler WebhooksUpdated { add => this._webhooksUpdated.Register(value); remove => this._webhooksUpdated.Unregister(value); } private AsyncEvent _webhooksUpdated; /// /// Fired whenever an error occurs within an event handler. /// public event AsyncEventHandler ClientErrored { add => this._clientErrored.Register(value); remove => this._clientErrored.Unregister(value); } private AsyncEvent _clientErrored; #endregion #region Error Handling /// /// Events the error handler. /// /// The async event. /// The ex. /// The handler. /// The sender. /// The event args. internal void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, DiscordClient sender, TArgs eventArgs) where TArgs : AsyncEventArgs { if (ex is AsyncEventTimeoutException) { this.Logger.LogWarning(LoggerEvents.EventHandlerException, $"An event handler for {asyncEvent.Name} took too long to execute. Defined as \"{handler.Method.ToString().Replace(handler.Method.ReturnType.ToString(), "").TrimStart()}\" located in \"{handler.Method.DeclaringType}\"."); return; } this.Logger.LogError(LoggerEvents.EventHandlerException, ex, "Event handler exception for event {0} thrown from {1} (defined in {2})", asyncEvent.Name, handler.Method, handler.Method.DeclaringType); this._clientErrored.InvokeAsync(sender, new ClientErrorEventArgs(this.ShardClients[0].ServiceProvider) { EventName = asyncEvent.Name, Exception = ex }).ConfigureAwait(false).GetAwaiter().GetResult(); } /// /// Fired on heartbeat attempt cancellation due to too many failed heartbeats. /// public event AsyncEventHandler Zombied { add => this._zombied.Register(value); remove => this._zombied.Unregister(value); } private AsyncEvent _zombied; /// /// Fired when a gateway /// public event AsyncEventHandler PayloadReceived { add => this._payloadReceived.Register(value); remove => this._payloadReceived.Unregister(value); } private AsyncEvent _payloadReceived; /// /// Goofs the. /// /// The async event. /// The ex. /// The handler. /// The sender. /// The event args. private void Goof(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, DiscordClient sender, TArgs eventArgs) where TArgs : AsyncEventArgs => this.Logger.LogCritical(LoggerEvents.EventHandlerException, ex, "Exception event handler {0} (defined in {1}) threw an exception", handler.Method, handler.Method.DeclaringType); #endregion #region Event Dispatchers /// /// Client_S the zombied. /// /// The client. /// The events. /// A Task. private Task Client_Zombied(DiscordClient client, ZombiedEventArgs e) => this._zombied.InvokeAsync(client, e); /// /// Client_S the embedded activity updated. /// /// The client. /// The events. /// A Task. private Task Client_EmbeddedActivityUpdated(DiscordClient client, EmbeddedActivityUpdateEventArgs e) => this._embeddedActivityUpdated.InvokeAsync(client, e); /// /// Payload_S the received. /// /// The client. /// The events. /// A Task. private Task Client_PayloadReceived(DiscordClient client, PayloadReceivedEventArgs e) => this._payloadReceived.InvokeAsync(client, e); /// /// Client_S the client error. /// /// The client. /// The events. /// A Task. private Task Client_ClientError(DiscordClient client, ClientErrorEventArgs e) => this._clientErrored.InvokeAsync(client, e); /// /// Client_S the socket error. /// /// The client. /// The events. /// A Task. private Task Client_SocketError(DiscordClient client, SocketErrorEventArgs e) => this._socketErrored.InvokeAsync(client, e); /// /// Client_S the socket opened. /// /// The client. /// The events. /// A Task. private Task Client_SocketOpened(DiscordClient client, SocketEventArgs e) => this._socketOpened.InvokeAsync(client, e); /// /// Client_S the socket closed. /// /// The client. /// The events. /// A Task. private Task Client_SocketClosed(DiscordClient client, SocketCloseEventArgs e) => this._socketClosed.InvokeAsync(client, e); /// /// Client_S the ready. /// /// The client. /// The events. /// A Task. private Task Client_Ready(DiscordClient client, ReadyEventArgs e) => this._ready.InvokeAsync(client, e); /// /// Client_S the resumed. /// /// The client. /// The events. /// A Task. private Task Client_Resumed(DiscordClient client, ReadyEventArgs e) => this._resumed.InvokeAsync(client, e); /// /// Client_S the channel created. /// /// The client. /// The events. /// A Task. private Task Client_ChannelCreated(DiscordClient client, ChannelCreateEventArgs e) => this._channelCreated.InvokeAsync(client, e); /// /// Client_S the channel updated. /// /// The client. /// The events. /// A Task. private Task Client_ChannelUpdated(DiscordClient client, ChannelUpdateEventArgs e) => this._channelUpdated.InvokeAsync(client, e); /// /// Client_S the channel deleted. /// /// The client. /// The events. /// A Task. private Task Client_ChannelDeleted(DiscordClient client, ChannelDeleteEventArgs e) => this._channelDeleted.InvokeAsync(client, e); /// /// Client_S the d m channel deleted. /// /// The client. /// The events. /// A Task. private Task Client_DMChannelDeleted(DiscordClient client, DmChannelDeleteEventArgs e) => this._dmChannelDeleted.InvokeAsync(client, e); /// /// Client_S the channel pins updated. /// /// The client. /// The events. /// A Task. private Task Client_ChannelPinsUpdated(DiscordClient client, ChannelPinsUpdateEventArgs e) => this._channelPinsUpdated.InvokeAsync(client, e); /// /// Client_S the guild created. /// /// The client. /// The events. /// A Task. private Task Client_GuildCreated(DiscordClient client, GuildCreateEventArgs e) => this._guildCreated.InvokeAsync(client, e); /// /// Client_S the guild available. /// /// The client. /// The events. /// A Task. private Task Client_GuildAvailable(DiscordClient client, GuildCreateEventArgs e) => this._guildAvailable.InvokeAsync(client, e); /// /// Client_S the guild updated. /// /// The client. /// The events. /// A Task. private Task Client_GuildUpdated(DiscordClient client, GuildUpdateEventArgs e) => this._guildUpdated.InvokeAsync(client, e); /// /// Client_S the guild deleted. /// /// The client. /// The events. /// A Task. private Task Client_GuildDeleted(DiscordClient client, GuildDeleteEventArgs e) => this._guildDeleted.InvokeAsync(client, e); /// /// Client_S the guild unavailable. /// /// The client. /// The events. /// A Task. private Task Client_GuildUnavailable(DiscordClient client, GuildDeleteEventArgs e) => this._guildUnavailable.InvokeAsync(client, e); /// /// Client_S the guild download completed. /// /// The client. /// The events. /// A Task. private Task Client_GuildDownloadCompleted(DiscordClient client, GuildDownloadCompletedEventArgs e) => this._guildDownloadCompleted.InvokeAsync(client, e); /// /// Client_S the message created. /// /// The client. /// The events. /// A Task. private Task Client_MessageCreated(DiscordClient client, MessageCreateEventArgs e) => this._messageCreated.InvokeAsync(client, e); /// /// Client_S the invite created. /// /// The client. /// The events. /// A Task. private Task Client_InviteCreated(DiscordClient client, InviteCreateEventArgs e) => this._inviteCreated.InvokeAsync(client, e); /// /// Client_S the invite deleted. /// /// The client. /// The events. /// A Task. private Task Client_InviteDeleted(DiscordClient client, InviteDeleteEventArgs e) => this._inviteDeleted.InvokeAsync(client, e); /// /// Client_S the presence update. /// /// The client. /// The events. /// A Task. private Task Client_PresenceUpdate(DiscordClient client, PresenceUpdateEventArgs e) => this._presenceUpdated.InvokeAsync(client, e); /// /// Client_S the guild ban add. /// /// The client. /// The events. /// A Task. private Task Client_GuildBanAdd(DiscordClient client, GuildBanAddEventArgs e) => this._guildBanAdded.InvokeAsync(client, e); /// /// Client_S the guild ban remove. /// /// The client. /// The events. /// A Task. private Task Client_GuildBanRemove(DiscordClient client, GuildBanRemoveEventArgs e) => this._guildBanRemoved.InvokeAsync(client, e); /// /// Client_S the guild emojis update. /// /// The client. /// The events. /// A Task. private Task Client_GuildEmojisUpdate(DiscordClient client, GuildEmojisUpdateEventArgs e) => this._guildEmojisUpdated.InvokeAsync(client, e); /// /// Client_S the guild stickers update. /// /// The client. /// The events. /// A Task. private Task Client_GuildStickersUpdate(DiscordClient client, GuildStickersUpdateEventArgs e) => this._guildStickersUpdated.InvokeAsync(client, e); /// /// Client_S the guild integrations update. /// /// The client. /// The events. /// A Task. private Task Client_GuildIntegrationsUpdate(DiscordClient client, GuildIntegrationsUpdateEventArgs e) => this._guildIntegrationsUpdated.InvokeAsync(client, e); /// /// Client_S the guild member add. /// /// The client. /// The events. /// A Task. private Task Client_GuildMemberAdd(DiscordClient client, GuildMemberAddEventArgs e) => this._guildMemberAdded.InvokeAsync(client, e); /// /// Client_S the guild member remove. /// /// The client. /// The events. /// A Task. private Task Client_GuildMemberRemove(DiscordClient client, GuildMemberRemoveEventArgs e) => this._guildMemberRemoved.InvokeAsync(client, e); /// /// Client_S the guild member update. /// /// The client. /// The events. /// A Task. private Task Client_GuildMemberUpdate(DiscordClient client, GuildMemberUpdateEventArgs e) => this._guildMemberUpdated.InvokeAsync(client, e); /// /// Client_S the guild role create. /// /// The client. /// The events. /// A Task. private Task Client_GuildRoleCreate(DiscordClient client, GuildRoleCreateEventArgs e) => this._guildRoleCreated.InvokeAsync(client, e); /// /// Client_S the guild role update. /// /// The client. /// The events. /// A Task. private Task Client_GuildRoleUpdate(DiscordClient client, GuildRoleUpdateEventArgs e) => this._guildRoleUpdated.InvokeAsync(client, e); /// /// Client_S the guild role delete. /// /// The client. /// The events. /// A Task. private Task Client_GuildRoleDelete(DiscordClient client, GuildRoleDeleteEventArgs e) => this._guildRoleDeleted.InvokeAsync(client, e); /// /// Client_S the message update. /// /// The client. /// The events. /// A Task. private Task Client_MessageUpdate(DiscordClient client, MessageUpdateEventArgs e) => this._messageUpdated.InvokeAsync(client, e); /// /// Client_S the message delete. /// /// The client. /// The events. /// A Task. private Task Client_MessageDelete(DiscordClient client, MessageDeleteEventArgs e) => this._messageDeleted.InvokeAsync(client, e); /// /// Client_S the message bulk delete. /// /// The client. /// The events. /// A Task. private Task Client_MessageBulkDelete(DiscordClient client, MessageBulkDeleteEventArgs e) => this._messageBulkDeleted.InvokeAsync(client, e); /// /// Client_S the typing start. /// /// The client. /// The events. /// A Task. private Task Client_TypingStart(DiscordClient client, TypingStartEventArgs e) => this._typingStarted.InvokeAsync(client, e); /// /// Client_S the user settings update. /// /// The client. /// The events. /// A Task. private Task Client_UserSettingsUpdate(DiscordClient client, UserSettingsUpdateEventArgs e) => this._userSettingsUpdated.InvokeAsync(client, e); /// /// Client_S the user update. /// /// The client. /// The events. /// A Task. private Task Client_UserUpdate(DiscordClient client, UserUpdateEventArgs e) => this._userUpdated.InvokeAsync(client, e); /// /// Client_S the voice state update. /// /// The client. /// The events. /// A Task. private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e) => this._voiceStateUpdated.InvokeAsync(client, e); /// /// Client_S the voice server update. /// /// The client. /// The events. /// A Task. private Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e) => this._voiceServerUpdated.InvokeAsync(client, e); /// /// Client_S the guild members chunk. /// /// The client. /// The events. /// A Task. private Task Client_GuildMembersChunk(DiscordClient client, GuildMembersChunkEventArgs e) => this._guildMembersChunk.InvokeAsync(client, e); /// /// Client_S the unknown event. /// /// The client. /// The events. /// A Task. private Task Client_UnknownEvent(DiscordClient client, UnknownEventArgs e) => this._unknownEvent.InvokeAsync(client, e); /// /// Client_S the message reaction add. /// /// The client. /// The events. /// A Task. private Task Client_MessageReactionAdd(DiscordClient client, MessageReactionAddEventArgs e) => this._messageReactionAdded.InvokeAsync(client, e); /// /// Client_S the message reaction remove. /// /// The client. /// The events. /// A Task. private Task Client_MessageReactionRemove(DiscordClient client, MessageReactionRemoveEventArgs e) => this._messageReactionRemoved.InvokeAsync(client, e); /// /// Client_S the message reaction remove all. /// /// The client. /// The events. /// A Task. private Task Client_MessageReactionRemoveAll(DiscordClient client, MessageReactionsClearEventArgs e) => this._messageReactionsCleared.InvokeAsync(client, e); /// /// Client_S the message reaction removed emoji. /// /// The client. /// The events. /// A Task. private Task Client_MessageReactionRemovedEmoji(DiscordClient client, MessageReactionRemoveEmojiEventArgs e) => this._messageReactionRemovedEmoji.InvokeAsync(client, e); /// /// Client_S the interaction create. /// /// The client. /// The events. /// A Task. private Task Client_InteractionCreate(DiscordClient client, InteractionCreateEventArgs e) => this._interactionCreated.InvokeAsync(client, e); /// /// Client_S the component interaction create. /// /// The client. /// The events. /// A Task. private Task Client_ComponentInteractionCreate(DiscordClient client, ComponentInteractionCreateEventArgs e) => this._componentInteractionCreated.InvokeAsync(client, e); /// /// Client_S the context menu interaction create. /// /// The client. /// The events. /// A Task. private Task Client_ContextMenuInteractionCreate(DiscordClient client, ContextMenuInteractionCreateEventArgs e) => this._contextMenuInteractionCreated.InvokeAsync(client, e); /// /// Client_S the webhooks update. /// /// The client. /// The events. /// A Task. private Task Client_WebhooksUpdate(DiscordClient client, WebhooksUpdateEventArgs e) => this._webhooksUpdated.InvokeAsync(client, e); /// /// Client_S the heart beated. /// /// The client. /// The events. /// A Task. private Task Client_HeartBeated(DiscordClient client, HeartbeatEventArgs e) => this._heartbeated.InvokeAsync(client, e); /// /// Client_S the application command created. /// /// The client. /// The events. /// A Task. private Task Client_ApplicationCommandCreated(DiscordClient client, ApplicationCommandEventArgs e) => this._applicationCommandCreated.InvokeAsync(client, e); /// /// Client_S the application command updated. /// /// The client. /// The events. private Task Client_ApplicationCommandUpdated(DiscordClient client, ApplicationCommandEventArgs e) => this._applicationCommandUpdated.InvokeAsync(client, e); /// /// Client_S the application command deleted. /// /// The client. /// The events. private Task Client_ApplicationCommandDeleted(DiscordClient client, ApplicationCommandEventArgs e) => this._applicationCommandDeleted.InvokeAsync(client, e); /// /// Client_S the guild application command count updated. /// /// The client. /// The events. private Task Client_GuildApplicationCommandCountUpdated(DiscordClient client, GuildApplicationCommandCountEventArgs e) => this._guildApplicationCommandCountUpdated.InvokeAsync(client, e); /// /// Client_S the application command permissions updated. /// /// The client. /// The events. private Task Client_ApplicationCommandPermissionsUpdated(DiscordClient client, ApplicationCommandPermissionsUpdateEventArgs e) => this._applicationCommandPermissionsUpdated.InvokeAsync(client, e); /// /// Client_S the guild integration created. /// /// The client. /// The events. /// A Task. private Task Client_GuildIntegrationCreated(DiscordClient client, GuildIntegrationCreateEventArgs e) => this._guildIntegrationCreated.InvokeAsync(client, e); /// /// Client_S the guild integration updated. /// /// The client. /// The events. /// A Task. private Task Client_GuildIntegrationUpdated(DiscordClient client, GuildIntegrationUpdateEventArgs e) => this._guildIntegrationUpdated.InvokeAsync(client, e); /// /// Client_S the guild integration deleted. /// /// The client. /// The events. /// A Task. private Task Client_GuildIntegrationDeleted(DiscordClient client, GuildIntegrationDeleteEventArgs e) => this._guildIntegrationDeleted.InvokeAsync(client, e); /// /// Client_S the stage instance created. /// /// The client. /// The events. /// A Task. private Task Client_StageInstanceCreated(DiscordClient client, StageInstanceCreateEventArgs e) => this._stageInstanceCreated.InvokeAsync(client, e); /// /// Client_S the stage instance updated. /// /// The client. /// The events. /// A Task. private Task Client_StageInstanceUpdated(DiscordClient client, StageInstanceUpdateEventArgs e) => this._stageInstanceUpdated.InvokeAsync(client, e); /// /// Client_S the stage instance deleted. /// /// The client. /// The events. /// A Task. private Task Client_StageInstanceDeleted(DiscordClient client, StageInstanceDeleteEventArgs e) => this._stageInstanceDeleted.InvokeAsync(client, e); /// /// Client_S the thread created. /// /// The client. /// The events. /// A Task. private Task Client_ThreadCreated(DiscordClient client, ThreadCreateEventArgs e) => this._threadCreated.InvokeAsync(client, e); /// /// Client_S the thread updated. /// /// The client. /// The events. /// A Task. private Task Client_ThreadUpdated(DiscordClient client, ThreadUpdateEventArgs e) => this._threadUpdated.InvokeAsync(client, e); /// /// Client_S the thread deleted. /// /// The client. /// The events. /// A Task. private Task Client_ThreadDeleted(DiscordClient client, ThreadDeleteEventArgs e) => this._threadDeleted.InvokeAsync(client, e); /// /// Client_S the thread list synced. /// /// The client. /// The events. /// A Task. private Task Client_ThreadListSynced(DiscordClient client, ThreadListSyncEventArgs e) => this._threadListSynced.InvokeAsync(client, e); /// /// Client_S the thread member updated. /// /// The client. /// The events. /// A Task. private Task Client_ThreadMemberUpdated(DiscordClient client, ThreadMemberUpdateEventArgs e) => this._threadMemberUpdated.InvokeAsync(client, e); /// /// Client_S the thread members updated. /// /// The client. /// The events. /// A Task. private Task Client_ThreadMembersUpdated(DiscordClient client, ThreadMembersUpdateEventArgs e) => this._threadMembersUpdated.InvokeAsync(client, e); /// /// Handles the scheduled event created. /// /// The client. /// The events. /// A Task. private Task Client_GuildScheduledEventCreated(DiscordClient client, GuildScheduledEventCreateEventArgs e) => this._guildScheduledEventCreated.InvokeAsync(client, e); /// /// Handles the scheduled event updated. /// /// The client. /// The events. /// A Task. private Task Client_GuildScheduledEventUpdated(DiscordClient client, GuildScheduledEventUpdateEventArgs e) => this._guildScheduledEventUpdated.InvokeAsync(client, e); /// /// Handles the scheduled event deleted. /// /// The client. /// The events. /// A Task. private Task Client_GuildScheduledEventDeleted(DiscordClient client, GuildScheduledEventDeleteEventArgs e) => this._guildScheduledEventDeleted.InvokeAsync(client, e); + /// + /// Handles the scheduled event user added. + /// + /// The client. + /// The events. + /// A Task. + private Task Client_GuildScheduledEventUserAdded(DiscordClient client, GuildScheduledEventUserAddEventArgs e) + => this._guildScheduledEventUserAdded.InvokeAsync(client, e); + + /// + /// Handles the scheduled event user removed. + /// + /// The client. + /// The events. + /// A Task. + private Task Client_GuildScheduledEventUserRemoved(DiscordClient client, GuildScheduledEventUserRemoveEventArgs e) + => this._guildScheduledEventUserRemoved.InvokeAsync(client, e); + #endregion } } diff --git a/DisCatSharp/Clients/DiscordShardedClient.cs b/DisCatSharp/Clients/DiscordShardedClient.cs index af4a1e008..444e951bc 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,735 +1,745 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; +using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; -using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// A Discord client that shards automatically. /// public sealed partial class DiscordShardedClient { #region Public Properties /// /// Gets the logger for this client. /// public ILogger Logger { get; } /// /// Gets all client shards. /// public IReadOnlyDictionary ShardClients { get; } /// /// Gets the gateway info for the client's session. /// public GatewayInfo GatewayInfo { get; private set; } /// /// Gets the current user. /// public DiscordUser CurrentUser { get; private set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; private set; } /// /// Gets the library team. /// public DisCatSharpTeam LibraryDeveloperTeam => this.GetShard(0).LibraryDeveloperTeam; /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voiceRegionsLazy?.Value; #endregion #region Private Properties/Fields /// /// Gets the configuration. /// private DiscordConfiguration Configuration { get; } /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// private ConcurrentDictionary _internalVoiceRegions; private readonly ConcurrentDictionary _shards = new(); private Lazy> _voiceRegionsLazy; private bool _isStarted; private readonly bool _manuallySharding; #endregion #region Constructor /// /// Initializes new auto-sharding Discord client. /// /// Configuration to use. public DiscordShardedClient(DiscordConfiguration config) { this.InternalSetup(); if (config.ShardCount > 1) this._manuallySharding = true; this.Configuration = config; this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards); if (this.Configuration.LoggerFactory == null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this.Configuration.MinimumLogLevel, this.Configuration.LogTimestampFormat)); } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); } #endregion #region Public Methods /// /// Initializes and connects all shards. /// - /// - /// + /// + /// /// public async Task StartAsync() { if (this._isStarted) throw new InvalidOperationException("This client has already been started."); this._isStarted = true; try { if (this.Configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this._botLibrary, this._versionString.Value); var shardc = await this.InitializeShardsAsync().ConfigureAwait(false); var connectTasks = new List(); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booting {0} shards.", shardc); for (var i = 0; i < shardc; i++) { //This should never happen, but in case it does... if (this.GatewayInfo.SessionBucket.MaxConcurrency < 1) this.GatewayInfo.SessionBucket.MaxConcurrency = 1; if (this.GatewayInfo.SessionBucket.MaxConcurrency == 1) await this.ConnectShardAsync(i).ConfigureAwait(false); else { //Concurrent login. connectTasks.Add(this.ConnectShardAsync(i)); if (connectTasks.Count == this.GatewayInfo.SessionBucket.MaxConcurrency) { await Task.WhenAll(connectTasks).ConfigureAwait(false); connectTasks.Clear(); } } } } catch (Exception ex) { await this.InternalStopAsync(false).ConfigureAwait(false); var message = $"Shard initialization failed, check inner exceptions for details: "; this.Logger.LogCritical(LoggerEvents.ShardClientError, $"{message}\n{ex}"); throw new AggregateException(message, ex); } } /// /// Disconnects and disposes of all shards. /// /// - /// + /// public Task StopAsync() => this.InternalStopAsync(); /// /// Gets a shard from a guild ID. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild ID for the shard. /// The found shard. Otherwise if the shard was not found for the guild ID. public DiscordClient GetShard(ulong guildId) { var index = this._manuallySharding ? this.GetShardIdFromGuilds(guildId) : Utilities.GetShardId(guildId, this.ShardClients.Count); return index != -1 ? this._shards[index] : null; } /// /// Gets a shard from a guild. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild for the shard. /// The found shard. Otherwise if the shard was not found for the guild. public DiscordClient GetShard(DiscordGuild guild) => this.GetShard(guild.Id); /// /// Updates playing statuses on all shards. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// Asynchronous operation. public async Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) { var tasks = new List(); foreach (var client in this._shards.Values) tasks.Add(client.UpdateStatusAsync(activity, userStatus, idleSince)); await Task.WhenAll(tasks).ConfigureAwait(false); } #endregion #region Internal Methods /// /// Initializes the shards async. /// /// A Task. internal async Task InitializeShardsAsync() { if (this._shards.Count != 0) return this._shards.Count; this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false); var shardc = this.Configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; var lf = new ShardedLoggerFactory(this.Logger); for (var i = 0; i < shardc; i++) { var cfg = new DiscordConfiguration(this.Configuration) { ShardId = i, ShardCount = shardc, LoggerFactory = lf }; var client = new DiscordClient(cfg); if (!this._shards.TryAdd(i, client)) throw new InvalidOperationException("Could not initialize shards."); } return shardc; } #endregion #region Private Methods/Version Property /// /// Gets the gateway info async. /// /// A Task. private async Task GetGatewayInfoAsync() { var url = $"{Utilities.GetApiBaseUri(this.Configuration)}{Endpoints.GATEWAY}{Endpoints.BOT}"; var http = new HttpClient(); http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(this.Configuration)); + if (this.Configuration != null && this.Configuration.Override != null) + { + http.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this.Configuration.Override); + } this.Logger.LogDebug(LoggerEvents.ShardRest, $"Obtaining gateway information from GET {Endpoints.GATEWAY}{Endpoints.BOT}..."); var resp = await http.GetAsync(url).ConfigureAwait(false); http.Dispose(); if (!resp.IsSuccessStatusCode) { var ratelimited = await HandleHttpError(url, resp).ConfigureAwait(false); if (ratelimited) return await this.GetGatewayInfoAsync().ConfigureAwait(false); } var timer = new Stopwatch(); timer.Start(); var jo = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); var info = jo.ToObject(); //There is a delay from parsing here. timer.Stop(); info.SessionBucket.resetAfter -= (int)timer.ElapsedMilliseconds; info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.resetAfter); return info; async Task HandleHttpError(string reqUrl, HttpResponseMessage msg) { var code = (int)msg.StatusCode; if (code == 401 || code == 403) { throw new Exception($"Authentication failed, check your token and try again: {code} {msg.ReasonPhrase}"); } else if (code == 429) { this.Logger.LogError(LoggerEvents.ShardClientError, $"Ratelimit hit, requeuing request to {reqUrl}"); var hs = msg.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); var waitInterval = 0; if (hs.TryGetValue("Retry-After", out var retry_after_raw)) waitInterval = int.Parse(retry_after_raw, CultureInfo.InvariantCulture); await Task.Delay(waitInterval).ConfigureAwait(false); return true; } else if (code >= 500) { throw new Exception($"Internal Server Error: {code} {msg.ReasonPhrase}"); } else { throw new Exception($"An unsuccessful HTTP status code was encountered: {code} {msg.ReasonPhrase}"); } } } private readonly Lazy _versionString = new(() => { var a = typeof(DiscordShardedClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); private readonly string _botLibrary = "DisCatSharp"; #endregion #region Private Connection Methods /// /// Connects the shard async. /// /// The i. /// A Task. private async Task ConnectShardAsync(int i) { if (!this._shards.TryGetValue(i, out var client)) throw new Exception($"Could not initialize shard {i}."); if (this.GatewayInfo != null) { client.GatewayInfo = this.GatewayInfo; client.GatewayUri = new Uri(client.GatewayInfo.Url); } if (this.CurrentUser != null) client.CurrentUser = this.CurrentUser; if (this.CurrentApplication != null) client.CurrentApplication = this.CurrentApplication; if (this._internalVoiceRegions != null) { client.InternalVoiceRegions = this._internalVoiceRegions; client._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(client.InternalVoiceRegions)); } this.HookEventHandlers(client); client._isShard = true; await client.ConnectAsync().ConfigureAwait(false); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booted shard {0}.", i); if (this.CurrentUser == null) this.CurrentUser = client.CurrentUser; if (this.CurrentApplication == null) this.CurrentApplication = client.CurrentApplication; if (this._internalVoiceRegions == null) { this._internalVoiceRegions = client.InternalVoiceRegions; this._voiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this._internalVoiceRegions)); } } /// /// Internals the stop async. /// /// If true, enable logger. /// A Task. private Task InternalStopAsync(bool enableLogger = true) { if (!this._isStarted) throw new InvalidOperationException("This client has not been started."); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {0} shards.", this._shards.Count); this._isStarted = false; this._voiceRegionsLazy = null; this.GatewayInfo = null; this.CurrentUser = null; this.CurrentApplication = null; for (var i = 0; i < this._shards.Count; i++) { if (this._shards.TryGetValue(i, out var client)) { this.UnhookEventHandlers(client); client.Dispose(); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disconnected shard {0}.", i); } } this._shards.Clear(); return Task.CompletedTask; } #endregion #region Event Handler Initialization/Registering /// /// Internals the setup. /// private void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompleted = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunk = new AsyncEvent("GUILD_MEMBERS_CHUNKED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); + this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); + this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); } /// /// Hooks the event handlers. /// /// The client. private void HookEventHandlers(DiscordClient client) { client.ClientErrored += this.Client_ClientError; client.SocketErrored += this.Client_SocketError; client.SocketOpened += this.Client_SocketOpened; client.SocketClosed += this.Client_SocketClosed; client.Ready += this.Client_Ready; client.Resumed += this.Client_Resumed; client.ChannelCreated += this.Client_ChannelCreated; client.ChannelUpdated += this.Client_ChannelUpdated; client.ChannelDeleted += this.Client_ChannelDeleted; client.DmChannelDeleted += this.Client_DMChannelDeleted; client.ChannelPinsUpdated += this.Client_ChannelPinsUpdated; client.GuildCreated += this.Client_GuildCreated; client.GuildAvailable += this.Client_GuildAvailable; client.GuildUpdated += this.Client_GuildUpdated; client.GuildDeleted += this.Client_GuildDeleted; client.GuildUnavailable += this.Client_GuildUnavailable; client.GuildDownloadCompleted += this.Client_GuildDownloadCompleted; client.InviteCreated += this.Client_InviteCreated; client.InviteDeleted += this.Client_InviteDeleted; client.MessageCreated += this.Client_MessageCreated; client.PresenceUpdated += this.Client_PresenceUpdate; client.GuildBanAdded += this.Client_GuildBanAdd; client.GuildBanRemoved += this.Client_GuildBanRemove; client.GuildEmojisUpdated += this.Client_GuildEmojisUpdate; client.GuildStickersUpdated += this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated += this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded += this.Client_GuildMemberAdd; client.GuildMemberRemoved += this.Client_GuildMemberRemove; client.GuildMemberUpdated += this.Client_GuildMemberUpdate; client.GuildRoleCreated += this.Client_GuildRoleCreate; client.GuildRoleUpdated += this.Client_GuildRoleUpdate; client.GuildRoleDeleted += this.Client_GuildRoleDelete; client.MessageUpdated += this.Client_MessageUpdate; client.MessageDeleted += this.Client_MessageDelete; client.MessagesBulkDeleted += this.Client_MessageBulkDelete; client.InteractionCreated += this.Client_InteractionCreate; client.ComponentInteractionCreated += this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated += this.Client_ContextMenuInteractionCreate; client.TypingStarted += this.Client_TypingStart; client.UserSettingsUpdated += this.Client_UserSettingsUpdate; client.UserUpdated += this.Client_UserUpdate; client.VoiceStateUpdated += this.Client_VoiceStateUpdate; client.VoiceServerUpdated += this.Client_VoiceServerUpdate; client.GuildMembersChunked += this.Client_GuildMembersChunk; client.UnknownEvent += this.Client_UnknownEvent; client.MessageReactionAdded += this.Client_MessageReactionAdd; client.MessageReactionRemoved += this.Client_MessageReactionRemove; client.MessageReactionsCleared += this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji += this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated += this.Client_WebhooksUpdate; client.Heartbeated += this.Client_HeartBeated; client.ApplicationCommandCreated += this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated += this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted += this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated += this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated += this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated += this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated += this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted += this.Client_GuildIntegrationDeleted; client.StageInstanceCreated += this.Client_StageInstanceCreated; client.StageInstanceUpdated += this.Client_StageInstanceUpdated; client.StageInstanceDeleted += this.Client_StageInstanceDeleted; client.ThreadCreated += this.Client_ThreadCreated; client.ThreadUpdated += this.Client_ThreadUpdated; client.ThreadDeleted += this.Client_ThreadDeleted; client.ThreadListSynced += this.Client_ThreadListSynced; client.ThreadMemberUpdated += this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated += this.Client_ThreadMembersUpdated; client.Zombied += this.Client_Zombied; client.PayloadReceived += this.Client_PayloadReceived; client.GuildScheduledEventCreated += this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated += this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted += this.Client_GuildScheduledEventDeleted; + client.GuildScheduledEventUserAdded += this.Client_GuildScheduledEventUserAdded; ; + client.GuildScheduledEventUserRemoved += this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated += this.Client_EmbeddedActivityUpdated; } /// /// Unhooks the event handlers. /// /// The client. private void UnhookEventHandlers(DiscordClient client) { client.ClientErrored -= this.Client_ClientError; client.SocketErrored -= this.Client_SocketError; client.SocketOpened -= this.Client_SocketOpened; client.SocketClosed -= this.Client_SocketClosed; client.Ready -= this.Client_Ready; client.Resumed -= this.Client_Resumed; client.ChannelCreated -= this.Client_ChannelCreated; client.ChannelUpdated -= this.Client_ChannelUpdated; client.ChannelDeleted -= this.Client_ChannelDeleted; client.DmChannelDeleted -= this.Client_DMChannelDeleted; client.ChannelPinsUpdated -= this.Client_ChannelPinsUpdated; client.GuildCreated -= this.Client_GuildCreated; client.GuildAvailable -= this.Client_GuildAvailable; client.GuildUpdated -= this.Client_GuildUpdated; client.GuildDeleted -= this.Client_GuildDeleted; client.GuildUnavailable -= this.Client_GuildUnavailable; client.GuildDownloadCompleted -= this.Client_GuildDownloadCompleted; client.InviteCreated -= this.Client_InviteCreated; client.InviteDeleted -= this.Client_InviteDeleted; client.MessageCreated -= this.Client_MessageCreated; client.PresenceUpdated -= this.Client_PresenceUpdate; client.GuildBanAdded -= this.Client_GuildBanAdd; client.GuildBanRemoved -= this.Client_GuildBanRemove; client.GuildEmojisUpdated -= this.Client_GuildEmojisUpdate; client.GuildStickersUpdated -= this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated -= this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded -= this.Client_GuildMemberAdd; client.GuildMemberRemoved -= this.Client_GuildMemberRemove; client.GuildMemberUpdated -= this.Client_GuildMemberUpdate; client.GuildRoleCreated -= this.Client_GuildRoleCreate; client.GuildRoleUpdated -= this.Client_GuildRoleUpdate; client.GuildRoleDeleted -= this.Client_GuildRoleDelete; client.MessageUpdated -= this.Client_MessageUpdate; client.MessageDeleted -= this.Client_MessageDelete; client.MessagesBulkDeleted -= this.Client_MessageBulkDelete; client.InteractionCreated -= this.Client_InteractionCreate; client.ComponentInteractionCreated -= this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated -= this.Client_ContextMenuInteractionCreate; client.TypingStarted -= this.Client_TypingStart; client.UserSettingsUpdated -= this.Client_UserSettingsUpdate; client.UserUpdated -= this.Client_UserUpdate; client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; client.GuildMembersChunked -= this.Client_GuildMembersChunk; client.UnknownEvent -= this.Client_UnknownEvent; client.MessageReactionAdded -= this.Client_MessageReactionAdd; client.MessageReactionRemoved -= this.Client_MessageReactionRemove; client.MessageReactionsCleared -= this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji -= this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated -= this.Client_WebhooksUpdate; client.Heartbeated -= this.Client_HeartBeated; client.ApplicationCommandCreated -= this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated -= this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted -= this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated -= this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated -= this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated -= this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated -= this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted -= this.Client_GuildIntegrationDeleted; client.StageInstanceCreated -= this.Client_StageInstanceCreated; client.StageInstanceUpdated -= this.Client_StageInstanceUpdated; client.StageInstanceDeleted -= this.Client_StageInstanceDeleted; client.ThreadCreated -= this.Client_ThreadCreated; client.ThreadUpdated -= this.Client_ThreadUpdated; client.ThreadDeleted -= this.Client_ThreadDeleted; client.ThreadListSynced -= this.Client_ThreadListSynced; client.ThreadMemberUpdated -= this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated -= this.Client_ThreadMembersUpdated; client.Zombied -= this.Client_Zombied; client.PayloadReceived -= this.Client_PayloadReceived; client.GuildScheduledEventCreated -= this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated -= this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted -= this.Client_GuildScheduledEventDeleted; + client.GuildScheduledEventUserAdded -= this.Client_GuildScheduledEventUserAdded; ; + client.GuildScheduledEventUserRemoved -= this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated -= this.Client_EmbeddedActivityUpdated; } /// /// Gets the shard id from guilds. /// /// The id. /// An int. private int GetShardIdFromGuilds(ulong id) { foreach (var s in this._shards.Values) { if (s._guilds.TryGetValue(id, out _)) { return s.ShardId; } } return -1; } #endregion #region Destructor ~DiscordShardedClient() => this.InternalStopAsync(false).GetAwaiter().GetResult(); #endregion } } diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj index a68d6a0c6..63c826ce2 100644 --- a/DisCatSharp/DisCatSharp.csproj +++ b/DisCatSharp/DisCatSharp.csproj @@ -1,50 +1,66 @@ DisCatSharp DisCatSharp Library netstandard2.0 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/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index 606e5e98d..0c3a70b96 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,268 +1,275 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Net; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents configuration for and . /// public sealed class DiscordConfiguration { /// /// Sets the token used to identify the client. /// public string Token { internal get => this._token; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace."); this._token = value.Trim(); } } private string _token = ""; /// /// Sets the type of the token used to identify the client. /// Defaults to . /// public TokenType TokenType { internal get; set; } = TokenType.Bot; /// /// Sets the minimum logging level for messages. - /// Typically, the default value of is ok for most uses. + /// Typically, the default value of is ok for most uses. /// public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information; /// /// Overwrites the api version. /// Defaults to 9. /// public string ApiVersion { internal get; set; } = "9"; /// /// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header. /// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one. /// This should only be set to false if the system clock is synced with NTP. /// Defaults to true. /// public bool UseRelativeRatelimit { internal get; set; } = true; /// /// Allows you to overwrite the time format used by the internal debug logger. /// Only applicable when is set left at default value. Defaults to ISO 8601-like format. /// public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz"; /// /// Sets the member count threshold at which guilds are considered large. /// Defaults to 250. /// public int LargeThreshold { internal get; set; } = 250; /// /// Sets whether to automatically reconnect in case a connection is lost. /// Defaults to true. /// public bool AutoReconnect { internal get; set; } = true; /// /// Sets the ID of the shard to connect to. /// If not sharding, or sharding automatically, this value should be left with the default value of 0. /// public int ShardId { internal get; set; } = 0; /// /// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1. /// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically. /// public int ShardCount { internal get; set; } = 1; /// /// Sets the level of compression for WebSocket traffic. /// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic. /// Defaults to . /// public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream; /// /// Sets the size of the global message cache. /// Setting this to 0 will disable message caching entirely. Defaults to 1024. /// public int MessageCacheSize { internal get; set; } = 1024; /// /// Sets the proxy to use for HTTP and WebSocket connections to Discord. /// Defaults to null. /// public IWebProxy Proxy { internal get; set; } = null; /// /// Sets the timeout for HTTP requests. /// Set to to disable timeouts. /// Defaults to 20 seconds. /// public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(20); /// /// Defines that the client should attempt to reconnect indefinitely. /// This is typically a very bad idea to set to true, as it will swallow all connection errors. /// Defaults to false. /// public bool ReconnectIndefinitely { internal get; set; } = false; /// /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. /// /// This will only take effect if there are no or /// intents specified. Otherwise, this will always be overwritten to true. /// /// Defaults to true. /// public bool AlwaysCacheMembers { internal get; set; } = true; /// /// Sets the gateway intents for this client. /// If set, the client will only receive events that they specify with intents. /// Defaults to . /// public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged; /// /// Sets the factory method used to create instances of WebSocket clients. - /// Use and equivalents on other implementations to switch out client implementations. - /// Defaults to . + /// Use and equivalents on other implementations to switch out client implementations. + /// Defaults to . /// public WebSocketClientFactoryDelegate WebSocketClientFactory { internal get => this._webSocketClientFactory; set { if (value == null) throw new InvalidOperationException("You need to supply a valid WebSocket client factory method."); this._webSocketClientFactory = value; } } private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew; /// /// Sets the factory method used to create instances of UDP clients. - /// Use and equivalents on other implementations to switch out client implementations. - /// Defaults to . + /// Use and equivalents on other implementations to switch out client implementations. + /// Defaults to . /// public UdpClientFactoryDelegate UdpClientFactory { internal get => this._udpClientFactory; set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); } private UdpClientFactoryDelegate _udpClientFactory = DCSUdpClient.CreateNew; /// /// Sets the logger implementation to use. - /// To create your own logger, implement the instance. + /// To create your own logger, implement the instance. /// Defaults to built-in implementation. /// public ILoggerFactory LoggerFactory { internal get; set; } = null; /// /// Sets if the bot's status should show the mobile icon. /// Defaults to false. /// public bool MobileStatus { internal get; set; } = false; - + /// /// Use canary. /// Defaults to false. /// public bool UseCanary { internal get; set; } = false; /// /// Refresh full guild channel cache. /// Defaults to false. /// public bool AutoRefreshChannelCache { internal get; set; } = false; + /// + /// Do not use, this is meant for DisCatSharp Devs. + /// Defaults to null. + /// + public string Override { internal get; set; } = null; + /// /// Sets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Creates a new configuration with default values. /// public DiscordConfiguration() { } /// /// Utilized via Dependency Injection Pipeline /// /// [ActivatorUtilitiesConstructor] public DiscordConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a clone of another discord configuration. /// /// Client configuration to clone. public DiscordConfiguration(DiscordConfiguration other) { this.Token = other.Token; this.TokenType = other.TokenType; this.MinimumLogLevel = other.MinimumLogLevel; this.UseRelativeRatelimit = other.UseRelativeRatelimit; this.LogTimestampFormat = other.LogTimestampFormat; this.LargeThreshold = other.LargeThreshold; this.AutoReconnect = other.AutoReconnect; this.ShardId = other.ShardId; this.ShardCount = other.ShardCount; this.GatewayCompressionLevel = other.GatewayCompressionLevel; this.MessageCacheSize = other.MessageCacheSize; this.WebSocketClientFactory = other.WebSocketClientFactory; this.UdpClientFactory = other.UdpClientFactory; this.Proxy = other.Proxy; this.HttpTimeout = other.HttpTimeout; this.ReconnectIndefinitely = other.ReconnectIndefinitely; this.Intents = other.Intents; this.LoggerFactory = other.LoggerFactory; this.MobileStatus = other.MobileStatus; this.UseCanary = other.UseCanary; this.AutoRefreshChannelCache = other.AutoRefreshChannelCache; this.ApiVersion = other.ApiVersion; this.ServiceProvider = other.ServiceProvider; + this.Override = other.Override; } } } diff --git a/DisCatSharp/DiscordIntents.cs b/DisCatSharp/DiscordIntents.cs index b1d091498..8e9570558 100644 --- a/DisCatSharp/DiscordIntents.cs +++ b/DisCatSharp/DiscordIntents.cs @@ -1,193 +1,199 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp { /// /// Represents a discord intent extensions. /// public static class DiscordIntentExtensions { /// /// Calculates whether these intents have a certain intent. /// /// The base intents. /// The intents to search for. /// public static bool HasIntent(this DiscordIntents intents, DiscordIntents search) => (intents & search) == search; /// /// Adds an intent to these intents. /// /// The base intents. /// The intents to add. /// public static DiscordIntents AddIntent(this DiscordIntents intents, DiscordIntents toAdd) => intents |= toAdd; /// /// Removes an intent from these intents. /// /// The base intents. /// The intents to remove. /// public static DiscordIntents RemoveIntent(this DiscordIntents intents, DiscordIntents toRemove) => intents &= ~toRemove; /// /// Whether it has all privileged intents. /// /// The intents. internal static bool HasAllPrivilegedIntents(this DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMembers | DiscordIntents.GuildPresences | DiscordIntents.GuildMessages); } /// /// Represents gateway intents to be specified for connecting to Discord. /// [Flags] public enum DiscordIntents { /// /// Whether to include general guild events. /// These include , , , , /// , , , /// , , , , /// , , , /// , , , /// , and . /// Guilds = 1 << 0, /// /// Whether to include guild member events. /// These include , , and . /// This is a privileged intent, and must be enabled on the bot's developer page. /// GuildMembers = 1 << 1, /// /// Whether to include guild ban events. - /// These include , and . + /// These include and . /// GuildBans = 1 << 2, /// /// Whether to include guild emoji and sticker events. /// This includes and . /// GuildEmojisAndStickers = 1 << 3, /// /// Whether to include guild integration events. /// This includes . /// GuildIntegrations = 1 << 4, /// /// Whether to include guild webhook events. /// This includes . /// GuildWebhooks = 1 << 5, /// /// Whether to include guild invite events. - /// These include , and . + /// These include and . /// GuildInvites = 1 << 6, /// /// Whether to include guild voice state events. /// This includes . /// GuildVoiceStates = 1 << 7, /// /// Whether to include guild presence events. /// This includes . /// This is a privileged intent, and must be enabled on the bot's developer page. /// GuildPresences = 1 << 8, /// /// Whether to include guild message events. /// These include , , and . /// This is a privileged intent, and must be enabled on the bot's developer page. /// See https://support-dev.discord.com/hc/en-us/articles/4404772028055 /// GuildMessages = 1 << 9, /// /// Whether to include guild reaction events. - /// These include , , , + /// These include , , /// and . /// GuildMessageReactions = 1 << 10, /// /// Whether to include guild typing events. /// These include . /// GuildMessageTyping = 1 << 11, /// /// Whether to include general direct message events. - /// These include , , , - /// , . + /// These include , , , + /// and . /// These events only fire for DM channels. /// DirectMessages = 1 << 12, /// /// Whether to include direct message reaction events. /// These include , , - /// , and . + /// and . /// These events only fire for DM channels. /// DirectMessageReactions = 1 << 13, /// /// Whether to include direct message typing events. /// This includes . /// This event only fires for DM channels. /// DirectMessageTyping = 1 << 14, + /// + /// Whether to include guild scheduled event events. + /// These include , , , + /// and . + /// The events and are in experiment and not officially supported. + /// + GuildScheduledEvents = 1 << 16, + /// /// Includes all unprivileged intents. /// These are all intents excluding and . - /// The will be excluded as of April 2022. /// - AllUnprivileged = Guilds | GuildBans | GuildEmojisAndStickers | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | - GuildMessageReactions | GuildMessageTyping | DirectMessages | DirectMessageReactions | DirectMessageTyping, + AllUnprivileged = Guilds | GuildBans | GuildEmojisAndStickers | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | GuildMessages | + GuildMessageReactions | GuildMessageTyping | DirectMessages | DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, /// /// Includes all intents. /// The and intents are privileged, and must be enabled on the bot's developer page. - /// The will be privileged as of April 2022. /// - All = AllUnprivileged | GuildMembers | GuildPresences | GuildMessages + All = AllUnprivileged | GuildMembers | GuildPresences } } diff --git a/DisCatSharp/Entities/Application/DiscordApplication.cs b/DisCatSharp/Entities/Application/DiscordApplication.cs index 9bd979ca3..0d3c485a7 100644 --- a/DisCatSharp/Entities/Application/DiscordApplication.cs +++ b/DisCatSharp/Entities/Application/DiscordApplication.cs @@ -1,399 +1,415 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents an OAuth2 application. /// public sealed class DiscordApplication : DiscordMessageApplication, IEquatable { /// /// Gets the application's summary. /// public string Summary { get; internal set; } /// /// Gets the application's icon. /// public override string Icon => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null; /// /// Gets the application's icon hash. /// public string IconHash { get; internal set; } /// /// Gets the application's allowed RPC origins. /// public IReadOnlyList RpcOrigins { get; internal set; } /// /// Gets the application's flags. /// public ApplicationFlags Flags { get; internal set; } /// /// Gets the application's owners. /// public IEnumerable Owners { get; internal set; } /// /// Gets whether this application's bot user requires code grant. /// public bool? RequiresCodeGrant { get; internal set; } /// /// Gets whether this bot application is public. /// public bool? IsPublic { get; internal set; } /// /// Gets the terms of service url of the application. /// public string TermsOfServiceUrl { get; internal set; } /// /// Gets the privacy policy url of the application. /// public string PrivacyPolicyUrl { get; internal set; } /// /// Gets the team name of the application. /// public string TeamName { get; internal set; } /// /// Gets the hash of the application's cover image. /// public string CoverImageHash { get; internal set; } /// /// Gets this application's cover image URL. /// public override string CoverImageUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png?size=1024"; /// /// Gets the team which owns this application. /// public DiscordTeam Team { get; internal set; } /// /// Gets the hex encoded key for verification in interactions and the GameSDK's GetTicket /// public string VerifyKey { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the guild to which it has been linked /// public ulong? GuildId { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists /// public ulong? PrimarySkuId { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the URL slug that links to the store page /// public string Slug { get; internal set; } /// /// Gets or sets a list of . /// private IReadOnlyList Assets { get; set; } + /// + /// A custom url for the Add To Server button. + /// + public string CustomInstallUrl { get; internal set; } + + /// + /// Install parameters for adding the application to a guild. + /// + public DiscordApplicationInstallParams InstallParams { get; internal set; } + + /// + /// The application tags. + /// Not used atm. + /// + public IReadOnlyList Tags { get; internal set; } + /// /// Initializes a new instance of the class. /// internal DiscordApplication() { } /// /// Gets the application's cover image URL, in requested format and size. /// /// Format of the image to get. /// Maximum size of the cover image. Must be a power of two, minimum 16, maximum 2048. /// URL of the application's cover image. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt == ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size < 16 || size > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 != 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = ""; sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Auto or ImageFormat.Png => "png", ImageFormat.WebP => "webp", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); return !string.IsNullOrWhiteSpace(this.CoverImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{sfmt}?size={ssize}" : null; } /// /// Retrieves this application's assets. /// /// This application's assets. public async Task> GetAssetsAsync() { if (this.Assets == null) this.Assets = await this.Discord.ApiClient.GetApplicationAssetsAsync(this).ConfigureAwait(false); return this.Assets; } /// /// Generates an oauth url for the application. /// /// The permissions. /// OAuth Url public string GenerateBotOAuth(Permissions permissions = Permissions.None) { permissions &= PermissionMethods.FULL_PERMS; // hey look, it's not all annoying and blue :P return new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", "bot") .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .ToString(); } /// /// 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 DiscordApplication); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordApplication e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First application to compare. /// Second application to compare. /// Whether the two applications are equal. public static bool operator ==(DiscordApplication e1, DiscordApplication 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); } /// /// Gets whether the two objects are not equal. /// /// First application to compare. /// Second application to compare. /// Whether the two applications are not equal. public static bool operator !=(DiscordApplication e1, DiscordApplication e2) => !(e1 == e2); } /// /// Represents an discord asset. /// public abstract class DiscordAsset { /// /// Gets the ID of this asset. /// public virtual string Id { get; set; } /// /// Gets the URL of this asset. /// public abstract Uri Url { get; } } /// /// Represents an asset for an OAuth2 application. /// public sealed class DiscordApplicationAsset : DiscordAsset, IEquatable { /// /// Gets the Discord client instance for this asset. /// internal BaseDiscordClient Discord { get; set; } /// /// Gets the asset's name. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the asset's type. /// [JsonProperty("type")] public ApplicationAssetType Type { get; internal set; } /// /// Gets the application this asset belongs to. /// public DiscordApplication Application { get; internal set; } /// /// Gets the Url of this asset. /// public override Uri Url => new($"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ASSETS}/{this.Application.Id.ToString(CultureInfo.InvariantCulture)}/{this.Id}.png"); /// /// Initializes a new instance of the class. /// internal DiscordApplicationAsset() { } /// /// Initializes a new instance of the class. /// /// The app. internal DiscordApplicationAsset(DiscordApplication app) { this.Discord = app.Discord; } /// /// 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 DiscordApplicationAsset); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordApplicationAsset e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First application asset to compare. /// Second application asset to compare. /// Whether the two application assets not equal. public static bool operator ==(DiscordApplicationAsset e1, DiscordApplicationAsset 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); } /// /// Gets whether the two objects are not equal. /// /// First application asset to compare. /// Second application asset to compare. /// Whether the two application assets are not equal. public static bool operator !=(DiscordApplicationAsset e1, DiscordApplicationAsset e2) => !(e1 == e2); } /// /// Represents an spotify asset. /// public sealed class DiscordSpotifyAsset : DiscordAsset { /// /// Gets the URL of this asset. /// public override Uri Url => this._url.Value; private readonly Lazy _url; /// /// Initializes a new instance of the class. /// public DiscordSpotifyAsset() { this._url = new Lazy(() => { var ids = this.Id.Split(':'); var id = ids[1]; return new Uri($"https://i.scdn.co/image/{id}"); }); } } /// /// Determines the type of the asset attached to the application. /// public enum ApplicationAssetType : int { /// /// Unknown type. This indicates something went terribly wrong. /// Unknown = 0, /// /// This asset can be used as small image for rich presences. /// SmallImage = 1, /// /// This asset can be used as large image for rich presences. /// LargeImage = 2 } } diff --git a/DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs b/DisCatSharp/Entities/Application/DiscordApplicationInstallParams.cs similarity index 69% copy from DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs copy to DisCatSharp/Entities/Application/DiscordApplicationInstallParams.cs index afa207f37..c3e04c1b0 100644 --- a/DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationInstallParams.cs @@ -1,45 +1,50 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Entities { /// - /// Represents an scheduled event. + /// The application install params. /// - public class DiscordEventEntityMetadata + public sealed class DiscordApplicationInstallParams { /// - /// Gets the the speakers of the stage channel. + /// Gets the scopes. /// - [JsonProperty("speaker_ids", NullValueHandling = NullValueHandling.Ignore)] - public List Speakers { get; internal set; } + [JsonProperty("scopes", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Scopes { get; internal set; } /// - /// Gets the the location of the event. + /// Gets or sets the permissions. /// - [JsonProperty("location", NullValueHandling = NullValueHandling.Ignore)] - public string Location { get; internal set; } + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? Permissions { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal DiscordApplicationInstallParams() { } } } diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index 46b4ea8cb..1b8e66b31 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1383 +1,1402 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; -using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord channel. /// public class DiscordChannel : SnowflakeObject, IEquatable { /// /// Gets ID of the guild to which this channel belongs. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets ID of the category that contains this channel. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] public ulong? ParentId { get; internal set; } /// /// Gets the category that contains this channel. /// [JsonIgnore] public DiscordChannel Parent => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; /// /// Gets the name of this channel. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the type of this channel. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public ChannelType Type { get; internal set; } /// /// Gets this channels's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this channels's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.CHANNELS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Gets the position of this channel. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the maximum available position to move the channel to. /// This can contain outdated informations. /// public int GetMaxPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().Last().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position; } /// /// Gets the minimum available position to move the channel to. /// public int GetMinPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().First().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position; } /// /// Gets whether this channel is a DM channel. /// [JsonIgnore] public bool IsPrivate => this.Type == ChannelType.Private || this.Type == ChannelType.Group; /// /// Gets whether this channel is a channel category. /// [JsonIgnore] public bool IsCategory => this.Type == ChannelType.Category; /// /// Gets whether this channel is a stage channel. /// [JsonIgnore] public bool IsStage => this.Type == ChannelType.Stage; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null; /// /// Gets a collection of permission overwrites for this channel. /// [JsonIgnore] public IReadOnlyList PermissionOverwrites => this._permissionOverwritesLazy.Value; [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] internal List _permissionOverwrites = new(); [JsonIgnore] private readonly Lazy> _permissionOverwritesLazy; /// /// Gets the channel's topic. This is applicable to text channels only. /// [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] public string Topic { get; internal set; } /// /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? LastMessageId { get; internal set; } /// /// Gets this channel's bitrate. This is applicable to voice channels only. /// [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] public int? Bitrate { get; internal set; } /// /// Gets this channel's user limit. This is applicable to voice channels only. /// [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] public int? UserLimit { get; internal set; } /// /// Gets the slow mode delay configured for this channel. /// All bots, as well as users with or permissions in the channel are exempt from slow mode. /// [JsonProperty("rate_limit_per_user")] public int? PerUserRateLimit { get; internal set; } /// /// Gets this channel's video quality mode. This is applicable to voice channels only. /// [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] public VideoQualityMode? QualityMode { get; internal set; } /// /// Gets when the last pinned message was pinned. /// [JsonIgnore] public DateTimeOffset? LastPinTimestamp => !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets when the last pinned message was pinned as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string LastPinTimestampRaw { get; set; } /// /// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity. /// [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; } /// /// Gets this channel's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this); /// /// Gets this channel's children. This applies only to channel categories. /// [JsonIgnore] public IReadOnlyList Children { get { return !this.IsCategory ? throw new ArgumentException("Only channel categories contain children.") : this.Guild._channels.Values.Where(e => e.ParentId == this.Id).ToList(); } } /// /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). /// [JsonIgnore] public virtual IReadOnlyList Users { get { return this.Guild == null ? throw new InvalidOperationException("Cannot query users outside of guild channels.") : this.IsVoiceJoinable() ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() : this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList(); } } /// /// Gets whether this channel is an NSFW channel. /// [JsonProperty("nsfw")] public bool IsNSFW { get; internal set; } /// /// Gets this channel's region id (if voice channel). /// [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] internal string RtcRegionId { get; set; } /// /// Gets this channel's region override (if voice channel). /// [JsonIgnore] public DiscordVoiceRegion RtcRegion => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; /// /// Only sent on the resolved channels of interaction responses for application commands. Gets the permissions of the user in this channel who invoked the command. /// [JsonProperty("permissions")] public Permissions? UserPermissions { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordChannel() { this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this._permissionOverwrites)); } #region Methods /// /// Sends a message to this channel. /// /// Content of the message to send. /// The sent message. - /// Thrown when the client does not have the permission if TTS is true and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission if TTS is true and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(string content) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// Content of the message to send. /// The sent message. - /// Thrown when the client does not have the permission if TTS is true and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission if TTS is true and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(string content, DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. - /// Thrown when the client does not have the permission TTS is true and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission TTS is true and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. - /// Thrown when the client does not have the permission TTS is true and if TTS is true. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission TTS is true and if TTS is true. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Deletes a guild channel /// /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); /// /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. /// /// Reason for audit logs. /// Newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task CloneAsync(string reason = null) { if (this.Guild == null) throw new InvalidOperationException("Non-guild channels cannot be cloned."); var ovrs = new List(); foreach (var ovr in this._permissionOverwrites) #pragma warning disable CS0618 // Type or member is obsolete ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false)); #pragma warning restore CS0618 // Type or member is obsolete var bitrate = this.Bitrate; var userLimit = this.UserLimit; Optional perUserRateLimit = this.PerUserRateLimit; if(!this.IsVoiceJoinable()) { bitrate = null; userLimit = null; } if (this.Type == ChannelType.Stage) { userLimit = null; } if (!this.IsWriteable()) { perUserRateLimit = Optional.FromNoValue(); } return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, reason).ConfigureAwait(false); } /// /// Returns a specific message /// /// The id of the message - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetMessageAsync(ulong id) { return this.Discord.Configuration.MessageCacheSize > 0 && this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg) ? msg : await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false); } /// /// Modifies the current channel. /// /// Action to perform on this channel - /// Thrown when the client does not have the . - /// Thrown when the client does not have the correct for modifying the . - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the . + /// Thrown when the client does not have the correct for modifying the . + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Action action) { var mdl = new ChannelEditModel(); action(mdl); if (mdl.DefaultAutoArchiveDuration.HasValue) { if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value)) throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } if (mdl.Banner.HasValue) { if (!this.Guild.Features.CanSetChannelBanner) throw new NotSupportedException($"Cannot modify Banner. Guild needs boost tier three."); } var bannerb64 = Optional.FromNoValue(); if (mdl.Banner.HasValue && mdl.Banner.Value != null) using (var imgtool = new ImageTool(mdl.Banner.Value)) bannerb64 = imgtool.GetBase64(); else if (mdl.Banner.HasValue) bannerb64 = null; return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, bannerb64, mdl.AuditLogReason); } /// /// Updates the channel position when it doesn't have a category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use for moving within a category. /// /// Position the channel should be moved to. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - [Obsolete("This will be replaced by ModifyPositionInCategoryAsync. Use it instead.")] + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyPositionAsync(int position, string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify order of non-guild channels."); if (!this.IsMovable()) throw new NotSupportedException("You can't move this type of channel in categories."); if (this.ParentId != null) throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead."); var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelReorderPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = chns[i].Id, }; pmds[i].Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position; } return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); } /// /// Updates the channel position within it's own category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The position. /// The reason. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when is out of range. - /// Thrown when function is called on a channel without a parent channel. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when is out of range. + /// Thrown when function is called on a channel without a parent channel. public async Task ModifyPositionInCategoryAsync(int position, string reason = null) { //if (this.ParentId == null) // throw new ArgumentException("You can call this function only on channels in categories."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var isUp = position > this.Position; var channels = await this.InternalRefreshChannelsAsync(); var chns = this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type) : this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type); var ochns = chns.OrderBy(xc => xc.Position).ToArray(); var min = ochns.First().Position; var max = ochns.Last().Position; if (position > max || position < min) throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position."); var pmds = new RestGuildChannelReorderPayload[ochns.Length]; for (var i = 0; i < ochns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = ochns[i].Id, }; if (ochns[i].Id == this.Id) { pmds[i].Position = position; } else { if (isUp) { if (ochns[i].Position <= position && ochns[i].Position > this.Position) { pmds[i].Position = ochns[i].Position - 1; } else if (ochns[i].Position < this.Position || ochns[i].Position > position) { pmds[i].Position = ochns[i].Position; } } else { if (ochns[i].Position >= position && ochns[i].Position < this.Position) { pmds[i].Position = ochns[i].Position + 1; } else if (ochns[i].Position > this.Position || ochns[i].Position < position) { pmds[i].Position = ochns[i].Position; } } } } await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false); } /// /// Internaly refreshes the channel list. /// private async Task> InternalRefreshChannelsAsync() { await this.RefreshPositionsAsync(); return this.Guild.Channels.Values.ToList().AsReadOnly(); } /// /// Refreshes the positions. /// public async Task RefreshPositionsAsync() { var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id); this.Guild._channels.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this.Discord; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = channel.Id; } this.Guild._channels[channel.Id] = channel; } } /// /// Updates the channel position within it's own category. /// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up /// The position. /// The reason. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when is out of range. - /// Thrown when function is called on a channel without a parent channel, a wrong mode is givven or given position is zero. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when is out of range. + /// Thrown when function is called on a channel without a parent channel, a wrong mode is givven or given position is zero. public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null) { if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (mode != "+" && mode != "-" && mode != "down" && mode != "up") throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up"); var positive = mode == "+" || mode == "positive" || mode == "down"; var negative = mode == "-" || mode == "negative" || mode == "up"; return positive ? position < this.GetMaxPosition() ? this.ModifyPositionInCategoryAsync(this.Position + position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : negative ? position > this.GetMinPosition() ? this.ModifyPositionInCategoryAsync(this.Position - position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : throw new ArgumentException("You can only modify with +X or -X. 0 is not valid."); } /// /// Updates the channel parent, moving the channel to the bottom of the new category. /// /// New parent for channel. Will move out of parent if null. /// Sync permissions with parent. Defaults to null. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public Task ModifyParentAsync(DiscordChannel? newParent = null, bool? lock_permissions = null, string reason = null) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (newParent.Type is not ChannelType.Category) throw new ArgumentException("Only category type channels can be parents."); var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNewParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNewParentPayload { ChannelId = chns[i].Id, Position = chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, }; if (chns[i].Id == this.Id) { pmds[i].Position = position; pmds[i].ParentId = newParent is not null ? newParent.Id : null; pmds[i].LockPermissions = lock_permissions; } } return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Moves the channel out of a category. /// /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveParentAsync(string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.Parent is null) //gets list of same type channels with no parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNoParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNoParentPayload { ChannelId = chns[i].Id, }; if (chns[i].Id == this.Id) { pmds[i].Position = 1; pmds[i].ParentId = null; } else { pmds[i].Position = chns[i].Position < this.Position ? chns[i].Position + 1 : chns[i].Position; } } return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Returns a list of messages before a certain message. /// The amount of messages to fetch. /// Message to fetch before from. /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetMessagesBeforeAsync(ulong before, int limit = 100) => this.GetMessagesInternalAsync(limit, before, null, null); /// /// Returns a list of messages after a certain message. /// The amount of messages to fetch. /// Message to fetch after from. /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetMessagesAfterAsync(ulong after, int limit = 100) => this.GetMessagesInternalAsync(limit, null, after, null); /// /// Returns a list of messages around a certain message. /// The amount of messages to fetch. /// Message to fetch around from. /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the channel. /// The amount of messages to fetch. /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetMessagesAsync(int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, null); /// /// Returns a list of messages /// /// How many messages should be returned. /// Get messages before snowflake. /// Get messages after snowflake. /// Get messages around snowflake. private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { if (!this.IsWriteable()) throw new ArgumentException("Cannot get the messages of a non-text channel."); if (limit < 0) throw new ArgumentException("Cannot get a negative number of messages."); if (limit == 0) return Array.Empty(); //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); if (limit > 100 && around != null) throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); var msgs = new List(limit); var remaining = limit; ulong? last = null; var isAfter = after != null; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; if (!isAfter) { msgs.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } else { msgs.InsertRange(0, fetch); last = fetch.FirstOrDefault()?.Id; } } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(msgs); } /// - /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. + /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. /// /// A collection of messages to delete. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task DeleteMessagesAsync(IEnumerable messages, string reason = null) { // don't enumerate more than once var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray(); if (messages == null || !msgs.Any()) throw new ArgumentException("You need to specify at least one message to delete."); if (msgs.Count() < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Count(); i += 100) await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false); } /// /// Deletes a message /// /// The message to be deleted. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Returns a list of invite objects /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetInvitesAsync() { return this.Guild == null ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") : this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); } /// /// Create a new invite object /// /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. /// Max number of uses or 0 for unlimited. Defaults to 0 /// Whether this invite should be temporary. Defaults to false. /// Whether this invite should be unique. Defaults to false. /// The target type. Defaults to null. /// The target activity. Defaults to null. /// The target user id. Defaults to null. /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, TargetType? target_type = null, TargetActivity? target_application = null, ulong? target_user = null, string reason = null) => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, target_type, target_application, target_user, temporary, unique, reason); #region Stage /// /// Opens a stage. /// /// Topic of the stage. /// Whether @everyone should be notified. - /// Privacy level of the stage (Defaults to . + /// Privacy level of the stage (Defaults to . /// Audit log reason. /// Stage instance - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task OpenStageAsync(string topic, bool send_start_notification = false, StagePrivacyLevel privacy_level = StagePrivacyLevel.GUILD_ONLY, string reason = null) + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task OpenStageAsync(string topic, bool send_start_notification = false, StagePrivacyLevel privacy_level = StagePrivacyLevel.GuildOnly, string reason = null) => await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, send_start_notification, privacy_level, reason); /// /// Modifies a stage topic. /// /// New topic of the stage. /// New privacy level of the stage. /// Audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyStageAsync(Optional topic, Optional privacy_level, string reason = null) => await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacy_level, reason); /// /// Closes a stage. /// /// Audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task CloseStageAsync(string reason = null) => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); /// /// Gets a stage. /// /// The requested stage. - /// Thrown when the client does not have the or permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetStageAsync() => await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); #endregion #region Threads /// /// Creates a thread. /// Depending on whether it is created inside an or an it is either an or an . /// Depending on whether the is set to it is either an or an (default). /// /// The name of the thread. /// till it gets archived. Defaults to . /// Can be either an , or an . /// The per user ratelimit, aka slowdown. /// Audit log reason. /// The created thread. - /// Thrown when the client does not have the or or if creating a private thread the permission. - /// Thrown when the guild hasn't enabled threads atm. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have + /// Thrown when the client does not have the or or if creating a private thread the permission. + /// Thrown when the guild hasn't enabled threads atm. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration auto_archive_duration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rate_limit_per_user = null, string reason = null) { return (type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread) ? throw new NotSupportedException("Wrong thread type given.") : (!this.IsThreadHolder()) - ? throw new NotSupportedException("Parent channel can't have threads") + ? throw new NotSupportedException("Parent channel can't have threads.") : type == ChannelType.PrivateThread ? Utilities.CheckThreadPrivateFeature(this.Guild) ? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration) ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, type, rate_limit_per_user, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.") : throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.") : Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration) ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rate_limit_per_user, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } + /// + /// Creates a scheduled event based on the channel type. + /// + /// The name. + /// The scheduled start time. + /// The description. + /// The reason. + /// A scheduled event. + /// Thrown when the ressource does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, string reason = null) + { + if (!this.IsVoiceJoinable()) + throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage."); + + var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance; + + return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, reason); + } + /// /// Gets joined archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived public threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . - /// Thrown when the client does not have the or permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit); #endregion /// /// Adds a channel permission overwrite for specified role. /// /// The role to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); /// /// Adds a channel permission overwrite for specified member. /// /// The member to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); /// /// Deletes a channel permission overwrite for specified member. /// /// The member to have the permission deleted. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteOverwriteAsync(DiscordMember member, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); /// /// Deletes a channel permission overwrite for specified role. /// /// The role to have the permission deleted. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteOverwriteAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); /// /// Post a typing indicator /// - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TriggerTypingAsync() { return !this.IsWriteable() ? throw new ArgumentException("Cannot start typing in a non-text channel.") : this.Discord.ApiClient.TriggerTypingAsync(this.Id); } /// /// Returns all pinned messages /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetPinnedMessagesAsync() { return !this.IsWriteable() ? throw new ArgumentException("A non-text channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); } /// /// Create a new webhook /// /// The name of the webhook. /// The image for the default webhook avatar. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) { var av64 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) using (var imgtool = new ImageTool(avatar.Value)) av64 = imgtool.GetBase64(); else if (avatar.HasValue) av64 = null; return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason).ConfigureAwait(false); } /// /// Returns a list of webhooks /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); /// /// Moves a member to this voice channel /// /// The member to be moved. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exists or if the Member does not exists. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exists or if the Member does not exists. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task PlaceMemberAsync(DiscordMember member) { if (!this.IsVoiceJoinable()) throw new ArgumentException("Cannot place a member in a non-voice channel."); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default, default, this.Id, null).ConfigureAwait(false); } /// /// Follows a news channel /// /// Channel to crosspost messages to - /// Thrown when trying to follow a non-news channel - /// Thrown when the current user doesn't have on the target channel + /// Thrown when trying to follow a non-news channel + /// Thrown when the current user doesn't have on the target channel public Task FollowAsync(DiscordChannel targetChannel) { return this.Type != ChannelType.News ? throw new ArgumentException("Cannot follow a non-news channel.") : this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); } /// /// Publishes a message in a news channel to following channels /// /// Message to publish - /// Thrown when the message has already been crossposted - /// + /// Thrown when the message has already been crossposted + /// /// Thrown when the current user doesn't have and/or /// public Task CrosspostMessageAsync(DiscordMessage message) { return (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted ? throw new ArgumentException("Message is already crossposted.") : this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); } /// /// Updates the current user's suppress state in this channel, if stage channel. /// /// Toggles the suppress state. /// Sets the time the user requested to speak. - /// Thrown when the channel is not a stage channel. + /// Thrown when the channel is not a stage channel. public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) { if (this.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false); } /// /// Calculates permissions for a given member. /// /// Member to calculate permissions for. /// Calculated permissions for a given member. public Permissions PermissionsFor(DiscordMember mbr) { // future note: might be able to simplify @everyone role checks to just check any role ... but I'm not sure // xoxo, ~uwx // // you should use a single tilde // ~emzi // user > role > everyone // allow > deny > undefined // => // user allow > user deny > role allow > role deny > everyone allow > everyone deny // thanks to meew0 if (this.IsPrivate || this.Guild == null) return Permissions.None; if (this.Guild.OwnerId == mbr.Id) return PermissionMethods.FULL_PERMS; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // roles that member is in var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray(); // assign permissions from member's roles (in order) perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Adminstrator grants all permissions and cannot be overridden if ((perms & Permissions.Administrator) == Permissions.Administrator) return PermissionMethods.FULL_PERMS; // channel overrides for roles that member is in var mbRoleOverrides = mbRoles .Select(xr => this._permissionOverwrites.FirstOrDefault(xo => xo.Id == xr.Id)) .Where(xo => xo != null) .ToList(); // assign channel permission overwrites for @everyone pseudo-role var everyoneOverwrites = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); if (everyoneOverwrites != null) { perms &= ~everyoneOverwrites.Denied; perms |= everyoneOverwrites.Allowed; } // assign channel permission overwrites for member's roles (explicit deny) perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied); // assign channel permission overwrites for member's roles (explicit allow) perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed); // channel overrides for just this member var mbOverrides = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == mbr.Id); if (mbOverrides == null) return perms; // assign channel permission overwrites for just this member perms &= ~mbOverrides.Denied; perms |= mbOverrides.Allowed; return perms; } /// /// Returns a string representation of this channel. /// /// String representation of this channel. public override string ToString() { return this.Type == ChannelType.Category ? $"Channel Category {this.Name} ({this.Id})" : this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread() ? $"Channel #{this.Name} ({this.Id})" : this.IsVoiceJoinable() ? $"Channel #!{this.Name} ({this.Id})" : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; } #endregion /// /// 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 DiscordChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordChannel e1, DiscordChannel 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); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordChannel e1, DiscordChannel e2) => !(e1 == e2); } } diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Entities/DiscordProtocol.cs similarity index 74% copy from DisCatSharp/Enums/Interaction/ComponentType.cs copy to DisCatSharp/Entities/DiscordProtocol.cs index e7faca56c..d02f125fb 100644 --- a/DisCatSharp/Enums/Interaction/ComponentType.cs +++ b/DisCatSharp/Entities/DiscordProtocol.cs @@ -1,45 +1,46 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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. -namespace DisCatSharp.Enums +namespace DisCatSharp.Entities { /// - /// Represents a type of component. + /// Represents the discord protocol. /// - public enum ComponentType + public class DiscordProtocol { - /// - /// A row of components. - /// - ActionRow = 1, + #region Properties - /// - /// A button. - /// - Button = 2, + #endregion /// - /// A select menu. + /// Initializes a new instance of the class. /// - Select = 3 + public DiscordProtocol() + { + + } + + #region Methods + + #endregion } } diff --git a/DisCatSharp/Entities/DiscordUri.cs b/DisCatSharp/Entities/DiscordUri.cs index 06c8c5b5a..8125885de 100644 --- a/DisCatSharp/Entities/DiscordUri.cs +++ b/DisCatSharp/Entities/DiscordUri.cs @@ -1,162 +1,162 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Runtime.CompilerServices; using Newtonsoft.Json; namespace DisCatSharp.Net { /// /// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the attachment:// /// protocol, it mustn't contain a trailing slash to be interpreted correctly as an embed attachment reference by /// Discord. /// [JsonConverter(typeof(DiscordUriJsonConverter))] public class DiscordUri { private readonly object _value; /// /// The type of this URI. /// public DiscordUriType Type { get; } /// /// Initializes a new instance of the class. /// /// The value. internal DiscordUri(Uri value) { this._value = value ?? throw new ArgumentNullException(nameof(value)); this.Type = DiscordUriType.Standard; } /// /// Initializes a new instance of the class. /// /// The value. internal DiscordUri(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); if (IsStandard(value)) { this._value = new Uri(value); this.Type = DiscordUriType.Standard; } else { this._value = value; this.Type = DiscordUriType.NonStandard; } } // can be changed in future /// /// If the uri is a standard uri /// /// Uri string [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsStandard(string value) => !value.StartsWith("attachment://"); /// /// Returns a string representation of this DiscordUri. /// /// This DiscordUri, as a string. public override string ToString() => this._value.ToString(); /// - /// Converts this DiscordUri into a canonical representation of a if it can be represented as + /// Converts this DiscordUri into a canonical representation of a if it can be represented as /// such, throwing an exception otherwise. /// /// A canonical representation of this DiscordUri. - /// If is not , as + /// If is not , as /// that would mean creating an invalid Uri, which would result in loss of data. public Uri ToUri() => this.Type == DiscordUriType.Standard ? this._value as Uri : throw new UriFormatException( $@"DiscordUri ""{this._value}"" would be invalid as a regular URI, please the {nameof(this.Type)} property first."); /// /// Represents a uri json converter. /// internal sealed class DiscordUriJsonConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue((value as DiscordUri)._value); /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var val = reader.Value; return val == null ? null : val is not string s - ? throw new JsonReaderException("DiscordUri value invalid format! This is a bug in DSharpPlus. " + + ? throw new JsonReaderException("DiscordUri value invalid format! This is a bug in DisCatSharp. " + $"Include the type in your bug report: [[{reader.TokenType}]]") : IsStandard(s) ? new DiscordUri(new Uri(s)) : new DiscordUri(s); } /// /// Whether it can be converted. /// /// The object type. /// A bool. public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUri); } } /// /// Represents a uri type. /// public enum DiscordUriType : byte { /// - /// Represents a URI that conforms to RFC 3986, meaning it's stored internally as a and will + /// Represents a URI that conforms to RFC 3986, meaning it's stored internally as a and will /// contain a trailing slash after the domain name. /// Standard, /// /// Represents a URI that does not conform to RFC 3986, meaning it's stored internally as a plain string and /// should be treated as one. /// NonStandard } } diff --git a/DisCatSharp/Entities/Event/DiscordEvent.cs b/DisCatSharp/Entities/Event/DiscordEvent.cs deleted file mode 100644 index 01acfef01..000000000 --- a/DisCatSharp/Entities/Event/DiscordEvent.cs +++ /dev/null @@ -1,224 +0,0 @@ -// This file is part of the DisCatSharp project. -// -// Copyright (c) 2021 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.Globalization; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DisCatSharp.Entities -{ - /// - /// Represents an scheduled event. - /// - public class DiscordEvent : SnowflakeObject, IEquatable - { - /// - /// Gets the associated channel. - /// - [JsonIgnore] - public Task Channel - => this.ChannelId.HasValue ? this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value) : null; - - /// - /// Gets id of the associated channel id. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; internal set; } - - - /// - /// Gets the guild id of the associated Stage channel. - /// - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong GuildId { get; internal set; } - - /// - /// Gets the guild to which this channel belongs. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null; - - /// - /// Gets the name of the scheduled event. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the description of the scheduled event. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// Gets the scheduled start time of the scheduled event. - /// - [JsonIgnore] - public DateTimeOffset? ScheduledStartTime - => !string.IsNullOrWhiteSpace(this.ScheduledStartTimeRaw) && DateTimeOffset.TryParse(this.ScheduledStartTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? - dto : null; - - /// - /// Gets the scheduled start time of the scheduled event as raw string. - /// - [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] - internal string ScheduledStartTimeRaw { get; set; } - - /// - /// Gets the scheduled end time of the scheduled event. - /// - [JsonIgnore] - public DateTimeOffset? ScheduledEndTime - => !string.IsNullOrWhiteSpace(this.ScheduledEndTimeRaw) && DateTimeOffset.TryParse(this.ScheduledEndTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? - dto : null; - - /// - /// Gets the scheduled end time of the scheduled event as raw string. - /// - [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] - internal string ScheduledEndTimeRaw { get; set; } - - /// - /// Gets the privacy level of the scheduled event. - /// - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public StagePrivacyLevel PrivacyLevel { get; internal set; } - - /// - /// Gets the status of the scheduled event. - /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public EventStatus Status { get; internal set; } - - /// - /// Gets the entity type. - /// - [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] - public EventEntityType EntityType { get; internal set; } - - /// - /// Gets id of the entity. - /// - [JsonProperty("entity_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? EntityId { get; internal set; } - - /// - /// Gets metadata of the entity. - /// - [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEventEntityMetadata EntityMetadata { get; internal set; } - - /// - /// Gets the total number of users subscribed to the scheduled event. - /// - [JsonProperty("user_count", NullValueHandling = NullValueHandling.Ignore)] - public int UserCount { get; internal set; } - - /// - /// Initializes a new instance of the class. - /// - internal DiscordEvent() { } - - #region Methods - - /// - /// Updates a scheduled event. - /// - /// New channel of the event. - /// New name of the event. - /// New DateTime when the event should start. - /// New description of the event. - /// New Privacy Level of the stage instance. - /// New of the event. - /// Audit log reason - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async Task ModifyAsync(Optional channel, Optional name, Optional description, Optional scheduled_start_time, Optional privacy_level, Optional type, string reason = null) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - => throw new NotImplementedException("This method is not implemented yet."); /*await this.Discord.ApiClient.ModifyStageEventAsync(this.Id, channel, name, scheduled_start_time, description, privacy_level, type, reason);*/ - - /// - /// Deletes a scheduled event. - /// - /// Audit log reason - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async Task DeleteAsync(string reason = null) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - => throw new NotImplementedException("This method is not implemented yet."); /*await this.Discord.ApiClient.DeleteStageEventAsync(this.Id, reason);*/ - - #endregion - - /// - /// 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 DiscordEvent); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordEvent e) - => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// Gets whether the two objects are equal. - /// - /// First event to compare. - /// Second ecent to compare. - /// Whether the two events are equal. - public static bool operator ==(DiscordEvent e1, DiscordEvent 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); - } - - /// - /// Gets whether the two objects are not equal. - /// - /// First event to compare. - /// Second event to compare. - /// Whether the two events are not equal. - public static bool operator !=(DiscordEvent e1, DiscordEvent e2) - => !(e1 == e2); - } -} diff --git a/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs b/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs index 820fad267..379095e4f 100644 --- a/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs +++ b/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs @@ -1,1047 +1,1062 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp.Entities { /// /// Represents an audit log entry. /// public abstract class DiscordAuditLogEntry : SnowflakeObject { /// /// Gets the entry's action type. /// public AuditLogActionType ActionType { get; internal set; } /// /// Gets the user responsible for the action. /// public DiscordUser UserResponsible { get; internal set; } /// /// Gets the reason defined in the action. /// public string Reason { get; internal set; } /// /// Gets the category under which the action falls. /// public AuditLogActionCategory ActionCategory { get; internal set; } } /// /// Represents a description of how a property changed. /// /// Type of the changed property. public sealed class PropertyChange { /// /// The property's value before it was changed. /// public T Before { get; internal set; } /// /// The property's value after it was changed. /// public T After { get; internal set; } } /// /// Represents a audit log guild entry. /// public sealed class DiscordAuditLogGuildEntry : DiscordAuditLogEntry { /// /// Gets the affected guild. /// public DiscordGuild Target { get; internal set; } /// /// Gets the description of guild name's change. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the description of owner's change. /// public PropertyChange OwnerChange { get; internal set; } /// /// Gets the description of icon's change. /// public PropertyChange IconChange { get; internal set; } /// /// Gets the description of verification level's change. /// public PropertyChange VerificationLevelChange { get; internal set; } /// /// Gets the description of afk channel's change. /// public PropertyChange AfkChannelChange { get; internal set; } /// /// Gets the description of widget channel's change. /// public PropertyChange EmbedChannelChange { get; internal set; } /// /// Gets the description of notification settings' change. /// public PropertyChange NotificationSettingsChange { get; internal set; } /// /// Gets the description of system message channel's change. /// public PropertyChange SystemChannelChange { get; internal set; } /// /// Gets the description of explicit content filter settings' change. /// public PropertyChange ExplicitContentFilterChange { get; internal set; } /// /// Gets the description of guild's mfa level change. /// public PropertyChange MfaLevelChange { get; internal set; } /// /// Gets the description of invite splash's change. /// public PropertyChange SplashChange { get; internal set; } /// /// Gets the description of the guild's region change. /// public PropertyChange RegionChange { get; internal set; } + /// + /// Gets the description of the guild's premium progress bar enabled state. + /// + public PropertyChange PremiumProgressBarChange { get; internal set; } + /// /// Initializes a new instance of the class. /// internal DiscordAuditLogGuildEntry() { } } /// /// Represents a audit log channel entry. /// public sealed class DiscordAuditLogChannelEntry : DiscordAuditLogEntry { /// /// Gets the affected channel. /// public DiscordChannel Target { get; internal set; } /// /// Gets the description of channel's name change. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the description of channel's type change. /// public PropertyChange TypeChange { get; internal set; } /// /// Gets the description of channel's nsfw flag change. /// public PropertyChange NsfwChange { get; internal set; } /// /// Gets the description of channel's bitrate change. /// public PropertyChange BitrateChange { get; internal set; } /// /// Gets the description of channel permission overwrites' change. /// public PropertyChange> OverwriteChange { get; internal set; } /// /// Gets the description of channel's topic change. /// public PropertyChange TopicChange { get; internal set; } /// /// Gets the description of channel's slow mode timeout change. /// public PropertyChange PerUserRateLimitChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogChannelEntry() { } } /// /// Represents a audit log overwrite entry. /// public sealed class DiscordAuditLogOverwriteEntry : DiscordAuditLogEntry { /// /// Gets the affected overwrite. /// public DiscordOverwrite Target { get; internal set; } /// /// Gets the channel for which the overwrite was changed. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the description of overwrite's allow value change. /// public PropertyChange AllowChange { get; internal set; } /// /// Gets the description of overwrite's deny value change. /// public PropertyChange DenyChange { get; internal set; } /// /// Gets the description of overwrite's type change. /// public PropertyChange TypeChange { get; internal set; } /// /// Gets the description of overwrite's target id change. /// public PropertyChange TargetIdChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogOverwriteEntry() { } } /// /// Represents a audit log kick entry. /// public sealed class DiscordAuditLogKickEntry : DiscordAuditLogEntry { /// /// Gets the kicked member. /// public DiscordMember Target { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogKickEntry() { } } /// /// Represents a audit log prune entry. /// public sealed class DiscordAuditLogPruneEntry : DiscordAuditLogEntry { /// /// Gets the number inactivity days after which members were pruned. /// public int Days { get; internal set; } /// /// Gets the number of members pruned. /// public int Toll { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogPruneEntry() { } } /// /// Represents a audit log ban entry. /// public sealed class DiscordAuditLogBanEntry : DiscordAuditLogEntry { /// /// Gets the banned member. /// public DiscordMember Target { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogBanEntry() { } } /// /// Represents a audit log member update entry. /// public sealed class DiscordAuditLogMemberUpdateEntry : DiscordAuditLogEntry { /// /// Gets the affected member. /// public DiscordMember Target { get; internal set; } /// /// Gets the description of member's nickname change. /// public PropertyChange NicknameChange { get; internal set; } /// /// Gets the roles that were removed from the member. /// public IReadOnlyList RemovedRoles { get; internal set; } /// /// Gets the roles that were added to the member. /// public IReadOnlyList AddedRoles { get; internal set; } /// /// Gets the description of member's mute status change. /// public PropertyChange MuteChange { get; internal set; } /// /// Gets the description of member's deaf status change. /// public PropertyChange DeafenChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogMemberUpdateEntry() { } } /// /// Represents a audit log role update entry. /// public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry { /// /// Gets the affected role. /// public DiscordRole Target { get; internal set; } /// /// Gets the description of role's name change. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the description of role's color change. /// public PropertyChange ColorChange { get; internal set; } /// /// Gets the description of role's permission set change. /// public PropertyChange PermissionChange { get; internal set; } /// /// Gets the description of the role's position change. /// public PropertyChange PositionChange { get; internal set; } /// /// Gets the description of the role's mentionability change. /// public PropertyChange MentionableChange { get; internal set; } /// /// Gets the description of the role's hoist status change. /// public PropertyChange HoistChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogRoleUpdateEntry() { } } /// /// Represents a audit log invite entry. /// public sealed class DiscordAuditLogInviteEntry : DiscordAuditLogEntry { /// /// Gets the affected invite. /// public DiscordInvite Target { get; internal set; } /// /// Gets the description of invite's max age change. /// public PropertyChange MaxAgeChange { get; internal set; } /// /// Gets the description of invite's code change. /// public PropertyChange CodeChange { get; internal set; } /// /// Gets the description of invite's temporariness change. /// public PropertyChange TemporaryChange { get; internal set; } /// /// Gets the description of invite's inviting member change. /// public PropertyChange InviterChange { get; internal set; } /// /// Gets the description of invite's target channel change. /// public PropertyChange ChannelChange { get; internal set; } /// /// Gets the description of invite's use count change. /// public PropertyChange UsesChange { get; internal set; } /// /// Gets the description of invite's max use count change. /// public PropertyChange MaxUsesChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogInviteEntry() { } } /// /// Represents a audit log webhook entry. /// public sealed class DiscordAuditLogWebhookEntry : DiscordAuditLogEntry { /// /// Gets the affected webhook. /// public DiscordWebhook Target { get; internal set; } /// /// Gets the description of webhook's name change. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the description of webhook's target channel change. /// public PropertyChange ChannelChange { get; internal set; } /// /// Gets the description of webhook's type change. /// public PropertyChange TypeChange { get; internal set; } /// /// Gets the description of webhook's avatar change. /// public PropertyChange AvatarHashChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogWebhookEntry() { } } /// /// Represents a audit log emoji entry. /// public sealed class DiscordAuditLogEmojiEntry : DiscordAuditLogEntry { /// /// Gets the affected emoji. /// public DiscordEmoji Target { get; internal set; } /// /// Gets the description of emoji's name change. /// public PropertyChange NameChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogEmojiEntry() { } } /// /// Represents a audit log sticker entry. /// public sealed class DiscordAuditLogStickerEntry : DiscordAuditLogEntry { /// /// Gets the affected sticker. /// public DiscordSticker Target { get; internal set; } /// /// Gets the description of sticker's name change. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the description of sticker's description change. /// public PropertyChange DescriptionChange { get; internal set; } /// /// Gets the description of sticker's tags change. /// public PropertyChange TagsChange { get; internal set; } /// /// Gets the description of sticker's tags change. /// public PropertyChange AssetChange { get; internal set; } /// /// Gets the description of sticker's guild id change. /// public PropertyChange GuildIdChange { get; internal set; } /// /// Gets the description of sticker's availability change. /// public PropertyChange AvailabilityChange { get; internal set; } /// /// Gets the description of sticker's id change. /// public PropertyChange IdChange { get; internal set; } /// /// Gets the description of sticker's type change. /// public PropertyChange TypeChange { get; internal set; } /// /// Gets the description of sticker's format change. /// public PropertyChange FormatChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogStickerEntry() { } } /// /// Represents a audit log message entry. /// public sealed class DiscordAuditLogMessageEntry : DiscordAuditLogEntry { /// /// Gets the affected message. Note that more often than not, this will only have ID specified. /// public DiscordMessage Target { get; internal set; } /// /// Gets the channel in which the action occurred. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the number of messages that were affected. /// public int? MessageCount { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogMessageEntry() { } } /// /// Represents a audit log message pin entry. /// public sealed class DiscordAuditLogMessagePinEntry : DiscordAuditLogEntry { /// /// Gets the affected message's user. /// public DiscordUser Target { get; internal set; } /// /// Gets the channel the message is in. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the message the pin action was for. /// public DiscordMessage Message { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogMessagePinEntry() { } } /// /// Represents a audit log bot add entry. /// public sealed class DiscordAuditLogBotAddEntry : DiscordAuditLogEntry { /// /// Gets the bot that has been added to the guild. /// public DiscordUser TargetBot { get; internal set; } } /// /// Represents a audit log member move entry. /// public sealed class DiscordAuditLogMemberMoveEntry : DiscordAuditLogEntry { /// /// Gets the channel the members were moved in. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the amount of users that were moved out from the voice channel. /// public int UserCount { get; internal set; } } /// /// Represents a audit log member disconnect entry. /// public sealed class DiscordAuditLogMemberDisconnectEntry : DiscordAuditLogEntry { /// /// Gets the amount of users that were disconnected from the voice channel. /// public int UserCount { get; internal set; } } /// /// Represents a audit log integration entry. /// public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry { /// /// Gets the description of emoticons' change. /// public PropertyChange EnableEmoticons { get; internal set; } /// /// Gets the description of expire grace period's change. /// public PropertyChange ExpireGracePeriod { get; internal set; } /// /// Gets the description of expire behavior change. /// public PropertyChange ExpireBehavior { get; internal set; } } /// /// Represents a audit log stage entry. /// public sealed class DiscordAuditLogStageEntry : DiscordAuditLogEntry { /// /// Gets the affected stage instance /// public DiscordStageInstance Target { get; internal set; } /// /// Gets the description of stage instance's topic change. /// public PropertyChange TopicChange { get; internal set; } /// /// Gets the description of stage instance's privacy level change. /// public PropertyChange PrivacyLevelChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogStageEntry() { } } /// /// Represents a audit log event entry. /// - public sealed class DiscordAuditLogScheduledEventEntry : DiscordAuditLogEntry + public sealed class DiscordAuditLogGuildScheduledEventEntry : DiscordAuditLogEntry { /// /// Gets the affected thread /// - public DiscordEvent Target { get; internal set; } + public DiscordScheduledEvent Target { get; internal set; } /// /// Gets the channel change. /// public PropertyChange ChannelIdChange { get; internal set; } /// /// Gets the description change. /// public PropertyChange DescriptionChange { get; internal set; } - /* + /* Will be added https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969137241 public PropertyChange<> ScheduledStartTimeChange { get; internal set; } public PropertyChange<> ScheduledEndTimeChange { get; internal set; } */ + /// + /// Gets the location change. + /// + public PropertyChange LocationChange { get; internal set; } + /// /// Gets the privacy level change. /// - public PropertyChange PrivacyLevelChange { get; internal set; } + public PropertyChange PrivacyLevelChange { get; internal set; } /// /// Gets the status change. /// - public PropertyChange StatusChange { get; internal set; } + public PropertyChange StatusChange { get; internal set; } /// + /// Gets the entity type change. + /// + public PropertyChange EntityTypeChange { get; internal set; } + + /*/// /// Gets the sku ids change. /// - public PropertyChange> SkuIdsChange { get; internal set; } + public PropertyChange> SkuIdsChange { get; internal set; }*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - internal DiscordAuditLogScheduledEventEntry() { } + internal DiscordAuditLogGuildScheduledEventEntry() { } } /// /// Represents a audit log thread entry. /// public sealed class DiscordAuditLogThreadEntry : DiscordAuditLogEntry { /// /// Gets the affected thread /// public DiscordThreadChannel Target { get; internal set; } /// /// Gets the name of the thread. /// public PropertyChange NameChange { get; internal set; } /// /// Gets the type of the thread. /// public PropertyChange TypeChange { get; internal set; } /// /// Gets the archived state of the thread. /// public PropertyChange ArchivedChange { get; internal set; } /// /// Gets the locked state of the thread. /// public PropertyChange LockedChange { get; internal set; } /// /// Gets the new auto archive duration of the thread. /// public PropertyChange AutoArchiveDurationChange { get; internal set; } /// /// Gets the new ratelimit of the thread. /// public PropertyChange PerUserRateLimitChange { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAuditLogThreadEntry() { } } /// /// Indicates audit log action category. /// public enum AuditLogActionCategory { /// /// Indicates that this action resulted in creation or addition of an object. /// Create, /// /// Indicates that this action resulted in update of an object. /// Update, /// /// Indicates that this action resulted in deletion or removal of an object. /// Delete, /// /// Indicates that this action resulted in something else than creation, addition, update, deleteion, or removal of an object. /// Other } // below is taken from // https://github.com/Rapptz/discord.py/blob/rewrite/discord/enums.py#L125 /// /// Represents type of the action that was taken in given audit log event. /// public enum AuditLogActionType : int { /// /// Indicates that the guild was updated. /// GuildUpdate = 1, /// /// Indicates that the channel was created. /// ChannelCreate = 10, /// /// Indicates that the channel was updated. /// ChannelUpdate = 11, /// /// Indicates that the channel was deleted. /// ChannelDelete = 12, /// /// Indicates that the channel permission overwrite was created. /// OverwriteCreate = 13, /// /// Indicates that the channel permission overwrite was updated. /// OverwriteUpdate = 14, /// /// Indicates that the channel permission overwrite was deleted. /// OverwriteDelete = 15, /// /// Indicates that the user was kicked. /// Kick = 20, /// /// Indicates that users were pruned. /// Prune = 21, /// /// Indicates that the user was banned. /// Ban = 22, /// /// Indicates that the user was unbanned. /// Unban = 23, /// /// Indicates that the member was updated. /// MemberUpdate = 24, /// /// Indicates that the member's roles were updated. /// MemberRoleUpdate = 25, /// /// Indicates that the member has moved to another voice channel. /// MemberMove = 26, /// /// Indicates that the member has disconnected from a voice channel. /// MemberDisconnect = 27, /// /// Indicates that a bot was added to the guild. /// BotAdd = 28, /// /// Indicates that the role was created. /// RoleCreate = 30, /// /// Indicates that the role was updated. /// RoleUpdate = 31, /// /// Indicates that the role was deleted. /// RoleDelete = 32, /// /// Indicates that the invite was created. /// InviteCreate = 40, /// /// Indicates that the invite was updated. /// InviteUpdate = 41, /// /// Indicates that the invite was deleted. /// InviteDelete = 42, /// /// Indicates that the webhook was created. /// WebhookCreate = 50, /// /// Indicates that the webook was updated. /// WebhookUpdate = 51, /// /// Indicates that the webhook was deleted. /// WebhookDelete = 52, /// /// Indicates that an emoji was created. /// EmojiCreate = 60, /// /// Indicates that an emoji was updated. /// EmojiUpdate = 61, /// /// Indicates that an emoji was deleted. /// EmojiDelete = 62, /// /// Indicates that the message was deleted. /// MessageDelete = 72, /// /// Indicates that messages were bulk-deleted. /// MessageBulkDelete = 73, /// /// Indicates that a message was pinned. /// MessagePin = 74, /// /// Indicates that a message was unpinned. /// MessageUnpin = 75, /// /// Indicates that an integration was created. /// IntegrationCreate = 80, /// /// Indicates that an integration was updated. /// IntegrationUpdate = 81, /// /// Indicates that an integration was deleted. /// IntegrationDelete = 82, /// /// Indicates that an stage instance was created. /// StageInstanceCreate = 83, /// /// Indicates that an stage instance was updated. /// StageInstanceUpdate = 84, /// /// Indicates that an stage instance was deleted. /// StageInstanceDelete = 85, /// /// Indicates that an sticker was created. /// StickerCreate = 90, /// /// Indicates that an sticker was updated. /// StickerUpdate = 91, /// /// Indicates that an sticker was deleted. /// StickerDelete = 92, /// /// Indicates that an event was created. /// - ScheduledEventCreate = 100, + GuildScheduledEventCreate = 100, /// /// Indicates that an event was updated. /// - ScheduledEventUpdate = 101, + GuildScheduledEventUpdate = 101, /// /// Indicates that an event was deleted. /// - ScheduledEventDelete = 102, + GuildScheduledEventDelete = 102, /// /// Indicates that an thread was created. /// ThreadCreate = 110, /// /// Indicates that an thread was updated. /// ThreadUpdate = 111, /// /// Indicates that an thread was deleted. /// ThreadDelete = 112, } } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs index b6d16fbfb..168a816e6 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs @@ -1,3375 +1,3483 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Entities { /// /// Represents a Discord guild. /// public class DiscordGuild : SnowflakeObject, IEquatable { /// /// Gets the guild's name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets the preferred locale of this guild. /// This is used for server discovery and notices from Discord. Defaults to en-US. /// [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] public string PreferredLocale { get; internal set; } /// /// Gets the ID of the guild's owner. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } - /// - /// Gets permissions for the user in the guild (does not include channel overrides) - /// - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public Permissions? Permissions { get; set; } - /// /// Gets the guild's owner. /// [JsonIgnore] public DiscordMember Owner => this.Members.TryGetValue(this.OwnerId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult(); + /// + /// Gets permissions for the user in the guild (does not include channel overrides) + /// + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? Permissions { get; set; } + /// /// Gets the guild's voice region ID. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] internal string VoiceRegionId { get; set; } /// /// Gets the guild's voice region. /// [JsonIgnore] public DiscordVoiceRegion VoiceRegion => this.Discord.VoiceRegions[this.VoiceRegionId]; /// /// Gets the guild's AFK voice channel ID. /// [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong AfkChannelId { get; set; } = 0; /// /// Gets the guild's AFK voice channel. /// [JsonIgnore] public DiscordChannel AfkChannel => this.GetChannel(this.AfkChannelId); /// /// Gets the guild's AFK timeout. /// [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] public int AfkTimeout { get; internal set; } /// /// Gets the guild's verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel VerificationLevel { get; internal set; } /// /// Gets the guild's default notification settings. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; } /// /// Gets the guild's explicit content filter settings. /// [JsonProperty("explicit_content_filter")] public ExplicitContentFilter ExplicitContentFilter { get; internal set; } /// /// Gets the guild's nsfw level. /// [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; internal set; } /// /// Gets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? SystemChannelId { get; set; } /// /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] public DiscordChannel SystemChannel => this.SystemChannelId.HasValue ? this.GetChannel(this.SystemChannelId.Value) : null; /// /// Gets the settings for this guild's system channel. /// [JsonProperty("system_channel_flags")] public SystemChannelFlags SystemChannelFlags { get; internal set; } /// /// Gets whether this guild's widget is enabled. /// [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? WidgetEnabled { get; internal set; } /// /// Gets the widget channel id. /// [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? WidgetChannelId { get; set; } /// /// Gets the widget channel for this guild. /// [JsonIgnore] public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue ? this.GetChannel(this.WidgetChannelId.Value) : null; /// /// Gets the rules channel id. /// [JsonProperty("rules_channel_id")] internal ulong? RulesChannelId { get; set; } /// /// Gets the rules channel for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel RulesChannel => this.RulesChannelId.HasValue ? this.GetChannel(this.RulesChannelId.Value) : null; /// /// Gets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] internal ulong? PublicUpdatesChannelId { get; set; } /// /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue ? this.GetChannel(this.PublicUpdatesChannelId.Value) : null; /// /// Gets the application id of this guild if it is bot created. /// [JsonProperty("application_id")] public ulong? ApplicationId { get; internal set; } /// /// Gets a collection of this guild's roles. /// [JsonIgnore] public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this._roles); [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _roles; /// /// Gets a collection of this guild's stickers. /// [JsonIgnore] public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this._stickers); [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _stickers; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this._emojis); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _emojis; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList RawFeatures { get; internal set; } /// /// Gets the guild's features. /// [JsonIgnore] public GuildFeatures Features => new(this); /// /// Gets the required multi-factor authentication level for this guild. /// [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] public MfaLevel MfaLevel { get; internal set; } /// /// Gets this guild's join date. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Gets whether this guild is considered to be a large guild. /// [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] public bool IsLarge { get; internal set; } /// /// Gets whether this guild is unavailable. /// [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] public bool IsUnavailable { get; internal set; } /// /// Gets the total number of members in this guild. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int MemberCount { get; internal set; } /// /// Gets the maximum amount of members allowed for this guild. /// [JsonProperty("max_members")] public int? MaxMembers { get; internal set; } /// /// Gets the maximum amount of presences allowed for this guild. /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } #pragma warning disable CS1734 /// /// Gets the approximate number of members in this guild, when using and having set to true. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the approximate number of presences in this guild, when using and having set to true. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } #pragma warning restore CS1734 /// /// Gets the maximum amount of users allowed per video channel. /// [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxVideoChannelUsers { get; internal set; } /// /// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user /// the voice state corresponds to. /// [JsonIgnore] public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this._voiceStates); [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _voiceStates; /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// [JsonIgnore] // TODO overhead of => vs Lazy? it's a struct public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this._members); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _members; /// /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this._channels); [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _channels; internal ConcurrentDictionary _invites; /// /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Threads { get; internal set; } [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _threads = new(); /// /// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID. /// [JsonIgnore] public IReadOnlyDictionary StageInstances { get; internal set; } [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _stageInstances = new(); /// /// Gets a dictionary of all scheduled events. /// [JsonIgnore] - public IReadOnlyDictionary ScheduledEvents { get; internal set; } + public IReadOnlyDictionary ScheduledEvents { get; internal set; } - [JsonProperty("events", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary _scheduledEvents = new(); + internal ConcurrentDictionary _scheduledEvents = new(); /// /// Gets the guild member for current user. /// [JsonIgnore] public DiscordMember CurrentMember => this._current_member_lazy.Value; [JsonIgnore] private readonly Lazy _current_member_lazy; /// /// Gets the @everyone role for this guild. /// [JsonIgnore] public DiscordRole EveryoneRole => this.GetRole(this.Id); [JsonIgnore] internal bool _isOwner; /// /// Gets whether the current user is the guild's owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] public bool IsOwner { get => this._isOwner || this.OwnerId == this.Discord.CurrentUser.Id; internal set => this._isOwner = value; } /// /// Gets the vanity URL code for this guild, when applicable. /// [JsonProperty("vanity_url_code")] public string VanityUrlCode { get; internal set; } /// /// Gets the guild description, when applicable. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Gets this guild's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this guild's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Whether this guild has the community feature enabled. /// [JsonIgnore] public bool IsCommunity => this.Features.HasCommunityEnabled; /// /// Whether this guild has enabled the welcome screen. /// [JsonIgnore] public bool HasWelcomeScreen => this.Features.HasWelcomeScreenEnabled; /// /// Whether this guild has enabled membership screening. /// [JsonIgnore] public bool HasMemberVerificationGate => this.Features.HasMembershipScreeningEnabled; /// /// Gets this guild's premium tier (Nitro boosting). /// [JsonProperty("premium_tier")] public PremiumTier PremiumTier { get; internal set; } /// /// Gets the amount of members that boosted this guild. /// [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] public int? PremiumSubscriptionCount { get; internal set; } /// /// Whether the premium progress bar is enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool PremiumProgressBarEnabled { get; internal set; } /// /// Gets whether this guild is designated as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNSFW { get; internal set; } + /// + /// Gets this guild's hub type, if applicable. + /// + [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] + public HubType HubType { get; internal set; } + /// /// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels()); /// /// Sorts the channels. /// private Dictionary InternalSortChannels() { Dictionary keyValuePairs = new(); var ochannels = this.GetOrderedChannels(); foreach (var ochan in ochannels) { if (ochan.Key != 0) keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key)); foreach (var chan in ochan.Value) keyValuePairs.Add(chan.Id, chan); } return keyValuePairs; } /// /// Gets an ordered list out of the channel cache. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public Dictionary> GetOrderedChannels() { IReadOnlyList raw_channels = this._channels.Values.ToList(); Dictionary> ordered_channels = new(); ordered_channels.Add(0, new List()); foreach (var channel in raw_channels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { ordered_channels.Add(channel.Id, new List()); } foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { ordered_channels[channel.ParentId.Value].Add(channel); } foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { ordered_channels[channel.ParentId.Value].Add(channel); } foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { ordered_channels[0].Add(channel); } foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { ordered_channels[0].Add(channel); } return ordered_channels; } /// /// Gets an ordered list. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public async Task>> GetOrderedChannelsAsync() { var raw_channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); Dictionary> ordered_channels = new(); ordered_channels.Add(0, new List()); foreach (var channel in raw_channels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { ordered_channels.Add(channel.Id, new List()); } foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { ordered_channels[channel.ParentId.Value].Add(channel); } foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { ordered_channels[channel.ParentId.Value].Add(channel); } foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { ordered_channels[0].Add(channel); } foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { ordered_channels[0].Add(channel); } return ordered_channels; } /// /// Whether it is synced. /// [JsonIgnore] internal bool IsSynced { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordGuild() { this._current_member_lazy = new Lazy(() => (this._members != null && this._members.TryGetValue(this.Discord.CurrentUser.Id, out var member)) ? member : null); this._invites = new ConcurrentDictionary(); this.Threads = new ReadOnlyConcurrentDictionary(this._threads); this.StageInstances = new ReadOnlyConcurrentDictionary(this._stageInstances); + this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this._scheduledEvents); } #region Guild Methods /// /// Searches the current guild for members who's display name start with the specified name. /// /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. public Task> SearchMembersAsync(string name, int? limit = 1) => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild /// /// User to add /// User's access token (OAuth2) /// new nickname /// new roles /// whether this user has to be muted /// whether this user has to be deafened /// - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordUser user, string access_token, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, access_token, nickname, roles, muted, deaf); /// /// Deletes this guild. Requires the caller to be the owner of the guild. /// /// - /// Thrown when the client is not the owner of the guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client is not the owner of the guild. + /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Modifies this guild. /// /// Action to perform on this guild.. /// The modified guild object. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild 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 GuildEditModel(); action(mdl); var afkChannelId = Optional.FromNoValue(); if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value.Type != ChannelType.Voice && mdl.AfkChannel.Value != null) throw new ArgumentException("AFK channel needs to be a voice channel."); else if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value != null) afkChannelId = mdl.AfkChannel.Value.Id; else if (mdl.AfkChannel.HasValue) afkChannelId = null; var rulesChannelId = Optional.FromNoValue(); if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null && mdl.RulesChannel.Value.Type != ChannelType.Text && mdl.RulesChannel.Value.Type != ChannelType.News ) throw new ArgumentException("Rules channel needs to be a text channel."); else if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null) rulesChannelId = mdl.RulesChannel.Value.Id; else if (mdl.RulesChannel.HasValue) rulesChannelId = null; var publicUpdatesChannelId = Optional.FromNoValue(); if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null && mdl.PublicUpdatesChannel.Value.Type != ChannelType.Text && mdl.PublicUpdatesChannel.Value.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null) publicUpdatesChannelId = mdl.PublicUpdatesChannel.Value.Id; else if (mdl.PublicUpdatesChannel.HasValue) publicUpdatesChannelId = null; var systemChannelId = Optional.FromNoValue(); if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null && mdl.SystemChannel.Value.Type != ChannelType.Text && mdl.SystemChannel.Value.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null) systemChannelId = mdl.SystemChannel.Value.Id; else if (mdl.SystemChannel.HasValue) systemChannelId = null; var iconb64 = Optional.FromNoValue(); if (mdl.Icon.HasValue && mdl.Icon.Value != null) using (var imgtool = new ImageTool(mdl.Icon.Value)) iconb64 = imgtool.GetBase64(); else if (mdl.Icon.HasValue) iconb64 = null; var splashb64 = Optional.FromNoValue(); if (mdl.Splash.HasValue && mdl.Splash.Value != null) using (var imgtool = new ImageTool(mdl.Splash.Value)) splashb64 = imgtool.GetBase64(); else if (mdl.Splash.HasValue) splashb64 = null; var bannerb64 = Optional.FromNoValue(); if (mdl.Banner.HasValue && mdl.Banner.Value != null) using (var imgtool = new ImageTool(mdl.Banner.Value)) bannerb64 = imgtool.GetBase64(); else if (mdl.Banner.HasValue) bannerb64 = null; var discoverySplash64 = Optional.FromNoValue(); if (mdl.DiscoverySplash.HasValue && mdl.DiscoverySplash.Value != null) using (var imgtool = new ImageTool(mdl.DiscoverySplash.Value)) discoverySplash64 = imgtool.GetBase64(); else if (mdl.DiscoverySplash.HasValue) discoverySplash64 = null; var description = Optional.FromNoValue(); if (mdl.Description.HasValue && mdl.Description.Value != null) description = mdl.Description; else if (mdl.Description.HasValue) description = null; return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(e => e.Id), splashb64, systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId, description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false); } /// /// Modifies the community settings async. /// This sets if not highest and . /// /// If true, enable . /// The rules channel. /// The public updates channel. /// The preferred locale. Defaults to en-US. /// The description. /// The default message notifications. Defaults to /// The auditlog reason. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel = null, DiscordChannel publicUpdatesChannel = null, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null) { var verificationLevel = this.VerificationLevel; if(this.VerificationLevel != VerificationLevel.Highest) { verificationLevel = VerificationLevel.High; } var explicitContentFilter = ExplicitContentFilter.AllMembers; var rulesChannelId = Optional.FromNoValue(); if (rulesChannel != null && rulesChannel.Type != ChannelType.Text && rulesChannel.Type != ChannelType.News) throw new ArgumentException("Rules channel needs to be a text channel."); else if (rulesChannel != null) rulesChannelId = rulesChannel.Id; else if (rulesChannel == null) rulesChannelId = null; var publicUpdatesChannelId = Optional.FromNoValue(); if (publicUpdatesChannel != null && publicUpdatesChannel.Type != ChannelType.Text && publicUpdatesChannel.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (publicUpdatesChannel != null) publicUpdatesChannelId = publicUpdatesChannel.Id; else if (publicUpdatesChannel == null) publicUpdatesChannelId = null; List features = new(); var rfeatures = this.RawFeatures.ToList(); if (this.RawFeatures.Contains("COMMUNITY") && enabled) { features = rfeatures; } else if(!this.RawFeatures.Contains("COMMUNITY") && enabled) { rfeatures.Add("COMMUNITY"); features = rfeatures; } else if (this.RawFeatures.Contains("COMMUNITY") && !enabled) { rfeatures.Remove("COMMUNITY"); features = rfeatures; } else if(!this.RawFeatures.Contains("COMMUNITY") && !enabled) { features = rfeatures; } return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false); } /// /// Bans a specified member from this guild. /// /// Member to ban. /// 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. + /// 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 BanMemberAsync(DiscordMember member, int delete_message_days = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, delete_message_days, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. /// /// ID of the user to ban. /// 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. + /// 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 BanMemberAsync(ulong user_id, int delete_message_days = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, user_id, delete_message_days, reason); /// /// Unbans a user from this guild. /// /// User to unban. /// Reason for audit logs. /// - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(DiscordUser user, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. /// /// ID of the user to unban. /// Reason for audit logs. /// - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(ulong user_id, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user_id, reason); /// /// Leaves this guild. /// /// - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild. /// /// Collection of bans in this guild. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetBansAsync() => this.Discord.ApiClient.GetGuildBansAsync(this.Id); /// /// Gets a ban for a specific user. /// /// The Id of the user to get the ban for. - /// Thrown when the specified user is not banned. + /// Thrown when the specified user is not banned. /// The requested ban object. public Task GetBanAsync(ulong userId) => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); /// /// Gets a ban for a specific user. /// /// The user to get the ban for. - /// Thrown when the specified user is not banned. + /// Thrown when the specified user is not banned. /// The requested ban object. public Task GetBanAsync(DiscordUser user) => this.GetBanAsync(user.Id); + #region Sheduled Events + + /// + /// Creates a scheduled event. + /// + /// The name. + /// The scheduled start time. + /// The scheduled end time. + /// The channel. + /// The metadata. + /// The description. + /// The type. + /// The reason. + /// A scheduled event. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, string reason = null) + => await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, reason); + + /// + /// Creates a scheduled event with type . + /// + /// The name. + /// The scheduled start time. + /// The scheduled end time. + /// The location of the external event. + /// The description. + /// The reason. + /// A scheduled event. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, string reason = null) + => await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new DiscordScheduledEventEntityMetadata(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, reason); + + + /// + /// Gets a specific scheduled events. + /// + /// The Id of the event to get. + /// Whether to include user count. + /// A scheduled event. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) + => this._scheduledEvents.TryGetValue(scheduledEventId, out var ev) ? ev : await this.Discord.ApiClient.GetGuildScheduledEventAsync(this.Id, scheduledEventId, withUserCount); + + /// + /// Gets a specific scheduled events. + /// + /// The event to get. + /// Whether to include user count. + /// A sheduled event. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) + => await this.GetScheduledEventAsync(scheduledEvent.Id, withUserCount); + + /// + /// Gets the guilds scheduled events. + /// + /// Whether to include user count. + /// A list of the guilds scheduled events. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetScheduledEventsAsync(bool? withUserCount = null) + => await this.Discord.ApiClient.ListGuildScheduledEventsAsync(this.Id, withUserCount); + #endregion + /// /// Creates a new text channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// Reason for audit logs. /// Slow mode timeout for users. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateTextChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, reason); /// /// Creates a new channel category in this guild. /// /// Name of the new category. /// Permission overwrites for this category. /// Reason for audit logs. /// The newly-created channel category. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateChannelCategoryAsync(string name, IEnumerable overwrites = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Category, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason); /// /// Creates a new stage channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this stage channel. /// Reason for audit logs. /// The newly-created stage channel. - /// Thrown when the client does not have the . - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the guilds has not enabled community. + /// Thrown when the client does not have the . + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the guilds has not enabled community. public Task CreateStageChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.Stage, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a stage channel."); /// /// Creates a new news channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this news channel. /// Reason for audit logs. /// The newly-created news channel. - /// Thrown when the client does not have the . - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the guilds has not enabled community. + /// Thrown when the client does not have the . + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the guilds has not enabled community. public Task CreateNewsChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.News, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a news channel."); /// /// Creates a new voice channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Bitrate of the channel. /// Maximum number of users in the channel. /// Permission overwrites for this channel. /// Video quality mode of the channel. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateVoiceChannelAsync(string name, DiscordChannel parent = null, int? bitrate = null, int? user_limit = null, IEnumerable overwrites = null, VideoQualityMode? qualityMode = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Voice, parent, Optional.FromNoValue(), bitrate, user_limit, overwrites, null, Optional.FromNoValue(), qualityMode, reason); /// /// Creates a new channel in this guild. /// /// Name of the new channel. /// Type of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Bitrate of the channel. Applies to voice only. /// Maximum number of users in the channel. Applies to voice only. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. Applies to text only. /// Slow mode timeout for users. /// Video quality mode of the channel. Applies to voice only. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, string reason = null) { // technically you can create news/store channels but not always return type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage ? throw new ArgumentException("Channel type must be text, voice, stage, or category.", nameof(type)) : type == ChannelType.Category && parent != null ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, reason); } /// /// Gets active threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetActiveThreadsAsync() => this.Discord.ApiClient.GetActiveThreadsAsync(this.Id); // this is to commemorate the Great DAPI Channel Massacre of 2017-11-19. /// /// Deletes all channels in this guild. /// Note that this is irreversible. Use carefully! /// /// public Task DeleteAllChannelsAsync() { var tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); return Task.WhenAll(tasks); } /// /// Estimates the number of users to be pruned. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// The roles to be included in the prune. /// Number of users that will be pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var roleCount = includedRoles.Count(); var roleArr = includedRoles.ToArray(); var rawRoleIds = new List(); for (var i = 0; i < roleCount; i++) { if (this._roles.ContainsKey(roleArr[i].Id)) rawRoleIds.Add(roleArr[i].Id); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// /// Prunes inactive users from this guild. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. /// The roles to be included in the prune. /// Reason for audit logs. /// Number of users pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var roleCount = includedRoles.Count(); var roleArr = includedRoles.ToArray(); var rawRoleIds = new List(); for (var i = 0; i < roleCount; i++) { if (this._roles.ContainsKey(roleArr[i].Id)) rawRoleIds.Add(roleArr[i].Id); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// /// Gets integrations attached to this guild. /// /// Collection of integrations attached to this guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetIntegrationsAsync() => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. /// /// Integration to attach. /// The integration after being attached to the guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AttachUserIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. /// /// Integration to modify. /// Number of days after which the integration expires. /// Length of grace period which allows for renewing the integration. /// Whether emotes should be synced from this integration. /// The modified integration. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyIntegrationAsync(DiscordIntegration integration, int expire_behaviour, int expire_grace_period, bool enable_emoticons) => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expire_behaviour, expire_grace_period, enable_emoticons); /// /// Removes an integration from this guild. /// /// Integration to remove. /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration); /// /// Forces re-synchronization of an integration for this guild. /// /// Integration to synchronize. /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SyncIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. /// /// Voice regions available for this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> ListVoiceRegionsAsync() { var vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id).ConfigureAwait(false); foreach (var xvr in vrs) this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); return vrs; } /// /// Gets an invite from this guild from an invite code. /// /// The invite code /// An invite, or null if not in cache. public DiscordInvite GetInvite(string code) => this._invites.TryGetValue(code, out var invite) ? invite : null; /// /// Gets all the invites created for all the channels in this guild. /// /// A collection of invites. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { var res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (!intents.HasIntent(DiscordIntents.GuildInvites)) { for (var i = 0; i < res.Count; i++) this._invites[res[i].Code] = res[i]; } return res; } /// /// Gets the vanity invite for this guild. /// /// A partial vanity invite. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task GetVanityInviteAsync() => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// /// Gets all the webhooks created for all the channels in this guild. /// /// A collection of webhooks this guild has. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. /// /// The format of the widget. /// The URL of the widget image. public string GetWidgetImage(WidgetType bannerType = WidgetType.Shield) { var param = bannerType switch { WidgetType.Banner1 => "banner1", WidgetType.Banner2 => "banner2", WidgetType.Banner3 => "banner3", WidgetType.Banner4 => "banner4", _ => "shield", }; return $"{Endpoints.BASE_URI}{Endpoints.GUILDS}/{this.Id}{Endpoints.WIDGET_PNG}?style={param}"; } /// /// Gets a member of this guild by their user ID. /// /// ID of the member to get. /// The requested member. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync(ulong userId) { if (this._members != null && this._members.TryGetValue(userId, out var mbr)) return mbr; mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (intents.HasIntent(DiscordIntents.GuildMembers)) { if (this._members != null) { this._members[userId] = mbr; } } return mbr; } /// /// Retrieves a full list of members from Discord. This method will bypass cache. /// /// A collection of all members in this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetAllMembersAsync() { var recmbr = new HashSet(); var recd = 1000; var last = 0ul; while (recd > 0) { var tms = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : (ulong?)last).ConfigureAwait(false); recd = tms.Count; foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); recmbr.Add(new DiscordMember(xtm) { Discord = this.Discord, _guild_id = this.Id }); } var tm = tms.LastOrDefault(); last = tm?.User.Id ?? 0; } return new ReadOnlySet(recmbr); } /// /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the event. /// If no arguments aside from and are specified, this will request all guild members. /// /// Filters the returned members based on what the username starts with. Either this or must not be null. /// The must also be greater than 0 if this is specified. /// Total number of members to request. This must be greater than 0 if is specified. - /// Whether to include the associated with the fetched members. + /// Whether to include the associated with the fetched members. /// Whether to limit the request to the specified user ids. Either this or must not be null. /// The unique string to identify the response. public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable userIds = null, string nonce = null) { if (this.Discord is not DiscordClient client) throw new InvalidOperationException("This operation is only valid for regular Discord clients."); if (query == null && userIds == null) throw new ArgumentException("The query and user IDs cannot both be null."); if (query != null && userIds != null) query = null; var grgm = new GatewayRequestGuildMembers(this) { Query = query, Limit = limit >= 0 ? limit : 0, Presences = presences, UserIds = userIds, Nonce = nonce }; var payload = new GatewayPayload { OpCode = GatewayOpCode.RequestGuildMembers, Data = grgm }; var payloadStr = JsonConvert.SerializeObject(payload, Formatting.None); await client.WsSendAsync(payloadStr).ConfigureAwait(false); } /// /// Gets all the channels this guild has. /// /// A collection of this guild's channels. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task> GetChannelsAsync() => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. /// /// Name of the role. /// Permissions for the role. /// Color for the role. /// Whether the role is to be hoisted. /// Whether the role is to be mentionable. /// Reason for audit logs. /// The newly-created role. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason); /// /// Gets a role from this guild by its ID. /// /// ID of the role to get. /// Requested role. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordRole GetRole(ulong id) => this._roles.TryGetValue(id, out var role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. /// Requested channel. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordChannel GetChannel(ulong id) => (this._channels != null && this._channels.TryGetValue(id, out var channel)) ? channel : null; /// /// Gets a thread from this guild by its ID. /// /// ID of the thread to get. /// Requested thread. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordThreadChannel GetThread(ulong id) => (this._threads != null && this._threads.TryGetValue(id, out var thread)) ? thread : null; /// /// Gets audit log entries for this guild. /// /// Maximum number of entries to fetch. /// Filter by member responsible. /// Filter by action type. /// A collection of requested audit log entries. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public async Task> GetAuditLogsAsync(int? limit = null, DiscordMember by_member = null, AuditLogActionType? action_type = null) { var alrs = new List(); int ac = 1, tc = 0, rmn = 100; var last = 0ul; while (ac > 0) { rmn = limit != null ? limit.Value - tc : 100; rmn = Math.Min(100, rmn); if (rmn <= 0) break; var alr = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, rmn, null, last == 0 ? null : (ulong?)last, by_member?.Id, (int?)action_type).ConfigureAwait(false); ac = alr.Entries.Count(); tc += ac; if (ac > 0) { last = alr.Entries.Last().Id; alrs.Add(alr); } } var amr = alrs.SelectMany(xa => xa.Users) .GroupBy(xu => xu.Id) .Select(xgu => xgu.First()); foreach (var xau in amr) { if (this.Discord.UserCache.ContainsKey(xau.Id)) continue; var xtu = new TransportUser { Id = xau.Id, Username = xau.Username, Discriminator = xau.Discriminator, AvatarHash = xau.AvatarHash }; var xu = new DiscordUser(xtu) { Discord = this.Discord }; xu = this.Discord.UserCache.AddOrUpdate(xu.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); } var ahr = alrs.SelectMany(xa => xa.Webhooks) .GroupBy(xh => xh.Id) .Select(xgh => xgh.First()); var ams = amr.Select(xau => (this._members != null && this._members.TryGetValue(xau.Id, out var member)) ? member : new DiscordMember { Discord = this.Discord, Id = xau.Id, _guild_id = this.Id }); var amd = ams.ToDictionary(xm => xm.Id, xm => xm); Dictionary ahd = null; if (ahr.Any()) { var whr = await this.GetWebhooksAsync().ConfigureAwait(false); var whs = whr.ToDictionary(xh => xh.Id, xh => xh); var amh = ahr.Select(xah => whs.TryGetValue(xah.Id, out var webhook) ? webhook : new DiscordWebhook { Discord = this.Discord, Name = xah.Name, Id = xah.Id, AvatarHash = xah.AvatarHash, ChannelId = xah.ChannelId, GuildId = xah.GuildId, Token = xah.Token }); ahd = amh.ToDictionary(xh => xh.Id, xh => xh); } var acs = alrs.SelectMany(xa => xa.Entries).OrderByDescending(xa => xa.Id); var entries = new List(); foreach (var xac in acs) { DiscordAuditLogEntry entry = null; ulong t1, t2; int t3, t4; long t5, t6; bool p1, p2; switch (xac.ActionType) { case AuditLogActionType.GuildUpdate: entry = new DiscordAuditLogGuildEntry { Target = this }; var entrygld = entry as DiscordAuditLogGuildEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrygld.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "owner_id": entrygld.OwnerChange = new PropertyChange { Before = (this._members != null && this._members.TryGetValue(xc.OldValueUlong, out var oldMember)) ? oldMember : await this.GetMemberAsync(xc.OldValueUlong).ConfigureAwait(false), After = (this._members != null && this._members.TryGetValue(xc.NewValueUlong, out var newMember)) ? newMember : await this.GetMemberAsync(xc.NewValueUlong).ConfigureAwait(false) }; break; case "icon_hash": entrygld.IconChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.OldValueString}.webp" : null, After = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.NewValueString}.webp" : null }; break; case "verification_level": entrygld.VerificationLevelChange = new PropertyChange { Before = (VerificationLevel)(long)xc.OldValue, After = (VerificationLevel)(long)xc.NewValue }; break; case "afk_channel_id": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.AfkChannelChange = new PropertyChange { Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } }; break; case "widget_channel_id": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.EmbedChannelChange = new PropertyChange { Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } }; break; case "splash_hash": entrygld.SplashChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.OldValueString}.webp?size=2048" : null, After = xc.NewValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.NewValueString}.webp?size=2048" : null }; break; case "default_message_notifications": entrygld.NotificationSettingsChange = new PropertyChange { Before = (DefaultMessageNotifications)(long)xc.OldValue, After = (DefaultMessageNotifications)(long)xc.NewValue }; break; case "system_channel_id": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.SystemChannelChange = new PropertyChange { Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } }; break; case "explicit_content_filter": entrygld.ExplicitContentFilterChange = new PropertyChange { Before = (ExplicitContentFilter)(long)xc.OldValue, After = (ExplicitContentFilter)(long)xc.NewValue }; break; case "mfa_level": entrygld.MfaLevelChange = new PropertyChange { Before = (MfaLevel)(long)xc.OldValue, After = (MfaLevel)(long)xc.NewValue }; break; case "region": entrygld.RegionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; + case "premium_progress_bar_enabled": + entrygld.PremiumProgressBarChange = new PropertyChange + { + Before = (bool)xc.OldValue, + After = (bool)xc.NewValue + }; + break; + default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in guild update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ChannelCreate: case AuditLogActionType.ChannelDelete: case AuditLogActionType.ChannelUpdate: entry = new DiscordAuditLogChannelEntry { Target = this.GetChannel(xac.TargetId.Value) ?? new DiscordChannel { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrychn = entry as DiscordAuditLogChannelEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrychn.NameChange = new PropertyChange { Before = xc.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrychn.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "permission_overwrites": var olds = xc.OldValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); var news = xc.NewValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); entrychn.OverwriteChange = new PropertyChange> { Before = olds != null ? new ReadOnlyCollection(new List(olds)) : null, After = news != null ? new ReadOnlyCollection(new List(news)) : null }; break; case "topic": entrychn.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "nsfw": entrychn.NsfwChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "bitrate": entrychn.BitrateChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "rate_limit_per_user": entrychn.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in channel update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.OverwriteCreate: case AuditLogActionType.OverwriteDelete: case AuditLogActionType.OverwriteUpdate: entry = new DiscordAuditLogOverwriteEntry { Target = this.GetChannel(xac.TargetId.Value)?.PermissionOverwrites.FirstOrDefault(xo => xo.Id == xac.Options.Id), Channel = this.GetChannel(xac.TargetId.Value) }; var entryovr = entry as DiscordAuditLogOverwriteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "deny": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.DenyChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "allow": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.AllowChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "type": entryovr.TypeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.TargetIdChange = new PropertyChange { Before = p1 ? (ulong?)t1 : null, After = p2 ? (ulong?)t2 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in overwrite update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.Kick: entry = new DiscordAuditLogKickEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var kickMember) ? kickMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, _guild_id = this.Id } }; break; case AuditLogActionType.Prune: entry = new DiscordAuditLogPruneEntry { Days = xac.Options.DeleteMemberDays, Toll = xac.Options.MembersRemoved }; break; case AuditLogActionType.Ban: case AuditLogActionType.Unban: entry = new DiscordAuditLogBanEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var unbanMember) ? unbanMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, _guild_id = this.Id } }; break; case AuditLogActionType.MemberUpdate: case AuditLogActionType.MemberRoleUpdate: entry = new DiscordAuditLogMemberUpdateEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var roleUpdMember) ? roleUpdMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, _guild_id = this.Id } }; var entrymbu = entry as DiscordAuditLogMemberUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "nick": entrymbu.NicknameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "deaf": entrymbu.DeafenChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "mute": entrymbu.MuteChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "$add": entrymbu.AddedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; case "$remove": entrymbu.RemovedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in member update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.RoleCreate: case AuditLogActionType.RoleDelete: case AuditLogActionType.RoleUpdate: entry = new DiscordAuditLogRoleUpdateEntry { Target = this.GetRole(xac.TargetId.Value) ?? new DiscordRole { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryrol = entry as DiscordAuditLogRoleUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryrol.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "color": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryrol.ColorChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "permissions": entryrol.PermissionChange = new PropertyChange { Before = xc.OldValue != null ? (Permissions?)long.Parse((string)xc.OldValue) : null, After = xc.NewValue != null ? (Permissions?)long.Parse((string)xc.NewValue) : null }; break; case "position": entryrol.PositionChange = new PropertyChange { Before = xc.OldValue != null ? (int?)(long)xc.OldValue : null, After = xc.NewValue != null ? (int?)(long)xc.NewValue : null, }; break; case "mentionable": entryrol.MentionableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "hoist": entryrol.HoistChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in role update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.InviteCreate: case AuditLogActionType.InviteDelete: case AuditLogActionType.InviteUpdate: entry = new DiscordAuditLogInviteEntry(); var inv = new DiscordInvite { Discord = this.Discord, Guild = new DiscordInviteGuild { Discord = this.Discord, Id = this.Id, Name = this.Name, SplashHash = this.SplashHash } }; var entryinv = entry as DiscordAuditLogInviteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "max_age": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxAgeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "code": inv.Code = xc.OldValueString ?? xc.NewValueString; entryinv.CodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "temporary": entryinv.TemporaryChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "inviter_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.InviterChange = new PropertyChange { Before = amd.TryGetValue(t1, out var propBeforeMember) ? propBeforeMember : new DiscordMember { Id = t1, Discord = this.Discord, _guild_id = this.Id }, After = amd.TryGetValue(t2, out var propAfterMember) ? propAfterMember : new DiscordMember { Id = t1, Discord = this.Discord, _guild_id = this.Id }, }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; var ch = entryinv.ChannelChange.Before ?? entryinv.ChannelChange.After; var cht = ch?.Type; inv.Channel = new DiscordInviteChannel { Discord = this.Discord, Id = p1 ? t1 : t2, Name = ch?.Name, Type = cht != null ? cht.Value : ChannelType.Unknown }; break; case "uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.UsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "max_uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxUsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in invite update: {0} - this should be reported to library developers", xc.Key); break; } } entryinv.Target = inv; break; case AuditLogActionType.WebhookCreate: case AuditLogActionType.WebhookDelete: case AuditLogActionType.WebhookUpdate: entry = new DiscordAuditLogWebhookEntry { Target = ahd.TryGetValue(xac.TargetId.Value, out var webhook) ? webhook : new DiscordWebhook { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrywhk = entry as DiscordAuditLogWebhookEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrywhk.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; break; case "type": // ??? p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entrywhk.TypeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "avatar_hash": entrywhk.AvatarHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in webhook update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.EmojiCreate: case AuditLogActionType.EmojiDelete: case AuditLogActionType.EmojiUpdate: entry = new DiscordAuditLogEmojiEntry { Target = this._emojis.TryGetValue(xac.TargetId.Value, out var target) ? target : new DiscordEmoji { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryemo = entry as DiscordAuditLogEmojiEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryemo.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in emote update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StageInstanceCreate: case AuditLogActionType.StageInstanceDelete: case AuditLogActionType.StageInstanceUpdate: entry = new DiscordAuditLogStageEntry { Target = this._stageInstances.TryGetValue(xac.TargetId.Value, out var stage) ? stage : new DiscordStageInstance { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysta = entry as DiscordAuditLogStageEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "topic": entrysta.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": entrysta.PrivacyLevelChange = new PropertyChange { Before = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5) ? (StagePrivacyLevel?)t5 : null, After = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6) ? (StagePrivacyLevel?)t6 : null, }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in stage instance update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StickerCreate: case AuditLogActionType.StickerDelete: case AuditLogActionType.StickerUpdate: entry = new DiscordAuditLogStickerEntry { Target = this._stickers.TryGetValue(xac.TargetId.Value, out var sticker) ? sticker : new DiscordSticker { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysti = entry as DiscordAuditLogStickerEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrysti.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entrysti.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "tags": entrysti.TagsChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "guild_id": entrysti.GuildIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "available": entrysti.AvailabilityChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue, }; break; case "asset": entrysti.AssetChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "id": entrysti.IdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var oid) ? oid : null, After = ulong.TryParse(xc.NewValueString, out var nid) ? nid : null }; break; case "type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.TypeChange = new PropertyChange { Before = p1 ? (StickerType?)t5 : null, After = p2 ? (StickerType?)t6 : null }; break; case "format_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.FormatChange = new PropertyChange { Before = p1 ? (StickerFormat?)t5 : null, After = p2 ? (StickerFormat?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in sticker update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.MessageDelete: case AuditLogActionType.MessageBulkDelete: { entry = new DiscordAuditLogMessageEntry(); var entrymsg = entry as DiscordAuditLogMessageEntry; if (xac.Options != null) { entrymsg.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrymsg.MessageCount = xac.Options.Count; } if (entrymsg.Channel != null) { entrymsg.Target = this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == xac.TargetId.Value && xm.ChannelId == entrymsg.Channel.Id, out var msg) ? msg : new DiscordMessage { Discord = this.Discord, Id = xac.TargetId.Value }; } break; } case AuditLogActionType.MessagePin: case AuditLogActionType.MessageUnpin: { entry = new DiscordAuditLogMessagePinEntry(); var entrypin = entry as DiscordAuditLogMessagePinEntry; if (this.Discord is not DiscordClient dc) { break; } if (xac.Options != null) { DiscordMessage message = default; dc.MessageCache?.TryGet(x => x.Id == xac.Options.MessageId && x.ChannelId == xac.Options.ChannelId, out message); entrypin.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrypin.Message = message ?? new DiscordMessage { Id = xac.Options.MessageId, Discord = this.Discord }; } if (xac.TargetId.HasValue) { dc.UserCache.TryGetValue(xac.TargetId.Value, out var user); entrypin.Target = user ?? new DiscordUser { Id = user.Id, Discord = this.Discord }; } break; } case AuditLogActionType.BotAdd: { entry = new DiscordAuditLogBotAddEntry(); if (!(this.Discord is DiscordClient dc && xac.TargetId.HasValue)) { break; } dc.UserCache.TryGetValue(xac.TargetId.Value, out var bot); (entry as DiscordAuditLogBotAddEntry).TargetBot = bot ?? new DiscordUser { Id = xac.TargetId.Value, Discord = this.Discord }; break; } case AuditLogActionType.MemberMove: entry = new DiscordAuditLogMemberMoveEntry(); if (xac.Options == null) { break; } var moveentry = entry as DiscordAuditLogMemberMoveEntry; moveentry.UserCount = xac.Options.Count; moveentry.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; break; case AuditLogActionType.MemberDisconnect: entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = xac.Options?.Count ?? 0 }; break; case AuditLogActionType.IntegrationCreate: case AuditLogActionType.IntegrationDelete: case AuditLogActionType.IntegrationUpdate: entry = new DiscordAuditLogIntegrationEntry(); var integentry = entry as DiscordAuditLogIntegrationEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "enable_emoticons": integentry.EnableEmoticons = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "expire_behavior": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; case "expire_grace_period": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in integration update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ThreadCreate: case AuditLogActionType.ThreadDelete: case AuditLogActionType.ThreadUpdate: entry = new DiscordAuditLogThreadEntry { Target = this._threads.TryGetValue(xac.TargetId.Value, out var thread) ? thread : new DiscordThreadChannel { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrythr = entry as DiscordAuditLogThreadEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrythr.NameChange = new PropertyChange { Before = xc.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrythr.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "archived": entrythr.ArchivedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "locked": entrythr.LockedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "auto_archive_duration": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrythr.AutoArchiveDurationChange = new PropertyChange { Before = p1 ? (ThreadAutoArchiveDuration?)t5 : null, After = p2 ? (ThreadAutoArchiveDuration?)t6 : null }; break; case "rate_limit_per_user": entrythr.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in thread update: {0} - this should be reported to library developers", xc.Key); break; } } break; - case AuditLogActionType.ScheduledEventCreate: - case AuditLogActionType.ScheduledEventDelete: - case AuditLogActionType.ScheduledEventUpdate: - entry = new DiscordAuditLogScheduledEventEntry + case AuditLogActionType.GuildScheduledEventCreate: + case AuditLogActionType.GuildScheduledEventDelete: + case AuditLogActionType.GuildScheduledEventUpdate: + entry = new DiscordAuditLogGuildScheduledEventEntry { - //Target = this._events.TryGetValue(xac.TargetId.Value, out var scheduled_event) ? scheduled_event : new DiscordEvent { Id = xac.TargetId.Value, Discord = this.Discord } + Target = this._scheduledEvents.TryGetValue(xac.TargetId.Value, out var scheduled_event) ? scheduled_event : new DiscordScheduledEvent { Id = xac.TargetId.Value, Discord = this.Discord } }; - var entryse = entry as DiscordAuditLogScheduledEventEntry; + var entryse = entry as DiscordAuditLogGuildScheduledEventEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "channel_id": entryse.ChannelIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "description": entryse.DescriptionChange = new PropertyChange { Before = xc.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; break; + case "location": + entryse.LocationChange = new PropertyChange + { + Before = xc.OldValue != null ? xc.OldValueString : null, + After = xc.NewValue != null ? xc.NewValueString : null + }; + break; + case "privacy_level": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); - entryse.PrivacyLevelChange = new PropertyChange + entryse.PrivacyLevelChange = new PropertyChange + { + Before = p1 ? (ScheduledEventPrivacyLevel?)t5 : null, + After = p2 ? (ScheduledEventPrivacyLevel?)t6 : null + }; + break; + + case "entity_type": + p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); + p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); + + entryse.EntityTypeChange = new PropertyChange { - Before = p1 ? (StagePrivacyLevel?)t5 : null, - After = p2 ? (StagePrivacyLevel?)t6 : null + Before = p1 ? (ScheduledEventEntityType?)t5 : null, + After = p2 ? (ScheduledEventEntityType?)t6 : null }; break; case "status": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); - entryse.StatusChange = new PropertyChange + entryse.StatusChange = new PropertyChange { - Before = p1 ? (EventStatus?)t5 : null, - After = p2 ? (EventStatus?)t6 : null + Before = p1 ? (ScheduledEventStatus?)t5 : null, + After = p2 ? (ScheduledEventStatus?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in scheduled event update: {0} - this should be reported to library developers", xc.Key); break; } } break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown audit log action type: {0} - this should be reported to library developers", (int)xac.ActionType); break; } if (entry == null) continue; entry.ActionCategory = xac.ActionType switch { - AuditLogActionType.ChannelCreate or AuditLogActionType.EmojiCreate or AuditLogActionType.InviteCreate or AuditLogActionType.OverwriteCreate or AuditLogActionType.RoleCreate or AuditLogActionType.WebhookCreate or AuditLogActionType.IntegrationCreate or AuditLogActionType.StickerCreate or AuditLogActionType.StageInstanceCreate or AuditLogActionType.ThreadCreate or AuditLogActionType.ScheduledEventCreate => AuditLogActionCategory.Create, - AuditLogActionType.ChannelDelete or AuditLogActionType.EmojiDelete or AuditLogActionType.InviteDelete or AuditLogActionType.MessageDelete or AuditLogActionType.MessageBulkDelete or AuditLogActionType.OverwriteDelete or AuditLogActionType.RoleDelete or AuditLogActionType.WebhookDelete or AuditLogActionType.IntegrationDelete or AuditLogActionType.StickerDelete or AuditLogActionType.StageInstanceDelete or AuditLogActionType.ThreadDelete or AuditLogActionType.ScheduledEventDelete => AuditLogActionCategory.Delete, - AuditLogActionType.ChannelUpdate or AuditLogActionType.EmojiUpdate or AuditLogActionType.InviteUpdate or AuditLogActionType.MemberRoleUpdate or AuditLogActionType.MemberUpdate or AuditLogActionType.OverwriteUpdate or AuditLogActionType.RoleUpdate or AuditLogActionType.WebhookUpdate or AuditLogActionType.IntegrationUpdate or AuditLogActionType.StickerUpdate or AuditLogActionType.StageInstanceUpdate or AuditLogActionType.ThreadUpdate or AuditLogActionType.ScheduledEventUpdate => AuditLogActionCategory.Update, + AuditLogActionType.ChannelCreate or AuditLogActionType.EmojiCreate or AuditLogActionType.InviteCreate or AuditLogActionType.OverwriteCreate or AuditLogActionType.RoleCreate or AuditLogActionType.WebhookCreate or AuditLogActionType.IntegrationCreate or AuditLogActionType.StickerCreate or AuditLogActionType.StageInstanceCreate or AuditLogActionType.ThreadCreate or AuditLogActionType.GuildScheduledEventCreate => AuditLogActionCategory.Create, + AuditLogActionType.ChannelDelete or AuditLogActionType.EmojiDelete or AuditLogActionType.InviteDelete or AuditLogActionType.MessageDelete or AuditLogActionType.MessageBulkDelete or AuditLogActionType.OverwriteDelete or AuditLogActionType.RoleDelete or AuditLogActionType.WebhookDelete or AuditLogActionType.IntegrationDelete or AuditLogActionType.StickerDelete or AuditLogActionType.StageInstanceDelete or AuditLogActionType.ThreadDelete or AuditLogActionType.GuildScheduledEventDelete => AuditLogActionCategory.Delete, + AuditLogActionType.ChannelUpdate or AuditLogActionType.EmojiUpdate or AuditLogActionType.InviteUpdate or AuditLogActionType.MemberRoleUpdate or AuditLogActionType.MemberUpdate or AuditLogActionType.OverwriteUpdate or AuditLogActionType.RoleUpdate or AuditLogActionType.WebhookUpdate or AuditLogActionType.IntegrationUpdate or AuditLogActionType.StickerUpdate or AuditLogActionType.StageInstanceUpdate or AuditLogActionType.ThreadUpdate or AuditLogActionType.GuildScheduledEventUpdate => AuditLogActionCategory.Update, _ => AuditLogActionCategory.Other, }; entry.Discord = this.Discord; entry.ActionType = xac.ActionType; entry.Id = xac.Id; entry.Reason = xac.Reason; entry.UserResponsible = amd[xac.UserId]; entries.Add(entry); } return new ReadOnlyCollection(entries); } /// /// Gets all of this guild's custom emojis. /// /// All of this guild's custom emojis. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task> GetEmojisAsync() => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. /// /// ID of the emoji to get. /// The requested custom emoji. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetEmojiAsync(ulong id) => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. /// /// Name of the new emoji. /// Image to use as the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The newly-created emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); if (name.Length < 2 || name.Length > 50) throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); if (image == null) throw new ArgumentNullException(nameof(image)); string image64 = null; using (var imgtool = new ImageTool(image)) image64 = imgtool.GetBase64(); return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// /// Modifies a this guild's custom emoji. /// /// Emoji to modify. /// New name for the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) throw new ArgumentNullException(nameof(emoji)); if (emoji.Guild.Id != this.Id) throw new ArgumentException("This emoji does not belong to this guild."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// /// Deletes this guild's custom emoji. /// /// Emoji to delete. /// Reason for audit log. /// - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) { return emoji == null ? throw new ArgumentNullException(nameof(emoji)) : emoji.Guild.Id != this.Id ? throw new ArgumentException("This emoji does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); } /// /// Gets all of this guild's custom stickers. /// /// All of this guild's custom stickers. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetStickersAsync() { var stickers = await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); foreach (var xstr in stickers) { this._stickers.AddOrUpdate(xstr.Id, xstr, (id, old) => { old.Name = xstr.Name; old.Description = xstr.Description; old._internalTags = xstr._internalTags; return old; }); } return stickers; } /// /// Gets a sticker /// - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task GetStickerAsync(ulong sticker_id) => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, sticker_id); /// /// Creates a sticker /// /// The name of the sticker. /// The optional description of the sticker. /// The emoji to associate the sticker with. /// The file format the sticker is written in. /// The sticker. /// Audit log reason - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateStickerAsync(string name, string description, DiscordEmoji emoji, Stream file, StickerFormat format, string reason = null) { var fileExt = format switch { StickerFormat.PNG => "png", StickerFormat.APNG => "png", StickerFormat.LOTTIE => "json", _ => throw new InvalidOperationException("This format is not supported.") }; var contentType = format switch { StickerFormat.PNG => "image/png", StickerFormat.APNG => "image/png", StickerFormat.LOTTIE => "application/json", _ => throw new InvalidOperationException("This format is not supported.") }; return emoji.Id is not 0 ? throw new InvalidOperationException("Only unicode emoji can be used for stickers.") : name.Length < 2 || name.Length > 30 ? throw new ArgumentOutOfRangeException(nameof(name), "Sticker name needs to be between 2 and 30 characters long.") : description.Length < 1 || description.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(description), "Sticker description needs to be between 1 and 100 characters long.") : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new("sticker", file, null, fileExt, contentType), reason); } /// /// Modifies a sticker /// /// The id of the sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public async Task ModifyStickerAsync(ulong sticker, Optional name, Optional description, Optional emoji, string reason = null) { string uemoji = null; if (!this._stickers.TryGetValue(sticker, out var stickerobj) || stickerobj.Guild.Id != this.Id) throw new ArgumentException("This sticker does not belong to this guild."); if (name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30)) throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long."); if (description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100)) throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long."); if (emoji.HasValue && emoji.Value.Id > 0) throw new ArgumentException("Only unicode emojis can be used with stickers."); else if (emoji.HasValue) uemoji = emoji.Value.GetDiscordName().Replace(":", ""); var usticker = await this.Discord.ApiClient.ModifyGuildStickerAsync(this.Id, sticker, name, description, uemoji, reason).ConfigureAwait(false); if (this._stickers.TryGetValue(usticker.Id, out var old)) this._stickers.TryUpdate(usticker.Id, usticker, old); return usticker; } /// /// Modifies a sticker /// /// The sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task ModifyStickerAsync(DiscordSticker sticker, Optional name, Optional description, Optional emoji, string reason = null) => this.ModifyStickerAsync(sticker.Id, name, description, emoji, reason); /// /// Deletes a sticker /// /// Id of sticker to delete /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteStickerAsync(ulong sticker, string reason = null) { return !this._stickers.TryGetValue(sticker, out var stickerobj) ? throw new ArgumentNullException(nameof(sticker)) : stickerobj.Guild.Id != this.Id ? throw new ArgumentException("This sticker does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); } /// /// Deletes a sticker /// /// Sticker to delete /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteStickerAsync(DiscordSticker sticker, string reason = null) => this.DeleteStickerAsync(sticker.Id, reason); /// /// Gets the default channel for this guild. /// Default channel is the first channel current member can see. /// /// This member's default guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordChannel GetDefaultChannel() { return this._channels?.Values.Where(xc => xc.Type == ChannelType.Text) .OrderBy(xc => xc.Position) .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Permissions.AccessChannels) == DisCatSharp.Permissions.AccessChannels); } /// /// Gets the guild's widget /// /// The guild's widget public Task GetWidgetAsync() => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings public Task GetWidgetSettingsAsync() => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings /// /// If the widget is enabled or not /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. /// /// All of the guild's templates. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetTemplatesAsync() => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. /// /// Name of the template. /// Description of the template. /// The template created. - /// Throws when a template already exists for the guild or a null parameter is provided for the name. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when a template already exists for the guild or a null parameter is provided for the name. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateTemplateAsync(string name, string description = null) => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. /// /// The code of the template to sync. /// The template synced. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task SyncTemplateAsync(string code) => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. /// /// The template's code. /// Name of the template. /// Description of the template. /// The template modified. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task ModifyTemplateAsync(string code, string name = null, string description = null) => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. /// /// The code of the template to delete. /// The deleted template. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task DeleteTemplateAsync(string code) => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetMembershipScreeningFormAsync() => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. /// /// Action to perform /// The modified screening form. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. public async Task ModifyMembershipScreeningFormAsync(Action action) { var mdl = new MembershipScreeningEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, mdl.Enabled, mdl.Fields, mdl.Description); } /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. public Task> GetApplicationCommandsAsync() => this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. /// /// The id of the command to edit. /// Action to perform. /// The edit command. public async Task EditApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission).ConfigureAwait(false); } /// /// Gets this guild's welcome screen. /// /// This guild's welcome screen object. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetWelcomeScreenAsync() => this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. /// /// Action to perform. /// The modified welcome screen. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. public async Task ModifyWelcomeScreenAsync(Action action) { var mdl = new WelcomeScreenEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, mdl.Enabled, mdl.WelcomeChannels, mdl.Description).ConfigureAwait(false); } #endregion /// /// Returns a string representation of this guild. /// /// String representation of this guild. public override string ToString() => $"Guild {this.Id}; {this.Name}"; /// /// 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 DiscordGuild); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are equal. public static bool operator ==(DiscordGuild e1, DiscordGuild 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); } /// /// Gets whether the two objects are not equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are not equal. public static bool operator !=(DiscordGuild e1, DiscordGuild e2) => !(e1 == e2); } /// /// Represents guild verification level. /// public enum VerificationLevel : int { /// /// No verification. Anyone can join and chat right away. /// None = 0, /// /// Low verification level. Users are required to have a verified email attached to their account in order to be able to chat. /// Low = 1, /// /// Medium verification level. Users are required to have a verified email attached to their account, and account age need to be at least 5 minutes in order to be able to chat. /// Medium = 2, /// /// High verification level. Users are required to have a verified email attached to their account, account age need to be at least 5 minutes, and they need to be in the server for at least 10 minutes in order to be able to chat. /// High = 3, /// /// Highest verification level. Users are required to have a verified phone number attached to their account. /// Highest = 4 } /// /// Represents default notification level for a guild. /// public enum DefaultMessageNotifications : int { /// /// All messages will trigger push notifications. /// AllMessages = 0, /// /// Only messages that mention the user (or a role he's in) will trigger push notifications. /// MentionsOnly = 1 } /// /// Represents multi-factor authentication level required by a guild to use administrator functionality. /// public enum MfaLevel : int { /// /// Multi-factor authentication is not required to use administrator functionality. /// Disabled = 0, /// /// Multi-factor authentication is required to use administrator functionality. /// Enabled = 1 } /// /// Represents the value of explicit content filter in a guild. /// public enum ExplicitContentFilter : int { /// /// Explicit content filter is disabled. /// Disabled = 0, /// /// Only messages from members without any roles are scanned. /// MembersWithoutRoles = 1, /// /// Messages from all members are scanned. /// AllMembers = 2 } /// /// Represents the formats for a guild widget. /// public enum WidgetType : int { /// /// The widget is represented in shield format. /// This is the default widget type. /// Shield = 0, /// /// The widget is represented as the first banner type. /// Banner1 = 1, /// /// The widget is represented as the second banner type. /// Banner2 = 2, /// /// The widget is represented as the third banner type. /// Banner3 = 3, /// /// The widget is represented in the fourth banner type. /// Banner4 = 4 } /// /// Represents the guild features. /// public class GuildFeatures { /// /// Guild has access to set an animated guild icon. /// public bool CanSetAnimatedIcon { get; } /// /// Guild has access to set a guild banner image. /// public bool CanSetBanner { get; } /// /// Guild has access to use commerce features (i.e. create store channels) /// public bool CanCreateStoreChannels { get; } /// /// Guild can enable Welcome Screen, Membership Screening, Stage Channels, News Channels and receives community updates. /// Furthermore the guild can apply as a partner and for the discovery (if the prerequisites are given). /// and is usable. /// public bool HasCommunityEnabled { get; } /// /// Guild is able to be discovered in the discovery. /// public bool IsDiscoverable { get; } /// /// Guild is able to be featured in the discovery. /// public bool IsFeatureable { get; } /// /// Guild has access to set an invite splash background. /// public bool CanSetInviteSplash { get; } /// /// Guild has enabled Membership Screening. /// public bool HasMembershipScreeningEnabled { get; } /// /// Guild has access to create news channels. /// is usable. /// public bool CanCreateNewsChannels { get; } /// /// Guild is partnered. /// public bool IsPartnered { get; } /// /// Guild has increased custom emoji slots. /// public bool CanUploadMoreEmojis { get; } /// /// Guild can be previewed before joining via Membership Screening or the discovery. /// public bool HasPreviewEnabled { get; } /// /// Guild has access to set a vanity URL. /// public bool CanSetVanityUrl { get; } /// /// Guild is verified. /// public bool IsVerified { get; } /// /// Guild has access to set 384kbps bitrate in voice (previously VIP voice servers). /// public bool CanAccessVipRegions { get; } /// /// Guild has enabled the welcome screen. /// public bool HasWelcomeScreenEnabled { get; } /// /// Guild has enabled ticketed events. /// public bool HasTicketedEventsEnabled { get; } /// /// Guild has enabled monetization. /// public bool HasMonetizationEnabled { get; } /// /// Guild has increased custom sticker slots. /// public bool CanUploadMoreStickers { get; } /// /// Guild has access to the three day archive time for threads. - /// Needs Premium Tier 1 (). + /// Needs Premium Tier 1 (). /// public bool CanSetThreadArchiveDurationThreeDays { get; } /// /// Guild has access to the seven day archive time for threads. - /// Needs Premium Tier 2 (). + /// Needs Premium Tier 2 (). /// public bool CanSetThreadArchiveDurationSevenDays { get; } /// /// Guild has access to create private threads. - /// Needs Premium Tier 2 (). + /// Needs Premium Tier 2 (). /// public bool CanCreatePrivateThreads { get; } /// /// Guild is a hub. /// is usable. /// public bool IsHub { get; } /// /// Guild is in a hub. /// https://github.com/discord/discord-api-docs/pull/3757/commits/4932d92c9d0c783861bc715bf7ebbabb15114e34 /// public bool HasDirectoryEntry { get; } /// /// Guild is linked to a hub. /// public bool IsLinkedToHub { get; } /// /// Guild has full access to threads. /// Old Feature. /// public bool HasThreadTestingEnabled { get; } /// /// Guild has access to threads. /// public bool HasThreadsEnabled { get; } /// /// Guild can set role icons. /// public bool CanSetRoleIcons { get; } /// /// Guild has the new thread permissions. /// Old Feature. /// public bool HasNewThreadPermissions { get; } /// /// Guild can set thread default auto archive duration. /// Old Feature. /// public bool CanSetThreadDefaultAutoArchiveDuration { get; } /// /// Guild has enabled role subsriptions. /// public bool HasRoleSubscriptionsEnabled { get; } /// /// Guild role subsriptions as purchaseable. /// public bool RoleSubscriptionsIsAvaiableForPurchase { get; } /// /// Guild has premium tier 3 override. /// public bool PremiumTierThreeOverride { get; } /// /// Guild has access to text in voice. + /// Restricted to . /// public bool TextInVoiceEnabled { get; } /// /// Guild can set an animated banner. - /// Needs Premium Tier 3 (). + /// Needs Premium Tier 3 (). /// public bool CanSetAnimatedBanner { get; } /// /// Guild can set an animated banner. - /// Needs Premium Tier 3 (). + /// Needs Premium Tier 3 (). /// public bool CanSetChannelBanner { get; } /// /// Allows members to customize their avatar, banner and bio for that server. /// public bool HasMemberProfiles { get; } /// - /// Guild is restricted to users with the badge. + /// Guild is restricted to users with the badge. /// public bool IsStaffOnly { get; } /// /// String of guild features. /// public string FeatureString { get; } /// /// Checks the guild features and constructs a new object. /// /// Guild to check public GuildFeatures(DiscordGuild guild) { this.CanSetAnimatedIcon = guild.RawFeatures.Contains("ANIMATED_ICON"); this.CanSetAnimatedBanner = guild.RawFeatures.Contains("ANIMATED_BANNER"); this.CanSetBanner = guild.RawFeatures.Contains("BANNER"); this.CanSetChannelBanner = guild.RawFeatures.Contains("CHANNEL_BANNER"); this.CanCreateStoreChannels = guild.RawFeatures.Contains("COMMERCE"); this.HasCommunityEnabled = guild.RawFeatures.Contains("COMMUNITY"); this.IsDiscoverable = !guild.RawFeatures.Contains("DISCOVERABLE_DISABLED") && guild.RawFeatures.Contains("DISCOVERABLE"); this.IsFeatureable = guild.RawFeatures.Contains("FEATURABLE"); this.CanSetInviteSplash = guild.RawFeatures.Contains("INVITE_SPLASH"); this.HasMembershipScreeningEnabled = guild.RawFeatures.Contains("MEMBER_VERIFICATION_GATE_ENABLED"); this.CanCreateNewsChannels = guild.RawFeatures.Contains("NEWS"); this.IsPartnered = guild.RawFeatures.Contains("PARTNERED"); this.CanUploadMoreEmojis = guild.RawFeatures.Contains("MORE_EMOJI"); this.HasPreviewEnabled = guild.RawFeatures.Contains("PREVIEW_ENABLED"); this.CanSetVanityUrl = guild.RawFeatures.Contains("VANITY_URL"); this.IsVerified = guild.RawFeatures.Contains("VERIFIED"); this.CanAccessVipRegions = guild.RawFeatures.Contains("VIP_REGIONS"); this.HasWelcomeScreenEnabled = guild.RawFeatures.Contains("WELCOME_SCREEN_ENABLED"); this.HasTicketedEventsEnabled = guild.RawFeatures.Contains("TICKETED_EVENTS_ENABLED"); this.HasMonetizationEnabled = guild.RawFeatures.Contains("MONETIZATION_ENABLED"); this.CanUploadMoreStickers = guild.RawFeatures.Contains("MORE_STICKERS"); this.CanSetThreadArchiveDurationThreeDays = guild.RawFeatures.Contains("THREE_DAY_THREAD_ARCHIVE"); this.CanSetThreadArchiveDurationSevenDays = guild.RawFeatures.Contains("SEVEN_DAY_THREAD_ARCHIVE"); this.CanCreatePrivateThreads = guild.RawFeatures.Contains("PRIVATE_THREADS"); this.IsHub = guild.RawFeatures.Contains("HUB"); this.HasThreadTestingEnabled = guild.RawFeatures.Contains("THREADS_ENABLED_TESTING"); this.HasThreadsEnabled = guild.RawFeatures.Contains("THREADS_ENABLED"); this.CanSetRoleIcons = guild.RawFeatures.Contains("ROLE_ICONS"); this.HasNewThreadPermissions = guild.RawFeatures.Contains("NEW_THREAD_PERMISSIONS"); this.HasRoleSubscriptionsEnabled = guild.RawFeatures.Contains("ROLE_SUBSCRIPTIONS_ENABLED"); this.PremiumTierThreeOverride = guild.RawFeatures.Contains("PREMIUM_TIER_3_OVERRIDE"); this.CanSetThreadDefaultAutoArchiveDuration = guild.RawFeatures.Contains("THREAD_DEFAULT_AUTO_ARCHIVE_DURATION"); this.TextInVoiceEnabled = guild.RawFeatures.Contains("TEXT_IN_VOICE_ENABLED"); this.HasDirectoryEntry = guild.RawFeatures.Contains("HAS_DIRECTORY_ENTRY"); this.IsLinkedToHub = guild.RawFeatures.Contains("LINKED_TO_HUB"); this.HasMemberProfiles = guild.RawFeatures.Contains("MEMBER_PROFILES"); this.IsStaffOnly = guild.RawFeatures.Contains("INTERNAL_EMPLOYEE_ONLY"); this.RoleSubscriptionsIsAvaiableForPurchase = guild.RawFeatures.Contains("ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"); - var _features = guild.RawFeatures.Any() ? "" : "NONE"; + var _features = guild.RawFeatures.Any() ? "" : "None"; foreach (var feature in guild.RawFeatures) { _features += feature + " "; } this.FeatureString = _features; } } } diff --git a/DisCatSharp/Entities/Guild/DiscordGuildPreview.cs b/DisCatSharp/Entities/Guild/DiscordGuildPreview.cs index c0c90281e..ea3e78b99 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuildPreview.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuildPreview.cs @@ -1,128 +1,134 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Concurrent; using System.Collections.Generic; using System.Globalization; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents the guild preview. /// public class DiscordGuildPreview : SnowflakeObject { /// /// Gets the guild name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this._emojis); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _emojis; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList Features { get; internal set; } /// /// Gets the approximate member count. /// [JsonProperty("approximate_member_count")] public int ApproximateMemberCount { get; internal set; } /// /// Gets the approximate presence count. /// [JsonProperty("approximate_presence_count")] public int ApproximatePresenceCount { get; internal set; } /// /// Gets the description for the guild, if the guild is discoverable. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } /// /// Gets the system channel flags for the guild. /// [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] public SystemChannelFlags SystemChannelFlags { get; internal set; } + /// + /// Gets this hub type for the guild, if the guild is a hub. + /// + [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] + public HubType HubType { get; internal set; } + /// /// Initializes a new instance of the class. /// internal DiscordGuildPreview() { } } } diff --git a/DisCatSharp/Entities/Guild/DiscordMember.cs b/DisCatSharp/Entities/Guild/DiscordMember.cs index c85d54d5c..66f7e55e2 100644 --- a/DisCatSharp/Entities/Guild/DiscordMember.cs +++ b/DisCatSharp/Entities/Guild/DiscordMember.cs @@ -1,778 +1,781 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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._role_ids_lazy = new Lazy>(() => new ReadOnlyCollection(this._role_ids)); } /// /// Initializes a new instance of the class. /// /// The user. internal DiscordMember(DiscordUser user) { this.Discord = user.Discord; this.Id = user.Id; this._role_ids = new List(); this._role_ids_lazy = new Lazy>(() => new ReadOnlyCollection(this._role_ids)); } /// /// 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._avatarHash = mbr.AvatarHash; this._role_ids = mbr.Roles ?? new List(); this._role_ids_lazy = new Lazy>(() => new ReadOnlyCollection(this._role_ids)); } /// /// 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._guild_id.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets this member's banner url. /// [JsonIgnore] #pragma warning disable CS0108 // Member hides inherited member; missing new keyword public string BannerUrl => this.User.BannerUrl; #pragma warning restore CS0108 // Member hides inherited member; missing new keyword /// /// Gets the member's banner hash. /// [JsonIgnore] public override string BannerHash { get => this.User.BannerHash; internal set => this.User.BannerHash = value; } /// /// 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._guild_id.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; } [JsonIgnore] internal string _avatarHash; /// /// Gets this member's display name. /// [JsonIgnore] public string DisplayName => this.Nickname ?? this.Username; /// /// List of role ids /// [JsonIgnore] internal IReadOnlyList RoleIds => this._role_ids_lazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] internal List _role_ids; [JsonIgnore] private readonly Lazy> _role_ids_lazy; /// /// 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 DateTimeOffset? 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._guild_id].VoiceStates.TryGetValue(this.Id, out var voiceState) ? voiceState : null; [JsonIgnore] internal ulong _guild_id = 0; /// /// Gets the guild of which this member is a part of. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds[this._guild_id]; /// /// 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. /// public override string Username { get => this.User.Username; internal set => this.User.Username = value; } /// /// Gets the member's 4-digit discriminator. /// 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 whether the member is a bot. /// 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. /// public override string Email { get => this.User.Email; internal set => this.User.Email = value; } /// /// Gets whether the member has multi-factor authentication enabled. /// 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. /// public override bool? Verified { get => this.User.Verified; internal set => this.User.Verified = value; } /// /// Gets the member's chosen language /// public override string Locale { get => this.User.Locale; internal set => this.User.Locale = value; } /// /// Gets the user's flags. /// public override UserFlags? OAuthFlags { get => this.User.OAuthFlags; internal set => this.User.OAuthFlags = value; } /// /// Gets the member's flags for OAuth. /// 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._guild_id, 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._guild_id, 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)); 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.FromNoValue(), mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } else { await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname, mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } } /// /// Adds a timeout to a member. /// - /// The datetime offset to time out the user. + /// 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) => this.Discord.ApiClient.ModifyTimeOutAsync(this.Guild.Id, this.Id, until, reason); + 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. + /// 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); + 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. + /// 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); + 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, new Optional>(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 delete_message_days = 0, string reason = null) => this.Guild.BanMemberAsync(this, delete_message_days, 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._guild_id, 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. + /// 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 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 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.FULL_PERMS; 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); // Adminstrator grants all permissions and cannot be overridden return (perms & Permissions.Administrator) == Permissions.Administrator ? PermissionMethods.FULL_PERMS : 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._guild_id == e._guild_id)); /// /// 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._guild_id.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._guild_id == e2._guild_id)); } /// /// 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/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs new file mode 100644 index 000000000..92dec9390 --- /dev/null +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs @@ -0,0 +1,328 @@ +// This file is part of the DisCatSharp project. +// +// Copyright (c) 2021 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.Threading.Tasks; +using DisCatSharp.Net.Models; +using Newtonsoft.Json; + +namespace DisCatSharp.Entities +{ + /// + /// Represents an scheduled event. + /// + public class DiscordScheduledEvent : SnowflakeObject, IEquatable + { + /// + /// Gets the guild id of the associated scheduled event. + /// + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong GuildId { get; internal set; } + + /// + /// Gets the guild to which this scheduled event belongs. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null; + + /// + /// Gets the associated channel. + /// + [JsonIgnore] + public Task Channel + => this.ChannelId.HasValue ? this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value) : null; + + /// + /// Gets id of the associated channel id. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; internal set; } + + /// + /// Gets the ID of the user that created the scheduled event. + /// + [JsonProperty("creator_id")] + public ulong CreatorId { get; internal set; } + + /// + /// Gets the user that created the scheduled event. + /// + [JsonProperty("creator")] + public DiscordUser Creator { get; internal set; } + + /// + /// Gets the member that created the scheduled event. + /// + [JsonIgnore] + public DiscordMember CreatorMember + => this.Guild._members.TryGetValue(this.CreatorId, out var owner) + ? owner + : this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId, this.CreatorId).Result; + + /// + /// Gets the name of the scheduled event. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the description of the scheduled event. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /* TODO|INFO: Is not available yet to users / clients / bots. + /// + /// Gets this event's cover hash, when applicable. + /// + [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] + public string ImageHash { get; internal set; } + + /// + /// Gets this event's cover in url form. + /// + [JsonIgnore] + public string ImageUrl + => !string.IsNullOrWhiteSpace(this.ImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.EVENTS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}/images/{this.ImageHash}.{(this.ImageHash.StartsWith("a_") ? "gif" : "png")}" : null; + */ + /// + /// Gets the scheduled start time of the scheduled event. + /// + [JsonIgnore] + public DateTimeOffset? ScheduledStartTime + => !string.IsNullOrWhiteSpace(this.ScheduledStartTimeRaw) && DateTimeOffset.TryParse(this.ScheduledStartTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? + dto : null; + + /// + /// Gets the scheduled start time of the scheduled event as raw string. + /// + [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] + internal string ScheduledStartTimeRaw { get; set; } + + /// + /// Gets the scheduled end time of the scheduled event. + /// + [JsonIgnore] + public DateTimeOffset? ScheduledEndTime + => !string.IsNullOrWhiteSpace(this.ScheduledEndTimeRaw) && DateTimeOffset.TryParse(this.ScheduledEndTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? + dto : null; + + /// + /// Gets the scheduled end time of the scheduled event as raw string. + /// + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] + internal string ScheduledEndTimeRaw { get; set; } + + /// + /// Gets the privacy level of the scheduled event. + /// + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + internal ScheduledEventPrivacyLevel PrivacyLevel { get; set; } + + /// + /// Gets the status of the scheduled event. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public ScheduledEventStatus Status { get; internal set; } + + /// + /// Gets the entity type. + /// + [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] + public ScheduledEventEntityType EntityType { get; internal set; } + + /// + /// Gets id of the entity. + /// + [JsonProperty("entity_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? EntityId { get; internal set; } + + /// + /// Gets metadata of the entity. + /// + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledEventEntityMetadata EntityMetadata { get; internal set; } + + /* This isn't used. + * See https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969066061. + * Was originally for paid stages. + /// + /// Gets the sku ids of the scheduled event. + /// + [JsonProperty("sku_ids", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList SkuIds { get; internal set; }*/ + + /// + /// Gets the total number of users subscribed to the scheduled event. + /// + [JsonProperty("user_count", NullValueHandling = NullValueHandling.Ignore)] + public int UserCount { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal DiscordScheduledEvent() { } + + #region Methods + /// + /// Modifies the current scheduled event. + /// + /// Action to perform on this thread + /// Thrown when the client does not have the permission. + /// Thrown when the event 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 ScheduledEventEditModel(); + action(mdl); + + var channelId = Optional.FromNoValue(); + if (mdl.Channel.HasValue && (mdl.Channel.Value.Type != ChannelType.Voice || mdl.Channel.Value.Type != ChannelType.Stage) && mdl.Channel.Value != null) + throw new ArgumentException("Channel needs to be a voice or stage channel."); + else if (mdl.Channel.HasValue && mdl.Channel.Value != null) + channelId = mdl.Channel.Value.Id; + + if (this.EntityType != ScheduledEventEntityType.External && mdl.EntityType == ScheduledEventEntityType.External) + channelId = null; + + var description = Optional.FromNoValue(); + if (mdl.Description.HasValue && mdl.Description.Value != null) + description = mdl.Description; + else if (mdl.Description.HasValue) + description = null; + + var scheduledEndTime = Optional.FromNoValue(); + if (mdl.ScheduledEndTime.HasValue && mdl.ScheduledEndTime.Value != null && mdl.EntityType.HasValue ? mdl.EntityType == ScheduledEventEntityType.External : this.EntityType == ScheduledEventEntityType.External) + scheduledEndTime = mdl.ScheduledEndTime.Value; + + await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, this.EntityType == ScheduledEventEntityType.External ? new DiscordScheduledEventEntityMetadata(mdl.Location.Value) : null, mdl.Name, mdl.ScheduledStartTime, scheduledEndTime, description, mdl.EntityType, mdl.Status, mdl.AuditLogReason); + } + + /// + /// Starts the current scheduled event. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task StartAsync(string reason = null) + => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Active, reason) : throw new InvalidOperationException("You can only start scheduled events"); + + /// + /// Cancels the current scheduled event. + /// + /// The audit log reason. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CancelAsync(string reason = null) + => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Canceled, reason) : throw new InvalidOperationException("You can only cancel scheduled events"); + + /// + /// Ends the current scheduled event. + /// + /// The audit log reason. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task EndAsync(string reason = null) + => this.Status == ScheduledEventStatus.Active ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Completed, reason) : throw new InvalidOperationException("You can only stop active events"); + + /// + /// Gets a list of users RSVP'd to the scheduled event. + /// + /// The limit how many users to receive from the event. + /// Get results of before the given snowflake. + /// Get results of after the given snowflake. + /// Wether to include guild member data. + /// Thrown when the client does not have the correct permissions. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetUsersAsync(int? limit = null, ulong? before = null, ulong? after = null, bool? withMember = null) + => await this.Discord.ApiClient.GetGuildScheduledEventRSPVUsersAsync(this.GuildId, this.Id, limit, before, after, withMember); + + /// + /// Deletes a scheduled event. + /// + /// The audit log reason. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteGuildScheduledEventAsync(this.GuildId ,this.Id, reason); + + #endregion + + /// + /// 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 DiscordScheduledEvent); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordScheduledEvent e) + => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Gets whether the two objects are equal. + /// + /// First event to compare. + /// Second ecent to compare. + /// Whether the two events are equal. + public static bool operator ==(DiscordScheduledEvent e1, DiscordScheduledEvent 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); + } + + /// + /// Gets whether the two objects are not equal. + /// + /// First event to compare. + /// Second event to compare. + /// Whether the two events are not equal. + public static bool operator !=(DiscordScheduledEvent e1, DiscordScheduledEvent e2) + => !(e1 == e2); + } +} diff --git a/DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventEntityMetadata.cs similarity index 68% rename from DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs rename to DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventEntityMetadata.cs index afa207f37..3b8d5dc66 100644 --- a/DisCatSharp/Entities/Event/DiscordEventEntityMetadata.cs +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventEntityMetadata.cs @@ -1,45 +1,55 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Concurrent; using System.Collections.Generic; +using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents an scheduled event. /// - public class DiscordEventEntityMetadata + public class DiscordScheduledEventEntityMetadata { /// - /// Gets the the speakers of the stage channel. + /// External location if event type is . /// - [JsonProperty("speaker_ids", NullValueHandling = NullValueHandling.Ignore)] - public List Speakers { get; internal set; } + [JsonProperty("location", NullValueHandling = NullValueHandling.Ignore)] + public string Location { get; internal set; } /// - /// Gets the the location of the event. + /// Initializes a new instance of the class. /// - [JsonProperty("location", NullValueHandling = NullValueHandling.Ignore)] - public string Location { get; internal set; } + internal DiscordScheduledEventEntityMetadata() { } + + /// + /// Initializes a new instance of the class. + /// + /// The location. + public DiscordScheduledEventEntityMetadata(string location) + { + this.Location = location; + } } } diff --git a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs new file mode 100644 index 000000000..33c01e801 --- /dev/null +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs @@ -0,0 +1,121 @@ +// This file is part of the DisCatSharp project, a fork of DSharpPlus. +// +// Copyright (c) 2021 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 Newtonsoft.Json; + +namespace DisCatSharp.Entities +{ + /// + /// The discord scheduled event user. + /// + public class DiscordScheduledEventUser : IEquatable + { + /// + /// Gets the client instance this object is tied to. + /// + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + /// + /// Gets the user. + /// + [JsonProperty("user")] + public DiscordUser User { get; internal set; } + + /// + /// Gets the member. + /// Only applicable when requested with `with_member`. + /// + [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] + internal DiscordMember Member { get; set; } + + /// + /// Gets the scheduled event. + /// + [JsonIgnore] + public DiscordScheduledEvent ScheduledEvent + => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild.ScheduledEvents.TryGetValue(this.EventId, out var scheduledEvent) ? scheduledEvent : null : null; + + /// + /// Gets or sets the event id. + /// + [JsonProperty("guild_scheduled_event_id")] + internal ulong EventId { get; set; } + + /// + /// Gets or sets the guild id. + /// + [JsonIgnore] + internal ulong GuildId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal DiscordScheduledEventUser() { } + + /// + /// 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 DiscordScheduledEventUser); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordScheduledEventUser e) + => e is not null && (ReferenceEquals(this, e) || HashCode.Combine(this.User.Id, this.EventId) == HashCode.Combine(e.User.Id, e.EventId)); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => HashCode.Combine(this.User.Id, this.EventId); + + /// + /// Gets whether the two objects are equal. + /// + /// First event to compare. + /// Second ecent to compare. + /// Whether the two events are equal. + public static bool operator ==(DiscordScheduledEventUser e1, DiscordScheduledEventUser e2) + { + var o1 = e1 as object; + var o2 = e2 as object; + + return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || HashCode.Combine(e1.User.Id, e1.EventId) == HashCode.Combine(e2.User.Id, e2.EventId)); + } + + /// + /// Gets whether the two objects are not equal. + /// + /// First event to compare. + /// Second event to compare. + /// Whether the two events are not equal. + public static bool operator !=(DiscordScheduledEventUser e1, DiscordScheduledEventUser e2) + => !(e1 == e2); + } +} diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs b/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs similarity index 98% copy from DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs copy to DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs index c9af8ffb4..e01da801d 100644 --- a/DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs +++ b/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs @@ -1,118 +1,117 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Enums; -using DisCatSharp.EventArgs; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// - /// Represents a button that can be pressed. Fires when pressed. + /// Represents a button that can be pressed. Fires event when pressed. /// public sealed class DiscordButtonComponent : DiscordComponent { /// /// The style of the button. /// [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] public ButtonStyle Style { get; internal set; } /// /// The text to apply to the button. If this is not specified becomes required. /// [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; internal set; } /// /// Whether this button can be pressed. /// [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] public bool Disabled { get; internal set; } /// /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. /// [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] public DiscordComponentEmoji Emoji { get; internal set; } /// /// Enables this component if it was disabled before. /// /// The current component. public DiscordButtonComponent Enable() { this.Disabled = false; return this; } /// /// Disables this component. /// /// The current component. public DiscordButtonComponent Disable() { this.Disabled = true; return this; } /// /// Constructs a new . /// internal DiscordButtonComponent() { this.Type = ComponentType.Button; } /// /// Constucts a new button based on another button. /// /// The button to copy. public DiscordButtonComponent(DiscordButtonComponent other) : this() { this.CustomId = other.CustomId; this.Style = other.Style; this.Label = other.Label; this.Disabled = other.Disabled; this.Emoji = other.Emoji; } /// /// Constructs a new button with the specified options. /// /// The style/color of the button. /// The Id to assign to the button. This is sent back when a user presses it. /// The text to display on the button, up to 80 characters. Can be left blank if is set. /// Whether this button should be initialized as being disabled. User sees a greyed out button that cannot be interacted with. /// The emoji to add to the button. This is required if is empty or null. public DiscordButtonComponent(ButtonStyle style, string customId, string label, bool disabled = false, DiscordComponentEmoji emoji = null) { this.Style = style; this.Label = label; this.CustomId = customId; this.Disabled = disabled; this.Emoji = emoji; this.Type = ComponentType.Button; } } } diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordLinkButtonComponent.cs b/DisCatSharp/Entities/Interaction/Components/Button/DiscordLinkButtonComponent.cs similarity index 100% rename from DisCatSharp/Entities/Interaction/Components/DiscordLinkButtonComponent.cs rename to DisCatSharp/Entities/Interaction/Components/Button/DiscordLinkButtonComponent.cs diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordSelectComponent.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs similarity index 100% rename from DisCatSharp/Entities/Interaction/Components/DiscordSelectComponent.cs rename to DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponent.cs diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordSelectComponentOption.cs b/DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponentOption.cs similarity index 100% rename from DisCatSharp/Entities/Interaction/Components/DiscordSelectComponentOption.cs rename to DisCatSharp/Entities/Interaction/Components/Select/DiscordSelectComponentOption.cs diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs b/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs similarity index 50% rename from DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs rename to DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs index c9af8ffb4..5cbae700c 100644 --- a/DisCatSharp/Entities/Interaction/Components/DiscordButtonComponent.cs +++ b/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs @@ -1,118 +1,135 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Enums; -using DisCatSharp.EventArgs; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// - /// Represents a button that can be pressed. Fires when pressed. + /// Represents a text component that can be submitted. Fires event when submitted. /// - public sealed class DiscordButtonComponent : DiscordComponent + public sealed class DiscordTextComponent : DiscordComponent { /// - /// The style of the button. + /// The style of the text component. /// [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] - public ButtonStyle Style { get; internal set; } + public TextComponentStyle Style { get; internal set; } /// - /// The text to apply to the button. If this is not specified becomes required. + /// The text description to apply to the text component. /// [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; internal set; } /// - /// Whether this button can be pressed. + /// The placeholder for the text component. /// - [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] - public bool Disabled { get; internal set; } + [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] + public string Placeholder { get; internal set; } + + /// + /// The minimal length of text input. + /// + [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MinLength { get; internal set; } /// - /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. + /// The maximal length of text input. /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentEmoji Emoji { get; internal set; } + [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MaxLength { get; internal set; } + + /// + /// Whether this text component can be used. + /// + [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] + public bool Disabled { get; internal set; } /// /// Enables this component if it was disabled before. /// /// The current component. - public DiscordButtonComponent Enable() + public DiscordTextComponent Enable() { this.Disabled = false; return this; } /// /// Disables this component. /// /// The current component. - public DiscordButtonComponent Disable() + public DiscordTextComponent Disable() { this.Disabled = true; return this; } /// - /// Constructs a new . + /// Constructs a new . /// - internal DiscordButtonComponent() + internal DiscordTextComponent() { - this.Type = ComponentType.Button; + this.Type = ComponentType.InputText; } /// - /// Constucts a new button based on another button. + /// Constucts a new text component based on another text component. /// /// The button to copy. - public DiscordButtonComponent(DiscordButtonComponent other) : this() + public DiscordTextComponent(DiscordTextComponent other) : this() { this.CustomId = other.CustomId; this.Style = other.Style; this.Label = other.Label; this.Disabled = other.Disabled; - this.Emoji = other.Emoji; + this.MinLength = other.MinLength; + this.MaxLength = other.MaxLength; + this.Placeholder = other.Placeholder; } /// - /// Constructs a new button with the specified options. + /// Constructs a new text component field with the specified options. /// - /// The style/color of the button. - /// The Id to assign to the button. This is sent back when a user presses it. - /// The text to display on the button, up to 80 characters. Can be left blank if is set. - /// Whether this button should be initialized as being disabled. User sees a greyed out button that cannot be interacted with. - /// The emoji to add to the button. This is required if is empty or null. - public DiscordButtonComponent(ButtonStyle style, string customId, string label, bool disabled = false, DiscordComponentEmoji emoji = null) + /// The style of the text component. + /// The Id to assign to the text component. This is sent back when a user presses it. + /// The text to display before the text component, up to 80 characters. + /// The placeholder for the text input. + /// The minimal length of text input. + /// The maximal length of text input. + /// Whether this text component should be initialized as being disabled. + public DiscordTextComponent(TextComponentStyle style, string customId, string label, string placeholder = null, int? minLength = null, int? maxLength = null, bool disabled = false) { this.Style = style; this.Label = label; this.CustomId = customId; + this.MinLength = minLength; + this.MaxLength = maxLength; + this.Placeholder = placeholder; this.Disabled = disabled; - this.Emoji = emoji; - this.Type = ComponentType.Button; + this.Type = ComponentType.InputText; } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs index 40a54c53f..847e5e264 100644 --- a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs @@ -1,313 +1,315 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; namespace DisCatSharp.Entities { /// /// Constructs a followup message to an interaction. /// public sealed class DiscordFollowupMessageBuilder { /// /// Whether this followup message is text-to-speech. /// public bool IsTTS { get; set; } /// /// Whether this followup message should be ephemeral. /// public bool IsEphemeral { get; set; } /// /// Indicates this message is emphemeral. /// internal int? Flags => this.IsEphemeral ? 64 : null; /// /// Message to send on followup message. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Embeds to send on followup message. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this followup message. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Components to send on this followup message. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Mentions to send on this followup message. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Appends a collection of components to the message. /// /// The collection of components to add. /// The builder to chain calls with. - /// contained more than 5 components. + /// contained more than 5 components. public DiscordFollowupMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordFollowupMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the message. /// /// The collection of components to add. /// The builder to chain calls with. - /// contained more than 5 components. + /// contained more than 5 components. public DiscordFollowupMessageBuilder AddComponents(IEnumerable components) { var compArr = components.ToArray(); var count = compArr.Length; if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var arc = new DiscordActionRowComponent(compArr); this._components.Add(arc); return this; } /// /// Indicates if the followup message must use text-to-speech. /// /// Text-to-speech /// The builder to chain calls with. public DiscordFollowupMessageBuilder WithTTS(bool tts) { this.IsTTS = tts; return this; } /// /// Sets the message to send with the followup message.. /// /// Message to send. /// The builder to chain calls with. public DiscordFollowupMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to the followup message. /// /// Embed to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbed(DiscordEmbed embed) { this._embeds.Add(embed); return this; } /// /// Adds the given embeds to the followup message. /// /// Embeds to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to the followup message. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The builder to chain calls with. - public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false) + public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(filename, data, data.Position)); + this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else - this._files.Add(new DiscordMessageFile(filename, data, null)); + this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The builder to chain calls with. - public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false) + public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new DiscordMessageFile(stream.Name, stream, null)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to the followup message. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Adds the mention to the mentions to parse, etc. with the followup message. /// /// Mention to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the followup message. /// /// Mentions to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Sets the followup message to be ephemeral. /// /// Ephemeral. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AsEphemeral(bool ephemeral) { this.IsEphemeral = ephemeral; return this; } /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Followup Message builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTTS = false; this._mentions.Clear(); this._files.Clear(); this.IsEphemeral = false; this._components.Clear(); } /// /// Validates the builder. /// internal void Validate() { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) throw new ArgumentException("You must specify content, an embed, or at least one file."); } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs index c89f8a003..4aa4c14ae 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs @@ -1,174 +1,181 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents an interaction that was invoked. /// public sealed class DiscordInteraction : SnowflakeObject { /// /// Gets the type of interaction invoked. /// [JsonProperty("type")] public InteractionType Type { get; internal set; } /// /// Gets the command data for this interaction. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionData Data { get; internal set; } /// /// Gets the Id of the guild that invoked this interaction. /// [JsonIgnore] public ulong? GuildId { get; internal set; } /// /// Gets the guild that invoked this interaction. /// [JsonIgnore] public DiscordGuild Guild => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); /// /// Gets the Id of the channel that invoked this interaction. /// [JsonIgnore] public ulong ChannelId { get; internal set; } /// /// Gets the channel that invoked this interaction. /// [JsonIgnore] public DiscordChannel Channel => (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord } : null); /// /// Gets the user that invoked this interaction. - /// This can be cast to a if created in a guild. + /// This can be cast to a if created in a guild. /// [JsonIgnore] public DiscordUser User { get; internal set; } /// /// Gets the continuation token for responding to this interaction. /// [JsonProperty("token")] public string Token { get; internal set; } /// /// Gets the version number for this interaction type. /// [JsonProperty("version")] public int Version { get; internal set; } /// /// Gets the ID of the application that created this interaction. /// [JsonProperty("application_id")] public ulong ApplicationId { get; internal set; } /// /// The message this interaction was created with, if any. /// [JsonProperty("message")] internal DiscordMessage Message { get; set; } /// /// Creates a response to this interaction. /// /// The type of the response. /// The data, if any, to send. public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder); + /// + /// Creates a modal response to this interaction. + /// + /// The data to send. + public Task CreateInteractionModalResponseAsync(DiscordInteractionModalBuilder builder) => + this.Discord.ApiClient.CreateInteractionModalResponseAsync(this.Id, this.Token, InteractionResponseType.Modal, builder); + /// /// Gets the original interaction response. /// /// The origingal message that was sent. This does not work on ephemeral messages. public Task GetOriginalResponseAsync() => this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Edits the original interaction response. /// /// The webhook builder. /// The edited . public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder) { builder.Validate(isInteractionResponse: true); return await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false); } /// /// Deletes the original interaction response. /// > public Task DeleteOriginalResponseAsync() => this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Creates a follow up message to this interaction. /// /// The webhook builder. /// The created . public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder) { builder.Validate(); return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false); } /// /// Gets a follow up message. /// /// The id of the follow up message. public Task GetFollowupMessageAsync(ulong messageId) => this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); /// /// Edits a follow up message. /// /// The id of the follow up message. /// The webhook builder. /// The edited . public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder) { builder.Validate(isFollowup: true); return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder).ConfigureAwait(false); } /// /// Deletes a follow up message. /// /// The id of the follow up message. public Task DeleteFollowupMessageAsync(ulong messageId) => this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs index 62bc949b7..dcbee1f15 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs @@ -1,75 +1,105 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represants a interactions application command callback data. /// internal class DiscordInteractionApplicationCommandCallbackData { /// /// Wheter this message is tts /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool? IsTTS { get; internal set; } /// /// Gets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; internal set; } /// /// Gets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; internal set; } /// /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Mentions { get; internal set; } /// /// Gets the flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; internal set; } /// /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; internal set; } /// /// Gets the autocomplete choices. /// [JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Choices { get; internal set; } + + /// + /// Gets the attachments. + /// + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public List Attachments { get; set; } + } + + /// + /// Represants a interactions application command callback data. + /// + internal class DiscordInteractionApplicationCommandModalCallbackData + { + /// + /// Gets the custom id. + /// + [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] + public string CustomId { get; internal set; } + + /// + /// Gets the content. + /// + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string Title { get; internal set; } + + /// + /// Gets the components. + /// + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyCollection ModalComponents { get; internal set; } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs index f2d43b5e1..af0166daa 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs @@ -1,89 +1,89 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents parameters for interaction commands. /// public sealed class DiscordInteractionDataOption { /// /// Gets the name of this interaction parameter. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the type of this interaction parameter. /// [JsonProperty("type")] public ApplicationCommandOptionType Type { get; internal set; } /// /// Whether this option is currently focused by the user. /// Only applicable for autocomplete option choices. /// [JsonProperty("focused")] public bool Focused { get; internal set; } /// /// Gets the value of this interaction parameter. /// [JsonProperty("value")] internal string RawValue { get; set; } /// /// Gets the value of this interaction parameter. - /// This can be cast to a , , , or depending on the + /// This can be cast to a , , , or depending on the /// [JsonIgnore] public object Value { get { return this.Type switch { ApplicationCommandOptionType.Boolean => bool.Parse(this.RawValue), ApplicationCommandOptionType.Integer => long.Parse(this.RawValue), ApplicationCommandOptionType.String => this.RawValue, ApplicationCommandOptionType.Channel => ulong.Parse(this.RawValue), ApplicationCommandOptionType.User => ulong.Parse(this.RawValue), ApplicationCommandOptionType.Role => ulong.Parse(this.RawValue), ApplicationCommandOptionType.Mentionable => ulong.Parse(this.RawValue), ApplicationCommandOptionType.Number => double.Parse(this.RawValue), ApplicationCommandOptionType.Attachment => this.RawValue, _ => this.RawValue, }; } } /// /// Gets the additional parameters if this parameter is a subcommand. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Options { get; internal set; } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs new file mode 100644 index 000000000..2c2b0d9da --- /dev/null +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs @@ -0,0 +1,156 @@ +// This file is part of the DisCatSharp project. +// +// Copyright (c) 2021 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; + +namespace DisCatSharp.Entities +{ + /// + /// Constructs an interaction modal response. + /// + public sealed class DiscordInteractionModalBuilder + { + /// + /// Title of modal. + /// + public string Title + { + get => this._title; + set + { + if (value != null && value.Length > 128) + throw new ArgumentException("Title length cannot exceed 128 characters.", nameof(value)); + this._title = value; + } + } + private string _title; + + /// + /// Custom id of modal. + /// + public string CustomId { get; set; } + + /// + /// Components to send on this interaction response. + /// + public IReadOnlyList ModalComponents => this._components; + private readonly List _components = new(); + + /// + /// Constructs a new empty interaction modal builder. + /// + public DiscordInteractionModalBuilder() { } + + public DiscordInteractionModalBuilder WithTitle(string title) + { + this.Title = title; + return this; + } + + public DiscordInteractionModalBuilder WithCustomId(string customId) + { + this.CustomId = customId; + return this; + } + + /// + /// Appends a collection of components to the builder. Each call will append to a new row. + /// + /// The components to append. Up to five. + /// The current builder to chain calls with. + /// Thrown when passing more than 5 components. + public DiscordInteractionModalBuilder AddModalComponents(params DiscordComponent[] components) + => this.AddModalComponents((IEnumerable)components); + + /// + /// Appends a text component to the builder. + /// + /// The component to append. + /// The current builder to chain calls with. + public DiscordInteractionModalBuilder AddTextComponent(DiscordTextComponent component) + { + List comp = new(1); + comp.Add(component); + + return this.AddModalComponents(comp); + } + + /// + /// Appends several rows of components to the message + /// + /// The rows of components to add, holding up to five each. + /// + public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) + { + var ara = components.ToArray(); + + if (ara.Length + this._components.Count > 5) + throw new ArgumentException("ActionRow count exceeds maximum of five."); + + foreach (var ar in ara) + this._components.Add(ar); + + return this; + } + + /// + /// Appends a collection of components to the builder. Each call will append to a new row. + /// If you add a you can only add one. + /// + /// The components to append. Up to five. + /// The current builder to chain calls with. + /// Thrown when passing more than 5 components. + public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) + { + var compArr = components.ToArray(); + var count = compArr.Length; + + if (count > 5) + throw new ArgumentException("Cannot add more than 5 components per action row!"); + + if (components.Where(c => c.Type == Enums.ComponentType.InputText).Any() && count < 1) + throw new ArgumentException("Cannot add more than 1 text components per action row!"); + + var arc = new DiscordActionRowComponent(compArr); + this._components.Add(arc); + return this; + } + + /// + /// Clears all message components on this builder. + /// + public void ClearComponents() + => this._components.Clear(); + + /// + /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. + /// + public void Clear() + { + this._components.Clear(); + this.Title = null; + this.CustomId = null; + } + } +} diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs index ec02dba1b..541f811f3 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs @@ -1,351 +1,353 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; namespace DisCatSharp.Entities { /// /// Constructs an interaction response. /// public sealed class DiscordInteractionResponseBuilder { /// /// Whether this interaction response is text-to-speech. /// public bool IsTTS { get; set; } /// /// Whether this interaction response should be ephemeral. /// public bool IsEphemeral { get; set; } /// /// Content of the message to send. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Embeds to send on this interaction response. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this interaction response. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Components to send on this interaction response. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// The choices to send on this interaction response. /// Mutually exclusive with content, embed, and components. /// public IReadOnlyList Choices => this._choices; private readonly List _choices = new(); /// /// Mentions to send on this interaction response. /// public IEnumerable Mentions => this._mentions; private readonly List _mentions = new(); /// /// Constructs a new empty interaction response builder. /// public DiscordInteractionResponseBuilder() { } /// - /// Constructs a new based on an existing . + /// Constructs a new based on an existing . /// /// The builder to copy. public DiscordInteractionResponseBuilder(DiscordMessageBuilder builder) { this._content = builder.Content; this._mentions = builder.Mentions; this._embeds.AddRange(builder.Embeds); this._components.AddRange(builder.Components); } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionResponseBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordInteractionResponseBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionResponseBuilder AddComponents(IEnumerable components) { var compArr = components.ToArray(); var count = compArr.Length; if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var arc = new DiscordActionRowComponent(compArr); this._components.Add(arc); return this; } /// /// Indicates if the interaction response will be text-to-speech. /// /// Text-to-speech public DiscordInteractionResponseBuilder WithTTS(bool tts) { this.IsTTS = tts; return this; } /// /// Sets the interaction response to be ephemeral. /// /// Ephemeral. public DiscordInteractionResponseBuilder AsEphemeral(bool ephemeral) { this.IsEphemeral = ephemeral; return this; } /// /// Sets the content of the message to send. /// /// Content to send. public DiscordInteractionResponseBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to send with the interaction response. /// /// Embed to add. public DiscordInteractionResponseBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); // Interactions will 400 silently // return this; } /// /// Adds the given embeds to send with the interaction response. /// /// Embeds to add. public DiscordInteractionResponseBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to the interaction response. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The builder to chain calls with. - public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false) + public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(filename, data, data.Position)); + this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else - this._files.Add(new DiscordMessageFile(filename, data, null)); + this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The builder to chain calls with. - public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetStreamPosition = false) + public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new DiscordMessageFile(stream.Name, stream, null)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to the interaction response builder. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Adds the mention to the mentions to parse, etc. with the interaction response. /// /// Mention to add. public DiscordInteractionResponseBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the interaction response. /// /// Mentions to add. public DiscordInteractionResponseBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Adds a single auto-complete choice to the builder. /// /// The choice to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordApplicationCommandAutocompleteChoice choice) { this._choices.Add(choice); return this; } /// /// Adds auto-complete choices to the builder. /// /// The choices to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices) { this._choices.AddRange(choices); return this; } /// /// Adds auto-complete choices to the builder. /// /// The choices to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoices(params DiscordApplicationCommandAutocompleteChoice[] choices) => this.AddAutoCompleteChoices((IEnumerable)choices); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTTS = false; this.IsEphemeral = false; this._mentions.Clear(); this._components.Clear(); this._choices.Clear(); this._files.Clear(); } } } diff --git a/DisCatSharp/Entities/Invite/DiscordInvite.cs b/DisCatSharp/Entities/Invite/DiscordInvite.cs index ff6dd80e0..963a0357d 100644 --- a/DisCatSharp/Entities/Invite/DiscordInvite.cs +++ b/DisCatSharp/Entities/Invite/DiscordInvite.cs @@ -1,189 +1,189 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; using DisCatSharp.Enums; namespace DisCatSharp.Entities { /// /// Represents a Discord invite. /// public class DiscordInvite { /// /// Gets the base cloent. /// internal BaseDiscordClient Discord { get; set; } /// /// Gets the invite's code. /// [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] public string Code { get; internal set; } /// /// Gets the invite's url. /// [JsonIgnore] public string Url => DiscordDomain.GetDomain(CoreDomain.DiscordShortlink).Url + "/" + this.Code; /// /// Gets the invite's url as Uri. /// [JsonIgnore] public Uri Uri => new(this.Url); /// /// Gets the guild this invite is for. /// [JsonProperty("guild", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteGuild Guild { get; internal set; } /// /// Gets the channel this invite is for. /// [JsonProperty("channel", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteChannel Channel { get; internal set; } /// /// Gets the target type for the voice channel this invite is for. /// [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] public TargetType? TargetType { get; internal set; } /// /// Gets the user that is currently livestreaming. /// [JsonProperty("target_user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser TargetUser { get; internal set; } /// /// Gets the embedded partial application to open for this voice channel. /// [JsonProperty("target_application", NullValueHandling = NullValueHandling.Ignore)] public DiscordApplication TargetApplication { get; internal set; } /// /// Gets the approximate guild online member count for the invite. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } /// /// Gets the approximate guild total member count for the invite. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the user who created the invite. /// [JsonProperty("inviter", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Inviter { get; internal set; } /// /// Gets the number of times this invite has been used. /// [JsonProperty("uses", NullValueHandling = NullValueHandling.Ignore)] public int Uses { get; internal set; } /// /// Gets the max number of times this invite can be used. /// [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] public int MaxUses { get; internal set; } /// /// Gets duration in seconds after which the invite expires. /// [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] public int MaxAge { get; internal set; } /// /// Gets whether this invite only grants temporary membership. /// [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] public bool IsTemporary { get; internal set; } /// /// Gets the date and time this invite was created. /// [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset CreatedAt { get; internal set; } /// /// Gets the date and time when this invite expires. /// [JsonProperty("expires_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset ExpiresAt { get; internal set; } /// /// Gets the date and time when this invite got expired. /// [JsonProperty("expired_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset ExpiredAt { get; internal set; } /// /// Gets whether this invite is revoked. /// [JsonProperty("revoked", NullValueHandling = NullValueHandling.Ignore)] public bool IsRevoked { get; internal set; } /// /// Gets the stage instance this invite is for. /// [JsonProperty("stage_instance", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteStage Stage { get; internal set; } /// /// Gets the guild scheduled event data for the invite. /// [JsonProperty("guild_scheduled_event", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEvent GuildScheduledEvent { get; internal set; } + public DiscordScheduledEvent GuildScheduledEvent { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordInvite() { } /// /// Deletes the invite. /// /// Reason for audit logs. /// /// Thrown when the client does not have the permission or the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteInviteAsync(this.Code, reason); /// /// Converts this invite into an invite link. /// /// A discord.gg invite link. public override string ToString() => $"{DiscordDomain.GetDomain(CoreDomain.DiscordShortlink).Url}/{this.Code}"; } } diff --git a/DisCatSharp/Entities/Message/DiscordAttachment.cs b/DisCatSharp/Entities/Message/DiscordAttachment.cs index b8cbf30bb..bba2ed59f 100644 --- a/DisCatSharp/Entities/Message/DiscordAttachment.cs +++ b/DisCatSharp/Entities/Message/DiscordAttachment.cs @@ -1,87 +1,93 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents an attachment for a message. /// public class DiscordAttachment : SnowflakeObject { /// /// Gets the name of the file. /// [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] public string FileName { get; internal set; } /// - /// Gets the file size in bytes. + /// Gets the description of the file. /// - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] - public int FileSize { get; internal set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } /// /// Gets the media, or MIME, type of the file. /// [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] public string MediaType { get; internal set; } + /// + /// Gets the file size in bytes. + /// + [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] + public int? FileSize { get; internal set; } + /// /// Gets the URL of the file. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] public string Url { get; internal set; } /// /// Gets the proxied URL of the file. /// [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] public string ProxyUrl { get; internal set; } /// /// Gets the height. Applicable only if the attachment is an image. /// [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] public int? Height { get; internal set; } /// /// Gets the width. Applicable only if the attachment is an image. /// [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] public int? Width { get; internal set; } /// /// Gets whether this attachment is ephemeral. /// Ephemeral attachments will automatically be removed after a set period of time. /// Ephemeral attachments on messages are guaranteed to be available as long as the message itself exists. /// [JsonProperty("ephemeral", NullValueHandling = NullValueHandling.Ignore)] public bool? Ephemeral { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordAttachment() { } } } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index 92a43ea98..5ecddffaa 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,881 +1,881 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord text message. /// public class DiscordMessage : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMessage() { this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this._attachments)); this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this._embeds)); this._mentionedChannelsLazy = new Lazy>(() => this._mentionedChannels != null ? new ReadOnlyCollection(this._mentionedChannels) : Array.Empty()); this._mentionedRolesLazy = new Lazy>(() => this._mentionedRoles != null ? new ReadOnlyCollection(this._mentionedRoles) : Array.Empty()); this._mentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this._mentionedUsers)); this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this._reactions)); this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this._stickers)); this._jumpLink = new Lazy(() => { var gid = this.Channel != null ? this.Channel is DiscordDmChannel ? "@me" : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture) : this.InternalThread.GuildId.Value.ToString(CultureInfo.InvariantCulture); var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); var mid = this.Id.ToString(CultureInfo.InvariantCulture); return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}"); }); } /// /// Initializes a new instance of the class. /// /// The other. internal DiscordMessage(DiscordMessage other) : this() { this.Discord = other.Discord; this._attachments = other._attachments; // the attachments cannot change, thus no need to copy and reallocate. this._embeds = new List(other._embeds); if (other._mentionedChannels != null) this._mentionedChannels = new List(other._mentionedChannels); if (other._mentionedRoles != null) this._mentionedRoles = new List(other._mentionedRoles); if (other._mentionedRoleIds != null) this._mentionedRoleIds = new List(other._mentionedRoleIds); this._mentionedUsers = new List(other._mentionedUsers); this._reactions = new List(other._reactions); this._stickers = new List(other._stickers); this.Author = other.Author; this.ChannelId = other.ChannelId; this.Content = other.Content; this.EditedTimestampRaw = other.EditedTimestampRaw; this.Id = other.Id; this.IsTTS = other.IsTTS; this.MessageType = other.MessageType; this.Pinned = other.Pinned; this.TimestampRaw = other.TimestampRaw; this.WebhookId = other.WebhookId; } /// /// Gets the channel in which the message was sent. /// [JsonIgnore] public DiscordChannel Channel { get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel; internal set => this._channel = value; } private DiscordChannel _channel; /// /// Gets the thread in which the message was sent. /// [JsonIgnore] private DiscordThreadChannel InternalThread { get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread; set => this._thread = value; } private DiscordThreadChannel _thread; /// /// Gets the ID of the channel in which the message was sent. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the components this message was sent with. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; internal set; } /// /// Gets the user or member that sent the message. /// [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Author { get; internal set; } /// /// Gets the message's content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; internal set; } /// /// Gets the message's creation timestamp. /// [JsonIgnore] public DateTimeOffset Timestamp => !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : this.CreationTimestamp; /// /// Gets the message's creation timestamp as raw string. /// [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string TimestampRaw { get; set; } /// /// Gets the message's edit timestamp. Will be null if the message was not edited. /// [JsonIgnore] public DateTimeOffset? EditedTimestamp => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? (DateTimeOffset?)dto : null; /// /// Gets the message's edit timestamp as raw string. Will be null if the message was not edited. /// [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string EditedTimestampRaw { get; set; } /// /// Gets whether this message was edited. /// [JsonIgnore] public bool IsEdited => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw); /// /// Gets whether the message is a text-to-speech message. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool IsTTS { get; internal set; } /// /// Gets whether the message mentions everyone. /// [JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)] public bool MentionEveryone { get; internal set; } /// /// Gets users or members mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedUsers => this._mentionedUsersLazy.Value; [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] internal List _mentionedUsers; [JsonIgnore] internal readonly Lazy> _mentionedUsersLazy; // TODO this will probably throw an exception in DMs since it tries to wrap around a null List... // this is probably low priority but need to find out a clean way to solve it... /// /// Gets roles mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedRoles => this._mentionedRolesLazy.Value; [JsonIgnore] internal List _mentionedRoles; [JsonProperty("mention_roles")] internal List _mentionedRoleIds; [JsonIgnore] private readonly Lazy> _mentionedRolesLazy; /// /// Gets channels mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedChannels => this._mentionedChannelsLazy.Value; [JsonIgnore] internal List _mentionedChannels; [JsonIgnore] private readonly Lazy> _mentionedChannelsLazy; /// /// Gets files attached to this message. /// [JsonIgnore] public IReadOnlyList Attachments => this._attachmentsLazy.Value; [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] internal List _attachments = new(); [JsonIgnore] private readonly Lazy> _attachmentsLazy; /// /// Gets embeds attached to this message. /// [JsonIgnore] public IReadOnlyList Embeds => this._embedsLazy.Value; [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] internal List _embeds = new(); [JsonIgnore] private readonly Lazy> _embedsLazy; /// /// Gets reactions used on this message. /// [JsonIgnore] public IReadOnlyList Reactions => this._reactionsLazy.Value; [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] internal List _reactions = new(); [JsonIgnore] private readonly Lazy> _reactionsLazy; /* /// /// Gets the nonce sent with the message, if the message was sent by the client. /// [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] public ulong? Nonce { get; internal set; } */ /// /// Gets whether the message is pinned. /// [JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)] public bool Pinned { get; internal set; } /// /// Gets the id of the webhook that generated this message. /// [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? WebhookId { get; internal set; } /// /// Gets the type of the message. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public MessageType? MessageType { get; internal set; } /// /// Gets the message activity in the Rich Presence embed. /// [JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageActivity Activity { get; internal set; } /// /// Gets the message application in the Rich Presence embed. /// [JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageApplication Application { get; internal set; } /// /// Gets the message application id in the Rich Presence embed. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ApplicationId { get; internal set; } /// /// Gets the internal reference. /// [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] internal InternalDiscordMessageReference? InternalReference { get; set; } /// /// Gets the original message reference from the crossposted message. /// [JsonIgnore] public DiscordMessageReference Reference => this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null; /// /// Gets the bitwise flags for this message. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; internal set; } /// /// Gets whether the message originated from a webhook. /// [JsonIgnore] public bool WebhookMessage => this.WebhookId != null; /// /// Gets the jump link to this message. /// [JsonIgnore] public Uri JumpLink => this._jumpLink.Value; private readonly Lazy _jumpLink; /// /// Gets stickers for this message. /// [JsonIgnore] public IReadOnlyList Stickers => this._stickersLazy.Value; [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] internal List _stickers = new(); [JsonIgnore] private readonly Lazy> _stickersLazy; /// /// Gets the guild id. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? GuildId { get; set; } /// /// Gets the message object for the referenced message /// [JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessage ReferencedMessage { get; internal set; } /// /// Gets whether the message is a response to an interaction. /// [JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageInteraction Interaction { get; internal set; } /// /// Gets the thread that was started from this message. /// [JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannel Thread { get; internal set; } /// /// Build the message reference. /// internal DiscordMessageReference InternalBuildMessageReference() { var client = this.Discord as DiscordClient; var guildId = this.InternalReference.Value.GuildId; var channelId = this.InternalReference.Value.ChannelId; var messageId = this.InternalReference.Value.MessageId; var reference = new DiscordMessageReference(); if (guildId.HasValue) reference.Guild = client._guilds.TryGetValue(guildId.Value, out var g) ? g : new DiscordGuild { Id = guildId.Value, Discord = client }; var channel = client.InternalGetCachedChannel(channelId.Value); if (channel == null) { reference.Channel = new DiscordChannel { Id = channelId.Value, Discord = client }; if (guildId.HasValue) reference.Channel.GuildId = guildId.Value; } else reference.Channel = channel; if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg)) reference.Message = msg; else { reference.Message = new DiscordMessage { ChannelId = this.ChannelId, Discord = client }; if (messageId.HasValue) reference.Message.Id = messageId.Value; } return reference; } /// /// Gets the mentions. /// /// An array of IMentions. private IMention[] GetMentions() { var mentions = new List(); if (this.ReferencedMessage != null && this._mentionedUsers.Contains(this.ReferencedMessage.Author)) mentions.Add(new RepliedUserMention()); // Return null to allow all mentions if (this._mentionedUsers.Any()) mentions.AddRange(this._mentionedUsers.Select(m => (IMention)new UserMention(m))); if (this._mentionedRoleIds.Any()) mentions.AddRange(this._mentionedRoleIds.Select(r => (IMention)new RoleMention(r))); return mentions.ToArray(); } /// /// Populates the mentions. /// internal void PopulateMentions() { var guild = this.Channel?.Guild; this._mentionedUsers ??= new List(); this._mentionedRoles ??= new List(); this._mentionedChannels ??= new List(); var mentionedUsers = new HashSet(new DiscordUserComparer()); if (guild != null) { foreach (var usr in this._mentionedUsers) { usr.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); mentionedUsers.Add(guild._members.TryGetValue(usr.Id, out var member) ? member : usr); } } if (!string.IsNullOrWhiteSpace(this.Content)) { //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); if (guild != null) { //this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); this._mentionedRoles = this._mentionedRoles.Union(this._mentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList(); this._mentionedChannels = this._mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList(); } } this._mentionedUsers = mentionedUsers.ToList(); } /// /// Edits the message. /// /// New content. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 ModifyAsync(Optional content) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty()); /// /// Edits the message. /// /// New embed. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 ModifyAsync(Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? new[] {embed.Value} : Array.Empty(), this.GetMentions(), default, default, Array.Empty()); /// /// Edits the message. /// /// New content. /// New embed. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 ModifyAsync(Optional content, Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? new[] {embed.Value} : Array.Empty(), this.GetMentions(), default, default, Array.Empty()); /// /// Edits the message. /// /// New content. /// New embeds. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 ModifyAsync(Optional content, Optional> embeds = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty()); /// /// Edits the message. /// /// The builder of the message to edit. /// /// Thrown when the client tried to modify a message not sent by them. /// 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(DiscordMessageBuilder builder) { builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files).ConfigureAwait(false); } /// /// Edits the message embed suppression. /// /// Suppress embeds. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 ModifySuppressionAsync(bool suppress = false) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, suppress, default); /// /// Edits the message. /// /// The builder of the message to edit. /// /// Thrown when the client tried to modify a message not sent by them. /// 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 builder = new DiscordMessageBuilder(); action(builder); builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files).ConfigureAwait(false); } /// /// Deletes the message. /// /// /// 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 DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); /// /// Creates a thread. /// Depending on the of the parent channel it's either a or a . /// /// The name of the thread. /// till it gets archived. Defaults to /// The per user ratelimit, aka slowdown. /// The reason. /// /// Thrown when the client does not have the or permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - /// Thrown when the cannot be modified. + /// Thrown when the cannot be modified. public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration auto_archive_duration = ThreadAutoArchiveDuration.OneHour, int? rate_limit_per_user = null, string reason = null) { return Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, auto_archive_duration) ? await this.Discord.ApiClient.CreateThreadWithMessageAsync(this.ChannelId, this.Id, name, auto_archive_duration, rate_limit_per_user, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Pins the message in its 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 PinAsync() => this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); /// /// Unpins the message in its 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 UnpinAsync() => this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// The sent message. /// 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 RespondAsync(string content) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Embed to attach to the message. /// The sent message. /// 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 RespondAsync(DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// Embed to attach to the message. /// The sent message. /// 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 RespondAsync(string content, DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// 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 RespondAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// 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 RespondAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); } /// /// Creates a reaction to this message. /// /// The emoji you want to react with, either an emoji or name:id /// /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes your own reaction /// /// Emoji for the reaction you want to remove, either an emoji or name:id /// /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteOwnReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes another user's reaction. /// /// Emoji for the reaction you want to remove, either an emoji or name:id. /// Member you want to remove the reaction for /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) => this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); /// /// Gets users that reacted with this emoji. /// /// Emoji to react with. /// Limit of users to fetch. /// Fetch users after this user's id. /// /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) => this.GetReactionsInternalAsync(emoji, limit, after); /// /// Deletes all reactions for this message. /// /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAllReactionsAsync(string reason = null) => this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); /// /// Deletes all reactions of a specific reaction for this message. /// /// The emoji to clear, either an emoji or name:id. /// /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Gets the reactions. /// /// The emoji to search for. /// The limit of results. /// Get the reasctions after snowflake. private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) { if (limit < 0) throw new ArgumentException("Cannot get a negative number of reactions' users."); if (limit == 0) return Array.Empty(); var users = new List(limit); var remaining = limit; var last = after; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; users.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(users); } /// /// Returns a string representation of this message. /// /// String representation of this message. public override string ToString() => $"Message {this.Id}; Attachment count: {this._attachments.Count}; Embed count: {this._embeds.Count}; Contents: {this.Content}"; /// /// 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 DiscordMessage); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMessage e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId)); /// /// 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.ChannelId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are equal. public static bool operator ==(DiscordMessage e1, DiscordMessage 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.ChannelId == e2.ChannelId)); } /// /// Gets whether the two objects are not equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are not equal. public static bool operator !=(DiscordMessage e1, DiscordMessage e2) => !(e1 == e2); } } diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs index c3b56bc3a..1b382a1fe 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs @@ -1,429 +1,432 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities { /// /// Constructs a Message to be sent. /// public sealed class DiscordMessageBuilder { /// /// Gets or Sets the Message to be sent. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Gets or sets the embed for the builder. This will always set the builder to have one embed. /// public DiscordEmbed Embed { get => this._embeds.Count > 0 ? this._embeds[0] : null; set { this._embeds.Clear(); this._embeds.Add(value); } } /// /// Gets the Sticker to be send. /// public DiscordSticker Sticker { get; set; } /// /// Gets the Embeds to be sent. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Gets or Sets if the message should be TTS. /// public bool IsTTS { get; set; } = false; /// /// Gets the Allowed Mentions for the message to be sent. /// public List Mentions { get; private set; } = null; /// /// Gets the Files to be sent in the Message. /// public IReadOnlyCollection Files => this._files; internal readonly List _files = new(); /// /// Gets the components that will be attached to the message. /// public IReadOnlyList Components => this._components; internal readonly List _components = new(5); /// /// Gets the Attachments to be sent in the Message. /// internal List Attachments { get; private set; } = null; /// /// Gets the Reply Message ID. /// public ulong? ReplyId { get; private set; } = null; /// /// Gets if the Reply should mention the user. /// public bool MentionOnReply { get; private set; } = false; /// /// Gets if the embeds should be suppressed. /// public bool Suppressed { get; private set; } = false; /// /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. /// If set to false, invalid replies are send as a regular message. /// Defaults to false. /// public bool FailOnInvalidReply { get; set; } /// /// Sets the Content of the Message. /// /// The content to be set. /// The current builder to be chained. public DiscordMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds a sticker to the message. Sticker must be from current guild. /// /// The sticker to add. /// The current builder to be chained. public DiscordMessageBuilder WithSticker(DiscordSticker sticker) { this.Sticker = sticker; return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordMessageBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this._components.Add(comp); return this; } /// /// Sets if the message should be TTS. /// /// If TTS should be set. /// The current builder to be chained. public DiscordMessageBuilder HasTTS(bool isTTS) { this.IsTTS = isTTS; return this; } /// /// Sets the embed for the current builder. /// /// The embed that should be set. /// The current builder to be chained. public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) { if (embed == null) return this; this.Embed = embed; return this; } /// /// Appends an embed to the current builder. /// /// The embed that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) { if (embed == null) return this; //Providing null embeds will produce a 400 response from Discord.// this._embeds.Add(embed); return this; } /// /// Appends several embeds to the current builder. /// /// The embeds that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mention that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) { if (this.Mentions != null) this.Mentions.Add(allowedMention); else this.Mentions = new List { allowedMention }; return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mentions that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) { if (this.Mentions != null) this.Mentions.AddRange(allowedMentions); else this.Mentions = allowedMentions.ToList(); return this; } /// /// Sets if the message has files to be sent. /// /// The fileName that the file should be sent as. /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The current builder to be chained. - public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false) + public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == fileName)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(fileName, stream, stream.Position)); + this._files.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description)); else - this._files.Add(new DiscordMessageFile(fileName, stream, null)); + this._files.Add(new DiscordMessageFile(fileName, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// The current builder to be chained. - public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false) + public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new DiscordMessageFile(stream.Name, stream, null)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Files that should be sent. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The current builder to be chained. public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Sets if the message is a reply /// /// The ID of the message to reply to. /// If we should mention the user in the reply. /// Whether sending a reply that references an invalid message should be /// The current builder to be chained. public DiscordMessageBuilder WithReply(ulong messageId, bool mention = false, bool failOnInvalidReply = false) { this.ReplyId = messageId; this.MentionOnReply = mention; this.FailOnInvalidReply = failOnInvalidReply; if (mention) { this.Mentions ??= new List(); this.Mentions.Add(new RepliedUserMention()); } return this; } /// /// Sends the Message to a specific channel /// /// The channel the message should be sent to. /// The current builder to be chained. public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); /// /// Sends the modified message. /// Note: Message replies cannot be modified. To clear the reply, simply pass to . /// /// The original Message to modify. /// The current builder to be chained. public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Message Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTTS = false; this.Mentions = null; this._files.Clear(); this.ReplyId = null; this.MentionOnReply = false; this._components.Clear(); this.Suppressed = false; this.Sticker = null; + this.Attachments.Clear(); } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. internal void Validate(bool isModify = false) { if (this._embeds.Count > 10) throw new ArgumentException("A message can only have up to 10 embeds."); if (!isModify) { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null) throw new ArgumentException("You must specify content, an embed, a sticker or at least one file."); if (this.Components.Count > 5) throw new InvalidOperationException("You can only have 5 action rows per message."); if (this.Components.Any(c => c.Components.Count > 5)) throw new InvalidOperationException("Action rows can only have 5 components"); } } } } diff --git a/DisCatSharp/Entities/Message/DiscordMessageFile.cs b/DisCatSharp/Entities/Message/DiscordMessageFile.cs index 05945d1e7..28d96b8a1 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageFile.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageFile.cs @@ -1,74 +1,81 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; namespace DisCatSharp.Entities { /// - /// Represents the File that should be sent to Discord from the . + /// Represents the File that should be sent to Discord from the . /// public class DiscordMessageFile { /// /// Initializes a new instance of the class. /// /// The file name. /// The stream. /// The reset position to. /// The file type. /// The content type. - internal DiscordMessageFile(string fileName, Stream stream, long? resetPositionTo, string fileType = null, string contentType = null) + /// The description. + internal DiscordMessageFile(string fileName, Stream stream, long? resetPositionTo, string fileType = null, string contentType = null, string description = null) { this.FileName = fileName; this.FileType = fileType; this.ContentType = contentType; this.Stream = stream; this.ResetPositionTo = resetPositionTo; + this.Description = description; } /// /// Gets the FileName of the File. /// public string FileName { get; internal set; } + /// + /// Gets the description of the File. + /// + public string Description { get; internal set; } + /// /// Gets the stream of the File. /// public Stream Stream { get; internal set; } /// /// Gets or sets the file type. /// internal string FileType { get; set; } /// /// Gets or sets the content type. /// internal string ContentType { get; set; } /// /// Gets the position the File should be reset to. /// internal long? ResetPositionTo { get; set; } } } diff --git a/DisCatSharp/Entities/Optional.cs b/DisCatSharp/Entities/Optional.cs index 68346e1fc..0a2a1610c 100644 --- a/DisCatSharp/Entities/Optional.cs +++ b/DisCatSharp/Entities/Optional.cs @@ -1,291 +1,291 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Reflection; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace DisCatSharp.Entities { /// /// Helper methods for instantiating an . /// /// /// This class only serves to allow type parameter inference on calls to or /// . /// public static class Optional { /// /// Creates a new with specified value and valid state. /// /// Value to populate the optional with. /// Type of the value. /// Created optional. public static Optional FromValue(T value) => new(value); /// /// Creates a new empty with no value and invalid state. /// /// The type that the created instance is wrapping around. /// Created optional. public static Optional FromNoValue() => default; } // used internally to make serialization more convenient, do NOT change this, do NOT implement this yourself /// /// Represents a IOptional interface. /// internal interface IOptional { /// /// Gets a whether it has a value. /// bool HasValue { get; } /// /// Gets the raw value. /// object RawValue { get; } // must NOT throw InvalidOperationException } /// /// Represents a wrapper which may or may not have a value. /// /// Type of the value. [JsonConverter(typeof(OptionalJsonConverter))] public readonly struct Optional : IEquatable>, IEquatable, IOptional { /// /// Gets whether this has a value. /// public bool HasValue { get; } /// /// Gets the value of this . /// - /// If this has no value. + /// If this has no value. public T Value => this.HasValue ? this._val : throw new InvalidOperationException("Value is not set."); /// /// Gets the raw value. /// object IOptional.RawValue => this._val; private readonly T _val; /// /// Creates a new with specified value. /// /// Value of this option. public Optional(T value) { this._val = value; this.HasValue = true; } /// /// Returns a string representation of this optional value. /// /// String representation of this optional value. public override string ToString() => $"Optional<{typeof(T)}> ({(this.HasValue ? this.Value.ToString() : "")})"; /// /// Checks whether this (or its value) are equal to another object. /// /// Object to compare to. /// Whether the object is equal to this or its value. public override bool Equals(object obj) { return obj switch { T t => this.Equals(t), Optional opt => this.Equals(opt), _ => false, }; } /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(Optional e) => (!this.HasValue && !e.HasValue) || (this.HasValue == e.HasValue && this.Value.Equals(e.Value)); /// /// Checks whether the value of this is equal to specified object. /// /// Object to compare to. /// Whether the object is equal to the value of this . public bool Equals(T e) => this.HasValue && ReferenceEquals(this.Value, e); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.HasValue ? this.Value.GetHashCode() : 0; public static implicit operator Optional(T val) => new(val); public static explicit operator T(Optional opt) => opt.Value; public static bool operator ==(Optional opt1, Optional opt2) => opt1.Equals(opt2); public static bool operator !=(Optional opt1, Optional opt2) => !opt1.Equals(opt2); public static bool operator ==(Optional opt, T t) => opt.Equals(t); public static bool operator !=(Optional opt, T t) => !opt.Equals(t); /// /// Performs a mapping operation on the current , turning it into an Optional holding a /// instance if the source optional contains a value; otherwise, returns an /// of that same type with no value. /// /// The mapping function to apply on the current value if it exists /// The type of the target value returned by /// /// An containing a value denoted by calling if the current /// contains a value; otherwise, an empty of the target /// type. /// public Optional IfPresent(Func mapper) => this.HasValue ? new Optional(mapper(this.Value)) : default; } /// /// Represents an optional json contract resolver. /// /// internal sealed class OptionalJsonContractResolver : DefaultContractResolver { /// /// Creates the property. /// /// The member. /// The member serialization. protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); var type = property.PropertyType; if (!type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional))) return property; // we cache the PropertyInfo object here (it's captured in closure). we don't have direct // access to the property value so we have to reflect into it from the parent instance // we use UnderlyingName instead of PropertyName in case the C# name is different from the Json name. var declaringMember = property.DeclaringType.GetTypeInfo().DeclaredMembers .FirstOrDefault(e => e.Name == property.UnderlyingName); switch (declaringMember) { case PropertyInfo declaringProp: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringProp.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; case FieldInfo declaringField: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringField.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; default: throw new InvalidOperationException( "Can only serialize Optional members that are fields or properties"); } } } /// /// Represents an optional json converter. /// internal sealed class OptionalJsonConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // we don't check for HasValue here since it's checked in OptionalJsonContractResolver var val = (value as IOptional).RawValue; // JToken.FromObject will throw if `null` so we manually write a null value. if (val == null) { // you can read serializer.NullValueHandling here, but unfortunately you can **not** skip serialization // here, or else you will get a nasty JsonWriterException, so we just ignore its value and manually // write the null. writer.WriteToken(JsonToken.Null); } else { // convert the value to a JSON object and write it to the property value. JToken.FromObject(val).WriteTo(writer); } } /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var genericType = objectType.GenericTypeArguments[0]; var constructor = objectType.GetTypeInfo().DeclaredConstructors .FirstOrDefault(e => e.GetParameters()[0].ParameterType == genericType); return constructor.Invoke(new[] { serializer.Deserialize(reader, genericType) }); } /// /// Whether it can convert. /// /// The object type. public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional)); } } diff --git a/DisCatSharp/Entities/Sticker/DiscordSticker.cs b/DisCatSharp/Entities/Sticker/DiscordSticker.cs index f81297850..bf954988f 100644 --- a/DisCatSharp/Entities/Sticker/DiscordSticker.cs +++ b/DisCatSharp/Entities/Sticker/DiscordSticker.cs @@ -1,216 +1,216 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; using DisCatSharp.Exceptions; using DisCatSharp.Enums; using Newtonsoft.Json; using DisCatSharp.Net; namespace DisCatSharp.Entities { /// /// Represents a Discord Sticker. /// public class DiscordSticker : SnowflakeObject, IEquatable { /// /// Gets the Pack ID of this sticker. /// [JsonProperty("pack_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? PackId { get; internal set; } /// /// Gets the Name of the sticker. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the Description of the sticker. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } /// /// Gets the type of sticker. /// [JsonProperty("type")] public StickerType Type { get; internal set; } /// /// For guild stickers, gets the user that made the sticker. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser User { get; internal set; } /// /// Gets the guild associated with this sticker, if any. /// public DiscordGuild Guild => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); /// /// Gets the guild id the sticker belongs too. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets whether this sticker is available. Only applicable to guild stickers. /// [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool Available { get; internal set; } /// /// Gets the sticker's sort order, if it's in a pack. /// [JsonProperty("sort_value", NullValueHandling = NullValueHandling.Ignore)] public int? SortValue { get; internal set; } /// /// Gets the list of tags for the sticker. /// [JsonIgnore] public IEnumerable Tags => this._internalTags != null ? this._internalTags.Split(',') : Array.Empty(); /// /// Gets the asset hash of the sticker. /// [JsonProperty("asset", NullValueHandling = NullValueHandling.Ignore)] public string Asset { get; internal set; } /// /// Gets the preview asset hash of the sticker. /// [JsonProperty("preview_asset", NullValueHandling = NullValueHandling.Ignore)] public string PreviewAsset { get; internal set; } /// /// Gets the Format type of the sticker. /// [JsonProperty("format_type")] public StickerFormat FormatType { get; internal set; } /// /// Gets the tags of the sticker. /// [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] internal string _internalTags { get; set; } /// /// Gets the url of the sticker. /// [JsonIgnore] public string Url => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.STICKERS}/{this.Id}.{(this.FormatType == StickerFormat.LOTTIE ? "json" : "png")}"; /// /// Initializes a new instance of the class. /// internal DiscordSticker() { } /// /// Whether to stickers are equal. /// /// DiscordSticker /// public bool Equals(DiscordSticker other) => this.Id == other.Id; /// /// Gets the sticker in readable format. /// public override string ToString() => $"Sticker {this.Id}; {this.Name}; {this.FormatType}"; /// /// Modifies the sticker /// /// The name of the sticker /// The description of the sticker /// The name of a unicode emoji representing the sticker's expression /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task ModifyAsync(Optional name, Optional description, Optional tags, string reason = null) { return !this.GuildId.HasValue ? throw new ArgumentException("This sticker does not belong to a guild.") : name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30) ? throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long.") : description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100) ? throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long.") : tags.HasValue && !DiscordEmoji.TryFromUnicode(this.Discord, tags.Value, out var emoji) ? throw new ArgumentException("Sticker tags needs to be a unicode emoji.") : this.Discord.ApiClient.ModifyGuildStickerAsync(this.GuildId.Value, this.Id, name, description, tags, reason); } /// /// Deletes the sticker /// /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteAsync(string reason = null) => this.GuildId.HasValue ? this.Discord.ApiClient.DeleteGuildStickerAsync(this.GuildId.Value, this.Id, reason) : throw new ArgumentException("The requested sticker is no guild sticker."); } /// /// The sticker type /// public enum StickerType : long { /// /// Standard nitro sticker /// Standard = 1, /// /// Custom guild sticker /// Guild = 2 } /// /// The sticker type /// public enum StickerFormat : long { /// /// Sticker is a png /// PNG = 1, /// /// Sticker is a animated png /// APNG = 2, /// /// Sticker is lottie /// LOTTIE = 3 } } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs index bcb7a6a4d..5f83d6bbd 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs @@ -1,661 +1,661 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Tasks; using DisCatSharp.Exceptions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord thread channel. /// public class DiscordThreadChannel : DiscordChannel, IEquatable { /// /// Gets ID of the owner that started this thread. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the name of this thread. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public new string Name { get; internal set; } /// /// Gets the type of this thread. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public new ChannelType Type { get; internal set; } /// /// Gets whether this thread is private. /// [JsonIgnore] public new bool IsPrivate => this.Type == ChannelType.PrivateThread; /// /// Gets the ID of the last message sent in this thread. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public new ulong? LastMessageId { get; internal set; } /// /// Gets the slowmode delay configured for this thread. /// All bots, as well as users with or permissions in the channel are exempt from slowmode. /// [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] public new int? PerUserRateLimit { get; internal set; } /// /// Gets an approximate count of messages in a thread, stops counting at 50. /// [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] public int? MessageCount { get; internal set; } /// /// Gets an approximate count of users in a thread, stops counting at 50. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int? MemberCount { get; internal set; } /// /// Represents the current member for this thread. This will have a value if the user has joined the thread. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMember CurrentMember { get; internal set; } /// /// Gets when the last pinned message was pinned in this thread. /// [JsonIgnore] public new DateTimeOffset? LastPinTimestamp => !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets when the last pinned message was pinned in this thread as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal new string LastPinTimestampRaw { get; set; } /// /// Gets the threads metadata. /// [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } /// /// Gets the thread members object. /// [JsonIgnore] public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this._threadMembers); [JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary _threadMembers; /// /// Initializes a new instance of the class. /// internal DiscordThreadChannel() { } #region Methods /// /// Deletes a thread. /// /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteThreadAsync(this.Id, reason); /// /// Modifies the current thread. /// /// Action to perform on this thread - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . public Task ModifyAsync(Action action) { var mdl = new ThreadEditModel(); action(mdl); var can_continue = !mdl.AutoArchiveDuration.HasValue || !mdl.AutoArchiveDuration.Value.HasValue || Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.AutoArchiveDuration.Value.Value); if (mdl.Invitable.HasValue) { can_continue = this.Guild.Features.CanCreatePrivateThreads; } return can_continue ? this.Discord.ApiClient.ModifyThreadAsync(this.Id, mdl.Name, mdl.Locked, mdl.Archived, mdl.AutoArchiveDuration, mdl.PerUserRateLimit, mdl.Invitable, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(mdl.AutoArchiveDuration.Value.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Archives a thread. /// /// Whether the thread should be locked. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ArchiveAsync(bool locked = true, string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, locked, true, null, null, null, reason: reason); /// /// Unarchives a thread. /// /// Reason for audit logs. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnarchiveAsync(string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, null, false, null, null, null, reason: reason); /// /// Gets the members of a thread. Needs the intent. /// - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task> GetMembersAsync() => await this.Discord.ApiClient.GetThreadMembersAsync(this.Id); /// /// Adds a member to this thread. /// /// The member id to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(ulong member_id) => this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member_id); /// /// Adds a member to this thread. /// /// The member to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordMember member) => this.AddMemberAsync(member.Id); /// /// Gets a member in this thread. /// /// The member to be added. - /// Thrown when the member is not part of the thread. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member is not part of the thread. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(ulong member_id) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member_id); /// /// Gets a member in this thread. /// /// The member to be added. - /// Thrown when the member is not part of the thread. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member is not part of the thread. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(DiscordMember member) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); /// /// Removes a member from this thread. /// /// The member id to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(ulong member_id) => this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member_id); /// /// Removes a member from this thread. Only applicable to private threads. /// /// The member to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(DiscordMember member) => this.RemoveMemberAsync(member.Id); /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role id to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task AddRoleAsync(ulong role_id) { var role = this.Guild.GetRole(role_id); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach(var member in roleMembers) { await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); } } /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddRoleAsync(DiscordRole role) => this.AddRoleAsync(role.Id); /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role id to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task RemoveRoleAsync(ulong role_id) { var role = this.Guild.GetRole(role_id); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); } } /// /// Removes a role to from thread. Only applicable to private threads. /// /// The role to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveRoleAsync(DiscordRole role) => this.RemoveRoleAsync(role.Id); /// /// Joins a thread. /// - /// Thrown when the client has no access to this thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client has no access to this thread. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task JoinAsync() => this.Discord.ApiClient.JoinThreadAsync(this.Id); /// /// Leaves a thread. /// - /// Thrown when the client has no access to this thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client has no access to this thread. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveThreadAsync(this.Id); /// /// Sends a message to this thread. /// /// Content of the message to send. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(string content) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this thread. /// /// Embed to attach to the message. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, new[] {embed}, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this thread. /// /// Content of the message to send. /// Embed to attach to the message. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(string content, DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, new[] {embed}, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this thread. /// /// The builder with all the items to thread. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Returns a specific message /// /// The id of the message - /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new async Task GetMessageAsync(ulong id) { return this.Discord.Configuration.MessageCacheSize > 0 && this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg) ? msg : await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false); } /// /// Returns a list of messages before a certain message. /// The amount of messages to fetch. /// Message to fetch before from. /// - /// Thrown when the client does not have the or the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task> GetMessagesBeforeAsync(ulong before, int limit = 100) => this.GetMessagesInternalAsync(limit, before, null, null); /// /// Returns a list of messages after a certain message. /// The amount of messages to fetch. /// Message to fetch after from. /// - /// Thrown when the client does not have the or the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAfterAsync(ulong after, int limit = 100) => this.GetMessagesInternalAsync(limit, null, after, null); /// /// Returns a list of messages around a certain message. /// The amount of messages to fetch. /// Message to fetch around from. /// - /// Thrown when the client does not have the or the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the thread. /// The amount of messages to fetch. /// - /// Thrown when the client does not have the or the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the or the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAsync(int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, null); /// /// Returns a list of messages /// /// How many messages should be returned. /// Get messages before snowflake. /// Get messages after snowflake. /// Get messages around snowflake. private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { if (this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread) throw new ArgumentException("Cannot get the messages of a non-thread channel."); if (limit < 0) throw new ArgumentException("Cannot get a negative number of messages."); if (limit == 0) return Array.Empty(); //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); if (limit > 100 && around != null) throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); var msgs = new List(limit); var remaining = limit; ulong? last = null; var isAfter = after != null; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; if (!isAfter) { msgs.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } else { msgs.InsertRange(0, fetch); last = fetch.FirstOrDefault()?.Id; } } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(msgs); } /// - /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. + /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. /// /// A collection of messages to delete. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new async Task DeleteMessagesAsync(IEnumerable messages, string reason = null) { // don't enumerate more than once var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray(); if (messages == null || !msgs.Any()) throw new ArgumentException("You need to specify at least one message to delete."); if (msgs.Count() < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Count(); i += 100) await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false); } /// /// Deletes a message /// /// The message to be deleted. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Post a typing indicator /// - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task TriggerTypingAsync() { return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread ? throw new ArgumentException("Cannot start typing in a non-text channel.") : this.Discord.ApiClient.TriggerTypingAsync(this.Id); } /// /// Returns all pinned messages /// - /// Thrown when the client does not have the permission or the client is missing . - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission or the client is missing . + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public new Task> GetPinnedMessagesAsync() { return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread&& this.Type != ChannelType.News ? throw new ArgumentException("A non-thread channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); } /// /// Returns a string representation of this thread. /// /// String representation of this thread. public override string ToString() { var threadchannel = (object)this.Type switch { ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", _ => $"Thread {this.Name} ({this.Id})", }; return threadchannel; } #endregion /// /// 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 DiscordThreadChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordThreadChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordThreadChannel e1, DiscordThreadChannel 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); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordThreadChannel e1, DiscordThreadChannel e2) => !(e1 == e2); } } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs index ba6574707..c4f2f239e 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs @@ -1,137 +1,137 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord thread member object. /// public class DiscordThreadChannelMember : SnowflakeObject, IEquatable { /// /// Gets the id of the user. /// [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] public ulong UserId { get; internal set; } /// /// Gets the member object of the user. /// - [JsonIgnore] + [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordMember Member { get; internal set; } /// /// Gets the presence of the user. /// [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] public DiscordPresence Presence { get; internal set; } /// /// Gets the timestamp when the user joined the thread. /// [JsonIgnore] public DateTimeOffset? JoinTimeStamp => !string.IsNullOrWhiteSpace(this.JoinTimeStampRaw) && DateTimeOffset.TryParse(this.JoinTimeStampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the timestamp when the user joined the thread as raw string. /// [JsonProperty("join_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string JoinTimeStampRaw { get; set; } /// /// Gets the thread member flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public ThreadMemberFlags Flags { get; internal set; } /// /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. /// [JsonIgnore] public DiscordChannel Thread => this.Guild != null ? (this.Guild._threads.TryGetValue(this.Id, out var thread) ? thread : null) : null; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds.TryGetValue(this._guild_id, out var guild) ? guild : null; [JsonIgnore] internal ulong _guild_id; /// /// 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 DiscordThreadChannelMember); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordThreadChannelMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.UserId == e.UserId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.Id.GetHashCode(), this.UserId.GetHashCode()); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordThreadChannelMember e1, DiscordThreadChannelMember 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.UserId == e2.UserId)); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) => !(e1 == e2); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannelMember() { } } } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannelMetadata.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannelMetadata.cs index 8f24c5a96..f596acd28 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannelMetadata.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannelMetadata.cs @@ -1,83 +1,82 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord thread metadata object. /// public class DiscordThreadChannelMetadata { /// /// Gets whether the thread is archived or not. /// [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] public bool Archived { get; internal set; } /// /// Gets ID of the archiver. /// [JsonProperty("archiver_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? Archiver { get; internal set; } /// /// Gets the time when it will be archived, while there is no action inside the thread (In minutes). /// [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] public ThreadAutoArchiveDuration AutoArchiveDuration { get; internal set; } /// /// Gets the timestamp when it was archived. /// public DateTimeOffset? ArchiveTimestamp => !string.IsNullOrWhiteSpace(this.ArchiveTimestampRaw) && DateTimeOffset.TryParse(this.ArchiveTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the timestamp when it was archived as raw string. /// [JsonProperty("archive_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string ArchiveTimestampRaw { get; set; } /// /// Gets whether the thread is locked. /// [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] public bool? Locked { get; internal set; } /// /// Gets whether non-moderators can add other non-moderators to a thread; only available on private threads. /// [JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)] public bool? Invitable { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordThreadChannelMetadata() { } - } } diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 45a4465dd..12377eb85 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,433 +1,433 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord user. /// public class DiscordUser : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordUser() { } /// /// Initializes a new instance of the class. /// /// The transport. internal DiscordUser(TransportUser transport) { this.Id = transport.Id; this.Username = transport.Username; this.Discriminator = transport.Discriminator; this.AvatarHash = transport.AvatarHash; this.BannerHash = transport.BannerHash; this._bannerColor = transport.BannerColor; this.IsBot = transport.IsBot; this.MfaEnabled = transport.MfaEnabled; this.Verified = transport.Verified; this.Email = transport.Email; this.PremiumType = transport.PremiumType; this.Locale = transport.Locale; this.Flags = transport.Flags; this.OAuthFlags = transport.OAuthFlags; this.Bio = transport.Bio; } /// /// Gets this user's username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public virtual string Username { get; internal set; } /// /// Gets this user's username with the discriminator. /// Example: Discord#0000 /// [JsonIgnore] public virtual string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the user's 4-digit discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] public virtual string Discriminator { get; internal set; } /// /// Gets the discriminator integer. /// [JsonIgnore] internal int DiscriminatorInt => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); /// /// Gets the user's banner color, if set. Mutually exclusive with . /// public virtual DiscordColor? BannerColor => !this._bannerColor.HasValue ? null : new DiscordColor(this._bannerColor.Value); [JsonProperty("accent_color")] internal int? _bannerColor; /// /// Gets the user's banner url /// [JsonIgnore] public string BannerUrl => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096"; /// /// Gets the user's profile banner hash. Mutually exclusive with . /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string BannerHash { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public virtual string Bio { get; internal set; } /// /// Gets the user's avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarHash { get; internal set; } /// /// Returns a uri to this users profile. /// public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}"); /// /// Returns a string representing the direct URL to this users profile. /// /// The URL of this users profile. public string ProfileUrl => this.ProfileUri.AbsoluteUri; /// /// Gets the user's avatar URL.s /// [JsonIgnore] public string AvatarUrl => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the URL of default avatar for this user. /// [JsonIgnore] public string DefaultAvatarUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; /// /// Gets whether the user is a bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public virtual bool IsBot { get; internal set; } /// /// Gets whether the user has multi-factor authentication enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? MfaEnabled { get; internal set; } /// /// Gets whether the user is an official Discord system user. /// [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSystem { get; internal set; } /// /// Gets whether the user is verified. /// This is only present in OAuth. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? Verified { get; internal set; } /// /// Gets the user's email address. /// This is only present in OAuth. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public virtual string Email { get; internal set; } /// /// Gets the user's premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public virtual PremiumType? PremiumType { get; internal set; } /// /// Gets the user's chosen language /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public virtual string Locale { get; internal set; } /// /// Gets the user's flags for OAuth. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? OAuthFlags { get; internal set; } /// /// Gets the user's flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? Flags { get; internal set; } /// /// Gets the user's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this, this is DiscordMember); /// /// Gets whether this user is the Client which created this object. /// [JsonIgnore] public bool IsCurrent => this.Id == this.Discord.CurrentUser.Id; #region Extension of DiscordUser /// - /// Whether this member is a + /// Whether this member is a /// /// [JsonIgnore] public bool IsMod - => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordCertifiedModerator); + => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator); /// - /// Whether this member is a + /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner - => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordPartner); + => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// - /// Whether this member is a + /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev - => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBotDeveloper); + => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper); /// - /// Whether this member is a + /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff - => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordEmployee); + => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff); #endregion /// /// Whether this user is in a /// /// /// /// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806); /// DiscordUser user = await Client.GetUserAsync(469957180968271873); /// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}"); /// /// results to J_M_Lutra is a member of Project Nyaw~. /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsInGuild(DiscordGuild guild) { try { var member = await guild.GetMemberAsync(this.Id); return member is not null; } catch (NotFoundException) { return false; } } /// /// Whether this user is not in a /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild); /// /// Unbans this user from a guild. /// /// Guild to unban this user from. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string reason = null) => guild.UnbanMemberAsync(this, reason); /// /// Gets this user's presence. /// [JsonIgnore] public DiscordPresence Presence => this.Discord is DiscordClient dc ? dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null : null; /// /// Gets the user's avatar URL, in requested format and size. /// /// Format of the avatar to get. /// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048. /// URL of the user's avatar. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt == ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size < 16 || size > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 != 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = ""; sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Png => "png", ImageFormat.WebP => "webp", ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? (this.AvatarHash.StartsWith("a_") ? "gif" : "png") : "png", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(this.AvatarHash)) { var id = this.Id.ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}"; } else { var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}"; } } /// /// Returns a string representation of this user. /// /// String representation of this user. public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; /// /// 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 DiscordUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordUser e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are equal. public static bool operator ==(DiscordUser e1, DiscordUser 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); } /// /// Gets whether the two objects are not equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are not equal. public static bool operator !=(DiscordUser e1, DiscordUser e2) => !(e1 == e2); } /// /// Represents a user comparer. /// internal class DiscordUserComparer : IEqualityComparer { /// /// Whether the users are equal. /// /// The first user /// The second user. public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); /// /// Gets the hash code. /// /// The user. public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); } } diff --git a/DisCatSharp/Entities/Voice/DiscordVoiceState.cs b/DisCatSharp/Entities/Voice/DiscordVoiceState.cs index e47d4e271..a48f56b6d 100644 --- a/DisCatSharp/Entities/Voice/DiscordVoiceState.cs +++ b/DisCatSharp/Entities/Voice/DiscordVoiceState.cs @@ -1,213 +1,213 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord voice state. /// public class DiscordVoiceState { /// /// Gets the discord client. /// internal DiscordClient Discord { get; set; } /// /// Gets ID of the guild this voice state is associated with. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? GuildId { get; set; } /// /// Gets the guild associated with this voice state. /// [JsonIgnore] public DiscordGuild Guild => this.GuildId != null ? this.Discord.Guilds[this.GuildId.Value] : this.Channel?.Guild; /// /// Gets ID of the channel this user is connected to. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? ChannelId { get; set; } /// /// Gets the channel this user is connected to. /// [JsonIgnore] public DiscordChannel Channel => this.ChannelId != null && this.ChannelId.Value != 0 ? this.Discord.InternalGetCachedChannel(this.ChannelId.Value) : null; /// /// Gets ID of the user to which this voice state belongs. /// [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong UserId { get; set; } /// /// Gets the user associated with this voice state. - /// This can be cast to a if this voice state was in a guild. + /// This can be cast to a if this voice state was in a guild. /// [JsonIgnore] public DiscordUser User { get { var usr = null as DiscordUser; if (this.Guild != null) usr = this.Guild._members.TryGetValue(this.UserId, out var member) ? member : null; if (usr == null) usr = this.Discord.GetCachedOrEmptyUserInternal(this.UserId); return usr; } } /// /// Gets ID of the session of this voice state. /// [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] internal string SessionId { get; set; } /// /// Gets whether this user is deafened. /// [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] public bool IsServerDeafened { get; internal set; } /// /// Gets whether this user is muted. /// [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] public bool IsServerMuted { get; internal set; } /// /// Gets whether this user is locally deafened. /// [JsonProperty("self_deaf", NullValueHandling = NullValueHandling.Ignore)] public bool IsSelfDeafened { get; internal set; } /// /// Gets whether this user is locally muted. /// [JsonProperty("self_mute", NullValueHandling = NullValueHandling.Ignore)] public bool IsSelfMuted { get; internal set; } /// /// Gets whether this user's camera is enabled. /// [JsonProperty("self_video", NullValueHandling = NullValueHandling.Ignore)] public bool IsSelfVideo { get; internal set; } /// /// Gets whether this user is using the Go Live feature. /// [JsonProperty("self_stream", NullValueHandling = NullValueHandling.Ignore)] public bool IsSelfStream { get; internal set; } /// /// Gets whether the current user has suppressed this user. /// [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] public bool IsSuppressed { get; internal set; } /// /// Gets the time at which this user requested to speak. /// [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal DateTimeOffset? RequestToSpeakTimestamp { get; set; } /// /// Gets the member this voice state belongs to. /// [JsonIgnore] public DiscordMember Member => this.Guild.Members.TryGetValue(this.TransportMember.User.Id, out var member) ? member : new DiscordMember(this.TransportMember) { Discord = this.Discord }; /// /// Gets the transport member. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] internal TransportMember TransportMember { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordVoiceState() { } // copy constructor for reduced boilerplate /// /// Initializes a new instance of the class. /// /// The other. internal DiscordVoiceState(DiscordVoiceState other) { this.Discord = other.Discord; this.UserId = other.UserId; this.ChannelId = other.ChannelId; this.GuildId = other.GuildId; this.IsServerDeafened = other.IsServerDeafened; this.IsServerMuted = other.IsServerMuted; this.IsSuppressed = other.IsSuppressed; this.IsSelfDeafened = other.IsSelfDeafened; this.IsSelfMuted = other.IsSelfMuted; this.IsSelfStream = other.IsSelfStream; this.IsSelfVideo = other.IsSelfVideo; this.SessionId = other.SessionId; this.RequestToSpeakTimestamp = other.RequestToSpeakTimestamp; } /// /// Initializes a new instance of the class. /// /// The m. internal DiscordVoiceState(DiscordMember m) { this.Discord = m.Discord as DiscordClient; this.UserId = m.Id; this.ChannelId = 0; this.GuildId = m._guild_id; this.IsServerDeafened = m.IsDeafened; this.IsServerMuted = m.IsMuted; // Values not filled out are values that are not known from a DiscordMember } /// /// Gets a readable voice state string. /// public override string ToString() => $"{this.UserId.ToString(CultureInfo.InvariantCulture)} in {(this.GuildId ?? this.Channel.GuildId.Value).ToString(CultureInfo.InvariantCulture)}"; } } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs index 1b0384b24..7e2412eea 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs @@ -1,413 +1,426 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities { /// /// Constructs ready-to-send webhook requests. /// public sealed class DiscordWebhookBuilder { /// /// Username to use for this webhook request. /// public Optional Username { get; set; } /// /// Avatar url to use for this webhook request. /// public Optional AvatarUrl { get; set; } /// /// Whether this webhook request is text-to-speech. /// public bool IsTTS { get; set; } /// /// Message to send on this webhook request. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Embeds to send on this webhook request. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this webhook request. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Mentions to send on this webhook request. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Gets the components. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Attachments to keep on this webhook request. /// - public IEnumerable Attachments { get; } + public IEnumerable Attachments => this._attachments; private readonly List _attachments = new(); /// /// Constructs a new empty webhook request builder. /// public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordWebhookBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the builder /// /// The rows of components to add, holding up to five each. /// public DiscordWebhookBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordWebhookBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this._components.Add(comp); return this; } /// /// Sets the username for this webhook builder. /// /// Username of the webhook public DiscordWebhookBuilder WithUsername(string username) { this.Username = username; return this; } /// /// Sets the avatar of this webhook builder from its url. /// /// Avatar url of the webhook public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) { this.AvatarUrl = avatarUrl; return this; } /// /// Indicates if the webhook must use text-to-speech. /// /// Text-to-speech public DiscordWebhookBuilder WithTTS(bool tts) { this.IsTTS = tts; return this; } /// /// Sets the message to send at the execution of the webhook. /// /// Message to send. public DiscordWebhookBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to send at the execution of the webhook. /// /// Embed to add. public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); return this; } /// /// Adds the given embeds to send at the execution of the webhook. /// /// Embeds to add. public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to send at the execution of the webhook. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. - public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false) + /// Description of the file. + public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(filename, data, data.Position)); + this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else - this._files.Add(new DiscordMessageFile(filename, data, null)); + this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// Description of the file. /// - public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false) + public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new DiscordMessageFile(stream.Name, stream, null)); + this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to send at the execution of the webhook. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. public DiscordWebhookBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count() + files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } + /// + /// Keeps the given attachments on edit. + /// + /// Attachments to keep (on edit). + /// + public DiscordWebhookBuilder KeepAttachments(IEnumerable attachments) + { + this._attachments.AddRange(attachments); + return this; + } + /// /// Adds the mention to the mentions to parse, etc. at the execution of the webhook. /// /// Mention to add. public DiscordWebhookBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. at the execution of the webhook. /// /// Mentions to add. public DiscordWebhookBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Executes a webhook. /// /// The webhook that should be executed. /// The message sent public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this).ConfigureAwait(false); /// /// Executes a webhook. /// /// The webhook that should be executed. /// Target thread id. /// The message sent public async Task SendAsync(DiscordWebhook webhook, ulong threadId) => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// Target thread. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// Target thread id. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Webhook Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTTS = false; this._mentions.Clear(); this._files.Clear(); this._attachments.Clear(); this._components.Clear(); } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. /// Tells the method to perform the follow up message validation. /// Tells the method to perform the interaction response validation. internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) { if (isModify) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a message."); } else if (isFollowup) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a follow up message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a follow up message."); } else if (isInteractionResponse) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of an interaction response."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of an interaction response."); } else { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) throw new ArgumentException("You must specify content, an embed, or at least one file."); } } } } diff --git a/DisCatSharp/Enums/Application/ApplicationCommandType.cs b/DisCatSharp/Enums/Application/ApplicationCommandType.cs index ef466bae0..df3713a9a 100644 --- a/DisCatSharp/Enums/Application/ApplicationCommandType.cs +++ b/DisCatSharp/Enums/Application/ApplicationCommandType.cs @@ -1,50 +1,54 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; - namespace DisCatSharp.Enums { /// - /// Represents the type of an . + /// Represents the type of an . /// public enum ApplicationCommandType { /// /// This command is registered as a slash-command, aka "Chat Input". /// ChatInput = 1, + /// /// This command is registered as a user context menu, and is applicable when interacting a user. /// User = 2, + /// /// This command is registered as a message context menu, and is applicable when interacting with a message. /// Message = 3, /// /// Inbound only: An auto-complete option is being interacted with. /// - AutoCompleteRequest = 4 + AutoCompleteRequest = 4, + + /// + /// Inbound only: A modal was submitted. + /// + ModalSubmit = 5 } } diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Enums/Guild/HubType.cs similarity index 81% copy from DisCatSharp/Enums/Interaction/ComponentType.cs copy to DisCatSharp/Enums/Guild/HubType.cs index e7faca56c..ff6d175af 100644 --- a/DisCatSharp/Enums/Interaction/ComponentType.cs +++ b/DisCatSharp/Enums/Guild/HubType.cs @@ -1,45 +1,45 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. -namespace DisCatSharp.Enums +namespace DisCatSharp { /// - /// Represents a type of component. + /// Represents a guilds hub type. /// - public enum ComponentType + public enum HubType : int { /// - /// A row of components. + /// Indicates that the hub is a default one. /// - ActionRow = 1, + Default = 0, /// - /// A button. + /// Indicates that the hub is a high school. /// - Button = 2, + HighSchool = 1, /// - /// A select menu. + /// Indicates that the hub is a college. /// - Select = 3 + College = 2 } } diff --git a/DisCatSharp/Enums/Guild/PremiumTier.cs b/DisCatSharp/Enums/Guild/PremiumTier.cs index c172a3283..51b930b7b 100644 --- a/DisCatSharp/Enums/Guild/PremiumTier.cs +++ b/DisCatSharp/Enums/Guild/PremiumTier.cs @@ -1,55 +1,55 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents a server's premium tier. /// public enum PremiumTier : int { /// /// Indicates that this server was not boosted. /// None = 0, /// /// Indicates that this server was boosted two times. /// - Tier_1 = 1, + TierOne = 1, /// /// Indicates that this server was boosted seven times. /// - Tier_2 = 2, + TierTwo = 2, /// /// Indicates that this server was boosted fourteen times. /// - Tier_3 = 3, + TierThree = 3, /// /// Indicates an unknown premium tier. /// Unknown = int.MaxValue } } diff --git a/DisCatSharp/Enums/Guild/PriceTierType.cs b/DisCatSharp/Enums/Guild/PriceTierType.cs index 174249d5a..a8985b674 100644 --- a/DisCatSharp/Enums/Guild/PriceTierType.cs +++ b/DisCatSharp/Enums/Guild/PriceTierType.cs @@ -1,35 +1,35 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents a price tier type. /// public enum PriceTierType : int { /// /// Indicates that this is a role subscriotion. /// - GUILD_ROLE_SUBSCRIPTIONS = 1 + GuildRoleSubscriptions = 1 } } diff --git a/DisCatSharp/Enums/Interaction/ButtonStyle.cs b/DisCatSharp/Enums/Interaction/ButtonStyle.cs index f3538deb8..88d518b19 100644 --- a/DisCatSharp/Enums/Interaction/ButtonStyle.cs +++ b/DisCatSharp/Enums/Interaction/ButtonStyle.cs @@ -1,47 +1,50 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp.Enums { /// /// Represents a button's style/color. /// public enum ButtonStyle : int { /// /// Blurple button. /// Primary = 1, + /// /// Grey button. /// Secondary = 2, + /// /// Green button. /// Success = 3, + /// /// Red button. /// Danger = 4, } } diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Enums/Interaction/ComponentType.cs index e7faca56c..8c86d38d6 100644 --- a/DisCatSharp/Enums/Interaction/ComponentType.cs +++ b/DisCatSharp/Enums/Interaction/ComponentType.cs @@ -1,45 +1,50 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp.Enums { /// /// Represents a type of component. /// public enum ComponentType { /// /// A row of components. /// ActionRow = 1, /// /// A button. /// Button = 2, /// /// A select menu. /// - Select = 3 + Select = 3, + + /// + /// A input text. + /// + InputText = 4 } } diff --git a/DisCatSharp/Enums/Interaction/InteractionResponseType.cs b/DisCatSharp/Enums/Interaction/InteractionResponseType.cs index 252da55aa..e73347c8e 100644 --- a/DisCatSharp/Enums/Interaction/InteractionResponseType.cs +++ b/DisCatSharp/Enums/Interaction/InteractionResponseType.cs @@ -1,60 +1,65 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents the type of interaction response /// public enum InteractionResponseType { /// /// Acknowledges a Ping. /// Pong = 1, /// /// Responds to the interaction with a message. /// ChannelMessageWithSource = 4, /// /// Acknowledges an interaction to edit to a response later. The user sees a "thinking" state. /// DeferredChannelMessageWithSource = 5, /// /// Acknowledges a component interaction to allow a response later. /// DeferredMessageUpdate = 6, /// /// Responds to a component interaction by editing the message it's attached to. /// UpdateMessage = 7, /// /// Responds to an auto-complete request. /// - AutoCompleteResult = 8 + AutoCompleteResult = 8, + + /// + /// Responds to the interaction with a modal. + /// + Modal = 9 } } diff --git a/DisCatSharp/Enums/Interaction/InteractionType.cs b/DisCatSharp/Enums/Interaction/InteractionType.cs index 565193995..76b56bc96 100644 --- a/DisCatSharp/Enums/Interaction/InteractionType.cs +++ b/DisCatSharp/Enums/Interaction/InteractionType.cs @@ -1,50 +1,55 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents the type of interaction used. /// public enum InteractionType { /// /// Sent when registering an HTTP interaction endpoint with Discord. Must be replied to with a Pong. /// Ping = 1, /// /// An application command. /// ApplicationCommand = 2, /// /// A component. /// Component = 3, /// /// An autocomplete field. /// - AutoComplete = 4 + AutoComplete = 4, + + /// + /// A modal component. + /// + ModalSubmit = 5 } } diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Enums/Interaction/TextComponentStyle.cs similarity index 83% copy from DisCatSharp/Enums/Interaction/ComponentType.cs copy to DisCatSharp/Enums/Interaction/TextComponentStyle.cs index e7faca56c..669579c8b 100644 --- a/DisCatSharp/Enums/Interaction/ComponentType.cs +++ b/DisCatSharp/Enums/Interaction/TextComponentStyle.cs @@ -1,45 +1,40 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp.Enums { /// - /// Represents a type of component. + /// Represents a button's style/color. /// - public enum ComponentType + public enum TextComponentStyle : int { /// - /// A row of components. + /// A small text input. /// - ActionRow = 1, + Small = 1, /// - /// A button. + /// A paragraph text input. /// - Button = 2, - - /// - /// A select menu. - /// - Select = 3 + Paragraph = 2 } } diff --git a/DisCatSharp/Enums/Permission.cs b/DisCatSharp/Enums/Permission.cs index 97c4a90d8..f53c301a1 100644 --- a/DisCatSharp/Enums/Permission.cs +++ b/DisCatSharp/Enums/Permission.cs @@ -1,363 +1,363 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp { /// /// Represents permission methods. /// public static class PermissionMethods { /// /// Gets the full permissions enum (long). /// - internal static Permissions FULL_PERMS { get; } = (Permissions)1099511627775L; // 2199023255551L + internal static Permissions FULL_PERMS { get; } = (Permissions)2199023255551L; /// /// Calculates whether this permission set contains the given permission. /// /// The permissions to calculate from /// permission you want to check /// public static bool HasPermission(this Permissions p, Permissions permission) => p.HasFlag(Permissions.Administrator) || (p & permission) == permission; /// /// Grants permissions. /// /// The permissions to add to. /// Permission to add. /// public static Permissions Grant(this Permissions p, Permissions grant) => p | grant; /// /// Revokes permissions. /// /// The permissions to take from. /// Permission to take. /// public static Permissions Revoke(this Permissions p, Permissions revoke) => p & ~revoke; } /// /// Whether a permission is allowed, denied or unset /// public enum PermissionLevel { /// /// Said permission is Allowed /// Allowed, /// /// Said permission is Denied /// Denied, /// /// Said permission is Unset /// Unset } /// /// Bitwise permission flags. /// [Flags] public enum Permissions : long { /// /// Indicates no permissions given. /// [PermissionString("No permissions")] None = 0x0000000000000000, /// /// Indicates all permissions are granted /// [PermissionString("All permissions")] - All = 1099511627775, // 2199023255551 + All = 2199023255551, /// /// Allows creation of instant channel invites. /// [PermissionString("Create instant invites")] CreateInstantInvite = 0x0000000000000001, /// /// Allows kicking members. /// [PermissionString("Kick members")] KickMembers = 0x0000000000000002, /// /// Allows banning and unbanning members. /// [PermissionString("Ban members")] BanMembers = 0x0000000000000004, /// /// Enables full access on a given guild. This also overrides other permissions. /// [PermissionString("Administrator")] Administrator = 0x0000000000000008, /// /// Allows managing channels. /// [PermissionString("Manage channels")] ManageChannels = 0x0000000000000010, /// /// Allows managing the guild. /// [PermissionString("Manage guild")] ManageGuild = 0x0000000000000020, /// /// Allows adding reactions to messages. /// [PermissionString("Add reactions")] AddReactions = 0x0000000000000040, /// /// Allows viewing audit log entries. /// [PermissionString("View audit log")] ViewAuditLog = 0x0000000000000080, /// /// Allows the use of priority speaker. /// [PermissionString("Use priority speaker")] PrioritySpeaker = 0x0000000000000100, /// /// Allows accessing text and voice channels. Disabling this permission hides channels. /// [PermissionString("Read messages")] AccessChannels = 0x0000000000000400, /// /// Allows sending messages (does not allow sending messages in threads). /// [PermissionString("Send messages")] SendMessages = 0x0000000000000800, /// /// Allows sending text-to-speech messages. /// [PermissionString("Send TTS messages")] SendTtsMessages = 0x0000000000001000, /// /// Allows managing messages of other users. /// [PermissionString("Manage messages")] ManageMessages = 0x0000000000002000, /// /// Allows embedding content in messages. /// [PermissionString("Use embeds")] EmbedLinks = 0x0000000000004000, /// /// Allows uploading files. /// [PermissionString("Attach files")] AttachFiles = 0x0000000000008000, /// /// Allows reading message history. /// [PermissionString("Read message history")] ReadMessageHistory = 0x0000000000010000, /// /// Allows using @everyone and @here mentions. /// [PermissionString("Mention everyone")] MentionEveryone = 0x0000000000020000, /// /// Allows using emojis from external servers, such as twitch or nitro emojis. /// [PermissionString("Use external emojis")] UseExternalEmojis = 0x0000000000040000, /// /// Allows connecting to voice chat. /// [PermissionString("Use voice chat")] UseVoice = 0x0000000000100000, /// /// Allows speaking in voice chat. /// [PermissionString("Speak")] Speak = 0x0000000000200000, /// /// Allows muting other members in voice chat. /// [PermissionString("Mute voice chat members")] MuteMembers = 0x0000000000400000, /// /// Allows deafening other members in voice chat. /// [PermissionString("Deafen voice chat members")] DeafenMembers = 0x0000000000800000, /// /// Allows moving voice chat members. /// [PermissionString("Move voice chat members")] MoveMembers = 0x0000000001000000, /// /// Allows using voice activation in voice chat. Revoking this will usage of push-to-talk. /// [PermissionString("Use voice activity detection")] UseVoiceDetection = 0x0000000002000000, /// /// Allows changing of own nickname. /// [PermissionString("Change own nickname")] ChangeNickname = 0x0000000004000000, /// /// Allows managing nicknames of other members. /// [PermissionString("Manage nicknames")] ManageNicknames = 0x0000000008000000, /// /// Allows managing roles in a guild. /// [PermissionString("Manage roles")] ManageRoles = 0x0000000010000000, /// /// Allows managing webhooks in a guild. /// [PermissionString("Manage webhooks")] ManageWebhooks = 0x0000000020000000, /// /// Allows managing guild emojis and stickers. /// [PermissionString("Manage emojis & stickers")] ManageEmojisAndStickers = 0x0000000040000000, /// /// Allows the user to go live. /// [PermissionString("Allow stream")] Stream = 0x0000000000000200, /// /// Allows the user to use slash commands. /// [PermissionString("Use application commands")] UseApplicationCommands = 0x0000000080000000, /// /// Allows for requesting to speak in stage channels. /// [PermissionString("Request to speak")] RequestToSpeak = 0x0000000100000000, /// /// Allows managing guild events. /// [PermissionString("Manage Events")] ManageEvents = 0x0000000200000000, /// /// Allows for deleting and archiving threads, and viewing all private threads. /// [PermissionString("Manage Threads")] ManageThreads = 0x0000000400000000, /// /// Allows for creating threads. /// [PermissionString("Create Public Threads")] CreatePublicThreads = 0x0000000800000000, /// /// Allows for creating private threads. /// [PermissionString("Create Private Threads")] CreatePrivateThreads = 0x0000001000000000, /// /// Allows the usage of custom stickers from other servers. /// [PermissionString("Use external Stickers")] UseExternalStickers = 0x0000002000000000, /// /// Allows for sending messages in threads. /// [PermissionString("Send messages in Threads")] SendMessagesInThreads = 0x0000004000000000, /// /// Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. /// [PermissionString("Start Embedded Activities")] - StartEmbeddedActivities = 0x0000008000000000//, + StartEmbeddedActivities = 0x0000008000000000, - /*/// - /// Allows to time-out a member. + /// + /// Allows to perform limited moderation actions (timeout). /// - [PermissionString("Time-out Members")] - TimeOutMembers = 0x0000010000000000*/ + [PermissionString("Moderate Members")] + ModerateMembers = 0x0000010000000000 } /// /// Defines a readable name for this permission. /// [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public sealed class PermissionStringAttribute : Attribute { /// /// Gets the readable name for this permission. /// public string String { get; } /// /// Defines a readable name for this permission. /// /// Readable name for this permission. public PermissionStringAttribute(string str) { this.String = str; } } } diff --git a/DisCatSharp/Enums/Event/EventEntityType.cs b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventEntityType.cs similarity index 84% rename from DisCatSharp/Enums/Event/EventEntityType.cs rename to DisCatSharp/Enums/ScheduledEvent/ScheduledEventEntityType.cs index f3fe2db2d..3940f32e4 100644 --- a/DisCatSharp/Enums/Event/EventEntityType.cs +++ b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventEntityType.cs @@ -1,50 +1,45 @@ -// This file is part of the DisCatSharp project. + // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents the entity type for a scheduled event. /// - public enum EventEntityType : int + public enum ScheduledEventEntityType : int { - /// - /// Indicates that the events is not hold anywhere. - /// - NONE = 0, - /// /// Indicates that the events is hold in a stage instance. /// - STAGE_INSTANCE = 1, + StageInstance = 1, /// /// Indicates that the events is hold in a voice channel. /// - VOICE = 2, + Voice = 2, /// /// Indicates that the events is hold external. /// - EXTERNAL = 3 + External = 3 } } diff --git a/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventPrivacyLevel.cs similarity index 79% copy from DisCatSharp/Enums/Stage/StagePrivacyLevel.cs copy to DisCatSharp/Enums/ScheduledEvent/ScheduledEventPrivacyLevel.cs index 4fd421515..5a749fb84 100644 --- a/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs +++ b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventPrivacyLevel.cs @@ -1,40 +1,40 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// - /// Represents the privacy level for a stage. + /// Represents the privacy level for a guild scheduled event. /// - public enum StagePrivacyLevel : int + public enum ScheduledEventPrivacyLevel : int { /// - /// Indicates that the stage is public visible, i.e. on stage discovery. + /// Indicates that the guild scheduled event is public and available in discovery. /// - PUBLIC = 1, + Public = 1, /// - /// Indicates that the stage is only visible to guild members. + /// Indicates that the the guild scheduled event is only accessable to guild members. /// - GUILD_ONLY = 2 + GuildOnly = 2 } } diff --git a/DisCatSharp/Enums/Event/EventStatus.cs b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventStatus.cs similarity index 89% rename from DisCatSharp/Enums/Event/EventStatus.cs rename to DisCatSharp/Enums/ScheduledEvent/ScheduledEventStatus.cs index 7fa364a82..025399ac9 100644 --- a/DisCatSharp/Enums/Event/EventStatus.cs +++ b/DisCatSharp/Enums/ScheduledEvent/ScheduledEventStatus.cs @@ -1,50 +1,50 @@ -// This file is part of the DisCatSharp project. + // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents the status for a scheduled event. /// - public enum EventStatus : int + public enum ScheduledEventStatus : int { /// /// Indicates that the event is scheduled. /// - SCHEDULED = 1, + Scheduled = 1, /// /// Indicates that the event is active. /// - ACTIVE = 2, + Active = 2, /// /// Indicates that the event is completed. /// - COMPLETED = 3, + Completed = 3, /// /// Indicates that the event is canceled. /// - CANCELED = 4 + Canceled = 4 } } diff --git a/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs b/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs index 4fd421515..488b1984b 100644 --- a/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs +++ b/DisCatSharp/Enums/Stage/StagePrivacyLevel.cs @@ -1,40 +1,40 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp { /// /// Represents the privacy level for a stage. /// public enum StagePrivacyLevel : int { /// /// Indicates that the stage is public visible, i.e. on stage discovery. /// - PUBLIC = 1, + Public = 1, /// /// Indicates that the stage is only visible to guild members. /// - GUILD_ONLY = 2 + GuildOnly = 2 } } diff --git a/DisCatSharp/Enums/User/UserFlags.cs b/DisCatSharp/Enums/User/UserFlags.cs index 2a2e8b9c3..1fc1a3fd5 100644 --- a/DisCatSharp/Enums/User/UserFlags.cs +++ b/DisCatSharp/Enums/User/UserFlags.cs @@ -1,143 +1,143 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; namespace DisCatSharp { /// /// Represents additional details of a users account. /// [Flags] public enum UserFlags { /// /// The user has no flags. /// None = 0, /// /// The user is a Discord employee. /// - DiscordEmployee = 1 << 0, + Staff = 1 << 0, /// /// The user is a Discord partner. /// - DiscordPartner = 1 << 1, + Partner = 1 << 1, /// /// The user has the HypeSquad badge. /// - HypeSquadEvents = 1 << 2, + HypeSquad = 1 << 2, /// /// The user reached the first bug hunter tier. /// BugHunterLevelOne = 1 << 3, /// /// The user has SMS recovery for 2FA enabled. /// MfaSms = 1 << 4, /// /// The user is marked as dismissed Nitro promotion /// PremiumPromoDismissed = 1 << 5, /// /// The user is a member of house bravery. /// HouseBravery = 1 << 6, /// /// The user is a member of house brilliance. /// HouseBrilliance = 1 << 7, /// /// The user is a member of house balance. /// HouseBalance = 1 << 8, /// /// The user has the early supporter badge. /// - EarlySupporter = 1 << 9, + PremiumEarlySupporter = 1 << 9, /// - /// Whether the user is apart of a Discord developer team. + /// User is a . /// - TeamUser = 1 << 10, + TeamPseudoUser = 1 << 10, /// /// Relates to partner/verification applications. /// PartnerOrVerificationApplication = 1 << 11, /// /// Whether the user is an official system user. /// System = 1 << 12, /// /// Whether the user has unread system messages. /// HasUnreadUrgentMessages = 1 << 13, /// /// The user reached the second bug hunter tier. /// BugHunterLevelTwo = 1 << 14, /// /// The user has a pending deletion for being underage in DOB prompt. /// UnderageDeleted = 1 << 15, /// /// The user is a verified bot. /// VerifiedBot = 1 << 16, /// /// The user is a verified bot developer. /// - VerifiedBotDeveloper = 1 << 17, + VerifiedDeveloper = 1 << 17, /// /// The user is a discord certified moderator. /// - DiscordCertifiedModerator = 1 << 18, + CertifiedModerator = 1 << 18, /// /// The user is a bot and has set an interactions endpoint url. /// BotHttpInteractions = 1 << 19, /// /// The user is disabled for being a spammer. /// Spammer = 1 << 20 } } diff --git a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventCreateEventArgs.cs b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventCreateEventArgs.cs similarity index 90% rename from DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventCreateEventArgs.cs rename to DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventCreateEventArgs.cs index 8c55d82f6..e11b9e7cd 100644 --- a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventCreateEventArgs.cs @@ -1,48 +1,48 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for event. /// public class GuildScheduledEventCreateEventArgs : DiscordEventArgs { /// - /// Gets the stage instance that was created. + /// Gets the scheduled event that was created. /// - public DiscordEvent ScheduledEvent { get; internal set; } + public DiscordScheduledEvent ScheduledEvent { get; internal set; } /// - /// Gets the guild in which the stage instance was created. + /// Gets the guild in which the scheduled event was created. /// public DiscordGuild Guild { get; internal set; } /// /// Initializes a new instance of the class. /// internal GuildScheduledEventCreateEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventDeleteEventArgs.cs similarity index 81% copy from DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs copy to DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventDeleteEventArgs.cs index 4057c1812..302540ca0 100644 --- a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs +++ b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventDeleteEventArgs.cs @@ -1,48 +1,54 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for event. /// public class GuildScheduledEventDeleteEventArgs : DiscordEventArgs { /// - /// Gets the stage instance that was created. + /// Gets the scheduled event that was deleted. /// - public DiscordEvent ScheduledEvent { get; internal set; } + public DiscordScheduledEvent ScheduledEvent { get; internal set; } /// - /// Gets the guild in which the stage instance was created. + /// Gets the reason of deletion for the scheduled event. + /// Important to determine why and how it was deleted. + /// + public ScheduledEventStatus Reason { get; internal set; } + + /// + /// Gets the guild in which the scheduled event was deleted. /// public DiscordGuild Guild { get; internal set; } /// /// Initializes a new instance of the class. /// internal GuildScheduledEventDeleteEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventUpdateEventArgs.cs b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUpdateEventArgs.cs similarity index 82% rename from DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventUpdateEventArgs.cs rename to DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUpdateEventArgs.cs index 8ce365512..a70353661 100644 --- a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventUpdateEventArgs.cs +++ b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUpdateEventArgs.cs @@ -1,48 +1,53 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for event. /// public class GuildScheduledEventUpdateEventArgs : DiscordEventArgs { /// - /// Gets the stage instance that was created. + /// Gets the scheduled event that was updated. /// - public DiscordEvent ScheduledEvent { get; internal set; } + public DiscordScheduledEvent ScheduledEventAfter { get; internal set; } /// - /// Gets the guild in which the stage instance was created. + /// Gets the old scheduled event that was updated. + /// + public DiscordScheduledEvent ScheduledEventBefore { get; internal set; } + + /// + /// Gets the guild in which the scheduled event was updated. /// public DiscordGuild Guild { get; internal set; } /// /// Initializes a new instance of the class. /// internal GuildScheduledEventUpdateEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserAddEventArgs.cs similarity index 68% copy from DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs copy to DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserAddEventArgs.cs index 4057c1812..5c37ff573 100644 --- a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs +++ b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserAddEventArgs.cs @@ -1,48 +1,58 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// - /// Represents arguments for event. + /// Represents arguments for event. /// - public class GuildScheduledEventDeleteEventArgs : DiscordEventArgs + public class GuildScheduledEventUserAddEventArgs : DiscordEventArgs { /// - /// Gets the stage instance that was created. + /// Gets the scheduled event. /// - public DiscordEvent ScheduledEvent { get; internal set; } + public DiscordScheduledEvent ScheduledEvent { get; internal set; } /// - /// Gets the guild in which the stage instance was created. + /// Gets the guild. /// public DiscordGuild Guild { get; internal set; } /// - /// Initializes a new instance of the class. + /// Gets the user which has subscribed to this scheduled event. /// - internal GuildScheduledEventDeleteEventArgs(IServiceProvider provider) : base(provider) { } + public DiscordUser User { get; internal set; } + + /// + /// Gets the member which has subscribed to this scheduled event. + /// + public DiscordMember Member { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal GuildScheduledEventUserAddEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserRemoveEventArgs.cs similarity index 68% rename from DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs rename to DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserRemoveEventArgs.cs index 4057c1812..9217fa4fb 100644 --- a/DisCatSharp/EventArgs/Guild/Event/GuildScheduledEventDeleteEventArgs.cs +++ b/DisCatSharp/EventArgs/Guild/ScheduledEvent/GuildScheduledEventUserRemoveEventArgs.cs @@ -1,48 +1,58 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// - /// Represents arguments for event. + /// Represents arguments for event. /// - public class GuildScheduledEventDeleteEventArgs : DiscordEventArgs + public class GuildScheduledEventUserRemoveEventArgs : DiscordEventArgs { /// - /// Gets the stage instance that was created. + /// Gets the scheduled event. /// - public DiscordEvent ScheduledEvent { get; internal set; } + public DiscordScheduledEvent ScheduledEvent { get; internal set; } /// - /// Gets the guild in which the stage instance was created. + /// Gets the guild. /// public DiscordGuild Guild { get; internal set; } /// - /// Initializes a new instance of the class. + /// Gets the user which has unsubscribed from this scheduled event. /// - internal GuildScheduledEventDeleteEventArgs(IServiceProvider provider) : base(provider) { } + public DiscordUser User { get; internal set; } + + /// + /// Gets the member which has unsubscribed from this scheduled event. + /// + public DiscordMember Member { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal GuildScheduledEventUserRemoveEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs index c50242a4b..d3b68258b 100644 --- a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs @@ -1,61 +1,61 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.EventArgs { /// /// The context menu interaction create event args. /// public sealed class ContextMenuInteractionCreateEventArgs : InteractionCreateEventArgs { /// - /// The type of context menu that was used. This is never . + /// The type of context menu that was used. This is never . /// public ApplicationCommandType Type { get; internal set; } /// /// The user that invoked this interaction. Can be casted to a member if this was on a guild. /// public DiscordUser User => this.Interaction.User; /// /// The user this interaction targets, if applicable. /// public DiscordUser TargetUser { get; internal set; } /// /// The message this interaction targets, if applicable. /// public DiscordMessage TargetMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The provider. public ContextMenuInteractionCreateEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs b/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs index ae857f927..5cff1905e 100644 --- a/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs +++ b/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs @@ -1,69 +1,69 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for event. /// public class MessageReactionAddEventArgs : DiscordEventArgs { /// /// Gets the message for which the update occurred. /// public DiscordMessage Message { get; internal set; } /// /// Gets the channel to which this message belongs. /// /// /// This will be null for an uncached channel, which will usually happen for when this event triggers on /// DM channels in which no prior messages were received or sent. /// public DiscordChannel Channel => this.Message.Channel; /// /// Gets the user who created the reaction. - /// This can be cast to a if the reaction was in a guild. + /// This can be cast to a if the reaction was in a guild. /// public DiscordUser User { get; internal set; } /// /// Gets the guild in which the reaction was added. /// public DiscordGuild Guild { get; internal set; } /// /// Gets the emoji used for this reaction. /// public DiscordEmoji Emoji { get; internal set; } /// /// Initializes a new instance of the class. /// internal MessageReactionAddEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/TypingStartEventArgs.cs b/DisCatSharp/EventArgs/TypingStartEventArgs.cs index 6bcf49ff6..60effab20 100644 --- a/DisCatSharp/EventArgs/TypingStartEventArgs.cs +++ b/DisCatSharp/EventArgs/TypingStartEventArgs.cs @@ -1,59 +1,59 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for event. /// public class TypingStartEventArgs : DiscordEventArgs { /// /// Gets the channel in which the indicator was triggered. /// public DiscordChannel Channel { get; internal set; } /// /// Gets the user that started typing. - /// This can be cast to a if the typing occurred in a guild. + /// This can be cast to a if the typing occurred in a guild. /// public DiscordUser User { get; internal set; } /// /// Gets the guild in which the indicator was triggered. /// public DiscordGuild Guild { get; internal set; } /// /// Gets the date and time at which the user started typing. /// public DateTimeOffset StartedAt { get; internal set; } /// /// Initializes a new instance of the class. /// internal TypingStartEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/Exceptions/BadRequestException.cs b/DisCatSharp/Exceptions/BadRequestException.cs index 9ad2c6f66..32b2db4f7 100644 --- a/DisCatSharp/Exceptions/BadRequestException.cs +++ b/DisCatSharp/Exceptions/BadRequestException.cs @@ -1,85 +1,85 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions { /// /// Represents an exception thrown when a malformed request is sent. /// public class BadRequestException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the error code for this exception. /// public int Code { get; internal set; } /// /// Gets the JSON message received. /// public string JsonMessage { get; internal set; } /// /// Gets the form error responses in JSON format. /// public string Errors { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal BadRequestException(BaseRestRequest request, RestResponse response) : base("Bad request: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["code"] != null) this.Code = (int)j["code"]; if (j["message"] != null) this.JsonMessage = j["message"].ToString(); if (j["errors"] != null) this.Errors = j["errors"].ToString(); } catch { } } } } diff --git a/DisCatSharp/Exceptions/NotFoundException.cs b/DisCatSharp/Exceptions/NotFoundException.cs index 8ad767ff7..f9483090d 100644 --- a/DisCatSharp/Exceptions/NotFoundException.cs +++ b/DisCatSharp/Exceptions/NotFoundException.cs @@ -1,69 +1,69 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions { /// /// Represents an exception thrown when a requested resource is not found. /// public class NotFoundException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal NotFoundException(BaseRestRequest request, RestResponse response) : base("Not found: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } } diff --git a/DisCatSharp/Exceptions/ServerErrorException.cs b/DisCatSharp/Exceptions/ServerErrorException.cs index 18ab76225..c1b9826a1 100644 --- a/DisCatSharp/Exceptions/ServerErrorException.cs +++ b/DisCatSharp/Exceptions/ServerErrorException.cs @@ -1,69 +1,69 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions { /// /// Represents an exception thrown when Discord returns an Internal Server Error. /// public class ServerErrorException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal ServerErrorException(BaseRestRequest request, RestResponse response) : base("Internal Server Error: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } } diff --git a/DisCatSharp/Exceptions/UnauthorizedException.cs b/DisCatSharp/Exceptions/UnauthorizedException.cs index 8e81130a3..7264c9918 100644 --- a/DisCatSharp/Exceptions/UnauthorizedException.cs +++ b/DisCatSharp/Exceptions/UnauthorizedException.cs @@ -1,69 +1,69 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions { /// /// Represents an exception thrown when requester doesn't have necessary permissions to complete the request. /// public class UnauthorizedException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal UnauthorizedException(BaseRestRequest request, RestResponse response) : base("Unauthorized: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } } diff --git a/DisCatSharp/GlobalSuppressions.cs b/DisCatSharp/GlobalSuppressions.cs index 61cb29272..af95c6bd9 100644 --- a/DisCatSharp/GlobalSuppressions.cs +++ b/DisCatSharp/GlobalSuppressions.cs @@ -1,36 +1,39 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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", "IDE1006:Naming Styles", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordColor.HexAlphabet")] [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:DisCatSharp.Entities.DiscordColor.HexAlphabet")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.PermissionStrings")] [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~P:DisCatSharp.Internals.VersionHeader")] +[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")] diff --git a/DisCatSharp/Internals.cs b/DisCatSharp/Internals.cs index 2be336bc0..e0acaac5b 100644 --- a/DisCatSharp/Internals.cs +++ b/DisCatSharp/Internals.cs @@ -1,101 +1,101 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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 Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Internal tools. /// public static class Internals { /// /// Gets the version of the library /// private static string VersionHeader => Utilities.VersionHeader; /// /// Gets the permission strings. /// private static Dictionary PermissionStrings => Utilities.PermissionStrings; /// /// Gets the utf8 encoding /// internal static UTF8Encoding UTF8 => Utilities.UTF8; /// /// Initializes a new instance of the class. /// static Internals() { } /// /// Whether the is joinable via voice. /// /// The channel. internal static bool IsVoiceJoinable(this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage; /// - /// Whether the is related to threads. + /// Whether the can have threads. /// /// The channel. internal static bool IsThreadHolder(this DiscordChannel channel) => channel.Type == ChannelType.Text || channel.Type == ChannelType.News || channel.Type == ChannelType.GuildForum; /// /// Whether the is related to threads. /// /// The channel. internal static bool IsThread(this DiscordChannel channel) => channel.Type == ChannelType.PublicThread || channel.Type == ChannelType.PrivateThread || channel.Type == ChannelType.NewsThread; /// /// Whether users can write the . /// /// The channel. internal static bool IsWriteable(this DiscordChannel channel) => channel.Type == ChannelType.PublicThread || channel.Type == ChannelType.PrivateThread || channel.Type == ChannelType.NewsThread || channel.Type == ChannelType.Text || channel.Type == ChannelType.News || channel.Type == ChannelType.Group || channel.Type == ChannelType.Private || channel.Type == ChannelType.Voice; /// /// Whether the is moveable in a parent. /// /// The channel. internal static bool IsMovableInParent(this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage || channel.Type == ChannelType.Text || channel.Type == ChannelType.GuildForum || channel.Type == ChannelType.News || channel.Type == ChannelType.Store; /// - /// Whether the is moveable in a parent. + /// Whether the is moveable. /// /// The channel. - internal static bool IsMovable (this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage || channel.Type == ChannelType.Text || channel.Type == ChannelType.Category || channel.Type == ChannelType.GuildForum || channel.Type == ChannelType.News || channel.Type == ChannelType.Store; + internal static bool IsMovable(this DiscordChannel channel) => channel.Type == ChannelType.Voice || channel.Type == ChannelType.Stage || channel.Type == ChannelType.Text || channel.Type == ChannelType.Category || channel.Type == ChannelType.GuildForum || channel.Type == ChannelType.News || channel.Type == ChannelType.Store; } } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index 29cc1c262..14e277043 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -1,185 +1,215 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { 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; } } /// /// Represents a application command edit payload. /// internal class RestApplicationCommandEditPayload { /// /// Gets the name. /// [JsonProperty("name")] public Optional Name { get; set; } /// /// Gets the description. /// [JsonProperty("description")] public Optional Description { get; set; } /// /// Gets the options. /// [JsonProperty("options")] public Optional> Options { get; set; } /// /// Gets the default permission. /// [JsonProperty("default_permission")] public Optional DefaultPermission { 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; } } /// /// Represents a application command permission edit payload. /// internal class RestApplicationCommandPermissionEditPayload { /// /// Gets the permissions. /// [JsonProperty("permissions")] public IEnumerable Permissions { get; set; } } /// /// Represents a guild application command permission edit payload. /// internal class RestGuildApplicationCommandPermissionEditPayload { /// /// Gets the command id. /// [JsonProperty("id")] public ulong CommandId { get; set; } /// /// Gets the permissions. /// [JsonProperty("permissions")] public IEnumerable Permissions { get; set; } } } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestGuildPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestGuildPayloads.cs index 337f94c53..e7c331067 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestGuildPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestGuildPayloads.cs @@ -1,716 +1,722 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// The reason action. /// internal interface IReasonAction { /// /// Gets or sets the reason. /// string Reason { get; set; } //[JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] //public string Reason { get; set; } } /// /// Represents a guild create payload. /// internal class RestGuildCreatePayload { /// /// Gets or sets the name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } /// /// Gets or sets the region id. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] public string RegionId { get; set; } /// /// Gets or sets the icon base64. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] public Optional IconBase64 { get; set; } /// /// Gets or sets the verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel? VerificationLevel { get; set; } /// /// Gets or sets the default message notifications. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications? DefaultMessageNotifications { get; set; } /// /// Gets or sets the system channel flags. /// [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] public SystemChannelFlags? SystemChannelFlags { get; set; } /// /// Gets or sets the roles. /// [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Roles { get; set; } /// /// Gets or sets the channels. /// [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Channels { get; set; } } /// /// Represents a guild create from template payload. /// internal sealed class RestGuildCreateFromTemplatePayload { /// /// Gets or sets the name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } /// /// Gets or sets the icon base64. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] public Optional IconBase64 { get; set; } } /// /// Represents a guild modify payload. /// internal sealed class RestGuildModifyPayload { /// /// Gets or sets the name. /// [JsonProperty("name")] public Optional Name { get; set; } /// /// Gets or sets the icon base64. /// [JsonProperty("icon")] public Optional IconBase64 { get; set; } /// /// Gets or sets the verification level. /// [JsonProperty("verification_level")] public Optional VerificationLevel { get; set; } /// /// Gets or sets the default message notifications. /// [JsonProperty("default_message_notifications")] public Optional DefaultMessageNotifications { get; set; } /// /// Gets or sets the owner id. /// [JsonProperty("owner_id")] public Optional OwnerId { get; set; } /// /// Gets or sets the splash base64. /// [JsonProperty("splash")] public Optional SplashBase64 { get; set; } /// /// Gets or sets the banner base64. /// [JsonProperty("banner")] public Optional BannerBase64 { get; set; } /// /// Gets or sets the discovery splash base64. /// [JsonProperty("discorvery_splash")] public Optional DiscoverySplashBase64 { get; set; } /// /// Gets or sets the afk channel id. /// [JsonProperty("afk_channel_id")] public Optional AfkChannelId { get; set; } /// /// Gets or sets the afk timeout. /// [JsonProperty("afk_timeout")] public Optional AfkTimeout { get; set; } /// /// Gets or sets the mfa level. /// [JsonProperty("mfa_level")] public Optional MfaLevel { get; set; } /// /// Gets or sets the explicit content filter. /// [JsonProperty("explicit_content_filter")] public Optional ExplicitContentFilter { get; set; } /// /// Gets or sets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] public Optional SystemChannelId { get; set; } /// /// Gets or sets the system channel flags. /// [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] public Optional SystemChannelFlags { get; set; } /// /// Gets or sets the rules channel id. /// [JsonProperty("rules_channel_id")] public Optional RulesChannelId { get; set; } /// /// Gets or sets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] public Optional PublicUpdatesChannelId { get; set; } /// /// Gets or sets the preferred locale. /// [JsonProperty("preferred_locale")] public Optional PreferredLocale { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] public Optional Description { get; set; } /// /// Gets or sets whether the premium progress bar should be enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public Optional PremiumProgressBarEnabled { get; set; } } /// /// Represents a guild community modify payload. /// internal sealed class RestGuildCommunityModifyPayload { /// /// Gets or sets the verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public Optional VerificationLevel { get; set; } /// /// Gets or sets the default message notifications. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public Optional DefaultMessageNotifications { get; set; } /// /// Gets or sets the explicit content filter. /// [JsonProperty("explicit_content_filter", NullValueHandling = NullValueHandling.Ignore)] public Optional ExplicitContentFilter { get; set; } /// /// Gets or sets the rules channel id. /// [JsonProperty("rules_channel_id", NullValueHandling = NullValueHandling.Ignore)] public Optional RulesChannelId { get; set; } /// /// Gets or sets the public updates channel id. /// [JsonProperty("public_updates_channel_id", NullValueHandling = NullValueHandling.Ignore)] public Optional PublicUpdatesChannelId { get; set; } /// /// Gets or sets the preferred locale. /// [JsonProperty("preferred_locale")] public Optional PreferredLocale { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] public Optional Description { get; set; } /// /// Gets or sets the features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public List Features { get; set; } } /// /// Represents a guild member add payload. /// internal sealed class RestGuildMemberAddPayload : IOAuth2Payload { /// /// Gets or sets the access token. /// [JsonProperty("access_token")] public string AccessToken { get; set; } /// /// Gets or sets the nickname. /// [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] public string Nickname { get; set; } /// /// Gets or sets the roles. /// [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Roles { get; set; } /// /// Gets or sets a value indicating whether mute. /// [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] public bool? Mute { get; set; } /// /// Gets or sets a value indicating whether deaf. /// [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] public bool? Deaf { get; set; } } /// /// Represents a guild channel reorder payload. /// internal sealed class RestGuildChannelReorderPayload { /// /// Gets or sets the channel id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; set; } /// /// Gets or sets the position. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; set; } } /// /// Represents a guild channel new parent payload. /// internal sealed class RestGuildChannelNewParentPayload { /// /// Gets or sets the channel id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; set; } /// /// Gets or sets the position. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; set; } /// /// Gets or sets the parent id. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] public Optional ParentId { get; set; } /// /// Gets or sets a value indicating whether lock permissions. /// [JsonProperty("lock_permissions", NullValueHandling = NullValueHandling.Ignore)] public bool? LockPermissions { get; set; } } /// /// Represents a guild channel no parent payload. /// internal sealed class RestGuildChannelNoParentPayload { /// /// Gets or sets the channel id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; set; } /// /// Gets or sets the position. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; set; } /// /// Gets or sets the parent id. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] public Optional ParentId { get; set; } } /// /// Represents a guild role reorder payload. /// internal sealed class RestGuildRoleReorderPayload { /// /// Gets or sets the role id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public ulong RoleId { get; set; } /// /// Gets or sets the position. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; set; } } /// /// Represents a guild member modify payload. /// internal sealed class RestGuildMemberModifyPayload { /// /// Gets or sets the nickname. /// [JsonProperty("nick")] public Optional Nickname { get; set; } /// /// Gets or sets the role ids. /// [JsonProperty("roles")] public Optional> RoleIds { get; set; } /// /// Gets or sets the mute. /// [JsonProperty("mute")] public Optional Mute { get; set; } /// /// Gets or sets the deafen. /// [JsonProperty("deaf")] public Optional Deafen { get; set; } /// /// Gets or sets the voice channel id. /// [JsonProperty("channel_id")] public Optional VoiceChannelId { get; set; } + } + /// + /// Represents a guild member timeout modify payload. + /// + internal sealed class RestGuildMemberTimeoutModifyPayload + { /// /// Date until the can communicate again. /// [JsonProperty("communication_disabled_until")] - public Optional CommunicationDisabledUntil { get; internal set; } + public DateTimeOffset? CommunicationDisabledUntil { get; internal set; } } - /// - /// Represents a guild role payload. - /// + /// + /// Represents a guild role payload. + /// internal sealed class RestGuildRolePayload { /// /// Gets or sets the name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } /// /// Gets or sets the permissions. /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } /// /// Gets or sets the color. /// [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] public int? Color { get; set; } /// /// Gets or sets a value indicating whether hoist. /// [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] public bool? Hoist { get; set; } /// /// Gets or sets a value indicating whether mentionable. /// [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] public bool? Mentionable { get; set; } /// /// Gets or sets the icon base64. /// [JsonProperty("icon")] public Optional IconBase64 { get; set; } /// /// Gets or sets the icon base64. /// [JsonProperty("unicode_emoji")] public Optional UnicodeEmoji { get; set; } } /// /// Represents a guild prune result payload. /// internal sealed class RestGuildPruneResultPayload { /// /// Gets or sets the pruned. /// [JsonProperty("pruned", NullValueHandling = NullValueHandling.Ignore)] public int? Pruned { get; set; } } /// /// Represents a guild integration attach payload. /// internal sealed class RestGuildIntegrationAttachPayload { /// /// Gets or sets the type. /// [JsonProperty("type")] public string Type { get; set; } /// /// Gets or sets the id. /// [JsonProperty("id")] public ulong Id { get; set; } } /// /// Represents a guild integration modify payload. /// internal sealed class RestGuildIntegrationModifyPayload { /// /// Gets or sets the expire behavior. /// [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] public int? ExpireBehavior { get; set; } /// /// Gets or sets the expire grace period. /// [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] public int? ExpireGracePeriod { get; set; } /// /// Gets or sets a value indicating whether enable emoticons. /// [JsonProperty("enable_emoticons", NullValueHandling = NullValueHandling.Ignore)] public bool? EnableEmoticons { get; set; } } /// /// Represents a guild emoji modify payload. /// internal class RestGuildEmojiModifyPayload { /// /// Gets or sets the name. /// [JsonProperty("name")] public string Name { get; set; } /// /// Gets or sets the roles. /// [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public ulong[] Roles { get; set; } } /// /// Represents a guild emoji create payload. /// internal class RestGuildEmojiCreatePayload : RestGuildEmojiModifyPayload { /// /// Gets or sets the image b64. /// [JsonProperty("image")] public string ImageB64 { get; set; } } /// /// Represents a guild widget settings payload. /// internal class RestGuildWidgetSettingsPayload { /// /// Gets or sets a value indicating whether enabled. /// [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? Enabled { get; set; } /// /// Gets or sets the channel id. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? ChannelId { get; set; } } /// /// Represents a guild template create or modify payload. /// internal class RestGuildTemplateCreateOrModifyPayload { /// /// Gets or sets the name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] public string Name { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] public string Description { get; set; } } /// /// Represents a guild membership screening form modify payload. /// internal class RestGuildMembershipScreeningFormModifyPayload { /// /// Gets or sets the enabled. /// [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] public Optional Enabled { get; set; } /// /// Gets or sets the fields. /// [JsonProperty("form_fields", NullValueHandling = NullValueHandling.Ignore)] public Optional Fields { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public Optional Description { get; set; } } /// /// Represents a guild welcome screen modify payload. /// internal class RestGuildWelcomeScreenModifyPayload { /// /// Gets or sets the enabled. /// [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] public Optional Enabled { get; set; } /// /// Gets or sets the welcome channels. /// [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] public Optional> WelcomeChannels { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public Optional Description { get; set; } } /// /// Represents a guild update current user voice state payload. /// internal class RestGuildUpdateCurrentUserVoiceStatePayload { /// /// Gets or sets the channel id. /// [JsonProperty("channel_id")] public ulong ChannelId { get; set; } /// /// Gets or sets a value indicating whether suppress. /// [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] public bool? Suppress { get; set; } /// /// Gets or sets the request to speak timestamp. /// [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset? RequestToSpeakTimestamp { get; set; } } /// /// Represents a guild update user voice state payload. /// internal class RestGuildUpdateUserVoiceStatePayload { /// /// Gets or sets the channel id. /// [JsonProperty("channel_id")] public ulong ChannelId { get; set; } /// /// Gets or sets a value indicating whether suppress. /// [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] public bool? Suppress { get; set; } } } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestGuildScheduledEventPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestGuildScheduledEventPayloads.cs new file mode 100644 index 000000000..f4622a969 --- /dev/null +++ b/DisCatSharp/Net/Abstractions/Rest/RestGuildScheduledEventPayloads.cs @@ -0,0 +1,148 @@ +// This file is part of the DisCatSharp project, a fork of DSharpPlus. +// +// Copyright (c) 2021 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 DisCatSharp.Entities; +using Newtonsoft.Json; + +namespace DisCatSharp.Net.Abstractions +{ + /// + /// The rest guild sheduled event create payload. + /// + internal class RestGuildScheduledEventCreatePayload + { + /// + /// Gets or sets the channel id. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; set; } + + /// + /// Gets or sets the entity metadata. + /// + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledEventEntityMetadata EntityMetadata { get; set; } + + /// + /// Gets or sets the name. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the description. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// + /// Gets or sets the privacy level of the scheduled event. + /// + [JsonProperty("privacy_level")] + public ScheduledEventPrivacyLevel PrivacyLevel { get; set; } + + /// + /// Gets or sets the time to schedule the scheduled event. + /// + [JsonProperty("scheduled_start_time")] + public DateTimeOffset ScheduledStartTime { get; internal set; } + + /// + /// Gets or sets the time when the scheduled event is scheduled to end. + /// + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? ScheduledEndTime { get; internal set; } + + /// + /// Gets or sets the entity type of the scheduled event. + /// + [JsonProperty("entity_type")] + public ScheduledEventEntityType EntityType { get; set; } + + /// + /// Gets or sets the image as base64. + /// + [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] + public string ImageBase64 { get; set; } + } + + /// + /// The rest guild sheduled event modify payload. + /// + internal class RestGuildSheduledEventModifyPayload + { + /// + /// Gets or sets the channel id. + /// + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + + /// + /// Gets or sets the entity metadata. + /// + [JsonProperty("entity_metadata")] + public Optional EntityMetadata { get; set; } + + /// + /// Gets or sets the name. + /// + [JsonProperty("name")] + public Optional Name { get; set; } + + /// + /// Gets or sets the description. + /// + [JsonProperty("description")] + public Optional Description { get; set; } + + /// + /// Gets or sets the time to schedule the scheduled event. + /// + [JsonProperty("scheduled_start_time")] + public Optional ScheduledStartTime { get; internal set; } + + /// + /// Gets or sets the time when the scheduled event is scheduled to end. + /// + [JsonProperty("scheduled_end_time")] + public Optional ScheduledEndTime { get; internal set; } + + /// + /// Gets or sets the entity type of the scheduled event. + /// + [JsonProperty("entity_type")] + public Optional EntityType { get; set; } + + /// + /// Gets or sets the cover image as base64. + /// + [JsonProperty("image")] + public Optional ImageBase64 { get; set; } + + /// + /// Gets or sets the status of the scheduled event. + /// + [JsonProperty("status")] + public Optional Status { get; set; } + } +} diff --git a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs index 1ba6d1095..3c53219a9 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs @@ -1,142 +1,154 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents a webhook payload. /// internal sealed class RestWebhookPayload { /// /// Gets or sets the name. /// [JsonProperty("name")] public string Name { get; set; } /// /// Gets or sets the avatar base64. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] public string AvatarBase64 { get; set; } /// /// Gets or sets the channel id. /// [JsonProperty("channel_id")] public ulong ChannelId { get; set; } /// /// Gets whether an avatar is set. /// [JsonProperty] public bool AvatarSet { get; set; } /// /// Gets whether the avatar should be serialized. /// public bool ShouldSerializeAvatarBase64() => this.AvatarSet; } /// /// Represents a webhook execute payload. /// internal sealed class RestWebhookExecutePayload { /// /// Gets or sets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; set; } /// /// Gets or sets the username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public string Username { get; set; } /// /// Gets or sets the avatar url. /// [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] public string AvatarUrl { get; set; } /// /// Whether this message is tts. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool? IsTTS { get; set; } /// /// Gets or sets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; set; } /// /// Gets or sets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public DiscordMentions Mentions { get; set; } + + /// + /// Gets or sets the components. + /// + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Components { get; set; } + + /// + /// Gets or sets the attachments. + /// + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public List Attachments { get; set; } } /// /// Represents a webhook message edit payload. /// internal sealed class RestWebhookMessageEditPayload { /// /// Gets or sets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public Optional Content { get; set; } /// /// Gets or sets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; set; } /// /// Gets or sets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Mentions { get; set; } /// /// Gets or sets the attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Attachments { get; set; } /// /// Gets or sets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Components { get; set; } } } diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs b/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs index 5fa71d4d4..337b7ce5a 100644 --- a/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs +++ b/DisCatSharp/Net/Abstractions/Transport/TransportApplication.cs @@ -1,148 +1,166 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// The transport application. /// internal sealed class TransportApplication { /// /// Gets or sets the id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Include)] public ulong Id { get; set; } /// /// Gets or sets the name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] public string Name { get; set; } /// /// Gets or sets the icon hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] public string IconHash { get; set; } /// /// Gets or sets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] public string Description { get; set; } /// /// Gets or sets the summary. /// [JsonProperty("summary", NullValueHandling = NullValueHandling.Include)] public string Summary { get; set; } /// /// Wwhether the bot is public. /// [JsonProperty("bot_public", NullValueHandling = NullValueHandling.Include)] public bool IsPublicBot { get; set; } /// /// Gets or sets the flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Include)] public ApplicationFlags Flags { get; set; } /// /// Gets or sets the terms of service url. /// [JsonProperty("terms_of_service_url", NullValueHandling = NullValueHandling.Include)] public string TermsOfServiceUrl { get; set; } /// /// Gets or sets the privacy policy url. /// [JsonProperty("privacy_policy_url", NullValueHandling = NullValueHandling.Include)] public string PrivacyPolicyUrl { get; set; } /// /// Gets or sets a value indicating whether the bot requires code grant. /// [JsonProperty("bot_require_code_grant", NullValueHandling = NullValueHandling.Include)] public bool BotRequiresCodeGrant { get; set; } // Json.NET can figure the type out /// /// Gets or sets the rpc origins. /// [JsonProperty("rpc_origins", NullValueHandling = NullValueHandling.Ignore)] public IList RpcOrigins { get; set; } /// /// Gets or sets the owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Include)] public TransportUser Owner { get; set; } /// /// Gets or sets the team. /// [JsonProperty("team", NullValueHandling = NullValueHandling.Include)] public TransportTeam Team { get; set; } /// /// Gets or sets the verify key. /// [JsonProperty("verify_key", NullValueHandling = NullValueHandling.Include)] public Optional VerifyKey { get; set; } /// /// Gets or sets the guild id. /// [JsonProperty("guild_id")] public Optional GuildId { get; set; } /// /// Gets or sets the primary sku id. /// [JsonProperty("primary_sku_id")] public Optional PrimarySkuId { get; set; } /// /// Gets or sets the slug. /// [JsonProperty("slug")] public Optional Slug { get; set; } /// /// Gets or sets the cover image hash. /// [JsonProperty("cover_image")] public Optional CoverImageHash { get; set; } + /// + /// Gets or sets the custom install url. + /// + [JsonProperty("custom_install_url")] + public string CustomInstallUrl { get; set; } + + /// + /// Gets or sets the install params. + /// + [JsonProperty("install_params", NullValueHandling = NullValueHandling.Include)] + public DiscordApplicationInstallParams InstallParams { get; set; } + + /// + /// Gets or sets the tags. + /// + [JsonProperty("tags", NullValueHandling = NullValueHandling.Include)] + public IEnumerable Tags { get; set; } + /// /// Initializes a new instance of the class. /// internal TransportApplication() { } } } diff --git a/DisCatSharp/Net/Models/ChannelEditModel.cs b/DisCatSharp/Net/Models/ChannelEditModel.cs index c58d3e932..7a02b3bf0 100644 --- a/DisCatSharp/Net/Models/ChannelEditModel.cs +++ b/DisCatSharp/Net/Models/ChannelEditModel.cs @@ -1,116 +1,116 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 System.IO; using DisCatSharp.Entities; using Newtonsoft.Json; namespace DisCatSharp.Net.Models { /// /// Represents a channel edit model. /// public class ChannelEditModel : BaseEditModel { /// /// Sets the channel's new name. /// public string Name { internal get; set; } /// /// Sets the channel's type. /// This can only be used to convert between text and news channels. /// public Optional Type { internal get; set; } /// /// Sets the channel's new position. /// public int? Position { internal get; set; } /// /// Sets the channel's new topic. /// public Optional Topic { internal get; set; } /// /// Sets whether the channel is to be marked as NSFW. /// public bool? Nsfw { internal get; set; } /// /// Sets the parent of this channel. - /// This should be channel with set to . + /// This should be channel with set to . /// public Optional Parent { internal get; set; } /// /// Sets the voice channel's new bitrate. /// public int? Bitrate { internal get; set; } /// /// Sets the voice channel's new user limit. /// Setting this to 0 will disable the user limit. /// public int? Userlimit { internal get; set; } /// /// Sets the channel's new slow mode timeout. /// Setting this to null or 0 will disable slow mode. /// public Optional PerUserRateLimit { internal get; set; } /// /// Sets the voice channel's region override. /// Setting this to null will set it to automatic. /// public Optional RtcRegion { internal get; set; } /// /// Sets the voice channel's video quality. /// public VideoQualityMode? QualityMode { internal get; set; } /// /// Sets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity. /// public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { internal get; set; } /// /// Sets the channel's permission overwrites. /// public IEnumerable PermissionOverwrites { internal get; set; } /// /// The new banner of the channel /// public Optional Banner { get; set; } /// /// Initializes a new instance of the class. /// internal ChannelEditModel() { } } } diff --git a/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs b/DisCatSharp/Net/Models/ScheduledEventEditModel.cs similarity index 50% copy from DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs copy to DisCatSharp/Net/Models/ScheduledEventEditModel.cs index ae857f927..af18aaed4 100644 --- a/DisCatSharp/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs +++ b/DisCatSharp/Net/Models/ScheduledEventEditModel.cs @@ -1,69 +1,83 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; -namespace DisCatSharp.EventArgs +namespace DisCatSharp.Net.Models { /// - /// Represents arguments for event. + /// Represents a scheduled event edit model. /// - public class MessageReactionAddEventArgs : DiscordEventArgs + public class ScheduledEventEditModel : BaseEditModel { /// - /// Gets the message for which the update occurred. + /// Gets or sets the channel. /// - public DiscordMessage Message { get; internal set; } + public Optional Channel { get; set; } /// - /// Gets the channel to which this message belongs. + /// Gets or sets the location. /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; + public Optional Location { get; set; } /// - /// Gets the user who created the reaction. - /// This can be cast to a if the reaction was in a guild. + /// Gets or sets the name. /// - public DiscordUser User { get; internal set; } + public Optional Name { get; set; } /// - /// Gets the guild in which the reaction was added. + /// Gets or sets the description. /// - public DiscordGuild Guild { get; internal set; } + public Optional Description { get; set; } /// - /// Gets the emoji used for this reaction. + /// Gets or sets the time to schedule the scheduled event. /// - public DiscordEmoji Emoji { get; internal set; } + public Optional ScheduledStartTime { get; internal set; } /// - /// Initializes a new instance of the class. + /// Gets or sets the time when the scheduled event is scheduled to end. /// - internal MessageReactionAddEventArgs(IServiceProvider provider) : base(provider) { } + public Optional ScheduledEndTime { get; internal set; } + + /// + /// Gets or sets the entity type of the scheduled event. + /// + public Optional EntityType { get; set; } + + /// + /// Gets or sets the cover image as base64. + /// + public Optional ImageBase64 { get; set; } + + /// + /// Gets or sets the status of the scheduled event. + /// + public Optional Status { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal ScheduledEventEditModel() { } } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index f41c060e5..c69155a38 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,4882 +1,5352 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Net; using System.Text; -using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; +using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net { /// /// Represents a discord api client. /// public sealed class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.Rest = new RestClient(client); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// If true, post. /// A string. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var vals_collection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", vals_collection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msg_raw) { var author = msg_raw["author"].ToObject(); var ret = msg_raw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msg_raw["referenced_message"]; if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; this.PopulateMessage(author, ret.ReferencedMessage); } if (ret.Channel != null) return ret; var channel = !ret.GuildId.HasValue ? new DiscordDmChannel { Id = ret.ChannelId, Discord = this.Discord, Type = ChannelType.Private } : new DiscordChannel { Id = ret.ChannelId, GuildId = ret.GuildId, Discord = this.Discord }; ret.Channel = channel; return ret; } /// /// Populates the message. /// /// The author. /// The ret. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { ret.Author = new DiscordUser(author) { Discord = this.Discord }; } else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) { this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord }; } if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this.Discord, _guild_id = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); if (ret._reactions == null) ret._reactions = new List(); foreach (var xr in ret._reactions) xr.Emoji.Discord = this.Discord; } /// /// Executes a rest request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The payload. /// The ratelimit wait override. /// A Task. internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) { var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart rest request for stickers. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The file. /// The sticker name. /// The sticker tag. /// The sticker description. /// The ratelimit wait override. /// A Task. private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null) { var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit wait override. /// A Task. private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, IReadOnlyCollection files = null, double? ratelimitWaitOverride = null) { var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } #region Guild /// /// Searches the members async. /// /// The guild_id. /// The name. /// The limit. /// A Task. internal async Task> SearchMembersAsync(ulong guild_id, string name, int? limit) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var querydict = new Dictionary { ["query"] = name, ["limit"] = limit.ToString() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var tms = json.ToObject>(); var mbrs = new List(); foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, _guild_id = guild_id }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. /// A Task. internal async Task GetGuildBanAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var path); var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ban = json.ToObject(); return ban; } /// /// Creates the guild async. /// /// The name. /// The region_id. /// The iconb64. /// The verification_level. /// The default_message_notifications. /// The system_channel_flags. internal async Task CreateGuildAsync(string name, string region_id, Optional iconb64, VerificationLevel? verification_level, DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags) { var pld = new RestGuildCreatePayload { Name = name, RegionId = region_id, DefaultMessageNotifications = default_message_notifications, VerificationLevel = verification_level, IconBase64 = iconb64, SystemChannelFlags = system_channel_flags }; var route = $"{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string template_code, string name, Optional iconb64) { var pld = new RestGuildCreateFromTemplatePayload { Name = name, IconBase64 = iconb64 }; var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); if (this.Discord is DiscordClient dc) { var gld = dc._guilds[guild_id]; await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false); } } /// /// Modifies the guild. /// /// The guild id. /// The name. /// The verification level. /// The default message notifications. /// The mfa level. /// The explicit content filter. /// The afk channel id. /// The afk timeout. /// The iconb64. /// The owner id. /// The splashb64. /// The system channel id. /// The system channel flags. /// The public updates channel id. /// The rules channel id. /// The description. /// The banner base64. /// The discovery base64. /// The preferred locale. /// Whether the premium progress bar should be enabled. /// The reason. internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel, Optional defaultMessageNotifications, Optional mfaLevel, Optional explicitContentFilter, Optional afkChannelId, Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, Optional systemChannelId, Optional systemChannelFlags, Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description, Optional bannerb64, Optional discorverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason) { var pld = new RestGuildModifyPayload { Name = name, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discorverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description, PremiumProgressBarEnabled = premiumProgressBarEnabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild community settings. /// /// The guild id. /// The guild features. /// The rules channel id. /// The public updates channel id. /// The preferred locale. /// The description. /// The default message notifications. /// The explicit content filter. /// The verification level. /// The reason. internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason) { var pld = new RestGuildCommunityModifyPayload { VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, ExplicitContentFilter = explicitContentFilter, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description ?? Optional.FromNoValue(), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Gets the guild bans async. /// /// The guild_id. /// A Task. internal async Task> GetGuildBansAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var bans_raw = JsonConvert.DeserializeObject>(res.Response).Select(xb => { if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) { usr = new DiscordUser(xb.RawUser) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } xb.User = usr; return xb; }); var bans = new ReadOnlyCollection(new List(bans_raw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. /// A Task. internal Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) { if (delete_message_days < 0 || delete_message_days > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(delete_message_days)); var urlparams = new Dictionary { ["delete_message_days"] = delete_message_days.ToString(CultureInfo.InvariantCulture) }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild ban async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Leaves the guild async. /// /// The guild_id. /// A Task. internal Task LeaveGuildAsync(ulong guild_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the guild member async. /// /// The guild_id. /// The user_id. /// The access_token. /// The nick. /// The roles. /// If true, muted. /// If true, deafened. /// A Task. internal async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) { var pld = new RestGuildMemberAddPayload { AccessToken = access_token, Nickname = nick ?? "", Roles = roles ?? new List(), Deaf = deafened, Mute = muted }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. /// A Task. internal async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) { var urlparams = new Dictionary(); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(members_raw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Modifies the guild channel position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelPositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Detaches the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task DetachGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild role position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildRolePositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the audit logs async. /// /// The guild_id. /// The limit. /// The after. /// The before. /// The responsible. /// The action_type. /// A Task. internal async Task GetAuditLogsAsync(ulong guild_id, int limit, ulong? after, ulong? before, ulong? responsible, int? action_type) { var urlparams = new Dictionary { ["limit"] = limit.ToString(CultureInfo.InvariantCulture) }; if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (responsible != null) urlparams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture); if (action_type != null) urlparams["action_type"] = action_type?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var audit_log_data_raw = JsonConvert.DeserializeObject(res.Response); return audit_log_data_raw; } /// /// Gets the guild vanity url async. /// /// The guild_id. /// A Task. internal async Task GetGuildVanityUrlAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invite = JsonConvert.DeserializeObject(res.Response); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawChannels = (JArray)json["channels"]; var ret = json.ToDiscordObject(); ret.Discord = this.Discord; ret.Guild = this.Discord.Guilds[guild_id]; ret.Channels = ret.Guild == null ? rawChannels.Select(r => new DiscordChannel { Id = (ulong)r["id"], Name = r["name"].ToString(), Position = (int)r["position"] }).ToList() : rawChannels.Select(r => { var c = ret.Guild.GetChannel((ulong)r["id"]); c.Position = (int)r["position"]; return c; }).ToList(); return ret; } /// /// Gets the guild widget settings async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetSettingsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. /// A Task. internal async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? isEnabled, ulong? channelId, string reason) { var pld = new RestGuildWidgetSettingsPayload { Enabled = isEnabled, ChannelId = channelId }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. /// A Task. internal async Task> GetGuildTemplatesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templates_raw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. /// A Task. internal async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Syncs the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task SyncGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. /// A Task. internal async Task ModifyGuildTemplateAsync(ulong guild_id, string template_code, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task DeleteGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. /// A Task. internal async Task GetGuildMembershipScreeningFormAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. /// A Task. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Optional enabled, Optional fields, Optional description) { var pld = new RestGuildMembershipScreeningFormModifyPayload { Enabled = enabled, Description = description, Fields = fields }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. /// A Task. internal async Task GetGuildWelcomeScreenAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Modifies the guild welcome screen async. /// /// The guild_id. /// The enabled. /// The welcome channels. /// The description. /// A Task. internal async Task ModifyGuildWelcomeScreenAsync(ulong guild_id, Optional enabled, Optional> welcomeChannels, Optional description) { var pld = new RestGuildWelcomeScreenModifyPayload { Enabled = enabled, WelcomeChannels = welcomeChannels, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Updates the current user voice state async. /// /// The guild_id. /// The channel id. /// If true, suppress. /// The request to speak timestamp. /// A Task. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guild_id, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) { var pld = new RestGuildUpdateCurrentUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress, RequestToSpeakTimestamp = requestToSpeakTimestamp }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Updates the user voice state async. /// /// The guild_id. /// The user_id. /// The channel id. /// If true, suppress. /// A Task. internal async Task UpdateUserVoiceStateAsync(ulong guild_id, ulong user_id, ulong channelId, bool? suppress) { var pld = new RestGuildUpdateUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } #endregion + #region Guild Scheduled Events + + /// + /// Creates a scheduled event. + /// + internal async Task CreateGuildScheduledEventAsync(ulong guild_id, ulong? channel_id, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduled_start_time, DateTimeOffset? scheduled_end_time, string description, ScheduledEventEntityType type, string reason = null) + { + var pld = new RestGuildScheduledEventCreatePayload + { + ChannelId = channel_id, + EntityMetadata = metadata, + Name = name, + ScheduledStartTime = scheduled_start_time, + ScheduledEndTime = scheduled_end_time, + Description = description, + EntityType = type + }; + + var headers = Utilities.GetBaseHeaders(); + if (!string.IsNullOrWhiteSpace(reason)) + headers[REASON_HEADER_NAME] = reason; + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + + var scheduled_event = JsonConvert.DeserializeObject(res.Response); + var guild = this.Discord.Guilds[guild_id]; + + scheduled_event.Discord = this.Discord; + + if (scheduled_event.Creator != null) + scheduled_event.Creator.Discord = this.Discord; + + if (this.Discord is DiscordClient dc) + await dc.OnGuildScheduledEventCreateEventAsync(scheduled_event, guild); + + return scheduled_event; + } + + /// + /// Modifies a scheduled event. + /// + internal async Task ModifyGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, Optional channel_id, Optional metadata, Optional name, Optional scheduled_start_time, Optional scheduled_end_time, Optional description, Optional type, Optional status, string reason = null) + { + var pld = new RestGuildSheduledEventModifyPayload + { + ChannelId = channel_id, + EntityMetadata = metadata, + Name = name, + ScheduledStartTime = scheduled_start_time, + ScheduledEndTime = scheduled_end_time, + Description = description, + EntityType = type, + Status = status + }; + + var headers = Utilities.GetBaseHeaders(); + if (!string.IsNullOrWhiteSpace(reason)) + headers[REASON_HEADER_NAME] = reason; + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + + var scheduled_event = JsonConvert.DeserializeObject(res.Response); + var guild = this.Discord.Guilds[guild_id]; + + scheduled_event.Discord = this.Discord; + + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this.Discord; + this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } + + if (this.Discord is DiscordClient dc) + await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild); + + return scheduled_event; + } + + /// + /// Modifies a scheduled event. + /// + internal async Task ModifyGuildScheduledEventStatusAsync(ulong guild_id, ulong scheduled_event_id, ScheduledEventStatus status, string reason = null) + { + var pld = new RestGuildSheduledEventModifyPayload + { + Status = status + }; + + var headers = Utilities.GetBaseHeaders(); + if (!string.IsNullOrWhiteSpace(reason)) + headers[REASON_HEADER_NAME] = reason; + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + + var scheduled_event = JsonConvert.DeserializeObject(res.Response); + var guild = this.Discord.Guilds[guild_id]; + + scheduled_event.Discord = this.Discord; + + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this.Discord; + this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } + + if (this.Discord is DiscordClient dc) + await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild); + + return scheduled_event; + } + + /// + /// Gets a scheduled event. + /// + /// The guild_id. + /// The event id. + /// Whether to include user count. + internal async Task GetGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, bool? with_user_count) + { + var urlparams = new Dictionary(); + if (with_user_count.HasValue) + urlparams["with_user_count"] = with_user_count?.ToString(); + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path); + + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); + + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); + + var scheduled_event = JsonConvert.DeserializeObject(res.Response); + var guild = this.Discord.Guilds[guild_id]; + + scheduled_event.Discord = this.Discord; + + if (scheduled_event.Creator != null) + { + scheduled_event.Creator.Discord = this.Discord; + this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => + { + old.Username = scheduled_event.Creator.Username; + old.Discriminator = scheduled_event.Creator.Discriminator; + old.AvatarHash = scheduled_event.Creator.AvatarHash; + old.Flags = scheduled_event.Creator.Flags; + return old; + }); + } + + return scheduled_event; + } + + /// + /// Gets the guilds scheduled events. + /// + /// The guild_id. + /// Whether to include the count of users subscribed to the scheduled event. + internal async Task> ListGuildScheduledEventsAsync(ulong guild_id, bool? with_user_count) + { + var urlparams = new Dictionary(); + if (with_user_count.HasValue) + urlparams["with_user_count"] = with_user_count?.ToString(); + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); + + var events = new Dictionary(); + var events_raw = JsonConvert.DeserializeObject>(res.Response); + var guild = this.Discord.Guilds[guild_id]; + + foreach (var ev in events_raw) + { + ev.Discord = this.Discord; + if(ev.Creator != null) + { + ev.Creator.Discord = this.Discord; + this.Discord.UserCache.AddOrUpdate(ev.Creator.Id, ev.Creator, (id, old) => + { + old.Username = ev.Creator.Username; + old.Discriminator = ev.Creator.Discriminator; + old.AvatarHash = ev.Creator.AvatarHash; + old.Flags = ev.Creator.Flags; + return old; + }); + } + + events.Add(ev.Id, ev); + } + + return new ReadOnlyDictionary(new Dictionary(events)); + } + + /// + /// Deletes a guild sheduled event. + /// + /// The guild_id. + /// The sheduled event id. + /// The reason. + internal Task DeleteGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, string reason) + { + var headers = Utilities.GetBaseHeaders(); + if (!string.IsNullOrWhiteSpace(reason)) + headers.Add(REASON_HEADER_NAME, reason); + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, scheduled_event_id }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); + } + + /// + /// Gets the users who RSVP'd to a sheduled event. + /// Optional with member objects. + /// This endpoint is paginated. + /// + /// The guild_id. + /// The sheduled event id. + /// The limit how many users to receive from the event. + /// Get results before the given id. + /// Get results after the given id. + /// Wether to include guild member data. attaches guild_member property to the user object. + internal async Task> GetGuildScheduledEventRSPVUsersAsync(ulong guild_id, ulong scheduled_event_id, int? limit, ulong? before, ulong? after, bool? with_member) + { + var urlparams = new Dictionary(); + if (limit != null && limit > 0) + urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); + if (before != null) + urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); + if (after != null) + urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); + if (with_member != null) + urlparams["with_member"] = with_member.Value.ToString(CultureInfo.InvariantCulture); + + var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id{Endpoints.USERS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path); + + var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); + + var rspv_users = JsonConvert.DeserializeObject>(res.Response); + Dictionary rspv = new(); + + foreach (var rspv_user in rspv_users) + { + + rspv_user.Discord = this.Discord; + rspv_user.GuildId = guild_id; + + rspv_user.User.Discord = this.Discord; + rspv_user.User = this.Discord.UserCache.AddOrUpdate(rspv_user.User.Id, rspv_user.User, (id, old) => + { + old.Username = rspv_user.User.Username; + old.Discriminator = rspv_user.User.Discriminator; + old.AvatarHash = rspv_user.User.AvatarHash; + old.BannerHash = rspv_user.User.BannerHash; + old._bannerColor = rspv_user.User._bannerColor; + return old; + }); + + /*if (with_member.HasValue && with_member.Value && rspv_user.Member != null) + { + rspv_user.Member.Discord = this.Discord; + }*/ + + rspv.Add(rspv_user.User.Id, rspv_user); + } + + return new ReadOnlyDictionary(new Dictionary(rspv)); + } + #endregion + #region Channel /// /// Creates the guild channel async. /// /// The guild_id. /// The name. /// The type. /// The parent. /// The topic. /// The bitrate. /// The user_limit. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The quality mode. /// The reason. /// A Task. internal async Task CreateGuildChannelAsync(ulong guild_id, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? user_limit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, string reason) { var restoverwrites = new List(); if (overwrites != null) foreach (var ow in overwrites) restoverwrites.Add(ow.Build()); var pld = new RestChannelCreatePayload { Name = name, Type = type, Parent = parent, Topic = topic, Bitrate = bitrate, UserLimit = user_limit, PermissionOverwrites = restoverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Modifies the channel async. /// /// The channel_id. /// The name. /// The position. /// The topic. /// If true, nsfw. /// The parent. /// The bitrate. /// The user_limit. /// The per user rate limit. /// The rtc region. /// The quality mode. /// The default auto archive duration. /// The type. /// The permission overwrites. /// The banner. /// The reason. internal Task ModifyChannelAsync(ulong channel_id, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? user_limit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, Optional bannerb64, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, Nsfw = nsfw, Parent = parent, Bitrate = bitrate, UserLimit = user_limit, PerUserRateLimit = perUserRateLimit, RtcRegion = rtcRegion, QualityMode = qualityMode, DefaultAutoArchiveDuration = autoArchiveDuration, Type = type, PermissionOverwrites = restoverwrites, BannerBase64 = bannerb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel async. /// /// The channel_id. /// A Task. internal async Task GetChannelAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. /// A Task. internal Task DeleteChannelAsync(ulong channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The content. /// The embeds. /// The sticker. /// The reply message id. /// If true, mention reply. /// If true, fail on invalid reply. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply) { if (content != null && content.Length > 2000) throw new ArgumentException("Message content length cannot exceed 2000 characters."); if (!embeds?.Any() ?? true) { if (content == null && sticker == null) throw new ArgumentException("You must specify message content, a sticker or an embed."); if (content.Length == 0) throw new ArgumentException("Message content must not be empty."); } if (embeds != null) foreach (var embed in embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = content != null, Content = content, StickersIds = sticker is null ? Array.Empty() : new[] {sticker.Id}, IsTTS = false, HasEmbed = embeds?.Any() ?? false, Embeds = embeds }; if (replyMessageId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; if (replyMessageId != null) pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The builder. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed?.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = builder.Content != null, Content = builder.Content, StickersIds = builder.Sticker is null ? Array.Empty() : new[] {builder.Sticker.Id}, IsTTS = builder.IsTTS, HasEmbed = builder.Embeds != null, Embeds = builder.Embeds, Components = builder.Components }; if (builder.ReplyId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); if (builder.Files.Count == 0) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } else { + ulong file_id = 0; + List attachments = new(builder.Files.Count); + foreach(var file in builder.Files) + { + DiscordAttachment att = new() + { + Id = file_id, + Discord = this.Discord, + Description = file.Description, + FileName = file.FileName + }; + attachments.Add(att); + file_id++; + } + pld.Attachments = attachments; + + this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); + var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); - var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); + try + { - var ret = this.PrepareMessage(JObject.Parse(res.Response)); + var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); + + var ret = this.PrepareMessage(JObject.Parse(res.Response)); - foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue)) + foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue)) + { + file.Stream.Position = file.ResetPositionTo.Value; + } + + return ret; + } catch(BadRequestException ex) { - file.Stream.Position = file.ResetPositionTo.Value; + this.Discord.Logger.LogError(ex.Message); + this.Discord.Logger.LogError(ex.StackTrace); + this.Discord.Logger.LogDebug(ex.WebResponse.Response); + return null; } - - return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. /// A Task. internal async Task> GetGuildChannelsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var channels_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channels_raw) foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return new ReadOnlyCollection(new List(channels_raw)); } /// /// Creates the stage instance async. /// /// The channel_id. /// The topic. /// Whether everyone should be notified about the stage. /// The privacy_level. /// The reason. internal async Task CreateStageInstanceAsync(ulong channel_id, string topic, bool send_start_notification, StagePrivacyLevel privacy_level, string reason) { var pld = new RestStageInstanceCreatePayload { ChannelId = channel_id, Topic = topic, PrivacyLevel = privacy_level, SendStartNotification = send_start_notification }; var route = $"{Endpoints.STAGE_INSTANCES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Gets the stage instance async. /// /// The channel_id. internal async Task GetStageInstanceAsync(ulong channel_id) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Modifies the stage instance async. /// /// The channel_id. /// The topic. /// The privacy_level. /// The reason. internal Task ModifyStageInstanceAsync(ulong channel_id, Optional topic, Optional privacy_level, string reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic, PrivacyLevel = privacy_level }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes the stage instance async. /// /// The channel_id. /// The reason. internal Task DeleteStageInstanceAsync(ulong channel_id, string reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel messages async. /// /// The channel id. /// The limit. /// The before. /// The after. /// The around. /// A Task. internal async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) { var urlparams = new Dictionary(); if (around != null) urlparams["around"] = around?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (limit > 0) urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetChannelMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Edits the message async. /// /// The channel_id. /// The message_id. /// The content. /// The embeds. /// The mentions. /// The components. /// The suppress_embed. /// The files. /// A Task. internal async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content, Optional> embeds, IEnumerable mentions, IReadOnlyList components, Optional suppress_embed, IReadOnlyCollection files) { if (embeds.HasValue && embeds.Value != null) foreach (var embed in embeds.Value) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageEditPayload { HasContent = content.HasValue, Content = content.HasValue ? (string)content : null, HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components, Flags = suppress_embed.HasValue ? (bool)suppress_embed ? MessageFlags.SuppressedEmbeds : null : null }; pld.Mentions = new DiscordMentions(mentions ?? Mentions.None, false, mentions?.OfType().Any() ?? false); var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } /// /// Deletes the message async. /// /// The channel_id. /// The message_id. /// The reason. /// A Task. internal Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the messages async. /// /// The channel_id. /// The message_ids. /// The reason. /// A Task. internal Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = message_ids }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel invites async. /// /// The channel_id. /// A Task. internal async Task> GetChannelInvitesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } /// /// Creates the channel invite async. /// /// The channel_id. /// The max_age. /// The max_uses. /// The target_type. /// The target_application. /// The target_user. /// If true, temporary. /// If true, unique. /// The reason. /// A Task. internal async Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, TargetType? target_type, TargetActivity? target_application, ulong? target_user, bool temporary, bool unique, string reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = max_age, MaxUses = max_uses, TargetType = target_type, TargetApplication = target_application, TargetUserId = target_user, Temporary = temporary, Unique = unique }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the channel permission async. /// /// The channel_id. /// The overwrite_id. /// The reason. /// A Task. internal Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, overwrite_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Edits the channel permissions async. /// /// The channel_id. /// The overwrite_id. /// The allow. /// The deny. /// The type. /// The reason. /// A Task. internal Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FULL_PERMS, Deny = deny & PermissionMethods.FULL_PERMS }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, overwrite_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Triggers the typing async. /// /// The channel_id. /// A Task. internal Task TriggerTypingAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the pinned messages async. /// /// The channel_id. /// A Task. internal async Task> GetPinnedMessagesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task PinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Unpins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task UnpinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the group dm recipient async. /// /// The channel_id. /// The user_id. /// The access_token. /// The nickname. /// A Task. internal Task AddGroupDmRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = access_token, Nickname = nickname }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Removes the group dm recipient async. /// /// The channel_id. /// The user_id. /// A Task. internal Task RemoveGroupDmRecipientAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Creates the group dm async. /// /// The access_tokens. /// The nicks. /// A Task. internal async Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = access_tokens, Nicknames = nicks }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the dm async. /// /// The recipient_id. /// A Task. internal async Task CreateDmAsync(ulong recipient_id) { var pld = new RestUserDmCreatePayload { Recipient = recipient_id }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Follows the channel async. /// /// The channel_id. /// The webhook_channel_id. /// A Task. internal async Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhook_channel_id }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } /// /// Crossposts the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task CrosspostMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } #endregion #region Member /// /// Gets the current user async. /// /// A Task. internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. /// A Task. internal Task GetUserAsync(ulong user_id) => this.GetUserAsync(user_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. /// A Task. internal async Task GetUserAsync(string user_id) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var user_raw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(user_raw) { Discord = this.Discord }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. /// A Task. internal async Task GetGuildMemberAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); var usr = new DiscordUser(tm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(tm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) { var urlparams = new Dictionary(); if (reason != null) urlparams["reason"] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. /// A Task. internal async Task ModifyCurrentUserAsync(string username, Optional base64_avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue }; var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var user_raw = JsonConvert.DeserializeObject(res.Response); return user_raw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. /// A Task. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var guilds_raw = JsonConvert.DeserializeObject>(res.Response); var glds = guilds_raw.Select(xug => (this.Discord as DiscordClient)?._guilds[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else { return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); } } /// /// Modifies the guild member async. /// /// The guild_id. /// The user_id. /// The nick. /// The role_ids. /// The mute. /// The deaf. /// The voice_channel_id. /// The reason. /// A Task. internal Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, Optional> role_ids, Optional mute, Optional deaf, Optional voice_channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = role_ids, Deafen = deaf, Mute = mute, VoiceChannelId = voice_channel_id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the time out of a guild member. /// /// The guild_id. /// The user_id. /// Datetime offset. /// The reason. /// A Task. - internal Task ModifyTimeOutAsync(ulong guild_id, ulong user_id, Optional until, string reason) + internal Task ModifyTimeOutAsync(ulong guild_id, ulong user_id, DateTimeOffset? until, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; - var pld = new RestGuildMemberModifyPayload + var pld = new RestGuildMemberTimeoutModifyPayload { CommunicationDisabledUntil = until }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the current member nickname async. /// /// The guild_id. /// The nick. /// The reason. /// A Task. internal Task ModifyCurrentMemberNicknameAsync(ulong guild_id, string nick, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}{Endpoints.NICK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Roles /// /// Gets the guild roles async. /// /// The guild_id. /// A Task. internal async Task> GetGuildRolesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var roles_raw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this.Discord; xr._guild_id = guild_id; return xr; }); return new ReadOnlyCollection(new List(roles_raw)); } /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. /// A Task. internal async Task GetGuildAsync(ulong guildId, bool? with_counts) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlparams).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guildRest = json.ToDiscordObject(); foreach (var r in guildRest._roles.Values) r._guild_id = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc._guilds[guildRest.Id]; } else { guildRest.Discord = this.Discord; return guildRest; } } /// /// Modifies the guild role async. /// /// The guild_id. /// The role_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The icon. /// The unicode emoji icon. /// The reason. internal async Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, Optional iconb64, Optional emoji, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable, }; if (emoji.HasValue && !iconb64.HasValue) pld.UnicodeEmoji = emoji; if (emoji.HasValue && iconb64.HasValue) { pld.IconBase64 = null; pld.UnicodeEmoji = emoji; } if (iconb64.HasValue) pld.IconBase64 = iconb64; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret._guild_id = guild_id; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. /// A Task. internal Task DeleteRoleAsync(ulong guild_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Creates the guild role async. /// /// The guild_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The reason. /// A Task. internal async Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret._guild_id = guild_id; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. /// A Task. internal async Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture) }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned.Value; } /// /// Begins the guild prune async. /// /// The guild_id. /// The days. /// If true, compute_prune_count. /// The include_roles. /// The reason. /// A Task. internal async Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture), ["compute_prune_count"] = compute_prune_count.ToString() }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned; } #endregion #region GuildVarious /// /// Gets the template async. /// /// The code. /// A Task. internal async Task GetTemplateAsync(string code) { var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject(res.Response); return templates_raw; } /// /// Gets the guild integrations async. /// /// The guild_id. /// A Task. internal async Task> GetGuildIntegrationsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var integrations_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(integrations_raw)); } /// /// Gets the guild preview async. /// /// The guild_id. /// A Task. internal async Task GetGuildPreviewAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the guild integration async. /// /// The guild_id. /// The type. /// The id. /// A Task. internal async Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) { var pld = new RestGuildIntegrationAttachPayload { Type = type, Id = id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild integration async. /// /// The guild_id. /// The integration_id. /// The expire_behaviour. /// The expire_grace_period. /// If true, enable_emoticons. /// A Task. internal async Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expire_behaviour, ExpireGracePeriod = expire_grace_period, EnableEmoticons = enable_emoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, integration_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild integration async. /// /// The guild_id. /// The integration. /// A Task. internal Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, integration_id = integration.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, payload: DiscordJson.SerializeObject(integration)); } /// /// Syncs the guild integration async. /// /// The guild_id. /// The integration_id. /// A Task. internal Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id, integration_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the guild voice regions async. /// /// The guild_id. /// A Task. internal async Task> GetGuildVoiceRegionsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions_raw)); } /// /// Gets the guild invites async. /// /// The guild_id. /// A Task. internal async Task> GetGuildInvitesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. /// A Task. internal async Task GetInviteAsync(string invite_code, bool? with_counts, bool? with_expiration, ulong? guild_scheduled_event_id) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); if (with_expiration.HasValue) urlparams["with_expiration"] = with_expiration?.ToString(); if (guild_scheduled_event_id.HasValue) urlparams["guild_scheduled_event_id"] = guild_scheduled_event_id?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { invite_code }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the invite async. /// /// The invite_code. /// The reason. /// A Task. internal async Task DeleteInviteAsync(string invite_code, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { invite_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /* * Disabled due to API restrictions * * internal async Task InternalAcceptInvite(string invite_code) * { * this.Discord.DebugLogger.LogMessage(LogLevel.Warning, "REST API", "Invite accept endpoint was used; this account is now likely unverified", DateTime.Now); * * var url = new Uri($"{Utils.GetApiBaseUri(this.Configuration), Endpoints.INVITES}/{invite_code)); * var bucket = this.Rest.GetBucket(0, MajorParameterType.Unbucketed, url, HttpRequestMethod.POST); * var res = await this.DoRequestAsync(this.Discord, bucket, url, HttpRequestMethod.POST).ConfigureAwait(false); * * var ret = JsonConvert.DeserializeObject(res.Response); * ret.Discord = this.Discord; * * return ret; * } */ #endregion #region Connections /// /// Gets the users connections async. /// /// A Task. internal async Task> GetUsersConnectionsAsync() { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var connections_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); return new ReadOnlyCollection(new List(connections_raw)); } #endregion #region Voice /// /// Lists the voice regions async. /// /// A Task. internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions)); } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task CreateWebhookAsync(ulong channel_id, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the channel webhooks async. /// /// The channel_id. /// A Task. internal async Task> GetChannelWebhooksAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. /// A Task. internal async Task> GetGuildWebhooksAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the webhook async. /// /// The webhook_id. /// A Task. internal async Task GetWebhookAsync(ulong webhook_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. /// A Task. internal async Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Token = webhook_token; ret.Id = webhook_id; ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The channel id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue, ChannelId = channelId }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The name. /// The base64_avatar. /// The webhook_token. /// The reason. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Deletes the webhook async. /// /// The webhook_id. /// The reason. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The reason. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, string webhook_token, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Executes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The builder. /// The thread_id. /// A Task. internal async Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder, string thread_id) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestWebhookExecutePayload { Content = builder.Content, Username = builder.Username.HasValue ? builder.Username.Value : null, AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, IsTTS = builder.IsTTS, - Embeds = builder.Embeds + Embeds = builder.Embeds, + Components = builder.Components }; if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) + if (builder.Files?.Count > 0) + { + ulong file_id = 0; + List attachments = new(); + foreach (var file in builder.Files) + { + DiscordAttachment att = new() + { + Id = file_id, + Discord = this.Discord, + Description = file.Description, + FileName = file.FileName, + FileSize = null + }; + attachments.Add(att); + file_id++; + } + pld.Attachments = attachments; + } + + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.Files?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); + var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); + foreach (var att in ret.Attachments) + att.Discord = this.Discord; + foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Executes the webhook slack async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. /// A Task. internal async Task ExecuteWebhookSlackAsync(ulong webhook_id, string webhook_token, string json_payload, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Executes the webhook github async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. /// A Task. internal async Task ExecuteWebhookGithubAsync(ulong webhook_id, string webhook_token, string json_payload, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. /// A Task. internal async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, DiscordWebhookBuilder builder, string thread_id) { builder.Validate(true); var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, - Attachments = builder.Attachments }; + if (builder.Files?.Count > 0) + { + ulong file_id = 0; + List attachments = new(); + foreach (var file in builder.Files) + { + DiscordAttachment att = new() + { + Id = file_id, + Discord = this.Discord, + Description = file.Description, + FileName = file.FileName, + FileSize = 0 + }; + attachments.Add(att); + file_id++; + } + if (builder.Attachments != null && builder.Attachments?.Count() > 0) + attachments.AddRange(builder.Attachments); + + pld.Attachments = attachments; + } else + { + pld.Attachments = builder.Attachments; + } + var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); + var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files); var ret = JsonConvert.DeserializeObject(res.Response); + ret.Discord = this.Discord; + foreach (var att in ret._attachments) + att.Discord = this.Discord; + foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. /// A Task. internal Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, DiscordWebhookBuilder builder, ulong thread_id) => this.EditWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), builder, thread_id.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal async Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.GetWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, ulong thread_id) => this.GetWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), thread_id.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal async Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, ulong thread_id) => this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), thread_id.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task CreateReactionAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the own reaction async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the user reaction async. /// /// The channel_id. /// The message_id. /// The user_id. /// The emoji. /// The reason. /// A Task. internal Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, string emoji, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Gets the reactions async. /// /// The channel_id. /// The message_id. /// The emoji. /// The after_id. /// The limit. /// A Task. internal async Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) { var urlparams = new Dictionary(); if (after_id.HasValue) urlparams["after"] = after_id.Value.ToString(CultureInfo.InvariantCulture); urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var reacters_raw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reacters_raw) { var usr = new DiscordUser(xr) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); reacters.Add(usr); } return new ReadOnlyCollection(new List(reacters)); } /// /// Deletes the all reactions async. /// /// The channel_id. /// The message_id. /// The reason. /// A Task. internal Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the reactions emoji async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task DeleteReactionsEmojiAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } #endregion #region Threads /// /// Creates the thread with message. /// /// The channel id to create the thread in. /// The message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// The rate limit per user. /// The reason. internal async Task CreateThreadWithMessageAsync(ulong channel_id, ulong message_id, string name, ThreadAutoArchiveDuration auto_archive_duration, int? rate_limit_per_user, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration, PerUserRateLimit = rate_limit_per_user }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Creates the thread without a message. /// /// The channel id to create the thread in. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The rate limit per user. /// The reason. internal async Task CreateThreadWithoutMessageAsync(ulong channel_id, string name, ThreadAutoArchiveDuration auto_archive_duration, ChannelType type = ChannelType.PublicThread, int? rate_limit_per_user = null, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration, PerUserRateLimit = rate_limit_per_user, Type = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Joins the thread. /// /// The channel id. internal async Task JoinThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Leaves the thread. /// /// The channel id. internal async Task LeaveThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds a thread member. /// /// The channel id to add the member to. /// The user id to add. internal async Task AddThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Gets a thread member. /// /// The channel id to get the member from. /// The user id to get. internal async Task GetThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_member = JsonConvert.DeserializeObject(res.Response); return thread_member; } /// /// Removes a thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the thread members. /// /// The thread id. internal async Task> GetThreadMembersAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(thread_members_raw); } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PUBLIC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Modifies a thread. /// /// The thread to modify. /// The new name. /// The new locked state. /// The new archived state. /// The new auto archive duration. /// The new per user rate limit. /// The new user invitable state. /// The reason for the modification. internal Task ModifyThreadAsync(ulong thread_id, string name, Optional locked, Optional archived, Optional autoArchiveDuration, Optional perUserRateLimit, Optional invitable, string reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes a thread. /// /// The thread to delete. /// The reason for deletion. internal Task DeleteThreadAsync(ulong thread_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. /// A Task. internal async Task> GetGuildEmojisAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var users = new Dictionary(); var emojis = new List(); foreach (var rawEmoji in emojisRaw) { var xge = rawEmoji.ToObject(); xge.Guild = gld; var xtu = rawEmoji["user"]?.ToObject(); if (xtu != null) { if (!users.ContainsKey(xtu.Id)) { var user = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); users[user.Id] = user; } xge.User = users[xtu.Id]; } emojis.Add(xge); } return new ReadOnlyCollection(emojis); } /// /// Gets the guild emoji async. /// /// The guild_id. /// The emoji_id. /// A Task. internal async Task GetGuildEmojiAsync(ulong guild_id, ulong emoji_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Creates the guild emoji async. /// /// The guild_id. /// The name. /// The imageb64. /// The roles. /// The reason. /// A Task. internal async Task CreateGuildEmojiAsync(ulong guild_id, string name, string imageb64, IEnumerable roles, string reason) { var pld = new RestGuildEmojiCreatePayload { Name = name, ImageB64 = imageb64, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); emoji.User = xtu != null ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) : this.Discord.CurrentUser; return emoji; } /// /// Modifies the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The name. /// The roles. /// The reason. /// A Task. internal async Task ModifyGuildEmojiAsync(ulong guild_id, ulong emoji_id, string name, IEnumerable roles, string reason) { var pld = new RestGuildEmojiModifyPayload { Name = name, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Deletes the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The reason. /// A Task. internal Task DeleteGuildEmojiAsync(ulong guild_id, ulong emoji_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Stickers /// /// Gets a sticker. /// /// The sticker id. internal async Task GetStickerAsync(ulong sticker_id) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Gets the sticker packs. /// internal async Task> GetStickerPacksAsync() { var route = $"{Endpoints.STICKERPACKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); return ret.ToList(); } /// /// Gets the guild stickers. /// /// The guild id. internal async Task> GetGuildStickersAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var ret = json.ToDiscordObject(); for (var i = 0; i < ret.Length; i++) { var stkr = ret[i]; stkr.Discord = this.Discord; if (json[i]["user"] is JObject obj) // Null = Missing stickers perm // { var tsr = obj.ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); stkr.User = usr; } } return ret.ToList(); } /// /// Gets a guild sticker. /// /// The guild id. /// The sticker id. internal async Task GetGuildStickerAsync(ulong guild_id, ulong sticker_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ret = json.ToDiscordObject(); if (json["user"] is not null) // Null = Missing stickers perm // { var tsr = json["user"].ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); ret.User = usr; } ret.Discord = this.Discord; return ret; } /// /// Creates the guild sticker. /// /// The guild id. /// The name. /// The description. /// The tags. /// The file. /// The reason. internal async Task CreateGuildStickerAsync(ulong guild_id, string name, string description, string tags, DiscordMessageFile file, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var res = await this.DoStickerMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, file, name, tags, description); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild sticker. /// /// The guild id. /// The sticker id. /// The name. /// The description. /// The tags. /// The reason. internal async Task ModifyGuildStickerAsync(ulong guild_id, ulong sticker_id, Optional name, Optional description, Optional tags, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id, sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var pld = new RestStickerModifyPayload() { Name = name, Description = description, Tags = tags }; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return null; } /// /// Deletes the guild sticker async. /// /// The guild id. /// The sticker id. /// The reason. internal async Task DeleteGuildStickerAsync(ulong guild_id, ulong sticker_id, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, sticker_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Application Commands /// /// Gets the global application commands async. /// /// The application_id. /// A Task. internal async Task> GetGlobalApplicationCommandsAsync(ulong application_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite global application commands async. /// /// The application_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong application_id, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }); } var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the global application command async. /// /// The application_id. /// The command. /// A Task. internal async Task CreateGlobalApplicationCommandAsync(ulong application_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task GetGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the global application command async. /// /// The application_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// A Task. internal async Task EditGlobalApplicationCommandAsync(ulong application_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task DeleteGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application commands async. /// /// The application_id. /// The guild_id. /// A Task. internal async Task> GetGuildApplicationCommandsAsync(ulong application_id, ulong guild_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite guild application commands async. /// /// The application_id. /// The guild_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong application_id, ulong guild_id, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }); } var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the guild application command async. /// /// The application_id. /// The guild_id. /// The command. /// A Task. internal async Task CreateGuildApplicationCommandAsync(ulong application_id, ulong guild_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task GetGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. internal async Task EditGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task DeleteGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Gets the application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetApplicationCommandPermissionAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Overwrites the guild application command permissions. /// /// The target application id. /// The target guild id. /// The target command id. /// Array of permissions. internal async Task OverwriteGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, ulong command_id, IEnumerable permissions) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id, command_id }, out var path); if (permissions.ToArray().Length > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); var pld = new RestApplicationCommandPermissionEditPayload { Permissions = permissions }; var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Bulks overwrite the application command permissions. /// /// The target application id. /// The target guild id. /// internal async Task> BulkOverwriteApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, IEnumerable permission_overwrites) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); var pld = new List(); foreach (var overwrite in permission_overwrites) { if (overwrite.Permissions.Count > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); pld.Add(new RestGuildApplicationCommandPermissionEditPayload { CommandId = overwrite.Id, Permissions = overwrite.Permissions }); } var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld.ToArray())); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the interaction response async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. internal async Task CreateInteractionResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { - try + if (builder?.Embeds != null) + foreach (var embed in builder.Embeds) + if (embed.Timestamp != null) + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + + RestInteractionResponsePayload pld; + + if (type != InteractionResponseType.AutoCompleteResult) { - if (builder?.Embeds != null) - foreach (var embed in builder.Embeds) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + var data = builder != null ? new DiscordInteractionApplicationCommandCallbackData + { + Content = builder.Content ?? null, + Embeds = builder.Embeds ?? null, + IsTTS = builder.IsTTS, + Mentions = builder.Mentions ?? null, + Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : null, + Components = builder.Components ?? null, + Choices = null + } : null; + + + pld = new RestInteractionResponsePayload + { + Type = type, + Data = data + }; - var pld = type == InteractionResponseType.AutoCompleteResult - ? new RestInteractionResponsePayload + + if (builder != null && builder.Files != null && builder.Files.Count > 0) + { + ulong file_id = 0; + List attachments = new(); + foreach (var file in builder.Files) + { + DiscordAttachment att = new() + { + Id = file_id, + Discord = this.Discord, + Description = file.Description, + FileName = file.FileName, + FileSize = null + }; + attachments.Add(att); + file_id++; + } + pld.Attachments = attachments; + pld.Data.Attachments = attachments; + } + } + else + { + pld = new RestInteractionResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandCallbackData { Content = null, Embeds = null, IsTTS = null, Mentions = null, Flags = null, Components = null, - Choices = builder.Choices - } - } - : new RestInteractionResponsePayload - { - Type = type, - Data = builder != null ? new DiscordInteractionApplicationCommandCallbackData - { - Content = builder.Content, - Embeds = builder.Embeds, - IsTTS = builder.IsTTS, - Mentions = builder.Mentions, - Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : 0, - Components = builder.Components, - Choices = null - } : null + Choices = builder.Choices, + Attachments = null + }, + Attachments = null }; - var values = new Dictionary(); + } - if (builder != null) - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) - values["payload_json"] = DiscordJson.SerializeObject(pld); + var values = new Dictionary(); - var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; - var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); + if (builder != null) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null || builder.Files?.Count > 0) + values["payload_json"] = DiscordJson.SerializeObject(pld); - var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); - if (builder != null) - { - await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); + var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); - foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) - { - file.Stream.Position = file.ResetPositionTo.Value; - } - } - else + var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "false").Build(); + if (builder != null) + { + await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); + + foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { - await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + file.Stream.Position = file.ResetPositionTo.Value; } - } catch(Exception ex) + } + else { - this.Discord.Logger.LogDebug(ex, ex.Message); + await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } } + /// + /// Creates the interaction response async. + /// + /// The interaction_id. + /// The interaction_token. + /// The type. + /// The builder. + /// A Task. + internal async Task CreateInteractionModalResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionModalBuilder builder) + { + var pld = new RestInteractionModalResponsePayload + { + Type = type, + Data = new DiscordInteractionApplicationCommandModalCallbackData + { + Title = builder.Title, + CustomId = builder.CustomId, + ModalComponents = builder.ModalComponents + } + }; + + var values = new Dictionary(); + + if (type == InteractionResponseType.Modal) + this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); + + var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); + + var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); + await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + } + /// /// Gets the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task GetOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.GetWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal Task EditOriginalInteractionResponseAsync(ulong application_id, string interaction_token, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task DeleteOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.DeleteWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, null); /// /// Creates the followup message async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal async Task CreateFollowupMessageAsync(ulong application_id, string interaction_token, DiscordFollowupMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload { Content = builder.Content, IsTTS = builder.IsTTS, Embeds = builder.Embeds, Flags = builder.Flags, Components = builder.Components }; + + if (builder.Files != null && builder.Files.Count > 0) + { + ulong file_id = 0; + List attachments = new(); + foreach (var file in builder.Files) + { + DiscordAttachment att = new() + { + Id = file_id, + Discord = this.Discord, + Description = file.Description, + FileName = file.FileName, + FileSize = null + }; + attachments.Add(att); + file_id++; + } + pld.Attachments = attachments; + } + if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id, interaction_token }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); + foreach (var att in ret._attachments) + { + att.Discord = this.Discord; + } + foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Gets the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task GetFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.GetWebhookMessageAsync(application_id, interaction_token, message_id); /// /// Edits the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// The builder. /// A Task. internal Task EditFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, message_id.ToString(), builder, null); /// /// Deletes the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task DeleteFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.DeleteWebhookMessageAsync(application_id, interaction_token, message_id); #endregion #region Misc /// /// Gets the current application info async. /// /// A Task. internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. /// A Task. internal Task GetApplicationInfoAsync(ulong application_id) => this.GetApplicationInfoAsync(application_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. /// A Task. private async Task GetApplicationInfoAsync(string application_id) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(res.Response); } /// /// Gets the application assets async. /// /// The application. /// A Task. internal async Task> GetApplicationAssetsAsync(DiscordApplication application) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// /// A Task. internal async Task GetGatewayInfoAsync() { var headers = Utilities.GetBaseHeaders(); var route = Endpoints.GATEWAY; if (this.Discord.Configuration.TokenType == TokenType.Bot) route += Endpoints.BOT; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.resetAfter); return info; } #endregion #region DCS Internals /// /// Gets the DisCatSharp team. /// > internal async Task GetDisCatSharpTeamAsync() { try { var wc = new WebClient(); var dcs = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/devs/")); var dcs_guild = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/guild/")); var app = JsonConvert.DeserializeObject(dcs); var guild = JsonConvert.DeserializeObject(dcs_guild); var dcst = new DisCatSharpTeam { IconHash = app.Team.IconHash, TeamName = app.Team.Name, PrivacyPolicyUrl = app.PrivacyPolicyUrl, TermsOfServiceUrl = app.TermsOfServiceUrl, RepoUrl = "https://github.com/Aiko-IT-Systems/DisCatSharp", DocsUrl = "https://docs.dcs.aitsys.dev", Id = app.Team.Id, BannerHash = guild.BannerHash, LogoHash = guild.IconHash, GuildId = guild.Id, Guild = guild, SupportInvite = await this.GetInviteAsync("discatsharp", true, true, null) }; List team = new(); DisCatSharpTeamMember owner = new(); foreach (var mb in app.Team.Members.OrderBy(m => m.User.Username)) { var tuser = await this.GetUserAsync(mb.User.Id); var user = mb.User; if (mb.User.Id == 856780995629154305) { owner.Id = user.Id; owner.Username = user.Username; owner.Discriminator = user.Discriminator; owner.AvatarHash = user.AvatarHash; owner.BannerHash = tuser.BannerHash; owner._bannerColor = tuser._bannerColor; team.Add(owner); } else { team.Add(new() { Id = user.Id, Username = user.Username, Discriminator = user.Discriminator, AvatarHash = user.AvatarHash, BannerHash = tuser.BannerHash, _bannerColor = tuser._bannerColor }); } } dcst.Owner = owner; dcst.Developers = team; return dcst; } catch(Exception ex) { this.Discord.Logger.LogDebug(ex.Message); this.Discord.Logger.LogDebug(ex.StackTrace); return null; } } #endregion } } diff --git a/DisCatSharp/Net/Rest/Endpoints.cs b/DisCatSharp/Net/Rest/Endpoints.cs index f23df96fd..a31c8b7e8 100644 --- a/DisCatSharp/Net/Rest/Endpoints.cs +++ b/DisCatSharp/Net/Rest/Endpoints.cs @@ -1,382 +1,385 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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. namespace DisCatSharp.Net { /// /// The discord endpoints. /// public static class Endpoints { /// /// The base discord api uri. /// public const string BASE_URI = "https://discord.com/api/v"; /// /// The base discord canary api uri. /// public const string CANARY_URI = "https://canary.discord.com/api/v"; /// /// The oauth2 endpoint. /// public const string OAUTH2 = "/oauth2"; /// /// The oauth2 authorize endpoint. /// public const string AUTHORIZE = "/authorize"; /// /// The applications endpoint. /// public const string APPLICATIONS = "/applications"; /// /// The message reactions endpoint. /// public const string REACTIONS = "/reactions"; /// /// The self (@me) endpoint. /// public const string ME = "/@me"; /// /// The @original endpoint. /// public const string ORIGINAL = "/@original"; /// /// The permissions endpoint. /// public const string PERMISSIONS = "/permissions"; /// /// The recipients endpoint. /// public const string RECIPIENTS = "/recipients"; /// /// The bulk-delete endpoint. /// public const string BULK_DELETE = "/bulk-delete"; /// /// The integrations endpoint. /// public const string INTEGRATIONS = "/integrations"; /// /// The applications endpoint. /// public const string SYNC = "/sync"; /// /// The prune endpoint. /// Used for user removal. /// public const string PRUNE = "/prune"; /// /// The regions endpoint. /// public const string REGIONS = "/regions"; /// /// The connections endpoint. /// public const string CONNECTIONS = "/connections"; /// /// The icons endpoint. /// public const string ICONS = "/icons"; /// /// The gateway endpoint. /// public const string GATEWAY = "/gateway"; /// /// The oauth2 auth endpoint. /// public const string AUTH = "/auth"; /// /// The oauth2 login endpoint. /// public const string LOGIN = "/login"; /// /// The channels endpoint. /// public const string CHANNELS = "/channels"; /// /// The messages endpoint. /// public const string MESSAGES = "/messages"; /// /// The pinned messages endpoint. /// public const string PINS = "/pins"; /// /// The users endpoint. /// public const string USERS = "/users"; /// /// The guilds endpoint. /// public const string GUILDS = "/guilds"; /// /// The guild discovery splash endpoint. /// public const string GUILD_DISCOVERY_SPLASHES = "/discovery-splashes"; /// /// The guild splash endpoint. /// public const string SPLASHES = "/splashes"; /// /// The search endpoint. /// public const string SEARCH = "/search"; /// /// The invites endpoint. /// public const string INVITES = "/invites"; /// /// The roles endpoint. /// public const string ROLES = "/roles"; /// /// The members endpoint. /// public const string MEMBERS = "/members"; /// /// The typing endpoint. /// Triggers a typing indicator inside a channel. /// public const string TYPING = "/typing"; /// /// The avatars endpoint. /// public const string AVATARS = "/avatars"; /// /// The bans endpoint. /// public const string BANS = "/bans"; /// /// The webhook endpoint. /// public const string WEBHOOKS = "/webhooks"; /// /// The slack endpoint. /// Used for . /// public const string SLACK = "/slack"; /// /// The github endpoint. /// Used for . /// public const string GITHUB = "/github"; /// /// The bot endpoint. /// public const string BOT = "/bot"; /// /// The voice endpoint. /// public const string VOICE = "/voice"; /// /// The audit logs endpoint. /// public const string AUDIT_LOGS = "/audit-logs"; /// /// The acknowledge endpoint. /// Indicates that a message is read. /// public const string ACK = "/ack"; /// /// The nickname endpoint. /// public const string NICK = "/nick"; /// /// The assets endpoint. /// public const string ASSETS = "/assets"; /// /// The embed endpoint. /// public const string EMBED = "/embed"; /// /// The emojis endpoint. /// public const string EMOJIS = "/emojis"; /// /// The vanity url endpoint. /// public const string VANITY_URL = "/vanity-url"; /// /// The guild preview endpoint. /// public const string PREVIEW = "/preview"; /// /// The followers endpoint. /// public const string FOLLOWERS = "/followers"; /// /// The crosspost endpoint. /// public const string CROSSPOST = "/crosspost"; /// /// The guild widget endpoint. /// public const string WIDGET = "/widget"; /// /// The guild widget json endpoint. /// public const string WIDGET_JSON = "/widget.json"; /// /// The guild widget png endpoint. /// public const string WIDGET_PNG = "/widget.png"; /// /// The templates endpoint. /// public const string TEMPLATES = "/templates"; /// /// The member verification gate endpoint. /// public const string MEMBER_VERIFICATION = "/member-verification"; /// /// The slash commands endpoint. /// public const string COMMANDS = "/commands"; /// /// The interactions endpoint. /// public const string INTERACTIONS = "/interactions"; /// /// The interaction/command callback endpoint. /// public const string CALLBACK = "/callback"; /// /// The welcome screen endpoint. /// public const string WELCOME_SCREEN = "/welcome-screen"; /// /// The voice states endpoint. /// public const string VOICE_STATES = "/voice-states"; /// /// The stage instances endpoint. /// public const string STAGE_INSTANCES = "/stage-instances"; /// /// The threads endpoint. /// public const string THREADS = "/threads"; /// /// The public threads endpoint. /// public const string THREAD_PUBLIC = "/public"; /// /// The private threads endpoint. /// public const string THREAD_PRIVATE = "/private"; /// /// The active threads endpoint. /// public const string THREAD_ACTIVE = "/active"; /// /// The archived threads endpoint. /// public const string THREAD_ARCHIVED = "/archived"; /// /// The thread members endpoint. /// public const string THREAD_MEMBERS = "/thread-members"; /// - /// The guild events endpoint. - /// Used for stage events. + /// The guild sheduled events endpoint. /// - public const string EVENTS = "/events"; + public const string SCHEDULED_EVENTS = "/scheduled-events"; /// /// The stickers endpoint. /// public const string STICKERS = "/stickers"; /// /// The sticker packs endpoint. /// Global nitro sticker packs. /// public const string STICKERPACKS = "/sticker-packs"; /// /// The store endpoint. /// public const string STORE = "/store"; /// /// The app assets endpoint. /// public const string APP_ASSETS = "/app-assets"; /// /// The app icons endpoint. /// public const string APP_ICONS = "/app-icons"; /// /// The team icons endpoint. /// public const string TEAM_ICONS = "/team-icons"; /// /// The channel icons endpoint. /// public const string CHANNEL_ICONS = "/channel-icons"; /// /// The user banners endpoint. /// public const string BANNERS = "/banners"; /// /// The sticker endpoint. /// This endpoint is the static nitro sticker application. /// public const string STICKER_APPLICATION = "/710982414301790216"; /// /// The role subscription endpoint. /// public const string ROLE_SUBSCRIPTIONS = "/role-subscriptions"; /// /// The group listings endpoint. /// public const string GROUP_LISTINGS = "/group-listings"; /// /// The subscription listings endpoint. /// public const string SUBSCRIPTION_LISTINGS = "/subscription-listings"; /// /// The directory entries endpoint. /// public const string DIRECTORY_ENTRIES = "/directory-entries"; /// /// The counts endpoint. /// public const string COUNTS = "/counts"; /// /// The list endpoint. /// public const string LIST = "/list"; /// /// The role icons endpoint. /// public const string ROLE_ICONS = "/role-icons"; /// /// The activities endpoint. /// public const string ACTIVITIES = "/activities"; /// /// The config endpoint. /// public const string CONFIG = "/config"; + /// + /// The ephemeral attachments endpoint. + /// + public const string EphemeralAttachments = "/ephemeral-attachments"; } } diff --git a/DisCatSharp/Net/Rest/MultipartWebRequest.cs b/DisCatSharp/Net/Rest/MultipartWebRequest.cs index a4c3fd074..10c524ea1 100644 --- a/DisCatSharp/Net/Rest/MultipartWebRequest.cs +++ b/DisCatSharp/Net/Rest/MultipartWebRequest.cs @@ -1,118 +1,125 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Runtime.InteropServices.ComTypes; using DisCatSharp.Entities; namespace DisCatSharp.Net { /// /// Represents a multipart HTTP request. /// internal sealed class MultipartWebRequest : BaseRestRequest { /// /// Gets the dictionary of values attached to this request. /// public IReadOnlyDictionary Values { get; } /// /// Gets the dictionary of files attached to this request. /// public IReadOnlyDictionary Files { get; } + /// + /// Overwrites the file id start. + /// + public int? OverwriteFileIdStart { get; } + /// /// Initializes a new instance of the class. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit_wait_override. + /// The file id start. internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, - IReadOnlyCollection files = null, double? ratelimit_wait_override = null) + IReadOnlyCollection files = null, double? ratelimit_wait_override = null, int? overwrite_file_id_start = null) : base(client, bucket, url, method, route, headers, ratelimit_wait_override) { this.Values = values; + this.OverwriteFileIdStart = overwrite_file_id_start; this.Files = files.ToDictionary(x => x.FileName, x => x.Stream); } } /// /// Represents a multipart HTTP request for stickers. /// internal sealed class MultipartStickerWebRequest : BaseRestRequest { /// /// Gets the file. /// public DiscordMessageFile File { get; } /// /// Gets the name. /// public string Name { get; } /// /// Gets the description. /// public string Description { get; } /// /// Gets the tags. /// public string Tags { get; } /// /// Initializes a new instance of the class. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The file. /// The sticker name. /// The sticker tag. /// The sticker description. /// The ratelimit_wait_override. internal MultipartStickerWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimit_wait_override = null) : base(client, bucket, url, method, route, headers, ratelimit_wait_override) { this.File = file; this.Name = name; this.Description = description; this.Tags = tags; } } } diff --git a/DisCatSharp/Net/Rest/RateLimitBucket.cs b/DisCatSharp/Net/Rest/RateLimitBucket.cs index c9054950d..82fe8c256 100644 --- a/DisCatSharp/Net/Rest/RateLimitBucket.cs +++ b/DisCatSharp/Net/Rest/RateLimitBucket.cs @@ -1,281 +1,291 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Threading; using System.Threading.Tasks; namespace DisCatSharp.Net { /// /// Represents a rate limit bucket. /// internal class RateLimitBucket : IEquatable { /// /// Gets the Id of the guild bucket. /// public string GuildId { get; internal set; } /// /// Gets the Id of the channel bucket. /// public string ChannelId { get; internal set; } /// /// Gets the ID of the webhook bucket. /// public string WebhookId { get; internal set; } /// /// Gets the Id of the ratelimit bucket. /// public volatile string BucketId; /// /// Gets or sets the ratelimit hash of this bucket. /// public string Hash { get => Volatile.Read(ref this._hash); internal set { this._isUnlimited = value.Contains(_unlimitedHash); if (this.BucketId != null && !this.BucketId.StartsWith(value)) { var id = GenerateBucketId(value, this.GuildId, this.ChannelId, this.WebhookId); this.BucketId = id; this.RouteHashes.Add(id); } Volatile.Write(ref this._hash, value); } } internal string _hash; /// /// Gets the past route hashes associated with this bucket. /// public ConcurrentBag RouteHashes { get; } /// /// Gets when this bucket was last called in a request. /// public DateTimeOffset LastAttemptAt { get; internal set; } /// /// Gets the number of uses left before pre-emptive rate limit is triggered. /// public int Remaining => this._remaining; /// /// Gets the maximum number of uses within a single bucket. /// public int Maximum { get; set; } /// /// Gets the timestamp at which the rate limit resets. /// public DateTimeOffset Reset { get; internal set; } /// /// Gets the time interval to wait before the rate limit resets. /// public TimeSpan? ResetAfter { get; internal set; } = null; + /// + /// Gets a value indicating whether the ratelimit global. + /// + public bool IsGlobal { get; internal set; } = false; + + /// + /// Gets the ratelimit scope. + /// + public string Scope { get; internal set; } = "user"; + /// /// Gets the time interval to wait before the rate limit resets as offset /// internal DateTimeOffset ResetAfterOffset { get; set; } internal volatile int _remaining; /// /// Gets whether this bucket has it's ratelimit determined. /// This will be if the ratelimit is determined. /// internal volatile bool _isUnlimited; /// /// If the initial request for this bucket that is deterternining the rate limits is currently executing /// This is a int because booleans can't be accessed atomically /// 0 => False, all other values => True /// internal volatile int _limitTesting; /// /// Task to wait for the rate limit test to finish /// internal volatile Task _limitTestFinished; /// /// If the rate limits have been determined /// internal volatile bool _limitValid; /// /// Rate limit reset in ticks, UTC on the next response after the rate limit has been reset /// internal long _nextReset; /// /// If the rate limit is currently being reset. /// This is a int because booleans can't be accessed atomically. /// 0 => False, all other values => True /// internal volatile int _limitResetting; private static readonly string _unlimitedHash = "unlimited"; /// /// Initializes a new instance of the class. /// /// The hash. /// The guild_id. /// The channel_id. /// The webhook_id. internal RateLimitBucket(string hash, string guild_id, string channel_id, string webhook_id) { this.Hash = hash; this.ChannelId = channel_id; this.GuildId = guild_id; this.WebhookId = webhook_id; this.BucketId = GenerateBucketId(hash, guild_id, channel_id, webhook_id); this.RouteHashes = new ConcurrentBag(); } /// /// Generates an ID for this request bucket. /// /// Hash for this bucket. /// Guild Id for this bucket. /// Channel Id for this bucket. /// Webhook Id for this bucket. /// Bucket Id. public static string GenerateBucketId(string hash, string guild_id, string channel_id, string webhook_id) => $"{hash}:{guild_id}:{channel_id}:{webhook_id}"; /// /// Generates the hash key. /// /// The method. /// The route. /// A string. public static string GenerateHashKey(RestRequestMethod method, string route) => $"{method}:{route}"; /// /// Generates the unlimited hash. /// /// The method. /// The route. /// A string. public static string GenerateUnlimitedHash(RestRequestMethod method, string route) => $"{GenerateHashKey(method, route)}:{_unlimitedHash}"; /// /// Returns a string representation of this bucket. /// /// String representation of this bucket. public override string ToString() { var guildId = this.GuildId != string.Empty ? this.GuildId : "guild_id"; var channelId = this.ChannelId != string.Empty ? this.ChannelId : "channel_id"; var webhookId = this.WebhookId != string.Empty ? this.WebhookId : "webhook_id"; - return $"rate limit bucket [{this.Hash}:{guildId}:{channelId}:{webhookId}] [{this.Remaining}/{this.Maximum}] {(this.ResetAfter.HasValue ? this.ResetAfterOffset : this.Reset)}"; + return $"{this.Scope} rate limit bucket [{this.Hash}:{guildId}:{channelId}:{webhookId}] [{this.Remaining}/{this.Maximum}] {(this.ResetAfter.HasValue ? this.ResetAfterOffset : this.Reset)}"; } /// /// 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 RateLimitBucket); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(RateLimitBucket e) => e is not null && (ReferenceEquals(this, e) || this.BucketId == e.BucketId); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.BucketId.GetHashCode(); /// /// Sets remaining number of requests to the maximum when the ratelimit is reset /// /// internal async Task TryResetLimitAsync(DateTimeOffset now) { if (this.ResetAfter.HasValue) this.ResetAfter = this.ResetAfterOffset - now; if (this._nextReset == 0) return; if (this._nextReset > now.UtcTicks) return; while (Interlocked.CompareExchange(ref this._limitResetting, 1, 0) != 0) -#pragma warning restore 420 + #pragma warning restore 420 await Task.Yield(); if (this._nextReset != 0) { this._remaining = this.Maximum; this._nextReset = 0; } this._limitResetting = 0; } /// /// Sets the initial values. /// /// The max. /// The uses left. /// The new reset. internal void SetInitialValues(int max, int usesLeft, DateTimeOffset newReset) { this.Maximum = max; this._remaining = usesLeft; this._nextReset = newReset.UtcTicks; this._limitValid = true; this._limitTestFinished = null; this._limitTesting = 0; } } } diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs index 447386b11..1495f7348 100644 --- a/DisCatSharp/Net/Rest/RestClient.cs +++ b/DisCatSharp/Net/Rest/RestClient.cs @@ -1,838 +1,860 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Exceptions; using Microsoft.Extensions.Logging; namespace DisCatSharp.Net { /// /// Represents a client used to make REST requests. /// internal sealed class RestClient : IDisposable { /// /// Gets the route argument regex. /// private static Regex RouteArgumentRegex { get; } = new Regex(@":([a-z_]+)"); /// /// Gets the http client. /// private HttpClient HttpClient { get; } /// /// Gets the discord client. /// private BaseDiscordClient Discord { get; } /// /// Gets a value indicating whether debug is enabled. /// internal bool Debug { get; set; } /// /// Gets the logger. /// private ILogger Logger { get; } /// /// Gets the routes to hashes. /// private ConcurrentDictionary RoutesToHashes { get; } /// /// Gets the hashes to buckets. /// private ConcurrentDictionary HashesToBuckets { get; } /// /// Gets the request queue. /// private ConcurrentDictionary RequestQueue { get; } /// /// Gets the global rate limit event. /// private AsyncManualResetEvent GlobalRateLimitEvent { get; } /// /// Gets a value indicating whether use reset after. /// private bool UseResetAfter { get; } private CancellationTokenSource _bucketCleanerTokenSource; private TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60); private volatile bool _cleanerRunning; private Task _cleanerTask; private volatile bool _disposed; /// /// Initializes a new instance of the class. /// /// The client. internal RestClient(BaseDiscordClient client) : this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger) { this.Discord = client; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); + if (client.Configuration.Override != null) + { + this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", client.Configuration.Override); + } } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative ratelimit. /// The logger. internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Logger = logger; var httphandler = new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseProxy = proxy != null, Proxy = proxy }; this.HttpClient = new HttpClient(httphandler) { BaseAddress = new Uri(Utilities.GetApiBaseUri(this.Discord?.Configuration)), Timeout = timeout }; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); + if (this.Discord != null && this.Discord.Configuration != null && this.Discord.Configuration.Override != null) + { + this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this.Discord.Configuration.Override); + } this.RoutesToHashes = new ConcurrentDictionary(); this.HashesToBuckets = new ConcurrentDictionary(); this.RequestQueue = new ConcurrentDictionary(); this.GlobalRateLimitEvent = new AsyncManualResetEvent(true); this.UseResetAfter = useRelativeRatelimit; } /// /// Gets a bucket. /// /// The method. /// The route. /// The route paramaters. /// The url. /// A ratelimit bucket. public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams_props = route_params.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary(); foreach (var xp in rparams_props) { var val = xp.GetValue(route_params); rparams[xp.Name] = val is string xs ? xs : val is DateTime dt ? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is DateTimeOffset dto ? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString(); } var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; // Create a generic route (minus major params) key // ex: POST:/channels/channel_id/messages var hashKey = RateLimitBucket.GenerateHashKey(method, route); // We check if the hash is present, using our generic route (without major params) // ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33 // If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant // and assign this to the route to hash cache // ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited var hash = this.RoutesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route)); // Next we use the hash to generate the key to obtain the bucket. // ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id // or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id var bucketId = RateLimitBucket.GenerateBucketId(hash, guild_id, channel_id, webhook_id); // If it's not in cache, create a new bucket and index it by its bucket id. var bucket = this.HashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guild_id, channel_id, webhook_id)); bucket.LastAttemptAt = DateTimeOffset.UtcNow; // Cache the routes for each bucket so it can be used for GC later. if (!bucket.RouteHashes.Contains(bucketId)) bucket.RouteHashes.Add(bucketId); // Add the current route to the request queue, which indexes the amount // of requests occurring to the bucket id. _ = this.RequestQueue.TryGetValue(bucketId, out var count); // Increment by one atomically due to concurrency this.RequestQueue[bucketId] = Interlocked.Increment(ref count); // Start bucket cleaner if not already running. if (!this._cleanerRunning) { this._cleanerRunning = true; this._bucketCleanerTokenSource = new CancellationTokenSource(); this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token); this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started."); } url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return bucket; } /// /// Executes the request async. /// /// The request to be executed. public Task ExecuteRequestAsync(BaseRestRequest request) => request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null); /// /// Executes the request async. /// This is to allow proper rescheduling of the first request from a bucket. /// /// The request to be executed. /// The bucket. /// The ratelimit task completion source. private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs) { if (this._disposed) return; HttpResponseMessage res = default; try { await this.GlobalRateLimitEvent.WaitAsync().ConfigureAwait(false); if (bucket == null) bucket = request.RateLimitBucket; if (ratelimitTcs == null) ratelimitTcs = await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false); if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request { var now = DateTimeOffset.UtcNow; await bucket.TryResetLimitAsync(now).ConfigureAwait(false); // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket if (Interlocked.Decrement(ref bucket._remaining) < 0) { this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is blocked", bucket.ToString()); var delay = bucket.Reset - now; var resetDate = bucket.Reset; if (this.UseResetAfter) { delay = bucket.ResetAfter.Value; resetDate = bucket.ResetAfterOffset; } if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) { this.Logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket"); bucket._remaining = 1; } if (delay < TimeSpan.Zero) delay = TimeSpan.FromMilliseconds(100); this.Logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Pre-emptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay); Task.Delay(delay) .ContinueWith(_ => this.ExecuteRequestAsync(request, null, null)) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); return; } this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is allowed", bucket.ToString()); } else this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {0} is allowed", bucket.ToString()); var req = this.BuildRequest(request); if(this.Debug) this.Logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync()); var response = new RestResponse(); try { if (this._disposed) return; res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false); var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); this.Logger.LogTrace(LoggerEvents.RestRx, txt); response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); response.Response = txt; response.ResponseCode = (int)res.StatusCode; } catch (HttpRequestException httpex) { this.Logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url); request.SetFaulted(httpex); this.FailInitialRateLimitTest(request, ratelimitTcs); return; } this.UpdateBucket(request, response, ratelimitTcs); Exception ex = null; switch (response.ResponseCode) { case 400: case 405: ex = new BadRequestException(request, response); break; case 401: case 403: ex = new UnauthorizedException(request, response); break; case 404: ex = new NotFoundException(request, response); break; case 413: ex = new RequestSizeException(request, response); break; case 429: ex = new RateLimitException(request, response); // check the limit info and requeue this.Handle429(response, out var wait, out var global); if (wait != null) { if (global) { + bucket.IsGlobal = true; this.Logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); try { this.GlobalRateLimitEvent.Reset(); await wait.ConfigureAwait(false); } finally { // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here _ = this.GlobalRateLimitEvent.SetAsync(); } this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } else { this.Logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing request to {0}", request.Url); await wait.ConfigureAwait(false); this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } return; } break; case 500: case 502: case 503: case 504: ex = new ServerErrorException(request, response); break; } if (ex != null) request.SetFaulted(ex); else request.SetCompleted(response); } catch (Exception ex) { this.Logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url); // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0) this.FailInitialRateLimitTest(request, ratelimitTcs); if (!request.TrySetFaulted(ex)) throw; } finally { res?.Dispose(); // Get and decrement active requests in this bucket by 1. _ = this.RequestQueue.TryGetValue(bucket.BucketId, out var count); this.RequestQueue[bucket.BucketId] = Interlocked.Decrement(ref count); // If it's 0 or less, we can remove the bucket from the active request queue, // along with any of its past routes. if (count <= 0) { foreach (var r in bucket.RouteHashes) { if (this.RequestQueue.ContainsKey(r)) { _ = this.RequestQueue.TryRemove(r, out _); } } } } } /// /// Fails the initial rate limit test. /// /// The request. /// The ratelimit task completion source. /// If true, reset to initial. private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false) { if (ratelimitTcs == null && !resetToInitial) return; var bucket = request.RateLimitBucket; bucket._limitValid = false; bucket._limitTestFinished = null; bucket._limitTesting = 0; //Reset to initial values. if (resetToInitial) { this.UpdateHashCaches(request, bucket); bucket.Maximum = 0; bucket._remaining = 0; return; } // no need to wait on all the potentially waiting tasks _ = Task.Run(() => ratelimitTcs.TrySetResult(false)); } /// /// Waits for the initial rate limit. /// /// The bucket. private async Task> WaitForInitialRateLimit(RateLimitBucket bucket) { while (!bucket._limitValid) { if (bucket._limitTesting == 0) { if (Interlocked.CompareExchange(ref bucket._limitTesting, 1, 0) == 0) { // if we got here when the first request was just finishing, we must not create the waiter task as it would signel ExecureRequestAsync to bypass rate limiting if (bucket._limitValid) return null; // allow exactly one request to go through without having rate limits available var ratelimitsTcs = new TaskCompletionSource(); bucket._limitTestFinished = ratelimitsTcs.Task; return ratelimitsTcs; } } // it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits Task waitTask = null; while (bucket._limitTesting != 0 && (waitTask = bucket._limitTestFinished) == null) await Task.Yield(); if (waitTask != null) await waitTask.ConfigureAwait(false); // if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here } return null; } /// /// Builds the request. /// /// The request. /// A http request message. private HttpRequestMessage BuildRequest(BaseRestRequest request) { var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url); if (request.Headers != null && request.Headers.Any()) foreach (var kvp in request.Headers) req.Headers.Add(kvp.Key, kvp.Value); if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload)) { this.Logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload); req.Content = new StringContent(nmprequest.Payload); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } if (request is MultipartWebRequest mprequest) { this.Logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var content = new MultipartFormDataContent(boundary); if (mprequest.Values != null && mprequest.Values.Any()) foreach (var kvp in mprequest.Values) content.Add(new StringContent(kvp.Value), kvp.Key); + var fileId = mprequest.OverwriteFileIdStart ?? 0; + if (mprequest.Files != null && mprequest.Files.Any()) { - var i = 1; foreach (var f in mprequest.Files) - content.Add(new StreamContent(f.Value), $"file{i++.ToString(CultureInfo.InvariantCulture)}", f.Key); + { + var name = $"files[{fileId.ToString(CultureInfo.InvariantCulture)}]"; + content.Add(new StreamContent(f.Value), name, f.Key); + fileId++; + } } req.Content = content; } if (request is MultipartStickerWebRequest mpsrequest) { this.Logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var sc = new StreamContent(mpsrequest.File.Stream); if (mpsrequest.File.ContentType != null) sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType); var fileName = mpsrequest.File.FileName; if (mpsrequest.File.FileType != null) fileName += '.' + mpsrequest.File.FileType; var content = new MultipartFormDataContent(boundary) { { new StringContent(mpsrequest.Name), "name" }, { new StringContent(mpsrequest.Tags), "tags" }, { new StringContent(mpsrequest.Description), "description" }, { sc, "file", fileName } }; req.Content = content; } - return req; } /// /// Handles the http 429 status. /// /// The response. /// The wait task. /// If true, global. private void Handle429(RestResponse response, out Task wait_task, out bool global) { wait_task = null; global = false; if (response.Headers == null) return; var hs = response.Headers; // handle the wait if (hs.TryGetValue("Retry-After", out var retry_after_raw)) { var retry_after = TimeSpan.FromSeconds(int.Parse(retry_after_raw, CultureInfo.InvariantCulture)); wait_task = Task.Delay(retry_after); } // check if global b1nzy if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true") { // global global = true; } } /// /// Updates the bucket. /// /// The request. /// The response. /// The ratelimit task completion source. private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs) { var bucket = request.RateLimitBucket; if (response.Headers == null) { if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again this.FailInitialRateLimitTest(request, ratelimitTcs); return; } var hs = response.Headers; + if (hs.TryGetValue("X-RateLimit-Scope", out var scope)) + { + bucket.Scope = scope; + } + + if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.ToLowerInvariant() == "true") { if (response.ResponseCode != 429) + { + bucket.IsGlobal = true; this.FailInitialRateLimitTest(request, ratelimitTcs); + } return; } var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesmax); var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesleft); var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset); var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter); var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash); if (!r1 || !r2 || !r3 || !r4) { //If the limits were determined before this request, make the bucket initial again. if (response.ResponseCode != 429) this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null); return; } var clienttime = DateTimeOffset.UtcNow; var resettime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture)); var servertime = clienttime; if (hs.TryGetValue("Date", out var raw_date)) servertime = DateTimeOffset.Parse(raw_date, CultureInfo.InvariantCulture).ToUniversalTime(); var resetdelta = resettime - servertime; //var difference = clienttime - servertime; //if (Math.Abs(difference.TotalSeconds) >= 1) //// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now); //else // difference = TimeSpan.Zero; if (request.RateLimitWaitOverride.HasValue) resetdelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value); var newReset = clienttime + resetdelta; if (this.UseResetAfter) { bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture)); newReset = clienttime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue ? resetdelta : TimeSpan.Zero); bucket.ResetAfterOffset = newReset; } else bucket.Reset = newReset; var maximum = int.Parse(usesmax, CultureInfo.InvariantCulture); var remaining = int.Parse(usesleft, CultureInfo.InvariantCulture); if (ratelimitTcs != null) { // initial population of the ratelimit data bucket.SetInitialValues(maximum, remaining, newReset); _ = Task.Run(() => ratelimitTcs.TrySetResult(true)); } else { // only update the bucket values if this request was for a newer interval than the one // currently in the bucket, to avoid issues with concurrent requests in one bucket // remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time if (bucket._nextReset == 0) bucket._nextReset = newReset.UtcTicks; } this.UpdateHashCaches(request, bucket, hash); } /// /// Updates the hash caches. /// /// The request. /// The bucket. /// The new hash. private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null) { var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route); if (!this.RoutesToHashes.TryGetValue(hashKey, out var oldHash)) return; // This is an unlimited bucket, which we don't need to keep track of. if (newHash == null) { _ = this.RoutesToHashes.TryRemove(hashKey, out _); _ = this.HashesToBuckets.TryRemove(bucket.BucketId, out _); return; } // Only update the hash once, due to a bug on Discord's end. // This will cause issues if the bucket hashes are dynamically changed from the API while running, // in which case, Dispose will need to be called to clear the caches. if (bucket._isUnlimited && newHash != oldHash) { this.Logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash); var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); _ = this.RoutesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) => { bucket.Hash = newHash; var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); // Remove the old unlimited bucket. _ = this.HashesToBuckets.TryRemove(oldBucketId, out _); _ = this.HashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket); return newHash; }); } return; } /// /// Cleanups the buckets. /// private async Task CleanupBucketsAsync() { while (!this._bucketCleanerTokenSource.IsCancellationRequested) { try { await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false); } catch { } if (this._disposed) return; //Check and clean request queue first in case it wasn't removed properly during requests. foreach (var key in this.RequestQueue.Keys) { var bucket = this.HashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key)); if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow)) _ = this.RequestQueue.TryRemove(key, out _); } var removedBuckets = 0; StringBuilder bucketIdStrBuilder = default; foreach (var kvp in this.HashesToBuckets) { if (bucketIdStrBuilder == null) bucketIdStrBuilder = new StringBuilder(); var key = kvp.Key; var value = kvp.Value; // Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket. if (this.RequestQueue.ContainsKey(value.BucketId) && !value._isUnlimited) continue; var resetOffset = this.UseResetAfter ? value.ResetAfterOffset : value.Reset; // Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket. if (resetOffset != null && !value._isUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay)) continue; _ = this.HashesToBuckets.TryRemove(key, out _); removedBuckets++; bucketIdStrBuilder.Append(value.BucketId + ", "); } if (removedBuckets > 0) this.Logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' ')); if (this.HashesToBuckets.Count == 0) break; } if (!this._bucketCleanerTokenSource.IsCancellationRequested) this._bucketCleanerTokenSource.Cancel(); this._cleanerRunning = false; this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } ~RestClient() => this.Dispose(); /// /// Disposes the rest client. /// public void Dispose() { if (this._disposed) return; this._disposed = true; this.GlobalRateLimitEvent.Reset(); if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) { this._bucketCleanerTokenSource?.Cancel(); this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } try { this._cleanerTask?.Dispose(); this._bucketCleanerTokenSource?.Dispose(); this.HttpClient?.Dispose(); } catch { } this.RoutesToHashes.Clear(); this.HashesToBuckets.Clear(); this.RequestQueue.Clear(); } } } diff --git a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs index d82faa1e7..ff96a2063 100644 --- a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs +++ b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs @@ -1,88 +1,89 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 DisCatSharp.Entities; using DisCatSharp.Enums; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net.Serialization { /// /// Represents a discord component json converter. /// internal sealed class DiscordComponentJsonConverter : JsonConverter { /// /// Whether the converter can write. /// public override bool CanWrite => false; /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var job = JObject.Load(reader); var type = job["type"]?.ToObject(); if (type == null) throw new ArgumentException($"Value {reader} does not have a component type specifier"); var cmp = type switch { ComponentType.ActionRow => new DiscordActionRowComponent(), ComponentType.Button => new DiscordButtonComponent(), ComponentType.Select => new DiscordSelectComponent(), + ComponentType.InputText => new DiscordTextComponent(), _ => new DiscordComponent() { Type = type.Value } }; // Populate the existing component with the values in the JObject. This avoids a recursive JsonConverter loop using var jreader = job.CreateReader(); serializer.Populate(jreader, cmp); return cmp; } /// /// Whether the json can convert. /// /// The object type. public override bool CanConvert(Type objectType) => typeof(DiscordComponent).IsAssignableFrom(objectType); } } diff --git a/DisCatSharp/Net/Serialization/DiscordJson.cs b/DisCatSharp/Net/Serialization/DiscordJson.cs index 077d7c749..b0dfd6711 100644 --- a/DisCatSharp/Net/Serialization/DiscordJson.cs +++ b/DisCatSharp/Net/Serialization/DiscordJson.cs @@ -1,83 +1,82 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using System.IO; using System.Text; using DisCatSharp.Entities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net.Serialization { /// /// Represents discord json. /// public static class DiscordJson { private static readonly JsonSerializer _serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new OptionalJsonContractResolver() }); /// Serializes the specified object to a JSON string. /// The object to serialize. /// A JSON string representation of the object. public static string SerializeObject(object value) => SerializeObjectInternal(value, null, _serializer); /// Populates an object with the values from a JSON node. /// The token to populate the object with. /// The object to populate. public static void PopulateObject(JToken value, object target) { using var reader = value.CreateReader(); _serializer.Populate(reader, target); } /// - /// Converts this token into an object, passing any properties through extra s if - /// needed. + /// Converts this token into an object, passing any properties through extra s if needed. /// /// The token to convert /// Type to convert to /// The converted token public static T ToDiscordObject(this JToken token) => token.ToObject(_serializer); /// /// Serializes the object. /// /// The value. /// The type. /// The json serializer. private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer) { var stringWriter = new StringWriter(new StringBuilder(256), CultureInfo.InvariantCulture); using (var jsonTextWriter = new JsonTextWriter(stringWriter)) { jsonTextWriter.Formatting = jsonSerializer.Formatting; jsonSerializer.Serialize(jsonTextWriter, value, type); } return stringWriter.ToString(); } } } diff --git a/DisCatSharp/Net/Udp/DCSUdpClient.cs b/DisCatSharp/Net/Udp/DCSUdpClient.cs index b469743d9..cfc7e76a3 100644 --- a/DisCatSharp/Net/Udp/DCSUdpClient.cs +++ b/DisCatSharp/Net/Udp/DCSUdpClient.cs @@ -1,141 +1,141 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.Net.Udp { /// /// The default, native-based UDP client implementation. /// internal class DCSUdpClient : BaseUdpClient { /// /// Gets the client. /// private UdpClient Client { get; set; } /// /// Gets the end point. /// private ConnectionEndpoint EndPoint { get; set; } /// /// Gets the packet queue. /// private BlockingCollection PacketQueue { get; } /// /// Gets the receiver task. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] private Task ReceiverTask { get; set; } /// /// Gets the cancellation token source. /// private CancellationTokenSource TokenSource { get; } /// /// Gets the cancellation token. /// private CancellationToken Token => this.TokenSource.Token; /// /// Creates a new UDP client instance. /// public DCSUdpClient() { this.PacketQueue = new BlockingCollection(); this.TokenSource = new CancellationTokenSource(); } /// /// Configures the UDP client. /// /// Endpoint that the client will be communicating with. public override void Setup(ConnectionEndpoint endpoint) { this.EndPoint = endpoint; this.Client = new UdpClient(); this.ReceiverTask = Task.Run(this.ReceiverLoopAsync, this.Token); } /// /// Sends a datagram. /// /// Datagram. /// Length of the datagram. /// public override Task SendAsync(byte[] data, int dataLength) => this.Client.SendAsync(data, dataLength, this.EndPoint.Hostname, this.EndPoint.Port); /// /// Receives a datagram. /// /// The received bytes. public override Task ReceiveAsync() => Task.FromResult(this.PacketQueue.Take(this.Token)); /// /// Closes and disposes the client. /// public override void Close() { this.TokenSource.Cancel(); #if !NETSTANDARD1_3 try { this.Client.Close(); } catch (Exception) { } #endif // dequeue all the packets this.PacketQueue.Dispose(); } /// /// Receivers the loop. /// private async Task ReceiverLoopAsync() { while (!this.Token.IsCancellationRequested) { try { var packet = await this.Client.ReceiveAsync().ConfigureAwait(false); this.PacketQueue.Add(packet.Buffer); } catch (Exception) { } } } /// - /// Creates a new instance of . + /// Creates a new instance of . /// public static BaseUdpClient CreateNew() => new DCSUdpClient(); } } diff --git a/DisCatSharp/Properties/Resources.Designer.cs b/DisCatSharp/Properties/Resources.Designer.cs new file mode 100644 index 000000000..5043e65f7 --- /dev/null +++ b/DisCatSharp/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DisCatSharp.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DisCatSharp.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/DisCatSharp/Properties/Resources.resx b/DisCatSharp/Properties/Resources.resx new file mode 100644 index 000000000..4fdb1b6af --- /dev/null +++ b/DisCatSharp/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DisCatSharp/RingBuffer.cs b/DisCatSharp/RingBuffer.cs index b0a4dd9ec..5bd8c81b3 100644 --- a/DisCatSharp/RingBuffer.cs +++ b/DisCatSharp/RingBuffer.cs @@ -1,241 +1,241 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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; using System.Collections.Generic; using System.Linq; namespace DisCatSharp { /// /// A circular buffer collection. /// /// Type of elements within this ring buffer. public class RingBuffer : ICollection { /// /// Gets the current index of the buffer items. /// public int CurrentIndex { get; protected set; } /// /// Gets the capacity of this ring buffer. /// public int Capacity { get; protected set; } /// /// Gets the number of items in this ring buffer. /// public int Count => this._reached_end ? this.Capacity : this.CurrentIndex; /// /// Gets whether this ring buffer is read-only. /// public bool IsReadOnly => false; /// /// Gets or sets the internal collection of items. /// protected T[] InternalBuffer { get; set; } private bool _reached_end = false; /// /// Creates a new ring buffer with specified size. /// /// Size of the buffer to create. - /// + /// public RingBuffer(int size) { if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); this.CurrentIndex = 0; this.Capacity = size; this.InternalBuffer = new T[this.Capacity]; } /// /// Creates a new ring buffer, filled with specified elements. /// /// Elements to fill the buffer with. - /// - /// + /// + /// public RingBuffer(IEnumerable elements) : this(elements, 0) { } /// /// Creates a new ring buffer, filled with specified elements, and starting at specified index. /// /// Elements to fill the buffer with. /// Starting element index. - /// - /// + /// + /// public RingBuffer(IEnumerable elements, int index) { if (elements == null || !elements.Any()) throw new ArgumentException(nameof(elements), "The collection cannot be null or empty."); this.CurrentIndex = index; this.InternalBuffer = elements.ToArray(); this.Capacity = this.InternalBuffer.Length; if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); } /// /// Inserts an item into this ring buffer. /// /// Item to insert. public void Add(T item) { this.InternalBuffer[this.CurrentIndex++] = item; if (this.CurrentIndex == this.Capacity) { this.CurrentIndex = 0; this._reached_end = true; } } /// /// Gets first item from the buffer that matches the predicate. /// /// Predicate used to find the item. /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. /// Whether an item that matches the predicate was found or not. public bool TryGet(Func predicate, out T item) { for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } for (var i = 0; i < this.CurrentIndex; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } item = default; return false; } /// /// Clears this ring buffer and resets the current item index. /// public void Clear() { for (var i = 0; i < this.InternalBuffer.Length; i++) this.InternalBuffer[i] = default; this.CurrentIndex = 0; } /// /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. /// /// Item to check for. /// Whether the buffer contains the item. - /// + /// public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use .Contains(predicate) instead."); /// /// Checks whether given item is present in the buffer using given predicate to find it. /// /// Predicate used to check for the item. /// Whether the buffer contains the item. public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); /// /// Copies this ring buffer to target array, attempting to maintain the order of items within. /// /// Target array. /// Index starting at which to copy the items to. public void CopyTo(T[] array, int index) { if (array.Length - index < 1) throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); var ci = 0; for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) array[ci++] = this.InternalBuffer[i]; for (var i = 0; i < this.CurrentIndex; i++) array[ci++] = this.InternalBuffer[i]; } /// /// Removes an item from the buffer. This method is not implemented. Use instead. /// /// Item to remove. /// Whether an item was removed or not. public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use .Remove(predicate) instead."); /// /// Removes an item from the buffer using given predicate to find it. /// /// Predicate used to find the item. /// Whether an item was removed or not. public bool Remove(Func predicate) { for (var i = 0; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { this.InternalBuffer[i] = default; return true; } } return false; } /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. public IEnumerator GetEnumerator() { return !this._reached_end ? this.InternalBuffer.AsEnumerable().GetEnumerator() : this.InternalBuffer.Skip(this.CurrentIndex) .Concat(this.InternalBuffer.Take(this.CurrentIndex)) .GetEnumerator(); } /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } } diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index a5891168c..e7664c2de 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,465 +1,465 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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 /// internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(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) { return config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), }; } /// /// 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); } /// /// 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); } /// /// 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); } /// /// 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); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$", RegexOptions.ECMAScript); 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) { return taad == ThreadAutoArchiveDuration.ThreeDays - ? (guild.PremiumTier.HasFlag(PremiumTier.Tier_1) || guild.Features.CanSetThreadArchiveDurationThreeDays) - : taad != ThreadAutoArchiveDuration.OneWeek || guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.CanSetThreadArchiveDurationSevenDays; + ? (guild.PremiumTier.HasFlag(PremiumTier.TierOne) || guild.Features.CanSetThreadArchiveDurationThreeDays) + : taad != ThreadAutoArchiveDuration.OneWeek || guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.CanSetThreadArchiveDurationSevenDays; } /// /// Checks the thread private feature. /// /// The guild. /// A bool. - internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.Tier_2) || guild.Features.CanCreatePrivateThreads; + 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 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. + /// 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 . + /// 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. + /// 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 . + /// 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. + /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// - /// to calculate Unix time for. + /// 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.FULL_PERMS; 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/Package.targets b/Package.targets index 801d09082..973a3057c 100644 --- a/Package.targets +++ b/Package.targets @@ -1,13 +1,13 @@ - 1591;NU5128 + 1591;NU5128;DV2001 9.0 True True Portable False diff --git a/README.md b/README.md index 5266aadd9..ec9477d3c 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,101 @@ # DisCatSharp [![GitHub](https://img.shields.io/github/license/Aiko-IT-Systems/DisCatSharp?label=License)](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/LICENSE.md) [![Sponsors](https://img.shields.io/github/sponsors/Lulalaby?label=Sponsors)](https://github.com/sponsors/Lulalaby) [![Discord Server](https://img.shields.io/discord/858089281214087179.svg?label=Discord)](https://discord.gg/discatsharp) -![oie_u4NkggjBd6fc](https://user-images.githubusercontent.com/14029133/133850667-11872a7b-1dad-4a47-baab-aad2ecfc29d5.jpg) +![Logo](DisCatSharp.Logos/logobig.png#64x64) -Discord Bot Library written in C# for .NET. https://discord.gg/discatsharp +[Discord Bot Library](https://discord.gg/discatsharp) written in C# for .NET. #### Status [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.svg?label=NuGet%20Overall%20Version)](https://nuget.dcs.aitsys.dev) [![Build status](https://ci.appveyor.com/api/projects/status/fy4xn9s3cq7j30j7/branch/main?svg=true)](https://ci.appveyor.com/project/AITSYS/discatsharp/branch/main) #### Commit Activities [![GitHub last commit](https://img.shields.io/github/last-commit/Aiko-IT-Systems/DisCatSharp?label=Last%20Commit)](https://aitsys.dev/source/DisCatSharp/history/) [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/Aiko-IT-Systems/DisCatSharp?label=Commit%20Activity)](https://github.com/Aiko-IT-Systems/DisCatSharp/commits/main) [![wakatime](https://wakatime.com/badge/github/Aiko-IT-Systems/DisCatSharp.svg)](https://wakatime.com/badge/github/Aiko-IT-Systems/DisCatSharp) #### Stats [![GitHub pull requests](https://img.shields.io/github/issues-pr/Aiko-IT-Systems/DisCatSharp?label=PRs)](https://github.com/Aiko-IT-Systems/DisCatSharp/pulls) [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/Aiko-IT-Systems/DisCatSharp?label=Size)](#) [![GitHub contributors](https://img.shields.io/github/contributors/Aiko-IT-Systems/DisCatSharp)](https://github.com/Aiko-IT-Systems/DisCatSharp/graphs/contributors) [![GitHub Repo stars](https://img.shields.io/github/stars/Aiko-IT-Systems/DisCatSharp?label=Stars)](https://github.com/Aiko-IT-Systems/DisCatSharp/stargazers) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5282/badge)](https://bestpractices.coreinfrastructure.org/projects/5282) [![StackShare](http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat)](https://stackshare.io/aiko-it-systems/discatsharp) + ## Why DisCatSharp? We want the lib always up-to-date. The newest features are important for us. So the API version is always the newest, in the actual case `v9`. ## Where is the Changelog? On our guild! You find it in [this channel](https://discord.com/channels/858089281214087179/858099438580006913). ## Installing You can install the library from following source: -The latest release is always available on [NuGet](https://www.nuget.org/profiles/Aiko-IT-Systems). +The latest release is always available on [NuGet](https://nuget.dcs.aitsys.dev). ## Documentation -~~The documentation for the latest stable version is available at [docs.dcs.aitsys.dev/lts](https://docs.dcs.aitsys.dev/lts).~~ *Outage* +The documentation for the latest stable version is available at [docs.dcs.aitsys.dev/lts](https://docs.dcs.aitsys.dev/lts). -The documentation of the latest nightly versions is available at [aiko-it-systems.github.io/DisCatSharp.Docs](https://aiko-it-systems.github.io/DisCatSharp.Docs). +The documentation of the latest nightly versions is available at [docs.dcs.aitsys.dev](https://docs.dcs.aitsys.dev). ## Bugs or Feature requests? Either join our [support guild](https://discord.gg/discatsharp) and open a support ticket. -~~Or write a mail to dcs@aitsys.dev.~~ *Outage* +Or write a mail to dcs@aitsys.dev. -~~All requests are tracked at [aitsys.dev](https://aitsys.dev).~~ *Outage* +All requests are tracked at [aitsys.dev](https://aitsys.dev). ## Tutorials -* [Howto](https://aiko-it-systems.github.io/DisCatSharp.Docs/articles/basics/bot_account.html) -* [Examples](https://github.com/Aiko-IT-Systems/DisCatSharp.Examples) +* [Howto](https://docs.dcs.aitsys.dev/articles/basics/bot_account.html) +* [Examples](https://examples.dcs.aitsys.dev) ## Snippts [Snippets for Visual Studio](https://github.com/Aiko-IT-Systems/DisCatSharp.Snippets) ## Latest NuGet Packages | Package | NuGet | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DisCatSharp | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp) | | DisCatSharp.ApplicationCommands | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ApplicationCommands.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.ApplicationCommands) | | DisCatSharp.CommandsNext | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.CommandsNext.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.CommandsNext) | | DisCatSharp.Common | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Common.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Common) | | DisCatSharp.Configuration | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Configuration.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Configuration) | | DisCatSharp.Hosting | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Hosting) | | DisCatSharp.Hosting.DependencyInjection | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.DependencyInjection.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Hosting.DependencyInjection) | | DisCatSharp.Interactivity | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Interactivity.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Interactivity) | | DisCatSharp.Lavalink | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Lavalink.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.Lavalink) | | DisCatSharp.VoiceNext | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.VoiceNext) | | DisCatSharp.VoiceNext.Natives | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.Natives.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.VoiceNext.Natives) | +| DisCatSharp.ProjectTemplates | [![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ProjectTemplates.svg?label=)](https://nuget.dcs.aitsys.dev/DisCatSharp.ProjectTemplates) | ## Releasing To release a new version do the following steps: - Create locally a repo named `release/VERSION` (Don't forget to replace VERSION with the target version number) - Replace version number with the correct version in [appveyor.yml#L78](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/appveyor.yml#L78) with the new release number and [appveyor.yml#L5](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/appveyor.yml#L5) with the next-ahead release number. - Replace nuget version number in [Version.targets#L4](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/Version.targets#L4) - Publish branch to GitHub - Wait for the CI/CD to complete. - Merge the branch into main and delete it afterwards ## Testing Docs Just visit [this](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/docs-preview.yml) Workflow. The workflow generates a docs artifact and deploys on Cloudflare Pages. Visit the channel [#github-feed](https://discord.com/channels/858089281214087179/861507952509976607) on discord and watch for this message: ![Deploy](./cf-deploy.png) If you can't see it, take a look into the selfroles channel. ## Thanks Big thanks goes to the following people who helps us ♥️ - [Auros Nexus](https://github.com/Auros) - [Lunar Starstrum](https://github.com/OoLunar) - [Johannes](https://github.com/JMLutra) - [Geferon](https://github.com/geferon) - [Alice](https://github.com/QuantuChi) + +## Special Thanks +The special thanks goes to Nagisa. Make sure to check out her [instagram](https://www.instagram.com/nagisaarts_/) ♥️♥️ diff --git a/Version.targets b/Version.targets index 4df4a9685..3ddf076fb 100644 --- a/Version.targets +++ b/Version.targets @@ -1,21 +1,21 @@ - 9.8.4 + 9.8.5 $(VersionPrefix)-$(VersionSuffix)-$(BuildNumber) $(VersionPrefix).$(BuildNumber) $(VersionPrefix).$(BuildNumber) $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 $(VersionPrefix) $(VersionPrefix).0 $(VersionPrefix).0 diff --git a/appveyor.yml b/appveyor.yml index b423c2543..29e646bde 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,140 +1,140 @@ - branches: only: - main - version: 9.8.4-nightly-{build} + version: 9.8.5-nightly-{build} pull_requests: do_not_increment_build_number: true skip_tags: true max_jobs: 1 - image: Visual Studio 2019 + image: Visual Studio 2022 clone_depth: 1 build_script: - ps: |- # Version number $BUILD_NUMBER = [int]$Env:APPVEYOR_BUILD_NUMBER $BUILD_SUFFIX = "nightly" # Branch $BRANCH = "$Env:APPVEYOR_REPO_BRANCH" $Env:DOCFX_SOURCE_BRANCH_NAME = "$BRANCH" # Output directory $Env:ARTIFACT_DIR = ".\artifacts" $dir = New-Item -type directory $env:ARTIFACT_DIR $dir = $dir.FullName # Verbosity Write-Host "Build: $BUILD_NUMBER / Branch: $BRANCH" Write-Host "Artifacts will be placed in: $dir" # Check if this is a PR if (-not $Env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host "Commencing complete build" & .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER & Remove-Item "$dir\*.symbols.nupkg" } else { Write-Host "Building from PR ($Env:APPVEYOR_PULL_REQUEST_NUMBER)" & .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER & Remove-Item "$dir\*.symbols.nupkg" } artifacts: - path: artifacts\*.snupkg - path: artifacts\*.nupkg - path: artifacts\dcs-docs.tar.xz deploy: - provider: NuGet server: api_key: secure: eml4lPttwjBZg7WdwX3tbx34ZDNssgb2zwthatNbolRY0PnaCIswbuPClf9IWrw7 skip_symbols: false - provider: GitHub auth_token: secure: oMF8sv9mhVjO7pBctQOwlmfd5aHQ4hvMoVCz77bgO9+1zBQSelPHxk0bCVfXNCCp prerelease: true force_update: true - provider: NuGet server: https://nuget.pkg.github.com/Aiko-IT-Systems/index.json username: lulalaby api_key: secure: SBGo8KrGJ7t5wwMNHKD0WSzrQ+PLJbqXE3FtDH2yGkSrQewO+kzmwp/xGk5a84He skip_symbols: true on_success: - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - ps: ./send.ps1 success $env:WEBHOOK_URL on_failure: - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - ps: ./send.ps1 failure $env:WEBHOOK_URL # Releases - branches: only: - /release/ - version: 9.8.3 + version: 9.8.4 pull_requests: do_not_increment_build_number: true skip_tags: true max_jobs: 1 - image: Visual Studio 2019 + image: Visual Studio 2022 clone_depth: 1 build_script: - ps: |- # Version number $BUILD_NUMBER = [int]$Env:APPVEYOR_BUILD_NUMBER # Branch $BRANCH = "$Env:APPVEYOR_REPO_BRANCH" $Env:DOCFX_SOURCE_BRANCH_NAME = "$BRANCH" # Output directory $Env:ARTIFACT_DIR = ".\artifacts" $dir = New-Item -type directory $env:ARTIFACT_DIR $dir = $dir.FullName # Verbosity Write-Host "Build: $BUILD_NUMBER / Branch: $BRANCH" Write-Host "Artifacts will be placed in: $dir" # Check if this is a PR if (-not $Env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host "Commencing complete build" & .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER & Remove-Item "$dir\*.symbols.nupkg" } else { Write-Host "Building from PR ($Env:APPVEYOR_PULL_REQUEST_NUMBER)" & .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER & Remove-Item "$dir\*.symbols.nupkg" } artifacts: - path: artifacts\*.snupkg - path: artifacts\*.nupkg - path: artifacts\dcs-docs.tar.xz deploy: - provider: NuGet server: api_key: secure: eml4lPttwjBZg7WdwX3tbx34ZDNssgb2zwthatNbolRY0PnaCIswbuPClf9IWrw7 skip_symbols: false - provider: GitHub auth_token: secure: oMF8sv9mhVjO7pBctQOwlmfd5aHQ4hvMoVCz77bgO9+1zBQSelPHxk0bCVfXNCCp prerelease: false force_update: true - provider: NuGet server: https://nuget.pkg.github.com/Aiko-IT-Systems/index.json username: lulalaby api_key: secure: SBGo8KrGJ7t5wwMNHKD0WSzrQ+PLJbqXE3FtDH2yGkSrQewO+kzmwp/xGk5a84He skip_symbols: true on_success: - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - ps: ./send.ps1 success $env:WEBHOOK_URL on_failure: - ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 - ps: ./send.ps1 failure $env:WEBHOOK_URL diff --git a/global.json b/global.json new file mode 100644 index 000000000..126149e6b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.100", + "rollForward": "latestMajor" + } +} diff --git a/rebuild-docs.ps1 b/rebuild-docs.ps1 index 75e3e6033..6e742d859 100644 --- a/rebuild-docs.ps1 +++ b/rebuild-docs.ps1 @@ -1,492 +1,492 @@ #!/usr/bin/env pwsh # Rebuild-docs # -# Rebuilds the documentation for DSharpPlus NextGen project, and places artifacts in specified directory. +# 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 | % { [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 succedded 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 ($Env:OS -eq $null) { $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 Build-Docs([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 ($Env:OS -eq $null) { # 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 function Package-Docs([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 # 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) { Return $LastExitCode } # 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 # 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 ($Env:OS -ne $null) { $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 = Build-Docs "$DocsPath" if ($result -eq 0) { $result = Package-Docs "$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 } diff --git a/rebuild-lib.ps1 b/rebuild-lib.ps1 index 451061710..2043b3ef2 100644 --- a/rebuild-lib.ps1 +++ b/rebuild-lib.ps1 @@ -1,175 +1,175 @@ #!/usr/bin/env pwsh # Rebuild-lib # -# Rebuilds the entire DSharpPlus NextGen project, and places artifacts in specified directory. +# Rebuilds the entire DisCatSharp project, and places artifacts in specified directory. # # Author: Emzi0767 # Version: 2018-08-30 14:41 # # Arguments: # .\rebuild-lib.ps1 [version suffix] [build number] # # Run as: # .\rebuild-lib.ps1 .\path\to\artifact\location Debug/Release version-suffix build-number # # or # .\rebuild-lib.ps1 .\path\to\artifact\location Debug/Release param ( [parameter(Mandatory = $true)] [string] $ArtifactLocation, [parameter(Mandatory = $true)] [string] $Configuration, [parameter(Mandatory = $false)] [string] $VersionSuffix, [parameter(Mandatory = $false)] [int] $BuildNumber = -1 ) # Check if configuration is valid if ($Configuration -ne "Debug" -and $Configuration -ne "Release") { Write-Host "Invalid configuration specified. Must be Release or Debug." Exit 1 } # Restores the environment function Restore-Environment() { Write-Host "Restoring environment" Remove-Item ./NuGet.config } # Prepares the environment function Prepare-Environment([string] $target_dir_path) { # Prepare the environment Copy-Item ./.nuget/NuGet.config ./ # 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 $dir = New-Item -type directory "$target_dir_path" } # Builds everything function Build-All([string] $target_dir_path, [string] $version_suffix, [string] $build_number, [string] $bcfg) { # Form target path $dir = Get-Item "$target_dir_path" $target_dir = $dir.FullName Write-Host "Will place packages in $target_dir" # Clean previous build results Write-Host "Cleaning previous build" & dotnet clean -v minimal -c "$bcfg" | Out-Host if ($LastExitCode -ne 0) { Write-Host "Cleanup failed" Return $LastExitCode } # Restore nuget packages Write-Host "Restoring NuGet packages" & dotnet restore -v minimal | Out-Host if ($LastExitCode -ne 0) { Write-Host "Restoring packages failed" Return $LastExitCode } # Create build number string if (-not $build_number) { $build_number_string = "" } else { $build_number_string = [int]::Parse($build_number).ToString("00000") } # Build in specified configuration Write-Host "Building everything" if (-not $version_suffix) { & dotnet build -v minimal -c "$bcfg" | Out-Host } elseif (-not $build_number_string) { & dotnet build -v minimal -c "$bcfg" --version-suffix "$version_suffix" | Out-Host } else { & dotnet build -v minimal -c "$bcfg" --version-suffix "$version_suffix" -p:BuildNumber="$build_number_string" | Out-Host } if ($LastExitCode -ne 0) { Write-Host "Build failed" Return $LastExitCode } # Package for NuGet Write-Host "Creating NuGet packages" if (-not $version_suffix) { & dotnet pack -v minimal -c "$bcfg" --no-build -o "$target_dir" --include-symbols | Out-Host } elseif (-not $build_number_string) { & dotnet pack -v minimal -c "$bcfg" --version-suffix "$version_suffix" --no-build -o "$target_dir" --include-symbols | Out-Host } else { & dotnet pack -v minimal -c "$bcfg" --version-suffix "$version_suffix" -p:BuildNumber="$build_number_string" --no-build -o "$target_dir" --include-symbols | Out-Host } if ($LastExitCode -ne 0) { Write-Host "Packaging failed" Return $LastExitCode } Return 0 } # Check if building a pre-production package if ($VersionSuffix -and $BuildNumber -ge 0) { Write-Host "Building pre-production package with version suffix of `"$VersionSuffix-$($BuildNumber.ToString("00000"))`"" } elseif ($VersionSuffix -and (-not $BuildNumber -or $BuildNumber -lt 0)) { Write-Host "Building pre-production package with version suffix of `"$VersionSuffix`"" Remove-Variable BuildNumber $BuildNumber = $null } # Prepare environment Prepare-Environment "$ArtifactLocation" # Build everything $BuildResult = Build-All "$ArtifactLocation" "$VersionSuffix" "$BuildNumber" "$Configuration" # Restore environment Restore-Environment # Check if there were any errors if ($BuildResult -ne 0) { Write-Host "Build failed with code $BuildResult" $host.SetShouldExit($BuildResult) Exit $BuildResult } else { Exit 0 }